Compare commits

..

1 commit

Author SHA1 Message Date
Pauline Pommeret
1fc605c7e0 [gest_crans] set_etat_civil passe une liste a dialog 2015-03-05 14:06:48 +01:00
338 changed files with 24750 additions and 33474 deletions

11
.gitignore vendored
View file

@ -17,8 +17,17 @@
# Cr@ns specific ignore files #
###############################
# On ne versionne pas les fiches de déconnexion
surveillance/fiche_deconnexion/*
# Mais on garde de quoi les générer
!/surveillance/fiche_deconnexion/deconnexion_p2p.tex
!/surveillance/fiche_deconnexion/deconnexion_upload.tex
!/surveillance/fiche_deconnexion/generate.py
!/surveillance/fiche_deconnexion/logo.eps
!/surveillance/fiche_deconnexion/logo.eps.old
# Les clés wifi privées
archive/gestion/clef-wifi*
gestion/clef-wifi*
# Autres dépôts git
gestion/logreader/

View file

@ -1,31 +0,0 @@
## Sous-dépôts
À cloner pour faire marcher certains scripts
* `./lc_ldap`
* `./wifi_new`
## Paquets Debian
Rajoutez-en si vous vous rendez compte qu'il en manque, à l'occasion.
* python-ldap
* python-netifaces
* python-psycopg2
* python-netsnmp
* python-pyparsing
* python-markdown
* python-jinja2
* python-beautifulsoup
* python-ipaddr
* python-passlib
* python-dateutil
* python-tz
* python-netaddr
## À faire
* Expliquer l'environnement de test
* tunnel pour apprentis
* http://stackoverflow.com/questions/8021/allow-user-to-set-up-an-ssh-tunnel-but-nothing-else
* snmp et les mibs ! !!

View file

@ -1,8 +1,7 @@
#!/bin/bash /usr/scripts/python.sh
#! /usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (C) Stéphane Glondu, Alexandre Bos, Michel Blockelet
# Remanié en 2015 par Gabriel Détraz
# Licence : GPLv2
u"""Ce script permet au secrétaire de repérer plus facilement les membres
@ -22,23 +21,22 @@ Les commandes sont :
import sys, os, re
sys.path.append('/usr/scripts/gestion')
import config
import config.mails
from email_tools import send_email, parse_mail_template
# Fonctions d'affichage
from gestion.affich_tools import coul, tableau, prompt, cprint
from utils.sendmail import actually_sendmail
from gestion import mail
from affich_tools import coul, tableau, prompt, cprint
# Importation de la base de données
from lc_ldap import shortcuts
from ldap_crans import crans_ldap, ann_scol
db = crans_ldap()
# Lors des tests, on m'envoie tous les mails !
from socket import gethostname
debug = False
# Conn à la db
ldap = shortcuts.lc_ldap_admin()
if __name__ == '__main__':
if len(sys.argv) > 3 and sys.argv[-2] == '--debug':
debug = sys.argv[-1]
@ -67,28 +65,33 @@ def _controle_interactif_adherents(liste):
nb = 0
for a in liste:
valeur = a['charteMA']
valeur = a.charteMA()
if valeur:
suggestion = 'o'
else:
suggestion = 'n'
ok = prompt(u'[%3d] %s, %s (%s) ?'
% (restant, a['nom'][0], a['prenom'][0], a['aid'][0]), suggestion, '').lower()
% (restant, a.nom(), a.prenom(), a.id()), suggestion, '').lower()
restant -= 1
if ok == 'o':
nb += 1
if a['charteMA'] != True :
modifiable = ldap.search(u'aid=%s' % a['aid'][0], mode='rw')
try:
with modifiable[0] as adh:
adh['charteMA']=True
adh.history_gen()
adh.save()
cprint(u'Controle OK')
except:
cprint(u'Adhérent %s locké, réessayer plus tard' % a['nom'][0], 'rouge')
elif ok != 'n':
cprint(u'Arrêt du contrôle des membres actifs', 'rouge')
if a.charteMA() == False :
modifiable = db.search('aid=%s' % a.id(), 'w')['adherent'][0]
if modifiable._modifiable:
modifiable.charteMA(True)
cprint(modifiable.save())
else:
cprint(u'Adhérent %s locké, réessayer plus tard' % modifiable.Nom(), 'rouge')
elif ok == 'n':
if a.charteMA() == True:
modifiable = db.search('aid=%s' % a.id(), 'w')['adherent'][0]
if modifiable._modifiable:
modifiable.charteMA(False)
cprint(modifiable.save())
else:
cprint(u'Adhérent %s locké, réessayer plus tard' % modifiable.Nom(), 'rouge')
else:
cprint(u'Arrêt du contrôle %s des membres actifs' % explicite, 'rouge')
break
return nb, len(liste)-nb
@ -96,12 +99,12 @@ def _controle_interactif_adherents(liste):
def liste_charte_nok():
"""Retourne la liste des membres actifs qui n'ont pas signé la charte."""
liste_actifs = ldap.search(u'droits=*')
liste_actifs = db.search('droits=*')['adherent']
liste_nok = []
for adh in liste_actifs:
if (len([droit for droit in adh['droits']
if (len([droit for droit in adh.droits()
if droit not in ['Multimachines', 'Webradio']]) > 0
and not adh['charteMA']):
and not adh.charteMA()):
liste_nok.append(adh)
return liste_nok
@ -113,7 +116,7 @@ def controle_interactif():
# Tri de la liste des adhérents selon nom, prénom
# Ça peut se faire plus facilement en Python 2.4 avec l'argument key
todo_list.sort(lambda x, y: cmp((x['nom'][0], x['prenom'][0]), (y['nom'][0], y['prenom'][0])))
todo_list.sort(lambda x, y: cmp((x.nom(), x.prenom()), (y.nom(), y.prenom())))
# Zou !
ok, nok = _controle_interactif_adherents(todo_list)
@ -129,18 +132,19 @@ def spammer():
todo_list = liste_charte_nok()
if todo_list:
from smtplib import SMTP
connexion = SMTP()
if gethostname().split(".")[0] == 'redisdead':
connexion.connect("localhost")
else: connexion.connect("redisdead.crans.org")
print "Envoi des mails de rappel pour les chartes des membres actifs"
for adh in todo_list:
to = adh['mail'][0]
to = adh.email()
print to
if not debug:
From = u"ca@crans.org"
data=mail.generate('missing_charte_MA', {
'To': unicode(to),
'From': From,
})
actually_sendmail(u'ca@crans.org', (unicode(to),), data)
data = config.mails.txt_charte_MA % {'From' : u"ca@crans.org", 'To' : to}
connexion.sendmail("ca@crans.org",to,data.encode('utf-8'))
def __usage(message=None):
""" Comment ça marche ? """
@ -159,7 +163,7 @@ if __name__ == '__main__' :
__usage(u'Mauvaise utilisation de liste')
print "Liste des membres actifs n'ayant pas signé la charte :"
for adh in liste_charte_nok():
print unicode(adh['prenom'][0]) + u" " + unicode(adh['nom'][0])
print adh.Nom()
elif sys.argv[1] == 'modif':
if len(sys.argv) != 2:
__usage(u'Mauvaise utilisation de modif')

View file

@ -1,4 +1,4 @@
#!/bin/bash /usr/scripts/python.sh
#! /usr/bin/env python
# -*- coding: utf-8 -*-
"""
@ -12,8 +12,9 @@ Licence : GPL v2
import os, sys, time
import subprocess
from lc_ldap import shortcuts
from gestion.config import upload
sys.path.append('/usr/scripts/gestion')
from ldap_crans import crans_ldap
from config import upload
# logging tools
import syslog
def log(x):
@ -29,8 +30,6 @@ import utils.exceptions
import locale
locale.setlocale(locale.LC_TIME, 'fr_FR.UTF-8')
# On blackliste 14 jours après que le script ait été éxécuté
DELAY = 14
help = """Script de déconnexion pour mail invalide.
Une fiche sera générée pour chaque adhérent.
@ -50,23 +49,22 @@ l'adhérent ayant l'aid 42."""
def generate_ps(proprio, mail):
"""On génère la feuille d'avertissement et on retourne son emplacement."""
barcode = "/usr/scripts/admin/mail_invalide/barcode.eps"
name = unicode(proprio['prenom'][0]) + u" " + unicode(proprio['nom'][0])
try:
log(u'Generate invalid mail notice for %s' % name)
log('Generate invalid mail notice for %s' % proprio.Nom())
# Dossier de génération du ps
dossier = '/usr/scripts/var/mails_invalides'
# Base pour le nom du fichier
fichier = time.strftime('%Y-%m-%d-%H-%M') + '-mail-%s' % (name.
fichier = time.strftime('%Y-%m-%d-%H-%M') + '-mail-%s' % (proprio.Nom().
lower().replace(' ', '-'))
# Création du fichier tex
format_date = '%A %d %B %Y'
with open('%s/mail_invalide.tex' % os.path.dirname(__file__), 'r') as tempfile:
template = tempfile.read()
template = template.replace('~prenom~', proprio['prenom'][0].encode('utf-8'))
template = template.replace('~nom~', proprio['nom'][0].encode('utf-8'))
template = template.replace('~chambre~', proprio['chbre'][0].encode('utf-8'))
template = template.replace('~prenom~', proprio.prenom().encode('utf-8'))
template = template.replace('~nom~', proprio.nom().encode('utf-8'))
template = template.replace('~chambre~', proprio.chbre().encode('utf-8'))
template = template.replace('~mail~', mail.encode('utf-8').replace('_', '\\_'))
template = template.replace('~fin~',
time.strftime(format_date, time.localtime(time.time()+14*86400)))
@ -85,37 +83,35 @@ def generate_ps(proprio, mail):
except Exception, e:
log('Erreur lors de la génération du ps : ')
log(str(e))
log("Values : adherent:%s" % name)
log("Values : adherent:%s" % proprio.Nom())
log(utils.exceptions.formatExc())
raise
def set_mail_invalide(adherent, mail, a_verifier, a_imprimer):
name = unicode(adherent['prenom'][0]) + u" " + unicode(adherent['nom'][0])
if adherent['chbre'][0] in ['????', 'EXT']:
print u"Chambre de %s : %s, générer la fiche ? [Yn]" % (name, adherent['chbre'][0])
if adherent.chbre() in ['????', 'EXT']:
print u"Chambre de %s : %s, générer la fiche ? [Yn]" % (adherent.Nom().encode('utf-8'), adherent.chbre())
read = ''
while read not in ['y', 'n']:
read = raw_input().lower()
if read == 'n':
print u"Chambre de %s : %s, impossible de générer la fiche." % (name, adherent['chbre'][0])
print u"Chambre de %s : %s, impossible de générer la fiche." % (adherent.Nom().encode('utf-8'), adherent.chbre())
a_verifier.append(mail)
return
print u"Génération de la fiche pour %s :" % name
print "Génération de la fiche pour %s :" % adherent.Nom().encode('utf-8')
fiche = generate_ps(adherent, mail)
print fiche
a_imprimer.append(fiche)
with adherent as adh:
adh.blacklist('mail_invalide','Mail Invalide - Script',debut=int(time.time()) + DELAY * 24 * 3600)
adh.history_gen()
adh.save()
adherent.blacklist([time.time() + 14 * 24 * 3600,
'-', 'mail_invalide', "Mail invalide"])
adherent.save()
if __name__ == "__main__":
if '--help' in sys.argv or '-h' in sys.argv or len(sys.argv) < 2:
print help
sys.exit(0)
ldap = shortcuts.lc_ldap_admin()
db = crans_ldap()
# On fait la liste des .forwards dans les homes
print " * Lecture des .forward ..."
@ -145,24 +141,24 @@ if __name__ == "__main__":
# Est-ce un aid ?
if adresse[0] == '-':
print " * Recherche de aid=%s ..." % adresse[1:]
res = ldap.search(u"aid=%s" % adresse[1:], mode='rw')
res = db.search("aid=%s" % adresse[1:], 'w')['adherent']
if len(res) == 0:
print "*** Erreur : aucun résultat pour aid=%s" % adresse[1:]
a_verifier.append(adresse)
elif len(res) > 1:
print "*** Erreur : plusieurs résultats pour aid=%s :" % adresse[1:]
for adh in res:
print unicode(adh['prenom'][0]) + u" " + unicode(adh['nom'][0])
print adh.Nom()
a_verifier.append(adresse)
else:
adherent = res[0]
set_mail_invalide(adherent, adherent['mail'][0], a_verifier, a_imprimer)
set_mail_invalide(adherent, adherent.email(), a_verifier, a_imprimer)
continue
print " * Recherche de %s ..." % adresse
# Est-ce un .forward ?
if forwards.has_key(adresse):
res = ldap.search(u"uid=%s" % forwards[adresse], mode='rw')
res = db.search("uid=%s" % forwards[adresse], 'w')['adherent']
if len(res) == 0:
print "*** Erreur : aucun résultat pour uid=%s" % forwards[adresse]
a_verifier.append(adresse)
@ -172,18 +168,18 @@ if __name__ == "__main__":
continue
# Est-ce une adresse mail sans compte Cr@ns ?
res = ldap.search(u"(|(mail=%s)(mailExt=%s))" % (adresse,adresse), mode='rw')
res = db.search("mail=%s" % adresse, 'w')['adherent']
if len(res) == 0:
print "*** Erreur : aucun résultat pour %s" % adresse
a_verifier.append(adresse)
elif len(res) > 1:
print "*** Erreur : plusieurs résultats pour %s :" % adresse
for adh in res:
print unicode(adh['prenom'][0]) + u" " + unicode(adh['nom'][0])
print adh.Nom()
a_verifier.append(adresse)
else:
adherent = res[0]
set_mail_invalide(adherent, adresse, a_verifier, a_imprimer)
set_mail_invalide(adherent, adherent.email(), a_verifier, a_imprimer)
if len(a_verifier) + len(a_imprimer) > 0:
print ''

View file

@ -1,8 +1,9 @@
#!/bin/bash /usr/scripts/python.sh
#! /usr/bin/env python
# -*- coding: utf-8 -*-
import sys
# Copyright (C) Stéphane Glondu, Alexandre Bos, et autres
# Copyright (C) Stéphane Glondu, Alexandre Bos
# Licence : GPLv2
__doc__ = u"""Ce script permet de faire le menages parmis les câbleurs qui ne
@ -20,14 +21,16 @@ Les commandes sont :
import sys, os, re
import gestion.config
sys.path.append('/usr/scripts/gestion')
import config
from email_tools import send_email, parse_mail_template
# Fonctions d'affichage
from gestion.affich_tools import coul, tableau, prompt, cprint
from affich_tools import coul, tableau, prompt, cprint
# Importation de la base de données
from lc_ldap import shortcuts
ldap = shortcuts.lc_ldap_admin()
from ldap_crans import crans_ldap, ann_scol
db = crans_ldap()
def _controle_interactif_adherents(liste):
"""
@ -47,28 +50,26 @@ def _controle_interactif_adherents(liste):
nb = 0
for a in liste:
ok = prompt(u'[%3d] %s, %s (%s) ?'
% (restant, a['nom'][0], a['prenom'][0], a['aid'][0]), 'n', '').lower()
% (restant, a.nom(), a.prenom(), a.id()), 'n', '').lower()
restant -= 1
if ok == 'o':
modifiable = ldap.search(u'aid=%s' % a['aid'][0], mode='rw')[0]
try:
with modifiable as adh:
adh['droits'].remove(u'Cableur')
adh.history_gen()
adh.save()
cprint(u'Droits cableurs retirés', 'rouge')
except:
cprint(u'Adhérent %s locké, réessayer plus tard' % modifiable['nom'][0], 'rouge')
modifiable = db.search('aid=%s' % a.id(), 'w')['adherent'][0]
if modifiable._modifiable:
modifiable.droits([])
cprint(modifiable.save())
else:
cprint(u'Adhérent %s locké, réessayer plus tard' % modifiable.Nom(), 'rouge')
elif ok != 'n':
cprint(u'Arrêt du contrôle %s des membres actifs' % explicite, 'rouge')
break
def candidats():
todo_list1 = ldap.search(u'droits=cableur')
todo_list1 = db.search('droits=*')['adherent']
todo_list = []
for adh in todo_list1:
if not adh.paiement_ok():
if adh.droitsGeles():
todo_list.append(adh)
todo_list.sort(lambda x, y: cmp((x.nom(), x.prenom()), (y.nom(), y.prenom())))
return todo_list
def lister():
@ -79,7 +80,7 @@ def lister():
print "Liste des câbleur dont la cotisation n'est pas à jour."
print
for adh in todo_list:
print unicode(adh['prenom'][0]) + u" " + unicode(adh['nom'][0])
print adh.prenom() + " " + adh.nom()
print
print "total : " + str(len(todo_list))

View file

@ -1,4 +1,4 @@
#!/bin/bash /usr/scripts/python.sh
#!/usr/bin/python
# -*- mode: python; coding: utf-8 -*-
#
# total_impression.py
@ -6,7 +6,6 @@
#
# Copyright (C) 2007 Michel Blockelet <blockelet@crans.org>
#
# Revu et corrigé en 2015 par Gabriel Détraz
# This file is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
@ -33,11 +32,14 @@ Options :
Les dates doivent etre de la forme jj/mm/aaaa."""
import sys
from lc_ldap import shortcuts
from gestion.affich_tools import cprint
sys.path.append("/usr/scripts/gestion/")
from ldap_crans import crans_ldap
from config import ann_scol
from affich_tools import cprint
import time
ldap = shortcuts.lc_ldap_admin()
db = crans_ldap()
date_debut_ann_scol = time.mktime((ann_scol, 8, 1, 0, 0, 0, 0, 0, 0))
def datestrtoint(strdate):
u""" Convertit une date en entier. """
@ -50,7 +52,7 @@ def soldes_adherent(dlinf, dlsup, adherent, verbose):
totaldebit = 0
totalcredit = 0
for hist in adherent['historique']:
for hist in adherent.historique():
sep = ' '
champ = hist.replace(',', '').replace(': ', '').split(sep)
if datestrtoint(champ[0]) >= dlinf and (dlsup == 0 or datestrtoint(champ[0]) <= dlsup):
@ -110,23 +112,19 @@ def calcul_soldes():
totaldebit = 0
totalcredit = 0
liste = ldap.search(u"uid=*",sizelimit=10000)
liste = db.search("login=*")['adherent']
for adherent in liste:
try:
adhdebit, adhcredit = soldes_adherent(dlinf, dlsup, adherent, verbose)
if adhdebit + adhcredit > 0 and adhdebit + adhcredit < 1000000: # On evite Toto Passoir
if verbose >= 2:
cprint('-' * 40, 'cyan')
if verbose >= 1:
name = unicode(adherent['prenom'][0]) + u" " + unicode(adherent['nom'][0])
cprint(u'Debit total pour ' + name + u' : ' + unicode(adhdebit) + u' euros', 'rouge')
cprint(u'Credit total pour ' + name + u' : ' + unicode(adhcredit) + u' euros', 'vert')
cprint('=' * 40, 'bleu')
totaldebit += adhdebit
totalcredit += adhcredit
except KeyError:
pass
adhdebit, adhcredit = soldes_adherent(dlinf, dlsup, adherent, verbose)
if adhdebit + adhcredit > 0 and adhdebit + adhcredit < 1000000: # On evite Toto Passoir
if verbose >= 2:
cprint('-' * 40, 'cyan')
if verbose >= 1:
cprint('Debit total pour ' + adherent.Nom() + ' : ' + str(adhdebit) + ' euros', 'rouge')
cprint('Credit total pour ' + adherent.Nom() + ' : ' + str(adhcredit) + ' euros', 'vert')
cprint('=' * 40, 'bleu')
totaldebit += adhdebit
totalcredit += adhcredit
if verbose >= 1:
cprint('=' * 80, 'bleu')
if dlinf == 0:

View file

@ -1,683 +0,0 @@
"""All Python Type client support for Bcfg2."""
__revision__ = '$Revision$'
import binascii
from datetime import datetime
import difflib
import errno
import grp
import logging
import os
import pwd
import shutil
import stat
import sys
import time
# py3k compatibility
if sys.hexversion >= 0x03000000:
unicode = str
import Bcfg2.Client.Tools
import Bcfg2.Options
from Bcfg2.Client import XML
log = logging.getLogger('python')
# map between dev_type attribute and stat constants
device_map = {'block': stat.S_IFBLK,
'char': stat.S_IFCHR,
'fifo': stat.S_IFIFO}
def calcPerms(initial, perms):
"""This compares ondisk permissions with specified ones."""
pdisp = [{1:stat.S_ISVTX, 2:stat.S_ISGID, 4:stat.S_ISUID},
{1:stat.S_IXUSR, 2:stat.S_IWUSR, 4:stat.S_IRUSR},
{1:stat.S_IXGRP, 2:stat.S_IWGRP, 4:stat.S_IRGRP},
{1:stat.S_IXOTH, 2:stat.S_IWOTH, 4:stat.S_IROTH}]
tempperms = initial
if len(perms) == 3:
perms = '0%s' % (perms)
pdigits = [int(perms[digit]) for digit in range(4)]
for index in range(4):
for (num, perm) in list(pdisp[index].items()):
if pdigits[index] & num:
tempperms |= perm
return tempperms
def normGid(entry):
"""
This takes a group name or gid and
returns the corresponding gid or False.
"""
try:
try:
return int(entry.get('group'))
except:
return int(grp.getgrnam(entry.get('group'))[2])
except (OSError, KeyError):
log.error('GID normalization failed for %s. Does group %s exist?'
% (entry.get('name'), entry.get('group')))
return False
def normUid(entry):
"""
This takes a user name or uid and
returns the corresponding uid or False.
"""
try:
try:
return int(entry.get('owner'))
except:
return int(pwd.getpwnam(entry.get('owner'))[2])
except (OSError, KeyError):
log.error('UID normalization failed for %s. Does owner %s exist?'
% (entry.get('name'), entry.get('owner')))
return False
def isString(strng, encoding):
"""
Returns true if the string contains no ASCII control characters
and can be decoded from the specified encoding.
"""
for char in strng:
if ord(char) < 9 or ord(char) > 13 and ord(char) < 32:
return False
try:
strng.decode(encoding)
return True
except:
return False
class Python(Bcfg2.Client.Tools.Tool):
"""Python File support code."""
name = 'Python'
__handles__ = [('Python', 'file'),
('Python', None)]
__req__ = {'Python': ['name']}
# grab paranoid options from /etc/bcfg2.conf
opts = {'ppath': Bcfg2.Options.PARANOID_PATH,
'max_copies': Bcfg2.Options.PARANOID_MAX_COPIES}
setup = Bcfg2.Options.OptionParser(opts)
setup.parse([])
ppath = setup['ppath']
max_copies = setup['max_copies']
def canInstall(self, entry):
"""Check if entry is complete for installation."""
if Bcfg2.Client.Tools.Tool.canInstall(self, entry):
if (entry.tag,
entry.get('type'),
entry.text,
entry.get('empty', 'false')) == ('Python',
'file',
None,
'false'):
return False
return True
else:
return False
def gatherCurrentData(self, entry):
if entry.tag == 'Python' and entry.get('type') == 'file':
try:
ondisk = os.stat(entry.get('name'))
except OSError:
entry.set('current_exists', 'false')
self.logger.debug("%s %s does not exist" %
(entry.tag, entry.get('name')))
return False
try:
entry.set('current_owner', str(ondisk[stat.ST_UID]))
entry.set('current_group', str(ondisk[stat.ST_GID]))
except (OSError, KeyError):
pass
entry.set('perms', str(oct(ondisk[stat.ST_MODE])[-4:]))
def Verifydirectory(self, entry, modlist):
"""Verify Path type='directory' entry."""
if entry.get('perms') == None or \
entry.get('owner') == None or \
entry.get('group') == None:
self.logger.error('Entry %s not completely specified. '
'Try running bcfg2-lint.' % (entry.get('name')))
return False
while len(entry.get('perms', '')) < 4:
entry.set('perms', '0' + entry.get('perms', ''))
try:
ondisk = os.stat(entry.get('name'))
except OSError:
entry.set('current_exists', 'false')
self.logger.debug("%s %s does not exist" %
(entry.tag, entry.get('name')))
return False
try:
owner = str(ondisk[stat.ST_UID])
group = str(ondisk[stat.ST_GID])
except (OSError, KeyError):
self.logger.error('User/Group resolution failed for path %s' % \
entry.get('name'))
owner = 'root'
group = '0'
finfo = os.stat(entry.get('name'))
perms = oct(finfo[stat.ST_MODE])[-4:]
if entry.get('mtime', '-1') != '-1':
mtime = str(finfo[stat.ST_MTIME])
else:
mtime = '-1'
pTrue = ((owner == str(normUid(entry))) and
(group == str(normGid(entry))) and
(perms == entry.get('perms')) and
(mtime == entry.get('mtime', '-1')))
pruneTrue = True
ex_ents = []
if entry.get('prune', 'false') == 'true' \
and (entry.tag == 'Path' and entry.get('type') == 'directory'):
# check for any extra entries when prune='true' attribute is set
try:
entries = ['/'.join([entry.get('name'), ent]) \
for ent in os.listdir(entry.get('name'))]
ex_ents = [e for e in entries if e not in modlist]
if ex_ents:
pruneTrue = False
self.logger.debug("Directory %s contains extra entries:" % \
entry.get('name'))
self.logger.debug(ex_ents)
nqtext = entry.get('qtext', '') + '\n'
nqtext += "Directory %s contains extra entries:" % \
entry.get('name')
nqtext += ":".join(ex_ents)
entry.set('qtest', nqtext)
[entry.append(XML.Element('Prune', path=x)) \
for x in ex_ents]
except OSError:
ex_ents = []
pruneTrue = True
if not pTrue:
if owner != str(normUid(entry)):
entry.set('current_owner', owner)
self.logger.debug("%s %s ownership wrong" % \
(entry.tag, entry.get('name')))
nqtext = entry.get('qtext', '') + '\n'
nqtext += "%s owner wrong. is %s should be %s" % \
(entry.get('name'), owner, entry.get('owner'))
entry.set('qtext', nqtext)
if group != str(normGid(entry)):
entry.set('current_group', group)
self.logger.debug("%s %s group wrong" % \
(entry.tag, entry.get('name')))
nqtext = entry.get('qtext', '') + '\n'
nqtext += "%s group is %s should be %s" % \
(entry.get('name'), group, entry.get('group'))
entry.set('qtext', nqtext)
if perms != entry.get('perms'):
entry.set('current_perms', perms)
self.logger.debug("%s %s permissions are %s should be %s" %
(entry.tag,
entry.get('name'),
perms,
entry.get('perms')))
nqtext = entry.get('qtext', '') + '\n'
nqtext += "%s %s perms are %s should be %s" % \
(entry.tag,
entry.get('name'),
perms,
entry.get('perms'))
entry.set('qtext', nqtext)
if mtime != entry.get('mtime', '-1'):
entry.set('current_mtime', mtime)
self.logger.debug("%s %s mtime is %s should be %s" \
% (entry.tag, entry.get('name'), mtime,
entry.get('mtime')))
nqtext = entry.get('qtext', '') + '\n'
nqtext += "%s mtime is %s should be %s" % \
(entry.get('name'), mtime, entry.get('mtime'))
entry.set('qtext', nqtext)
if entry.get('type') != 'file':
nnqtext = entry.get('qtext')
nnqtext += '\nInstall %s %s: (y/N) ' % (entry.get('type'),
entry.get('name'))
entry.set('qtext', nnqtext)
return pTrue and pruneTrue
def Installdirectory(self, entry):
"""Install Path type='directory' entry."""
if entry.get('perms') == None or \
entry.get('owner') == None or \
entry.get('group') == None:
self.logger.error('Entry %s not completely specified. '
'Try running bcfg2-lint.' % \
(entry.get('name')))
return False
self.logger.info("Installing directory %s" % (entry.get('name')))
try:
fmode = os.lstat(entry.get('name'))
if not stat.S_ISDIR(fmode[stat.ST_MODE]):
self.logger.debug("Found a non-directory entry at %s" % \
(entry.get('name')))
try:
os.unlink(entry.get('name'))
exists = False
except OSError:
self.logger.info("Failed to unlink %s" % \
(entry.get('name')))
return False
else:
self.logger.debug("Found a pre-existing directory at %s" % \
(entry.get('name')))
exists = True
except OSError:
# stat failed
exists = False
if not exists:
parent = "/".join(entry.get('name').split('/')[:-1])
if parent:
try:
os.stat(parent)
except:
self.logger.debug('Creating parent path for directory %s' % (entry.get('name')))
for idx in range(len(parent.split('/')[:-1])):
current = '/'+'/'.join(parent.split('/')[1:2+idx])
try:
sloc = os.stat(current)
except OSError:
try:
os.mkdir(current)
continue
except OSError:
return False
if not stat.S_ISDIR(sloc[stat.ST_MODE]):
try:
os.unlink(current)
os.mkdir(current)
except OSError:
return False
try:
os.mkdir(entry.get('name'))
except OSError:
self.logger.error('Failed to create directory %s' % \
(entry.get('name')))
return False
if entry.get('prune', 'false') == 'true' and entry.get("qtest"):
for pent in entry.findall('Prune'):
pname = pent.get('path')
ulfailed = False
if os.path.isdir(pname):
self.logger.info("Not removing extra directory %s, "
"please check and remove manually" % pname)
continue
try:
self.logger.debug("Unlinking file %s" % pname)
os.unlink(pname)
except OSError:
self.logger.error("Failed to unlink path %s" % pname)
ulfailed = True
if ulfailed:
return False
return self.Installpermissions(entry)
def Verifyfile(self, entry, _):
"""Verify Python type='file' entry."""
# permissions check + content check
permissionStatus = self.Verifydirectory(entry, _)
tbin = False
if entry.text == None and entry.get('empty', 'false') == 'false':
self.logger.error("Cannot verify incomplete Python type='%s' %s" %
(entry.get('type'), entry.get('name')))
return False
if entry.get('encoding', 'ascii') == 'base64':
tempdata = binascii.a2b_base64(entry.text)
tbin = True
elif entry.get('empty', 'false') == 'true':
tempdata = ''
else:
tempdata = entry.text
if type(tempdata) == unicode:
try:
tempdata = tempdata.encode(self.setup['encoding'])
except UnicodeEncodeError:
e = sys.exc_info()[1]
self.logger.error("Error encoding file %s:\n %s" % \
(entry.get('name'), e))
different = False
content = None
if not os.path.exists(entry.get("name")):
# first, see if the target file exists at all; if not,
# they're clearly different
different = True
content = ""
else:
# next, see if the size of the target file is different
# from the size of the desired content
try:
estat = os.stat(entry.get('name'))
except OSError:
err = sys.exc_info()[1]
self.logger.error("Failed to stat %s: %s" %
(err.filename, err))
return False
if len(tempdata) != estat[stat.ST_SIZE]:
different = True
else:
# finally, read in the target file and compare them
# directly. comparison could be done with a checksum,
# which might be faster for big binary files, but
# slower for everything else
try:
content = open(entry.get('name')).read()
except IOError:
err = sys.exc_info()[1]
self.logger.error("Failed to read %s: %s" %
(err.filename, err))
return False
different = content != tempdata
if different:
if self.setup['interactive']:
prompt = [entry.get('qtext', '')]
if not tbin and content is None:
# it's possible that we figured out the files are
# different without reading in the local file. if
# the supplied version of the file is not binary,
# we now have to read in the local file to figure
# out if _it_ is binary, and either include that
# fact or the diff in our prompts for -I
try:
content = open(entry.get('name')).read()
except IOError:
err = sys.exc_info()[1]
self.logger.error("Failed to read %s: %s" %
(err.filename, err))
return False
if tbin or not isString(content, self.setup['encoding']):
# don't compute diffs if the file is binary
prompt.append('Binary file, no printable diff')
else:
diff = self._diff(content, tempdata,
difflib.unified_diff,
filename=entry.get("name"))
if diff:
udiff = '\n'.join(diff)
try:
prompt.append(udiff.decode(self.setup['encoding']))
except UnicodeDecodeError:
prompt.append("Binary file, no printable diff")
else:
prompt.append("Diff took too long to compute, no "
"printable diff")
prompt.append("Install %s %s: (y/N): " % (entry.tag,
entry.get('name')))
entry.set("qtext", "\n".join(prompt))
if entry.get('sensitive', 'false').lower() != 'true':
if content is None:
# it's possible that we figured out the files are
# different without reading in the local file. we
# now have to read in the local file to figure out
# if _it_ is binary, and either include the whole
# file or the diff for reports
try:
content = open(entry.get('name')).read()
except IOError:
err = sys.exc_info()[1]
self.logger.error("Failed to read %s: %s" %
(err.filename, err))
return False
if tbin or not isString(content, self.setup['encoding']):
# don't compute diffs if the file is binary
entry.set('current_bfile', binascii.b2a_base64(content))
else:
diff = self._diff(content, tempdata, difflib.ndiff,
filename=entry.get("name"))
if diff:
entry.set("current_bdiff",
binascii.b2a_base64("\n".join(diff)))
elif not tbin and isString(content, self.setup['encoding']):
entry.set('current_bfile', binascii.b2a_base64(content))
elif permissionStatus == False and self.setup['interactive']:
prompt = [entry.get('qtext', '')]
prompt.append("Install %s %s: (y/N): " % (entry.tag,
entry.get('name')))
entry.set("qtext", "\n".join(prompt))
return permissionStatus and not different
def Installfile(self, entry):
"""Install Python type='file' entry."""
self.logger.info("Installing file %s" % (entry.get('name')))
parent = "/".join(entry.get('name').split('/')[:-1])
if parent:
try:
os.stat(parent)
except:
self.logger.debug('Creating parent path for config file %s' % \
(entry.get('name')))
current = '/'
for next in parent.split('/')[1:]:
current += next + '/'
try:
sloc = os.stat(current)
try:
if not stat.S_ISDIR(sloc[stat.ST_MODE]):
self.logger.debug('%s is not a directory; recreating' \
% (current))
os.unlink(current)
os.mkdir(current)
except OSError:
return False
except OSError:
try:
self.logger.debug("Creating non-existent path %s" % current)
os.mkdir(current)
except OSError:
return False
# If we get here, then the parent directory should exist
if (entry.get("paranoid", False) in ['true', 'True']) and \
self.setup.get("paranoid", False) and not \
(entry.get('current_exists', 'true') == 'false'):
bkupnam = entry.get('name').replace('/', '_')
# current list of backups for this file
try:
bkuplist = [f for f in os.listdir(self.ppath) if
f.startswith(bkupnam)]
except OSError:
e = sys.exc_info()[1]
self.logger.error("Failed to create backup list in %s: %s" %
(self.ppath, e.strerror))
return False
bkuplist.sort()
while len(bkuplist) >= int(self.max_copies):
# remove the oldest backup available
oldest = bkuplist.pop(0)
self.logger.info("Removing %s" % oldest)
try:
os.remove("%s/%s" % (self.ppath, oldest))
except:
self.logger.error("Failed to remove %s/%s" % \
(self.ppath, oldest))
return False
try:
# backup existing file
shutil.copy(entry.get('name'),
"%s/%s_%s" % (self.ppath, bkupnam,
datetime.isoformat(datetime.now())))
self.logger.info("Backup of %s saved to %s" %
(entry.get('name'), self.ppath))
except IOError:
e = sys.exc_info()[1]
self.logger.error("Failed to create backup file for %s" % \
(entry.get('name')))
self.logger.error(e)
return False
try:
newfile = open("%s.new"%(entry.get('name')), 'w')
if entry.get('encoding', 'ascii') == 'base64':
filedata = binascii.a2b_base64(entry.text)
elif entry.get('empty', 'false') == 'true':
filedata = ''
else:
if type(entry.text) == unicode:
filedata = entry.text.encode(self.setup['encoding'])
else:
filedata = entry.text
newfile.write(filedata)
newfile.close()
try:
os.chown(newfile.name, normUid(entry), normGid(entry))
except KeyError:
self.logger.error("Failed to chown %s to %s:%s" %
(newfile.name, entry.get('owner'),
entry.get('group')))
os.chown(newfile.name, 0, 0)
except OSError:
err = sys.exc_info()[1]
self.logger.error("Could not chown %s: %s" % (newfile.name,
err))
os.chmod(newfile.name, calcPerms(stat.S_IFREG, entry.get('perms')))
os.rename(newfile.name, entry.get('name'))
if entry.get('mtime', '-1') != '-1':
try:
os.utime(entry.get('name'), (int(entry.get('mtime')),
int(entry.get('mtime'))))
except:
self.logger.error("File %s mtime fix failed" \
% (entry.get('name')))
return False
return True
except (OSError, IOError):
err = sys.exc_info()[1]
if err.errno == errno.EACCES:
self.logger.info("Failed to open %s for writing" % (entry.get('name')))
else:
print(err)
return False
def Verifypermissions(self, entry, _):
"""Verify Path type='permissions' entry"""
if entry.get('perms') == None or \
entry.get('owner') == None or \
entry.get('group') == None:
self.logger.error('Entry %s not completely specified. '
'Try running bcfg2-lint.' % (entry.get('name')))
return False
if entry.get('recursive') in ['True', 'true']:
# verify ownership information recursively
owner = normUid(entry)
group = normGid(entry)
for root, dirs, files in os.walk(entry.get('name')):
for p in dirs + files:
path = os.path.join(root, p)
pstat = os.stat(path)
if owner != pstat.st_uid:
# owner mismatch for path
entry.set('current_owner', str(pstat.st_uid))
self.logger.debug("%s %s ownership wrong" % \
(entry.tag, path))
nqtext = entry.get('qtext', '') + '\n'
nqtext += ("Owner for path %s is incorrect. "
"Current owner is %s but should be %s\n" % \
(path, pstat.st_uid, entry.get('owner')))
nqtext += ("\nInstall %s %s: (y/N): " %
(entry.tag, entry.get('name')))
entry.set('qtext', nqtext)
return False
if group != pstat.st_gid:
# group mismatch for path
entry.set('current_group', str(pstat.st_gid))
self.logger.debug("%s %s group wrong" % \
(entry.tag, path))
nqtext = entry.get('qtext', '') + '\n'
nqtext += ("Group for path %s is incorrect. "
"Current group is %s but should be %s\n" % \
(path, pstat.st_gid, entry.get('group')))
nqtext += ("\nInstall %s %s: (y/N): " %
(entry.tag, entry.get('name')))
entry.set('qtext', nqtext)
return False
return self.Verifydirectory(entry, _)
def _diff(self, content1, content2, difffunc, filename=None):
rv = []
start = time.time()
longtime = False
for diffline in difffunc(content1.split('\n'),
content2.split('\n')):
now = time.time()
rv.append(diffline)
if now - start > 5 and not longtime:
if filename:
self.logger.info("Diff of %s taking a long time" %
filename)
else:
self.logger.info("Diff taking a long time")
longtime = True
elif now - start > 30:
if filename:
self.logger.error("Diff of %s took too long; giving up" %
filename)
else:
self.logger.error("Diff took too long; giving up")
return False
return rv
def Installpermissions(self, entry):
"""Install POSIX permissions"""
if entry.get('perms') == None or \
entry.get('owner') == None or \
entry.get('group') == None:
self.logger.error('Entry %s not completely specified. '
'Try running bcfg2-lint.' % (entry.get('name')))
return False
plist = [entry.get('name')]
if entry.get('recursive') in ['True', 'true']:
# verify ownership information recursively
owner = normUid(entry)
group = normGid(entry)
for root, dirs, files in os.walk(entry.get('name')):
for p in dirs + files:
path = os.path.join(root, p)
pstat = os.stat(path)
if owner != pstat.st_uid or group != pstat.st_gid:
# owner mismatch for path
plist.append(path)
try:
for p in plist:
os.chown(p, normUid(entry), normGid(entry))
os.chmod(p, calcPerms(stat.S_IFDIR, entry.get('perms')))
return True
except (OSError, KeyError):
self.logger.error('Permission fixup failed for %s' % \
(entry.get('name')))
return False
def InstallNone(self, entry):
return self.Installfile(entry)
def VerifyNone(self, entry, _):
return self.Verifyfile(entry, _)
def InstallPython(self, entry):
"""Dispatch install to the proper method according to type"""
ret = getattr(self, 'Install%s' % entry.get('type'))
return ret(entry)
def VerifyPython(self, entry, _):
"""Dispatch verify to the proper method according to type"""
ret = getattr(self, 'Verify%s' % entry.get('type'))
return ret(entry, _)

View file

@ -1,50 +0,0 @@
# -*- coding: utf8 -*-
import os
LABELS = {
"/home":u"Dossier personnel",
"/var/mail":u"Boite de réception"
}
def getFloat( chose ):
chose = chose.replace(',', '.')
return float(chose)
def getUserQuota( userLogin ):
pipe = os.popen("sudo quota %s" % userLogin)
string_result = pipe.read()
pipe.close()
string_result = string_result.split("\n")
quotas = []
for a_line in string_result[2:-1]:
usage, quota, limite, percentage, fs = a_line.split("\t")
line_dict = {
"label": "Quota personnel",
"usage":getFloat(usage),
"quota":getFloat(quota),
"limite":getFloat(limite),
"%":getFloat(percentage),
"filesystem":fs, # pourquoi pas ?
}
quotas.append(line_dict)
return quotas
def fake_getUserQuota( userLogin ):
return [
{'%': 33.9,
'quota': 390.62,
'label': u'Dossier personnel (fake)',
'limite': 585.94,
'filesystem': '/home',
'usage': 420.32},
{'%': 0.1,
'quota': 100.00,
'label': u'Boite de r\xe9ception (fake)',
'limite': 150.00,
'filesystem': '/var/mail',
'usage': 0.06}
]

View file

@ -1,10 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Contient les valeurs par défaut du plugin python
de Bcfg2"""
DEFAULT_USER = 'root'
DEFAULT_GROUP = 'root'
DEFAULT_ACLS = 0644
INCLUDES = "../etc/python"

View file

@ -1,113 +0,0 @@
#!/usr/bin/env python2.7
# -*- coding: utf-8 -*-
"""SafeEnvironment implementation for use of exec"""
import os
import cStringIO
import PythonDefaults
import PythonFile
class SafeEnvironment(dict):
"""Environnement isolé dans lequel on exécute un script"""
def __init__(self, additionnal=None, parent=None):
# Création de l'environment initial
super(self.__class__, self).__init__({
# Écrit: variable keysep tostring(value)
"defvar": self.defvar,
# La convertion en chaîne de charactère
"tostring": self.tostring,
# Définition des convertions
"conv": {
bool: {
True: "yes",
False: "no",
},
list: lambda l: ", ".join([
str(x)
for x in l
]),
tuple: lambda l: ", ".join([
str(x)
for x in l
]),
},
# Fonction de base pour imprimer quelque chose
"out": self.out,
"_out": self._out,
# Le séparateur pour la forme: variable keysep valeur
"keysep": "=",
# Le charactère de commentaire
"comment_start": "#",
# Du mapping de certaines fonctions
"include": self.include,
# Du mapping de certaines fonctions
"dump": self.dump,
# Infos standard pour le fichier (écrasable localement)
"info": {
'owner': PythonDefaults.DEFAULT_USER,
'group': PythonDefaults.DEFAULT_GROUP,
'mode': PythonDefaults.DEFAULT_ACLS,
}
})
if additionnal is None:
additionnal = {}
super(self.__class__, self).update(additionnal)
# On crée le flux dans lequel le fichier de config sera généré
self.stream = cStringIO.StringIO()
# Le Pythonfile parent est référencé ici
self.parent = parent
# Les trucs inclus
self.included = []
def __setitem__(self, variable, value):
"""Lorsqu'on définit une variable, si elle est listée dans la variable
exports, on l'incorpore dans le fichier produit"""
super(self.__class__, self).__setitem__(variable, value)
def defvar(self, variable, value):
"""Quand on fait un export, on utilise defvar pour incorporer la variable
et sa valeur dans le fichier produit"""
# On écrit mavariable = toto, en appliquant une éventuelle conversion à toto
self.out("%s%s%s" % (variable, self['keysep'], self.tostring(value)))
def out(self, string=""):
"""C'est le print local. Sauf qu'on écrit dans self.stream"""
self._out("%s\n" % (string,))
def _out(self, string=""):
"""C'est le print local sans retour à la ligne."""
self.stream.write(string)
def tostring(self, value):
"""On convertit un objet python dans un format "string" sympa.
En vrai c'est horrible et il faudrait virer ce genre de kludge."""
convertor = self["conv"].get(type(value))
if convertor:
if type(convertor) == dict:
return convertor[value]
else:
return convertor(value)
else:
return str(value)
def dump(self, incfile):
"""On exécute le fichier python dans l'environnement courant
incfile est le nom du fichier, sans le .py"""
filename = os.path.join(self.parent.parent.include, "%s.py" % (incfile,))
python_file = PythonFile.PythonFile(filename, self.parent.parent)
python_file.run(environment=self)
def include(self, incfile):
"""Pareil qu'au dessus, mais on ne le fait que si ça n'a pas
été fait"""
if incfile in self.included:
return
self.included.append(incfile)
self.dump(incfile)

View file

@ -1,33 +0,0 @@
#!/usr/bin/env python2.7
# -*- coding: utf-8 -*-
"""Ce module est prévu pour héberger des factories, stockant toute
instance d'un fichier Python déjà compilé."""
class PythonFileFactory(object):
"""Cette Factory stocke l'ensemble des fichiers Python déjà instanciés.
Elle garantit entre autre leur unicité dans le fonctionnement du plugin"""
#: Stocke la liste des instances avec leur chemin absolu.
files = {}
@classmethod
def get(cls, path):
"""Récupère l'instance si elle existe, ou renvoit None"""
return cls.files.get(path, None)
@classmethod
def record(cls, path, instance):
"""Enregistre l'instance dans la Factory"""
cls.files[path] = instance
@classmethod
def flush_one(cls, path):
"""Vire une instance du dico"""
instance_to_delete = cls.files.pop(path, None)
del instance_to_delete
@classmethod
def flush(cls):
"""Vire toutes les instances du dico"""
for path in cls.files.keys():
cls.flush_one(path)

View file

@ -1,221 +0,0 @@
#!/usr/bin/env python2.7
# -*- coding: utf-8 -*-
"""Fournit une couche d'abstraction Python pour les fichiers du même
nom"""
import os
import sys
import re
import marshal
import cStringIO
from Bcfg2.Server.Plugin import Debuggable
from .PythonFactories import PythonFileFactory
import PythonEnv
import PythonTools
__RE_SPECIAL_LINE = re.compile(r"^([ \t]*)(@|%)(.*)$", re.MULTILINE)
__RE_AFFECTATION = re.compile(r"([a-zA-Z_][a-zA-Z_0-9]*)[ \t]*=")
__RE_SPACE_SEP = re.compile(r"([^ \t]*)[ \t]+=?(.*)")
class PythonFile(Debuggable):
"""Classe représentant un fichier Python"""
#: Permet de savoir si l'instance a déjà été initialisée
initialized = False
def __new__(cls, path, parent=None):
"""Si le fichier a déjà été enregistré dans la Factory, on
le retourne, et on évite de réinstancier la classe.
path est le chemin absolu du fichier"""
path = os.path.normpath(path)
file_instance = PythonFileFactory.get(path)
if file_instance is None:
file_instance = super(PythonFile, cls).__new__(cls)
PythonFileFactory.record(path, file_instance)
return file_instance
def __init__(self, path, parent=None):
"""Initialisation, si non déjà faite"""
if self.initialized:
return
super(self.__class__, self).__init__()
#: A string containing the raw data in this file
self.data = None
#: Le chemin complet du fichier
self.path = os.path.normpath(path)
#: Le nom du fichier
self.name = os.path.basename(self.path)
#: Un logger
self.logger = PythonTools.LOGGER
#: Le plugin parent est pointé pour des raisons pratiques
self.parent = parent
#: C'est bon, c'est initialisé
self.initialized = True
def exists(self):
"""Teste l'existence du fichier"""
return os.path.exists(self.path)
def HandleEvent(self, event=None):
""" HandleEvent is called whenever the FAM registers an event.
:param event: The event object
:type event: Bcfg2.Server.FileMonitor.Event
:returns: None
"""
if event and event.code2str() not in ['exists', 'changed', 'created']:
return
try:
self.load()
except IOError:
err = sys.exc_info()[1]
self.logger.error("Failed to read file %s: %s" % (self.name, err))
except:
err = sys.exc_info()[1]
self.logger.error("Failed to parse file %s: %s" % (self.name, err))
def __repr__(self):
return "%s: %s" % (self.__class__.__name__, self.name)
def load(self, refresh=True):
"""Charge le fichier"""
if self.data is not None and not refresh:
return
try:
directory = os.path.dirname(self.path)
compiled_file = os.path.join(directory, ".%s.COMPILED" % (self.name,))
if os.path.exists(compiled_file) and os.stat(self.path).st_mtime <= os.stat(compiled_file).st_mtime:
self.data = marshal.load(open(compiled_file, 'r'))
else:
self.data = compileSource(open(self.path, 'r').read(), self.path, self.logger)
cfile = open(compiled_file, "w")
marshal.dump(self.data, cfile)
cfile.close()
except Exception as error:
PythonTools.log_traceback(self.path, 'compilation', error, self.logger)
def run(self, additionnal=None, environment=None):
"""Exécute le code"""
if self.data is None:
self.load(True)
if additionnal is None:
additionnal = {}
if environment is None:
environment = PythonEnv.SafeEnvironment(additionnal, self)
# Lors de l'exécution d'un fichier, on inclut
# toujours common (ie on l'exécute dans l'environnement)
environment.include("common")
try:
exec(self.data, environment)
except Exception:
sys.stderr.write('code: %r\n' % (self.data,))
raise
return environment.stream.getvalue(), environment['info']
#+---------------------------------------------+
#| Tools for compilation |
#+---------------------------------------------+
def compileSource(source, filename="", logger=None):
'''Compile un script'''
# On commence par remplacer les lignes de la forme
# @xxx par out("xxx")
newsource = cStringIO.StringIO()
start = 0
# Parsing de goret : on boucle sur les lignes spéciales,
# c'est-à-dire celles commençant par un @ ou un % précédé
# par d'éventuelles espaces/tabs.
for match in __RE_SPECIAL_LINE.finditer(source):
# On prend tout ce qui ne nous intéresse pas et on l'ajoute.
newsource.write(source[start:match.start()])
# On redéfinit start.
start = match.end()
# On écrit le premier groupe (les espaces et cie)
newsource.write(match.group(1))
# Le linetype est soit @ soit %
linetype = match.group(2)
# @ c'est du print.
if linetype == "@":
# On prend ce qui nous intéresse, et on fait quelques remplacements
# pour éviter les plantages.
line = match.group(3).replace("\\", "\\\\").replace('"', '\\"')
# Si la ligne est un commentaire, on la reproduit en remplaçant éventuellement
# le # par le bon caractère.
if line and line[0] == "#":
newsource.write('out(comment_start + "')
line = line[1:]
# Sinon bah....
else:
newsource.write('out("')
# On écrit ladite ligne
newsource.write(line)
# Et un superbe \n.
newsource.write('")')
# %, affectation.
elif linetype == "%":
# On récupère le reste.
line = match.group(3)
# On fait du matching clef/valeur
match = __RE_AFFECTATION.match(line)
if match:
# Le nom est le premier groupe.
# Et après c'est weird...
varname = match.group(1)
newsource.write(line)
newsource.write("; defvar('")
newsource.write(varname)
newsource.write("', tostring(")
newsource.write(varname)
newsource.write("))\n")
else:
# Pareil, sauf que cette fois, ce qu'on fait a un sens.
match = __RE_SPACE_SEP.match(line)
newsource.write("defvar('")
newsource.write(match.group(1))
# Le tostring est facultatif.
newsource.write("', tostring(")
newsource.write(match.group(2))
newsource.write("))\n")
# On continue.
newsource.write(source[start:])
if logger:
try:
logger.info(newsource.getvalue())
except:
print "Le logger de BCFG2 c'est de la merde, il refuse le non ascii."
print "Voici ce que j'ai essayé de logguer."
print newsource.getvalue()
return compile(newsource.getvalue(), filename, "exec")

View file

@ -1,294 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# PythonPlugin.py
# ---------
#
# Copyright © 2015 Pierre-Elliott Bécue <becue@crans.org>
"""Plugin servant à gérer des fichiers python, dont la sortie sera
la configuration d'un client."""
#: N'exporte que la classe Python
__all__ = [
"Python",
]
import os
import re
import binascii
from Bcfg2.Server.Plugin import Plugin, Generator, PluginExecutionError, track_statistics
from Bcfg2.Server.Plugin.base import Debuggable
import PythonTools
import PythonDefaults
import PythonFile
class Python(Plugin, Generator, Debuggable):
"""Générateur offrant des fonctionnalités de templating pour les fichiers python"""
name = 'Python'
#: Les DirectoryBacked ont des fonctions de monitoring
#: intégrées. Quand des changements arrivent sur les dossiers,
#: c'est la merde, il est préférable de relancer Bcfg2, car
#: FileMonitor ne sait pas démonitorer/remonitorer.
#: En revanche, pour les fichiers, il appelle __child__ comme
#: "générateur" pour les trucs à surveiller. Quand un fichier
#: est créé/modifié, sa méthode HandleEvent est appelée.
__child__ = PythonFile.PythonFile
#: Ce module gère plein de choses.
patterns = re.compile(r'.*')
#: Ignore ces chemins spécifiques
ignore = re.compile(r'.*\.(COMPILED|pyc)')
__version__ = '2.0'
__author__ = 'becue@crans.org'
def __init__(self, core, datastore):
"""Pour initialiser le plugin"""
#: Initialise un certain nombre de choses en background
Plugin.__init__(self, core, datastore)
Debuggable.__init__(self)
#: self.entries contains information about the files monitored
#: by this object. The keys of the dict are the relative
#: paths to the files. The values are the objects (of type
#: :attr:`__child__`) that handle their contents.
self.entries = {}
#: self.handles contains information about the directories
#: monitored by this object. The keys of the dict are the
#: values returned by the initial fam.AddMonitor() call (which
#: appear to be integers). The values are the relative paths of
#: the directories.
self.handles = {}
#: FileMonitor
self.fam = self.core.fam
#: Monitor everything in the plugin's directory
if not os.path.exists(self.data):
self.logger.warning("%s does not exist, creating" % (self.data,))
os.makedirs(self.data)
self.add_directory_monitor('')
#: Dossier des includes
self.include = os.path.abspath(os.path.join(self.data, PythonDefaults.INCLUDES))
#: Quand on initialise un DirectoryBacked, on a déjà un monitoring de
#: self.data, donc on a besoin que des includes
self.add_directory_monitor(PythonDefaults.INCLUDES)
@track_statistics()
def HandlesEntry(self, entry, metadata):
"""Vérifie si l'entrée est gérée par le plugin"""
relpath = entry.get('name')[1:]
if relpath in self.entries:
return True
return False
@track_statistics()
def HandleEntry(self, entry, metadata):
"""Construit le fichier demandé"""
# On récupère le code qui va bien.
relpath = entry.get('name')[1:]
python_file = self.entries[relpath]
# Et le nom de fichier.
fname = entry.get('realname', entry.get('name'))
# Si on est en débug, on loggue ce qu'on fait.
PythonTools.debug("Building config file: %s" % (fname,), PythonTools.LOGGER, 'blue')
# On crée un environnement autonome pour exécuter le fichier.
additionnal = {
'metadata': metadata,
}
try:
text, info = python_file.run(additionnal)
except Exception as error:
PythonTools.log_traceback(fname, 'exec', error, PythonTools.LOGGER)
raise PluginExecutionError
# On récupère les infos
if info.get('encoding', '') == 'base64':
text = binascii.b2a_base64(text)
# lxml n'accepte que de l'ascii ou de l'unicode
# donc faut décoder.
try:
entry.text = text.decode("UTF-8")
except:
# solution de fallback
entry.text = text.decode("ISO-8859-15")
# En cas de débug, on stocke les données
PythonTools.debug(entry.text, PythonTools.LOGGER)
# On récupère les permissions depuis le dico "info".
# En théorie, les valeurs par défaut ne devraient pas être utilisées
# Car elles sont déjà affectées dans Pygen
entry.attrib['owner'] = info.get('owner', PythonDefaults.DEFAULT_USER)
entry.attrib['group'] = info.get('group', PythonDefaults.DEFAULT_GROUP)
entry.attrib['mode'] = oct(info.get('mode', PythonDefaults.DEFAULT_ACLS))
if 'encoding' in info:
entry.attrib['encoding'] = info['encoding']
def add_directory_monitor(self, relative):
""" Add a new directory to the FAM for monitoring.
:param relative: Path name to monitor. This must be relative
to the plugin's directory. An empty string
value ("") will cause the plugin directory
itself to be monitored.
:type relative: string
:returns: None
"""
#: On normalise pour éviter des problèmes quand le FileMonitor
#: voit des changements par la suite.
#: Les chemins sont absolus pour la même raison.
dirpathname = os.path.normpath(os.path.join(self.data, relative))
if relative not in self.handles.values():
if not os.path.isdir(dirpathname):
self.logger.error("%s is not a directory" % (dirpathname,))
return
#: reqid est un chemin absolu sans trailing slash
reqid = self.fam.AddMonitor(dirpathname, self)
self.handles[reqid] = relative
def add_entry(self, relative, event):
""" Add a new file to our tracked entries, and to our FAM for
monitoring.
:param relative: Path name to monitor. This must be relative
to the plugin's directory.
:type relative: string:
:param event: FAM event that caused this entry to be added.
:type event: Bcfg2.Server.FileMonitor.Event
:returns: None
"""
#: Les entrées sont en relatif depuis le dossier de config
self.entries[relative] = self.__child__(
os.path.join(self.data, relative),
self
)
self.entries[relative].HandleEvent(event)
def HandleEvent(self, event):
""" Handle FAM events.
This method is invoked by the FAM when it detects a change to
a filesystem object we have requsted to be monitored.
This method manages the lifecycle of events related to the
monitored objects, adding them to our list of entries and
creating objects of type :attr:`__child__` that actually do
the domain-specific processing. When appropriate, it
propogates events those objects by invoking their HandleEvent
method in turn.
:param event: FAM event that caused this entry to be added.
:type event: Bcfg2.Server.FileMonitor.Event
:returns: None
"""
action = event.code2str()
# Exclude events for actions we don't care about
if action == 'endExist':
return
if event.requestID not in self.handles:
self.logger.warn(
"Got %s event with unknown handle (%s) for %s" % (action, event.requestID, event.filename)
)
return
# Clean up path names
event.filename = os.path.normpath(event.filename)
if event.filename.startswith(self.data) or os.path.normpath(event.requestID) == event.filename:
# the first event we get is on the data directory itself
event.filename = event.filename[len(os.path.normpath(event.requestID)) + 1:]
if self.ignore and self.ignore.search(event.filename):
self.logger.debug("Ignoring event %s" % (event.filename,))
return
# Calculate the absolute and relative paths this event refers to
abspath = os.path.join(self.data, self.handles[event.requestID],
event.filename)
relpath = os.path.join(self.handles[event.requestID],
event.filename).lstrip('/')
if action == 'deleted':
for key in list(self.entries.keys()):
if key.startswith(relpath):
del self.entries[key]
# We remove values from self.entries, but not
# self.handles, because the FileMonitor doesn't stop
# watching a directory just because it gets deleted. If it
# is recreated, we will start getting notifications for it
# again without having to add a new monitor.
elif os.path.isdir(abspath):
# Deal with events for directories
if action in ['exists', 'created']:
self.add_directory_monitor(relpath)
elif action == 'changed':
if relpath in self.entries:
# Ownerships, permissions or timestamps changed on
# the directory. None of these should affect the
# contents of the files, though it could change
# our ability to access them.
#
# It seems like the right thing to do is to cancel
# monitoring the directory and then begin
# monitoring it again. But the current FileMonitor
# class doesn't support canceling, so at least let
# the user know that a restart might be a good
# idea.
self.logger.warn(
"Directory properties for %s changed, please consider restarting the server" % (abspath)
)
else:
# Got a "changed" event for a directory that we
# didn't know about. Go ahead and treat it like a
# "created" event, but log a warning, because this
# is unexpected.
self.logger.warn(
"Got %s event for unexpected dir %s" % (action, abspath)
)
self.add_directory_monitor(relpath)
else:
self.logger.warn(
"Got unknown dir event %s %s %s" % (event.requestID, event.code2str(), abspath)
)
elif self.patterns.search(event.filename):
if action in ['exists', 'created']:
self.add_entry(relpath, event)
elif action == 'changed':
if relpath in self.entries:
self.entries[relpath].HandleEvent(event)
else:
# Got a "changed" event for a file that we didn't
# know about. Go ahead and treat it like a
# "created" event, but log a warning, because this
# is unexpected.
self.logger.warn(
"Got %s event for unexpected file %s" % (action, abspath)
)
self.add_entry(relpath, event)
else:
self.logger.warn(
"Got unknown file event %s %s %s" % (event.requestID, event.code2str(), abspath)
)
else:
self.logger.warn(
"Could not process filename %s; ignoring" % (event.filename)
)

View file

@ -1,73 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Fournit quelques outils pour le plugin Python"""
import os
import logging
import cStringIO
import traceback
LOGGER = logging.getLogger('Bcfg2.Plugins.Python')
COLOR_CODE = {
'grey': 30,
'red': 31,
'green': 32,
'yellow': 33,
'blue': 34,
'purple': 35,
'cyan': 36,
}
BCFG2_DEBUG = os.getenv("BCFG2_DEBUG")
BCFG2_DEBUG_COLOR = os.getenv("BCFG2_DEBUG_COLOR")
def debug(message, logger, color=None):
"""Stocke dans un logger les messages de debug"""
if not BCFG2_DEBUG:
return
if BCFG2_DEBUG_COLOR and color:
logger.info("\033[1;%dm%s\033[0m" % (COLOR_CODE[color], message))
else:
logger.info(message)
def log_traceback(fname, section, exn, logger):
"""En cas de traceback, on le loggue sans faire planter
le serveur bcfg2"""
logger.error('Python %s error: %s: %s: %s' % (section, fname, str(exn.__class__).split('.', 2)[1], str(exn)))
stream = cStringIO.StringIO()
traceback.print_exc(file=stream)
for line in stream.getvalue().splitlines():
logger.error('Python %s error: -> %s' % (section, line))
class PythonIncludePaths(object):
"""C'est un objet qui stocke les dossier d'inclusion python"""
includes = []
@classmethod
def get(cls, index, default):
"""Retourne includes[index] ou default"""
if len(cls.includes) > index:
return cls.includes[index]
return default
@classmethod
def append(cls, value):
"""Ajoute une valeur à la liste"""
cls.includes.append(value)
@classmethod
def remove(cls, value):
"""Retire une valeur à la liste"""
if value in cls.includes:
cls.includes.remove(value)
@classmethod
def pop(cls, index):
"""Vire un index si existant"""
if len(cls.includes) > index:
return cls.includes.pop(index)

View file

@ -1,6 +0,0 @@
#!/usr/bin/env python2.7
# -*- coding: utf-8 -*-
"""Python plugin initializator for
Bcfg2"""
from .PythonPlugin import Python

View file

@ -29,21 +29,21 @@ device_map = {'block': stat.S_IFBLK,
'fifo': stat.S_IFIFO}
def calcMode(initial, mode):
def calcPerms(initial, perms):
"""This compares ondisk permissions with specified ones."""
pdisp = [{1:stat.S_ISVTX, 2:stat.S_ISGID, 4:stat.S_ISUID},
{1:stat.S_IXUSR, 2:stat.S_IWUSR, 4:stat.S_IRUSR},
{1:stat.S_IXGRP, 2:stat.S_IWGRP, 4:stat.S_IRGRP},
{1:stat.S_IXOTH, 2:stat.S_IWOTH, 4:stat.S_IROTH}]
tempmode = initial
if len(mode) == 3:
mode = '0%s' % (mode)
pdigits = [int(mode[digit]) for digit in range(4)]
tempperms = initial
if len(perms) == 3:
perms = '0%s' % (perms)
pdigits = [int(perms[digit]) for digit in range(4)]
for index in range(4):
for (num, perm) in list(pdisp[index].items()):
if pdigits[index] & num:
tempmode |= perm
return tempmode
tempperms |= perm
return tempperms
def normGid(entry):
@ -137,18 +137,18 @@ class Python(Bcfg2.Client.Tools.Tool):
entry.set('current_group', str(ondisk[stat.ST_GID]))
except (OSError, KeyError):
pass
entry.set('mode', str(oct(ondisk[stat.ST_MODE])[-4:]))
entry.set('perms', str(oct(ondisk[stat.ST_MODE])[-4:]))
def Verifydirectory(self, entry, modlist):
"""Verify Path type='directory' entry."""
if entry.get('mode') == None or \
if entry.get('perms') == None or \
entry.get('owner') == None or \
entry.get('group') == None:
self.logger.error('Entry %s not completely specified. '
'Try running bcfg2-lint.' % (entry.get('name')))
return False
while len(entry.get('mode', '')) < 4:
entry.set('mode', '0' + entry.get('mode', ''))
while len(entry.get('perms', '')) < 4:
entry.set('perms', '0' + entry.get('perms', ''))
try:
ondisk = os.stat(entry.get('name'))
except OSError:
@ -165,14 +165,14 @@ class Python(Bcfg2.Client.Tools.Tool):
owner = 'root'
group = '0'
finfo = os.stat(entry.get('name'))
mode = oct(finfo[stat.ST_MODE])[-4:]
perms = oct(finfo[stat.ST_MODE])[-4:]
if entry.get('mtime', '-1') != '-1':
mtime = str(finfo[stat.ST_MTIME])
else:
mtime = '-1'
pTrue = ((owner == str(normUid(entry))) and
(group == str(normGid(entry))) and
(mode == entry.get('mode')) and
(perms == entry.get('perms')) and
(mtime == entry.get('mtime', '-1')))
pruneTrue = True
@ -217,19 +217,19 @@ class Python(Bcfg2.Client.Tools.Tool):
nqtext += "%s group is %s should be %s" % \
(entry.get('name'), group, entry.get('group'))
entry.set('qtext', nqtext)
if mode != entry.get('mode'):
entry.set('current_mode', mode)
if perms != entry.get('perms'):
entry.set('current_perms', perms)
self.logger.debug("%s %s permissions are %s should be %s" %
(entry.tag,
entry.get('name'),
mode,
entry.get('mode')))
perms,
entry.get('perms')))
nqtext = entry.get('qtext', '') + '\n'
nqtext += "%s %s mode are %s should be %s" % \
nqtext += "%s %s perms are %s should be %s" % \
(entry.tag,
entry.get('name'),
mode,
entry.get('mode'))
perms,
entry.get('perms'))
entry.set('qtext', nqtext)
if mtime != entry.get('mtime', '-1'):
entry.set('current_mtime', mtime)
@ -249,7 +249,7 @@ class Python(Bcfg2.Client.Tools.Tool):
def Installdirectory(self, entry):
"""Install Path type='directory' entry."""
if entry.get('mode') == None or \
if entry.get('perms') == None or \
entry.get('owner') == None or \
entry.get('group') == None:
self.logger.error('Entry %s not completely specified. '
@ -547,7 +547,7 @@ class Python(Bcfg2.Client.Tools.Tool):
err = sys.exc_info()[1]
self.logger.error("Could not chown %s: %s" % (newfile.name,
err))
os.chmod(newfile.name, calcMode(stat.S_IFREG, entry.get('mode')))
os.chmod(newfile.name, calcPerms(stat.S_IFREG, entry.get('perms')))
os.rename(newfile.name, entry.get('name'))
if entry.get('mtime', '-1') != '-1':
try:
@ -568,7 +568,7 @@ class Python(Bcfg2.Client.Tools.Tool):
def Verifypermissions(self, entry, _):
"""Verify Path type='permissions' entry"""
if entry.get('mode') == None or \
if entry.get('perms') == None or \
entry.get('owner') == None or \
entry.get('group') == None:
self.logger.error('Entry %s not completely specified. '
@ -637,7 +637,7 @@ class Python(Bcfg2.Client.Tools.Tool):
def Installpermissions(self, entry):
"""Install POSIX permissions"""
if entry.get('mode') == None or \
if entry.get('perms') == None or \
entry.get('owner') == None or \
entry.get('group') == None:
self.logger.error('Entry %s not completely specified. '
@ -659,7 +659,7 @@ class Python(Bcfg2.Client.Tools.Tool):
try:
for p in plist:
os.chown(p, normUid(entry), normGid(entry))
os.chmod(p, calcMode(stat.S_IFDIR, entry.get('mode')))
os.chmod(p, calcPerms(stat.S_IFDIR, entry.get('perms')))
return True
except (OSError, KeyError):
self.logger.error('Permission fixup failed for %s' % \

View file

@ -1,4 +1,4 @@
#!/bin/bash /usr/scripts/python.sh
#!/usr/bin/env python
# -*- coding: utf-8 -*-
""" Envoie un mail avec la liste des serveurs qui ne sont pas synchro avec bcfg2.
@ -43,6 +43,7 @@ if __name__ == "__main__":
debug = "--debug" in sys.argv
if "--mail" in sys.argv:
if hosts != "":
sys.path.append("/usr/scripts/")
import utils.sendmail
utils.sendmail.sendmail("root@crans.org", "roots@crans.org", u"Serveurs non synchronisés avec bcfg2", hosts, more_headers={"X-Mailer" : "bcfg2-reports"}, debug=debug)
elif debug:

View file

@ -1,16 +0,0 @@
#!/bin/bash
if [[ $1 = "" ]] || [[ $1 = $USER ]] ; then
/usr/bin/quota
else
/usr/bin/quota $*
fi | sed 's/home-adh/home/' | awk -F'(:| *)' '
BEGIN { fs = "" }
/Disk/ { print; print "utilisé\tquota\tlimite\t%\t(en Mo)" }
{
if (NF == 2) { fs = $2 }
else if (fs != "") {
printf "%3.2f\t%3.2f\t%3.2f\t%3.1f\t%s\n", $2/1024, $3/1024, $4/1024, $2*100/$3, fs
fs = ""
}
}'

View file

@ -6,117 +6,38 @@
# License : GPLv3
# Date : 27/04/2014
import os
import datetime
import pytz
import logging
TZ = pytz.timezone('Europe/Paris')
LDIRPATH = os.getenv('DBG_CLOGGER_PATH', '/var/log/clogger')
class CLogger(logging.Logger):
"""
Crans logger.
Crans logger
"""
def __init__(self, loggerName, service=None, level="info", debug=False):
def __init__(self, loggerName, service, level, debug=False):
"""
Initializes logger. The debug variable is useful to have a print to stdout (when debugging)
"""
super(CLogger, self).__init__(loggerName)
self.c_formatter = None
self.c_file_handler = None
self.c_sh = None
self.c_level = level
# When no service is specified, we don't put the reference in the format.
if service is None:
self.c_format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
else:
self.c_format = "%%(asctime)s - %%(name)s - %(service)s - %%(levelname)s - %%(message)s" % {'service': service}
self.create_formatter()
self.apply_file_handler(loggerName)
if debug:
self.apply_stream_handler()
def get_file_handler_path(self):
"""Returns the file handler path"""
if self.__file_handler_path is None:
return ''
return self.__file_handler_path
def create_formatter(self):
"""Creates a formatter based on CFormatter class.
It uses self.format as a source."""
if self.c_formatter is not None:
return
# Creates formatter
self.c_formatter = CFormatter(self.c_format, "%Y-%m-%dT%H:%M:%S.%f%z")
def apply_stream_handler(self):
"""Creates a streamhandler that prints to stdout.
Its level is debug"""
self.c_sh = logging.StreamHandler()
self.c_shlevel = logging.DEBUG
self.c_sh.setLevel(self.c_shlevel)
self.c_sh.setFormatter(self.c_formatter)
self.addHandler(self.c_sh)
def apply_file_handler(self, loggerName):
"""Creates a file handler which level is given by self.c_level"""
if self.c_file_handler is not None:
return
# Computes the file handler name using service name.
self.__file_handler_path = os.path.join(LDIRPATH, "%s.log" % (loggerName,))
# Creates FileHandler
self.c_file_handler = logging.FileHandler(self.__file_handler_path)
self.fh = logging.FileHandler("/var/log/clogger/%s.log" % (loggerName,))
# Catches appropriate level in logging.
self.c_file_handler_level = getattr(logging, self.c_level.upper(), logging.INFO)
self.c_file_handler.setLevel(self.c_file_handler_level)
self.fhlevel = getattr(logging, level.upper(), logging.INFO)
self.fh.setLevel(self.fhlevel)
# Creates formatter
self.formatter = logging.Formatter('%%(asctime)s - %%(name)s - %(service)s - %%(levelname)s - %%(message)s' % {'service': service})
# Adds formatter to FileHandler
self.c_file_handler.setFormatter(self.c_formatter)
self.fh.setFormatter(self.formatter)
if debug:
self.sh = logging.StreamHandler()
self.shlevel = logging.DEBUG
self.sh.setLevel(self.shlevel)
self.sh.setFormatter(self.formatter)
self.addHandler(self.sh)
# Adds FileHandler to Handlers
self.addHandler(self.c_file_handler)
class CFormatter(logging.Formatter):
"""
This Formatter subclasses the classic formatter to provide a
timezone-aware logging.
"""
converter = datetime.datetime.fromtimestamp
def formatTime(self, record, datefmt=None):
"""
Return the creation time of the specified LogRecord as formatted text.
This method should be called from format() by a formatter which
wants to make use of a formatted time. This method can be overridden
in formatters to provide for any specific requirement, but the
basic behaviour is as follows: if datefmt (a string) is specified,
it is used with time.strftime() to format the creation time of the
record. Otherwise, the ISO8601 format is used. The resulting
string is returned. This function uses a user-configurable function
to convert the creation time to a tuple. By default, time.localtime()
is used; to change this for a particular formatter instance, set the
'converter' attribute to a function with the same signature as
time.localtime() or time.gmtime(). To change it for all formatters,
for example if you want all logging times to be shown in GMT,
set the 'converter' attribute in the Formatter class.
"""
ct = self.converter(record.created, TZ)
ct = ct.replace(microsecond=int(record.msecs * 1000))
if datefmt:
s = ct.strftime(datefmt)
else:
s = ct.strftime("%Y-%m-%d %H:%M:%S.%f")
return s
self.addHandler(self.fh)

View file

@ -1,20 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import functools
def static_var(*couples):
"""Decorator setting static variable
to a function.
"""
# Using setattr magic, we set static
# variable on function. This avoid
# computing stuff again.
def decorate(fun):
functools.wraps(fun)
for (name, val) in couples:
setattr(fun, name, val)
return fun
return decorate

View file

@ -1,22 +1,15 @@
# ⁻*- coding: utf-8 -*-
"""
Backend python pour freeradius.
Ce fichier contient la définition de plusieurs fonctions d'interface à
freeradius qui peuvent être appelées (suivant les configurations) à certains
moment de l'authentification, en WiFi, filaire, ou par les NAS eux-mêmes.
Inspirés d'autres exemples trouvés ici :
https://github.com/FreeRADIUS/freeradius-server/blob/master/src/modules/rlm_python/
"""
#
# Ce fichier contient la définition de plusieurs fonctions d'interface à freeradius
# qui peuvent être appelées (suivant les configurations) à certains moment de
# l'éxécution.
#
import logging
import netaddr
import radiusd # Module magique freeradius (radiusd.py is dummy)
import ldap
import os
import binascii
import hashlib
import lc_ldap.shortcuts
from lc_ldap.crans_utils import escape as escape_ldap
@ -25,63 +18,40 @@ import lc_ldap.objets
import gestion.config.config as config
from gestion.gen_confs.trigger import trigger_generate_cochon as trigger_generate
import annuaires_pg
from gestion import secrets_new as secrets
#: Serveur radius de test (pas la prod)
TEST_SERVER = bool(os.getenv('DBG_FREERADIUS', False))
#: Le taggage dynamique de vlan (dans la réponse) est désactivé sur WiFi
WIFI_DYN_VLAN = TEST_SERVER
#: Suffixe à retirer du username si présent (en wifi)
USERNAME_SUFFIX_WIFI = '.wifi.crans.org'
#: Suffixe à retirer du username si présent (filaire)
USERNAME_SUFFIX_FIL = '.crans.org'
## -*- Logging -*-
class RadiusdHandler(logging.Handler):
"""Handler de logs pour freeradius"""
def emit(self, record):
"""Process un message de log, en convertissant les niveaux"""
if record.levelno >= logging.WARN:
rad_sig = radiusd.L_ERR
elif record.levelno >= logging.INFO:
rad_sig = radiusd.L_INFO
else:
rad_sig = radiusd.L_DBG
radiusd.radlog(rad_sig, record.msg)
# Initialisation d'un logger (pour logguer unifié)
# Initialisation d'un logger pour faire des stats etc
# pour l'instant, on centralise tout sur thot en mode debug
logger = logging.getLogger('auth.py')
logger.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(name)s: [%(levelname)s] %(message)s')
handler = RadiusdHandler()
handler.setFormatter(formatter)
handler = logging.handlers.SysLogHandler(address = '/dev/log')
try:
handler.addFormatter(formatter)
except AttributeError:
handler.formatter = formatter
logger.addHandler(handler)
## -*- Types de blacklists -*-
#: reject tout de suite
BL_REJECT = [u'bloq']
bl_reject = [u'bloq']
#: place sur le vlan isolement
BL_ISOLEMENT = [u'virus', u'autodisc_virus', u'autodisc_p2p', u'ipv6_ra']
bl_isolement = [u'virus', u'autodisc_virus', u'autodisc_p2p', u'ipv6_ra']
# TODO carte_etudiant: dépend si sursis ou non (regarder lc_ldap)
# TODO LOGSSSSS
#: place sur accueil
BL_ACCUEIL = [u'paiement']
bl_accueil = []
# À classer:
# [u'carte_etudiant', u'chambre_invalide', ]
# TODO: mettre ça dans config.py en explicitant un peu comment ça marche
# et en trouvant moyen de refresh en fonction de la période de l'année
# (bl soft/hard parefeu ou pas)
#: chambre qui n'en sont pas vraiment. Il s'agit de prises en libre accès,
# pour lequelles il est donc idiot d'activer la protection antisquattage:
# personne n'y habite ! ( G091 -> G097: salle d'étude du rdc du G)
PUBLIC_CHBRE = ['G091', 'G092', 'G093', 'G094', 'G095', 'G096', 'G097']
# Ces blacklists ont des effets soft (portail captif port 80)
#bl_accueil = [u'carte_etudiant', u'chambre_invalide', u'paiement']
## -*- Decorateurs -*-
# À appliquer sur les fonctions qui ont besoin d'une conn ldap
@ -90,7 +60,7 @@ use_ldap_admin = lc_ldap.shortcuts.with_ldap_conn(retries=2, delay=5,
use_ldap = lc_ldap.shortcuts.with_ldap_conn(retries=2, delay=5,
constructor=lc_ldap.shortcuts.lc_ldap_anonymous)
def radius_event(fun):
def radius_event(f):
"""Décorateur pour les fonctions d'interfaces avec radius.
Une telle fonction prend un uniquement argument, qui est une liste de tuples
(clé, valeur) et renvoie un triplet dont les composantes sont :
@ -99,23 +69,22 @@ def radius_event(fun):
et autres trucs du genre)
* un tuple de couples (clé, valeur) pour les valeurs internes à mettre à
jour (mot de passe par exemple)
Voir des exemples plus complets ici:
https://github.com/FreeRADIUS/freeradius-server/blob/master/src/modules/rlm_python/
On se contente avec ce décorateur (pour l'instant) de convertir la liste de
tuples en entrée en un dictionnaire."""
def new_f(auth_data):
if type(auth_data) == dict:
data = auth_data
else:
data = dict()
for (key, value) in auth_data or []:
# Beware: les valeurs scalaires sont entre guillemets
# Ex: Calling-Station-Id: "une_adresse_mac"
data[key] = value.replace('"', '')
data = dict()
for (key, value) in auth_data or []:
# Beware: les valeurs scalaires sont entre guillemets
# Ex: Calling-Station-Id: "une_adresse_mac"
data[key] = value.replace('"', '')
try:
return fun(data)
except Exception as err:
logger.error('Failed %r on data %r' % (err, auth_data))
return f(data)
except Exception as e:
logger.error(repr(e) + ' on data ' + repr(auth_data))
raise
return new_f
@ -135,21 +104,18 @@ def get_machines(data, conn, is_wifi=True, proprio=None):
try:
mac = lc_ldap.crans_utils.format_mac(mac.decode('ascii', 'ignore'))
except:
logger.error('Cannot format MAC !')
radiusd.radlog(radiusd.L_ERR, 'Cannot format MAC !')
mac = None
username = data.get('User-Name', None)
if username:
# Pour les requètes venant de federezwifi
username = username.split('@', 1)[0]
username = escape_ldap(username.decode('ascii', 'ignore'))
if username.endswith(suffix):
username = username[:-len(suffix)]
if mac is None:
logger.error('Cannot read mac from AP')
radiusd.radlog(radiusd.L_ERR, 'Cannot read client MAC from AP !')
if username is None:
logger.error('Cannot read client User-Name !')
radiusd.radlog(radiusd.L_ERR, 'Cannot read client User-Name !')
# Liste de recherches ldap à essayer, dans l'ordre
# ** Case 1: Search by mac
@ -172,9 +138,6 @@ def get_machines(data, conn, is_wifi=True, proprio=None):
res = conn.search(u'(&%s(macAddress=<automatique>)(host=%s%s))' %
(base, username, suffix), **opt)
if TEST_SERVER:
res += conn.search(u'(&%s(host=%s%s))' %
(base, username, suffix), **opt)
return res
def get_prise_chbre(data):
@ -201,7 +164,7 @@ def get_prise_chbre(data):
try:
bat_name = nas[3].upper()
bat_num = int(nas.split('-', 1)[1])
except (IndexError, ValueError):
except IndexError, ValueError:
pass
port = data.get('NAS-Port', None)
if port:
@ -219,7 +182,7 @@ def get_prise_chbre(data):
def realm_of_machine(machine):
"""Renvoie le `realm` d'une machine. Don't ask"""
if isinstance(machine, lc_ldap.objets.machineFixe):
return 'adherents'
return 'fil'
elif isinstance(machine, lc_ldap.objets.machineWifi):
return 'wifi-adh'
else:
@ -227,29 +190,29 @@ def realm_of_machine(machine):
def get_fresh_rid(machine):
"""Génère un rid tout frais pour la machine. Fonction kludge"""
lock_id = machine.conn.lockholder.newid()
lockId = machine.conn.lockholder.newid()
realm = realm_of_machine(machine)
try:
return machine.conn._find_id('rid', realm, lock_id)
return machine.conn._find_id('rid', realm, lockId)
finally:
machine.conn.lockholder.purge(lock_id)
machine.conn.lockholder.purge(lockId)
@use_ldap_admin
def register_machine(data, machine, conn):
"""Enregistre la mac actuelle et/ou assigne le rid sur une machine donnée."""
def register_mac(data, machine, conn):
"""Enregistre la mac actuelle sur une machine donnée."""
# TODO lc_ldap devrait posséder une fonction pour passer en rw depuis un ro
if 'w' not in machine.mode:
machine = conn.search(dn=machine.dn, scope=ldap.SCOPE_BASE, mode='rw')[0]
mac = data.get('Calling-Station-Id', None)
if mac is None:
logger.warn('Cannot find MAC for registration (aborting)')
radiusd.radlog(radiusd.L_ERR, 'Cannot find MAC')
return
mac = mac.decode('ascii', 'ignore').replace('"','')
try:
mac = lc_ldap.crans_utils.format_mac(mac).lower()
except Exception:
logger.warn('Cannot format MAC for registration (aborting)')
except:
radiusd.radlog(radiusd.L_ERR, 'Cannot format MAC !')
return
with machine:
@ -270,25 +233,13 @@ def register_machine(data, machine, conn):
@radius_event
@use_ldap_admin
@use_ldap
def instantiate(*_):
def instantiate(p, *conns):
"""Utile pour initialiser les connexions ldap une première fois (otherwise,
do nothing)"""
logger.info('Instantiation')
if TEST_SERVER:
logger.info('DBG_FREERADIUS is enabled')
@radius_event
def authorize(data):
"""Fonction qui aiguille entre nas, wifi et filaire pour authorize
On se contecte de faire une verification basique de ce que contien la requète
pour déterminer la fonction à utiliser"""
if data.get('NAS-Port-Type', '')==u'Ethernet':
return authorize_fil(data)
elif u"Wireless" in data.get('NAS-Port-Type', ''):
return authorize_wifi(data)
else:
return authorize_nas(data)
@radius_event
def authorize_wifi(data):
"""Section authorize pour le wifi
@ -300,29 +251,26 @@ def authorize_wifi(data):
items = get_machines(data)
if not items:
logger.error('No machine found in lc_ldap')
radiusd.radlog(radiusd.L_ERR, 'lc_ldap: Nobody found')
return radiusd.RLM_MODULE_NOTFOUND
if len(items) > 1:
logger.warn('lc_ldap: Too many results (taking first)')
radiusd.radlog(radiusd.L_ERR, 'lc_ldap: Too many results (took first)')
machine = items[0]
proprio = machine.proprio()
if isinstance(proprio, lc_ldap.objets.AssociationCrans):
logger.error('Crans machine trying to authenticate !')
radiusd.radlog(radiusd.L_ERR, 'Crans machine trying to authenticate !')
return radiusd.RLM_MODULE_INVALID
for bl in machine.blacklist_actif():
if bl.value['type'] in BL_REJECT:
if bl.value['type'] in bl_reject:
return radiusd.RLM_MODULE_REJECT
# Kludge : vlan isolement pas possible, donc reject quand-même
if not WIFI_DYN_VLAN and bl.value['type'] in BL_ISOLEMENT:
return radiusd.RLM_MODULE_REJECT
if not machine.get('ipsec', False):
logger.error('WiFi auth but machine has no password')
radiusd.radlog(radiusd.L_ERR, 'WiFi authentication but machine has no' +
'password')
return radiusd.RLM_MODULE_REJECT
password = machine['ipsec'][0].value.encode('ascii', 'ignore')
@ -337,124 +285,17 @@ def authorize_wifi(data):
@radius_event
def authorize_fil(data):
"""For now, do nothing.
TODO: check bl_reject.
TODO: check chap auth
"""
Check le challenge chap, et accepte.
TODO: check BL_REJECT.
"""
chap_ok = False
# Teste l'authentification chap fournie
# password et challenge doivent être données
# en hexa (avec ou sans le 0x devant)
# le User-Name est en réalité la mac ( xx:xx:xx:xx:xx )
password = data.get('CHAP-Password', '')
challenge = data.get('CHAP-Challenge', '')
mac = data.get('User-Name', '')
logger.debug('(fil) authorize(%r)' % ((password, challenge, mac),))
try:
challenge = binascii.a2b_hex(challenge.replace('0x',''))
password = binascii.a2b_hex(password.replace('0x',''))
if hashlib.md5(password[0] + mac + challenge).digest() == password[1:]:
logger.info("(fil) Chap ok")
chap_ok = True
else:
logger.info("(fil) Chap wrong")
except Exception as err:
logger.info("(fil) Chap challenge check failed with %r" % err)
if not chap_ok:
if TEST_SERVER:
logger.debug('(fil) Continue auth (debug)')
else:
return radiusd.RLM_MODULE_REJECT
return (radiusd.RLM_MODULE_UPDATED,
(),
(
("Auth-Type", "Accept"),
("Auth-Type", "crans_fil"),
),
)
def radius_password(secret_name, machine=None):
"""Cherche le mdp radius pour la machine donnée, et fallback sur le
secret canonique nommé"""
if machine and machine.has_key('TODO'):
pass
return secrets.get(secret_name)
@radius_event
@use_ldap
def authorize_nas(data, ldap):
"""Remplis le mdp d'une borne, ou d'un switch"""
logger.info('nas_auth with %r' % data)
ip = data.get('NAS-Identifier', '')
is_v6 = ':' in ip
ip_stm = ("FreeRADIUS-Client-IP%s-Address" % ('v6'*is_v6, ), ip)
# Find machine
# On rajoute les Machines du club federez au base_filter (federez-wifi):
fed = ldap.search(u'(nom=Federez)')[0]
mach_fed = fed.machines()
base_filter = u'(|(objectClass=machineCrans)(objectClass=borneWifi)'
for mach in mach_fed:
base_filter = base_filter + "(mid=%s)" % mach['mid'][0]
base_filter = base_filter + u')'
if is_v6:
addr = netaddr.IPAddress(ip).value
# EUI64, hein ?
assert ((addr >> 24) & 0xffff) == 0xfffe
# Extrait la mac de l'EUI64 (« trust me, it works »)
mac = (addr >> 16) & (0xffffff << 24) ^ (addr & 0xffffff) ^ (1 << 41)
mac = lc_ldap.crans_utils.format_mac("%012x" % mac)
m_filter = u'(macAddress=%s)' % mac
else:
m_filter = u'(ipHostNumber=%s)' % escape_ldap(ip)
machines = ldap.search(u'(&%s%s)' % (base_filter, m_filter))
if not machines:
if TEST_SERVER or ip == '127.0.0.1':
password = radius_password('radius_eap_key')
shortname = "wifi"
vserver = 'inner-tunnel'
else:
logger.info('not found %r' % m_filter)
return radiusd.RLM_MODULE_NOTFOUND
elif unicode(machines[0]['host'][0]).startswith(u'bat'):
password = radius_password('radius_key', machines[0])
shortname = 'switchs'
vserver = 'filaire'
else:
password = radius_password('radius_eap_key', machines[0])
shortname = "wifi"
vserver = 'wifi'
return (radiusd.RLM_MODULE_OK,
(),
(
ip_stm,
("FreeRADIUS-Client-Require-MA", "no"),
("FreeRADIUS-Client-Secret", password),
("FreeRADIUS-Client-Shortname", shortname),
("FreeRADIUS-Client-NAS-Type", "other"),
# On teste avec une équipe qui marche
("FreeRADIUS-Client-Virtual-Server", vserver),
),
)
@radius_event
def post_auth(data):
# On cherche quel est le type de machine, et quel sites lui appliquer
if data.get('NAS-Port-Type', '')==u'Ethernet':
return post_auth_fil(data)
elif u"Wireless" in data.get('NAS-Port-Type', ''):
return post_auth_wifi(data)
@radius_event
def post_auth_wifi(data):
"""Appelé une fois que l'authentification est ok.
@ -467,13 +308,14 @@ def post_auth_wifi(data):
log_message = '(wifi) %s -> %s [%s%s]' % \
(port, mac, vlan_name, (reason and u': ' + reason).encode('utf-8'))
logger.info(log_message)
radiusd.radlog(radiusd.L_AUTH, log_message)
# Si NAS ayant des mapping particuliers, à signaler ici
vlan_id = config.vlans[vlan_name]
# WiFi : Pour l'instant, on ne met pas d'infos de vlans dans la réponse
# les bornes wifi ont du mal avec cela
if WIFI_DYN_VLAN:
if TEST_SERVER:
return (radiusd.RLM_MODULE_UPDATED,
(
("Tunnel-Type", "VLAN"),
@ -496,6 +338,7 @@ def post_auth_fil(data):
log_message = '(fil) %s -> %s [%s%s]' % \
(port, mac, vlan_name, (reason and u': ' + reason).encode('utf-8'))
logger.info(log_message)
radiusd.radlog(radiusd.L_AUTH, log_message)
# Si NAS ayant des mapping particuliers, à signaler ici
vlan_id = config.vlans[vlan_name]
@ -546,8 +389,8 @@ def decide_vlan(data, is_wifi, conn):
proprio = machine.proprio()
# Avant de continuer, on assigne la mac à la machine candidat
if '<automatique>' in machine['macAddress'] or not machine['rid']:
register_machine(data, machine)
if '<automatique>' in machine['macAddress']:
register_mac(data, machine)
if not machine['ipHostNumber']:
decision = 'v6only', u'No IPv4'
@ -560,9 +403,9 @@ def decide_vlan(data, is_wifi, conn):
# Application des blacklists
for bl in machine.blacklist_actif():
if bl.value['type'] in BL_ISOLEMENT:
if bl.value['type'] in bl_isolement:
decision = 'isolement', unicode(bl)
if bl.value['type'] in BL_ACCUEIL:
if bl.value['type'] in bl_accueil:
decision = 'accueil', unicode(bl)
# Filaire : protection anti-"squattage"
@ -580,8 +423,6 @@ def decide_vlan(data, is_wifi, conn):
# Pour les locaux clubs, il n'y a pas forcément un club sédentaire
# (typiquement, les locaux sous digicode)
decision = decision[0], decision[1] + u' (local club)'
elif chbre in PUBLIC_CHBRE:
decision = decision[0], decision[1] + u' (lieu de vie)'
else:
for hebergeur in hebergeurs:
# Si on est hébergé par un adhérent ok, ou que c'est notre
@ -614,12 +455,28 @@ def decide_vlan(data, is_wifi, conn):
return (port,) + decision
@radius_event
def dummy_fun(_):
"""Do nothing, successfully. (C'est pour avoir un truc à mettre)"""
def dummy_fun(p):
return radiusd.RLM_MODULE_OK
def detach(_=None):
def detach(p=None):
"""Appelé lors du déchargement du module (enfin, normalement)"""
print "*** goodbye from auth.py ***"
return radiusd.RLM_MODULE_OK
# à réimplémenter dans le authorize
# chap_ok(os.getenv('CHAP_PASSWORD'), os.getenv('CHAP_CHALLENGE'), mac)
def chap_ok(password, challenge, clear_pass) :
""" Test l'authentification chap fournie
password et chalenge doivent être données
en hexa (avec ou sans le 0x devant)
retourne True si l'authentification est OK
retourne False sinon
"""
try :
challenge = binascii.a2b_hex(challenge.replace('0x',''))
password = binascii.a2b_hex(password.replace('0x',''))
if hashlib.md5(password[0] + clear_pass + challenge).digest() == password[1:] :
return True
except :
return False

View file

@ -1,31 +0,0 @@
# Define a network where clients may be dynamically defined.
client dynamic {
#
# You MUST specify a netmask!
# IPv4 /32 or IPv6 /128 are NOT allowed!
ipv6addr = 0::
netmask = 0
#
# Define the virtual server used to discover dynamic clients.
dynamic_clients = dynamic_clients
#
# Define the lifetime (in seconds) for dynamic clients.
# They will be cached for this lifetime, and deleted afterwards.
#
# If the lifetime is "0", then the dynamic client is never
# deleted. The only way to delete the client is to re-start
# the server.
lifetime = 3600
}
# Le même, en ipv4
client dynamic {
ipaddr = 0.0.0.0
netmask = 0
dynamic_clients = dynamic_clients
lifetime = 3600
}

View file

@ -1 +0,0 @@
../rlm_python_unifie.conf

View file

@ -2,10 +2,6 @@
#
# Definitions for RADIUS programs
#
# This file should *NOT* be available in production mode : importing this dummy
# module in place of the radiusd module exposed by freeradius avoid logging
# function radlog to work.
#
# Copyright 2002 Miguel A.L. Paraz <mparaz@mparaz.com>
#
# This should only be used when testing modules.

View file

@ -0,0 +1,37 @@
# Configuration for the Python module.
#
#
python crans_fil {
mod_instantiate = freeradius.auth
func_instantiate = instantiate
# Spécifique au filaire: accepte direct
mod_authorize = freeradius.auth
func_authorize = authorize_fil
# Renseigne le vlan
# remplacer par dummy_fun pour ignorer le tagging de vlan
mod_post_auth = freeradius.auth
func_post_auth = post_auth_fil
# Que faire avant de quitter
mod_detach = freeradius.auth
func_detach = detach
# Le reste est dumb et inutile
mod_accounting = freeradius.auth
func_accounting = dummy_fun
mod_pre_proxy = freeradius.auth
func_pre_proxy = dummy_fun
mod_post_proxy = freeradius.auth
func_post_proxy = dummy_fun
mod_recv_coa = freeradius.auth
func_recv_coa = dummy_fun
mod_send_coa = freeradius.auth
func_send_coa = dummy_fun
}

View file

@ -1,38 +0,0 @@
# Configuration for the Python module.
#
#
python crans_unifie {
mod_instantiate = freeradius.auth
func_instantiate = instantiate
# Pour le authorize, c'est auth.py qui fait le tri maintenant
mod_authorize = freeradius.auth
func_authorize = authorize
# Renseigne le vlan si necessaire
# remplacer par dummy_fun pour ignorer le tagging de vlan
mod_post_auth = freeradius.auth
func_post_auth = post_auth
# Que faire avant de quitter
mod_detach = freeradius.auth
func_detach = detach
# Le reste sert à rien
mod_accounting = freeradius.auth
func_accounting = dummy_fun
mod_pre_proxy = freeradius.auth
func_pre_proxy = dummy_fun
mod_post_proxy = freeradius.auth
func_post_proxy = dummy_fun
mod_recv_coa = freeradius.auth
func_recv_coa = dummy_fun
mod_send_coa = freeradius.auth
func_send_coa = dummy_fun
}

View file

@ -0,0 +1,37 @@
# Configuration for the Python module.
#
#
python crans_wifi {
mod_instantiate = freeradius.auth
func_instantiate = instantiate
# Spécifique au WiFi : rempli le mdp
mod_authorize = freeradius.auth
func_authorize = authorize_wifi
# Renseigne le vlan
# remplacer par dummy_fun pour ignorer le tagging de vlan
mod_post_auth = freeradius.auth
func_post_auth = post_auth_wifi
# Que faire avant de quitter
mod_detach = freeradius.auth
func_detach = detach
# Le reste est dumb et inutile
mod_accounting = freeradius.auth
func_accounting = dummy_fun
mod_pre_proxy = freeradius.auth
func_pre_proxy = dummy_fun
mod_post_proxy = freeradius.auth
func_post_proxy = dummy_fun
mod_recv_coa = freeradius.auth
func_recv_coa = dummy_fun
mod_send_coa = freeradius.auth
func_send_coa = dummy_fun
}

View file

@ -1,17 +0,0 @@
#
# This is the virtual server referenced above by "dynamic_clients".
server dynamic_clients {
#
# The only contents of the virtual server is the "authorize" section.
authorize {
# Hack dégueux: crans_nas est un backend python. Or, rlm_python ne
# fournit pas en entrée les variables "control", uniquement les variables
# "request", du coup on met ce qui nous intéresse là.
update request {
NAS-Identifier = "%{Packet-Src-IP-Address:-%{Packet-Src-IPv6-Address}}"
}
crans_unifie
}
}

View file

@ -1,20 +0,0 @@
######################################################################
#
# Authentification filaire du crans
#
######################################################################
server filaire {
authorize{
preprocess
crans_unifie
}
authenticate{
crans_unifie
}
post-auth{
crans_unifie
}
}

View file

@ -1,397 +0,0 @@
# -*- text -*-
######################################################################
#
# This is a virtual server that handles *only* inner tunnel
# requests for EAP-TTLS and PEAP types.
#
# $Id: inner-tunnel,v 1.6 2008/03/29 21:33:12 aland Exp $
#
######################################################################
server inner-tunnel {
# Authorization. First preprocess (hints and huntgroups files),
# then realms, and finally look in the "users" file.
#
# The order of the realm modules will determine the order that
# we try to find a matching realm.
#
# Make *sure* that 'preprocess' comes before any realm if you
# need to setup hints for the remote radius server
authorize {
#preprocess
crans_unifie
#
# The chap module will set 'Auth-Type := CHAP' if we are
# handling a CHAP request and Auth-Type has not already been set
#chap
#
# Pull crypt'd passwords from /etc/passwd or /etc/shadow,
# using the system API's to get the password. If you want
# to read /etc/passwd or /etc/shadow directly, see the
# passwd module, above.
#
#unix
#
# Look for IPASS style 'realm/', and if not found, look for
# '@realm', and decide whether or not to proxy, based on
# that.
# IPASS
#
# If you are using multiple kinds of realms, you probably
# want to set "ignore_null = yes" for all of them.
# Otherwise, when the first style of realm doesn't match,
# the other styles won't be checked.
#
# Note that proxying the inner tunnel authentication means
# that the user MAY use one identity in the outer session
# (e.g. "anonymous", and a different one here
# (e.g. "user@example.com"). The inner session will then be
# proxied elsewhere for authentication. If you are not
# careful, this means that the user can cause you to forward
# the authentication to another RADIUS server, and have the
# accounting logs *not* sent to the other server. This makes
# it difficult to bill people for their network activity.
#
#suffix
# ntdomain
#
# The "suffix" module takes care of stripping the domain
# (e.g. "@example.com") from the User-Name attribute, and the
# next few lines ensure that the request is not proxied.
#
# If you want the inner tunnel request to be proxied, delete
# the next few lines.
#
#update control {
# Proxy-To-Realm := LOCAL
#}
#
# This module takes care of EAP-MSCHAPv2 authentication.
#
# It also sets the EAP-Type attribute in the request
# attribute list to the EAP type from the packet.
#
# The example below uses module failover to avoid querying all
# of the following modules if the EAP module returns "ok".
# Therefore, your LDAP and/or SQL servers will not be queried
# for the many packets that go back and forth to set up TTLS
# or PEAP. The load on those servers will therefore be reduced.
#
eap {
ok = return
}
#
# Read the 'users' file
# files
#
# Look in an SQL database. The schema of the database
# is meant to mirror the "users" file.
#
# See "Authorization Queries" in sql.conf
# sql
#
# If you are using /etc/smbpasswd, and are also doing
# mschap authentication, the un-comment this line, and
# configure the 'etc_smbpasswd' module, above.
# etc_smbpasswd
#
# The ldap module will set Auth-Type to LDAP if it has not
# already been set
#ldap
#
# If the users are logging in with an MS-CHAP-Challenge
# attribute for authentication, the mschap module will find
# the MS-CHAP-Challenge attribute, and add 'Auth-Type := MS-CHAP'
# to the request, which will cause the server to then use
# the mschap module for authentication.
mschap
#
# Enforce daily limits on time spent logged in.
# daily
#
# Use the checkval module
# checkval
#expiration
#logintime
#
# If no other module has claimed responsibility for
# authentication, then try to use PAP. This allows the
# other modules listed above to add a "known good" password
# to the request, and to do nothing else. The PAP module
# will then see that password, and use it to do PAP
# authentication.
#
# This module should be listed last, so that the other modules
# get a chance to set Auth-Type for themselves.
#
#pap
}
# Authentication.
#
#
# This section lists which modules are available for authentication.
# Note that it does NOT mean 'try each module in order'. It means
# that a module from the 'authorize' section adds a configuration
# attribute 'Auth-Type := FOO'. That authentication type is then
# used to pick the apropriate module from the list below.
#
# In general, you SHOULD NOT set the Auth-Type attribute. The server
# will figure it out on its own, and will do the right thing. The
# most common side effect of erroneously setting the Auth-Type
# attribute is that one authentication method will work, but the
# others will not.
#
# The common reasons to set the Auth-Type attribute by hand
# is to either forcibly reject the user, or forcibly accept him.
#
authenticate {
#
# PAP authentication, when a back-end database listed
# in the 'authorize' section supplies a password. The
# password can be clear-text, or encrypted.
#Auth-Type PAP {
# pap
#}
#
# Most people want CHAP authentication
# A back-end database listed in the 'authorize' section
# MUST supply a CLEAR TEXT password. Encrypted passwords
# won't work.
Auth-Type CHAP {
chap
}
#
# MSCHAP authentication.
Auth-Type MS-CHAP {
mschap
}
#
# Pluggable Authentication Modules.
# pam
#
# See 'man getpwent' for information on how the 'unix'
# module checks the users password. Note that packets
# containing CHAP-Password attributes CANNOT be authenticated
# against /etc/passwd! See the FAQ for details.
#
# unix
# Uncomment it if you want to use ldap for authentication
#
# Note that this means "check plain-text password against
# the ldap database", which means that EAP won't work,
# as it does not supply a plain-text password.
#Auth-Type LDAP {
# ldap
#}
#
# Allow EAP authentication.
eap
}
######################################################################
#
# There are no accounting requests inside of EAP-TTLS or PEAP
# tunnels.
#
######################################################################
# Session database, used for checking Simultaneous-Use. Either the radutmp
# or rlm_sql module can handle this.
# The rlm_sql module is *much* faster
session {
# radutmp
#
# See "Simultaneous Use Checking Queries" in sql.conf
# sql
}
# Post-Authentication
# Once we KNOW that the user has been authenticated, there are
# additional steps we can take.
post-auth {
crans_unifie
# Note that we do NOT assign IP addresses here.
# If you try to assign IP addresses for EAP authentication types,
# it WILL NOT WORK. You MUST use DHCP.
#
# If you want to have a log of authentication replies,
# un-comment the following line, and the 'detail reply_log'
# section, above.
# reply_log
#
# After authenticating the user, do another SQL query.
#
# See "Authentication Logging Queries" in sql.conf
# sql
#
# Instead of sending the query to the SQL server,
# write it into a log file.
#
# sql_log
#
# Un-comment the following if you have set
# 'edir_account_policy_check = yes' in the ldap module sub-section of
# the 'modules' section.
#
# ldap
#
# Access-Reject packets are sent through the REJECT sub-section of the
# post-auth section.
#
# Add the ldap module name (or instance) if you have set
# 'edir_account_policy_check = yes' in the ldap module configuration
#
Post-Auth-Type REJECT {
# attr_filter.access_reject
}
#
# The example policy below updates the outer tunnel reply
# (usually Access-Accept) with the User-Name from the inner
# tunnel User-Name. Since this section is processed in the
# context of the inner tunnel, "request" here means "inner
# tunnel request", and "outer.reply" means "outer tunnel
# reply attributes".
#
# This example is most useful when the outer session contains
# a User-Name of "anonymous@....", or a MAC address. If it
# is enabled, the NAS SHOULD use the inner tunnel User-Name
# in subsequent accounting packets. This makes it easier to
# track user sessions, as they will all be based on the real
# name, and not on "anonymous".
#
# The problem with doing this is that it ALSO exposes the
# real user name to any intermediate proxies. People use
# "anonymous" identifiers outside of the tunnel for a very
# good reason: it gives them more privacy. Setting the reply
# to contain the real user name removes ALL privacy from
# their session.
#
# If you want privacy to remain, see the
# Chargeable-User-Identity attribute from RFC 4372. In order
# to use that attribute, you will have to allocate a
# per-session identifier for the user, and store it in a
# long-term database (e.g. SQL). You should also use that
# attribute INSTEAD of the configuration below.
#
#update outer.reply {
# User-Name = "%{request:User-Name}"
#}
}
#
# When the server decides to proxy a request to a home server,
# the proxied request is first passed through the pre-proxy
# stage. This stage can re-write the request, or decide to
# cancel the proxy.
#
# Only a few modules currently have this method.
#
pre-proxy {
# attr_rewrite
# Uncomment the following line if you want to change attributes
# as defined in the preproxy_users file.
# files
# Uncomment the following line if you want to filter requests
# sent to remote servers based on the rules defined in the
# 'attrs.pre-proxy' file.
# attr_filter.pre-proxy
# If you want to have a log of packets proxied to a home
# server, un-comment the following line, and the
# 'detail pre_proxy_log' section, above.
# pre_proxy_log
}
#
# When the server receives a reply to a request it proxied
# to a home server, the request may be massaged here, in the
# post-proxy stage.
#
post-proxy {
# If you want to have a log of replies from a home server,
# un-comment the following line, and the 'detail post_proxy_log'
# section, above.
# post_proxy_log
# attr_rewrite
# Uncomment the following line if you want to filter replies from
# remote proxies based on the rules defined in the 'attrs' file.
# attr_filter.post-proxy
#
# If you are proxying LEAP, you MUST configure the EAP
# module, and you MUST list it here, in the post-proxy
# stage.
#
# You MUST also use the 'nostrip' option in the 'realm'
# configuration. Otherwise, the User-Name attribute
# in the proxied request will not match the user name
# hidden inside of the EAP packet, and the end server will
# reject the EAP request.
#
eap
#
# If the server tries to proxy a request and fails, then the
# request is processed through the modules in this section.
#
# The main use of this section is to permit robust proxying
# of accounting packets. The server can be configured to
# proxy accounting packets as part of normal processing.
# Then, if the home server goes down, accounting packets can
# be logged to a local "detail" file, for processing with
# radrelay. When the home server comes back up, radrelay
# will read the detail file, and send the packets to the
# home server.
#
# With this configuration, the server always responds to
# Accounting-Requests from the NAS, but only writes
# accounting packets to disk if the home server is down.
#
# Post-Proxy-Type Fail {
# detail
# }
}
} # inner-tunnel server block

View file

@ -1,466 +0,0 @@
######################################################################
#
# Authentification wifi du crans
#
######################################################################
#
# Authorization. First preprocess (hints and huntgroups files),
# then realms, and finally look in the "users" file.
#
# The order of the realm modules will determine the order that
# we try to find a matching realm.
#
# Make *sure* that 'preprocess' comes before any realm if you
# need to setup hints for the remote radius server
server wifi {
authorize {
if (User-Name !~ /crans$/) {
if (User-Name =~ /^(.*)@(.*)/) {
update control {
Proxy-To-Realm := 'FEDEREZ'
}
}
}
#
# The preprocess module takes care of sanitizing some bizarre
# attributes in the request, and turning them into attributes
# which are more standard.
#
# It takes care of processing the 'raddb/hints' and the
# 'raddb/huntgroups' files.
#
# It also adds the %{Client-IP-Address} attribute to the request.
#preprocess
#
# If you want to have a log of authentication requests,
# un-comment the following line, and the 'detail auth_log'
# section, above.
# auth_log
#
# The chap module will set 'Auth-Type := CHAP' if we are
# handling a CHAP request and Auth-Type has not already been set
# chap
#
# If the users are logging in with an MS-CHAP-Challenge
# attribute for authentication, the mschap module will find
# the MS-CHAP-Challenge attribute, and add 'Auth-Type := MS-CHAP'
# to the request, which will cause the server to then use
# the mschap module for authentication.
#mschap
#
# If you have a Cisco SIP server authenticating against
# FreeRADIUS, uncomment the following line, and the 'digest'
# line in the 'authenticate' section.
# digest
#
# Look for IPASS style 'realm/', and if not found, look for
# '@realm', and decide whether or not to proxy, based on
# that.
# IPASS
#
# If you are using multiple kinds of realms, you probably
# want to set "ignore_null = yes" for all of them.
# Otherwise, when the first style of realm doesn't match,
# the other styles won't be checked.
#
# suffix
# ntdomain
#
# This module takes care of EAP-MD5, EAP-TLS, and EAP-LEAP
# authentication.
#
# It also sets the EAP-Type attribute in the request
# attribute list to the EAP type from the packet.
#
# As of 2.0, the EAP module returns "ok" in the authorize stage
# for TTLS and PEAP. In 1.x, it never returned "ok" here, so
# this change is compatible with older configurations.
#
# The example below uses module failover to avoid querying all
# of the following modules if the EAP module returns "ok".
# Therefore, your LDAP and/or SQL servers will not be queried
# for the many packets that go back and forth to set up TTLS
# or PEAP. The load on those servers will therefore be reduced.
#
eap {
ok = return
}
#
# Pull crypt'd passwords from /etc/passwd or /etc/shadow,
# using the system API's to get the password. If you want
# to read /etc/passwd or /etc/shadow directly, see the
# passwd module in radiusd.conf.
#
# unix
#
# Read the 'users' file
# files
#
# Look in an SQL database. The schema of the database
# is meant to mirror the "users" file.
#
# See "Authorization Queries" in sql.conf
# sql
#
# If you are using /etc/smbpasswd, and are also doing
# mschap authentication, the un-comment this line, and
# configure the 'etc_smbpasswd' module, above.
# etc_smbpasswd
#
# The ldap module will set Auth-Type to LDAP if it has not
# already been set
#ldap
#
# Enforce daily limits on time spent logged in.
# daily
#
# Use the checkval module
# checkval
# expiration
# logintime
#
# If no other module has claimed responsibility for
# authentication, then try to use PAP. This allows the
# other modules listed above to add a "known good" password
# to the request, and to do nothing else. The PAP module
# will then see that password, and use it to do PAP
# authentication.
#
# This module should be listed last, so that the other modules
# get a chance to set Auth-Type for themselves.
#
#pap
#
# If "status_server = yes", then Status-Server messages are passed
# through the following section, and ONLY the following section.
# This permits you to do DB queries, for example. If the modules
# listed here return "fail", then NO response is sent.
#
# Autz-Type Status-Server {
#
# }
}
# Authentication.
#
#
# This section lists which modules are available for authentication.
# Note that it does NOT mean 'try each module in order'. It means
# that a module from the 'authorize' section adds a configuration
# attribute 'Auth-Type := FOO'. That authentication type is then
# used to pick the apropriate module from the list below.
#
# In general, you SHOULD NOT set the Auth-Type attribute. The server
# will figure it out on its own, and will do the right thing. The
# most common side effect of erroneously setting the Auth-Type
# attribute is that one authentication method will work, but the
# others will not.
#
# The common reasons to set the Auth-Type attribute by hand
# is to either forcibly reject the user (Auth-Type := Reject),
# or to or forcibly accept the user (Auth-Type := Accept).
#
# Note that Auth-Type := Accept will NOT work with EAP.
#
# Please do not put "unlang" configurations into the "authenticate"
# section. Put them in the "post-auth" section instead. That's what
# the post-auth section is for.
#
authenticate {
#
# PAP authentication, when a back-end database listed
# in the 'authorize' section supplies a password. The
# password can be clear-text, or encrypted.
#Auth-Type PAP {
# pap
#}
#
# Most people want CHAP authentication
# A back-end database listed in the 'authorize' section
# MUST supply a CLEAR TEXT password. Encrypted passwords
# won't work.
#Auth-Type CHAP {
# chap
#}
#
# MSCHAP authentication.
Auth-Type MS-CHAP {
mschap
}
#
# If you have a Cisco SIP server authenticating against
# FreeRADIUS, uncomment the following line, and the 'digest'
# line in the 'authorize' section.
# digest
#
# Pluggable Authentication Modules.
# pam
#
# See 'man getpwent' for information on how the 'unix'
# module checks the users password. Note that packets
# containing CHAP-Password attributes CANNOT be authenticated
# against /etc/passwd! See the FAQ for details.
#
# unix
# Uncomment it if you want to use ldap for authentication
#
# Note that this means "check plain-text password against
# the ldap database", which means that EAP won't work,
# as it does not supply a plain-text password.
#Auth-Type LDAP {
# ldap
#}
#
# Allow EAP authentication.
eap
}
#
# Pre-accounting. Decide which accounting type to use.
#
preacct {
preprocess
#
# Ensure that we have a semi-unique identifier for every
# request, and many NAS boxes are broken.
acct_unique
#
# Look for IPASS-style 'realm/', and if not found, look for
# '@realm', and decide whether or not to proxy, based on
# that.
#
# Accounting requests are generally proxied to the same
# home server as authentication requests.
# IPASS
suffix
# ntdomain
#
# Read the 'acct_users' file
# files
}
#
# Accounting. Log the accounting data.
#
accounting {
#
# Create a 'detail'ed log of the packets.
# Note that accounting requests which are proxied
# are also logged in the detail file.
# detail
# daily
# Update the wtmp file
#
# If you don't use "radlast", you can delete this line.
# unix
#
# For Simultaneous-Use tracking.
#
# Due to packet losses in the network, the data here
# may be incorrect. There is little we can do about it.
# radutmp
# sradutmp
# Return an address to the IP Pool when we see a stop record.
# main_pool
#
# Log traffic to an SQL database.
#
# See "Accounting queries" in sql.conf
# sql
#
# Instead of sending the query to the SQL server,
# write it into a log file.
#
# sql_log
# Cisco VoIP specific bulk accounting
# pgsql-voip
# Filter attributes from the accounting response.
# attr_filter.accounting_response
#
# See "Autz-Type Status-Server" for how this works.
#
# Acct-Type Status-Server {
#
# }
}
# Session database, used for checking Simultaneous-Use. Either the radutmp
# or rlm_sql module can handle this.
# The rlm_sql module is *much* faster
session {
# radutmp
#
# See "Simultaneous Use Checking Queries" in sql.conf
# sql
}
# Post-Authentication
# Once we KNOW that the user has been authenticated, there are
# additional steps we can take.
post-auth {
# Get an address from the IP Pool.
# main_pool
#
# If you want to have a log of authentication replies,
# un-comment the following line, and the 'detail reply_log'
# section, above.
# reply_log
#
# After authenticating the user, do another SQL query.
#
# See "Authentication Logging Queries" in sql.conf
# sql
#
# Instead of sending the query to the SQL server,
# write it into a log file.
#
# sql_log
#
# Un-comment the following if you have set
# 'edir_account_policy_check = yes' in the ldap module sub-section of
# the 'modules' section.
#
#ldap
# exec
#
# Access-Reject packets are sent through the REJECT sub-section of the
# post-auth section.
#
# Add the ldap module name (or instance) if you have set
# 'edir_account_policy_check = yes' in the ldap module configuration
#
Post-Auth-Type REJECT {
# attr_filter.access_reject
}
}
#
# When the server decides to proxy a request to a home server,
# the proxied request is first passed through the pre-proxy
# stage. This stage can re-write the request, or decide to
# cancel the proxy.
#
# Only a few modules currently have this method.
#
pre-proxy {
# attr_rewrite
# Uncomment the following line if you want to change attributes
# as defined in the preproxy_users file.
# files
# Uncomment the following line if you want to filter requests
# sent to remote servers based on the rules defined in the
# 'attrs.pre-proxy' file.
# attr_filter.pre-proxy
# If you want to have a log of packets proxied to a home
# server, un-comment the following line, and the
# 'detail pre_proxy_log' section, above.
# pre_proxy_log
}
#
# When the server receives a reply to a request it proxied
# to a home server, the request may be massaged here, in the
# post-proxy stage.
#
post-proxy {
# If you want to have a log of replies from a home server,
# un-comment the following line, and the 'detail post_proxy_log'
# section, above.
# post_proxy_log
# attr_rewrite
# Uncomment the following line if you want to filter replies from
# remote proxies based on the rules defined in the 'attrs' file.
# attr_filter.post-proxy
#
# If you are proxying LEAP, you MUST configure the EAP
# module, and you MUST list it here, in the post-proxy
# stage.
#
# You MUST also use the 'nostrip' option in the 'realm'
# configuration. Otherwise, the User-Name attribute
# in the proxied request will not match the user name
# hidden inside of the EAP packet, and the end server will
# reject the EAP request.
#
eap
#
# If the server tries to proxy a request and fails, then the
# request is processed through the modules in this section.
#
# The main use of this section is to permit robust proxying
# of accounting packets. The server can be configured to
# proxy accounting packets as part of normal processing.
# Then, if the home server goes down, accounting packets can
# be logged to a local "detail" file, for processing with
# radrelay. When the home server comes back up, radrelay
# will read the detail file, and send the packets to the
# home server.
#
# With this configuration, the server always responds to
# Accounting-Requests from the NAS, but only writes
# accounting packets to disk if the home server is down.
#
# Post-Proxy-Type Fail {
# detail
# }
}
}

View file

@ -16,10 +16,9 @@ delattr(sys, 'argv')
auth.instantiate(())
# Test avec l'interface wifi d'apprentis
p=(
('Calling-Station-Id', '02:69:75:42:24:03'),
('User-Name', 'apprentis-wifi'),
('Calling-Station-Id', 'b0:79:94:cf:d1:9a'),
('User-Name', 'moo-torola'),
)
print repr(auth.authorize_wifi(p))

View file

@ -1 +0,0 @@
../auth.py

View file

@ -1,16 +0,0 @@
#!/bin/bash
# Teste le inner-tunnel en se connectant directement au serveur
MAC=moo-torola
PASSWORD=7syxqbbkdb
#SECRET=`PYTHONPATH=/etc/crans/secrets/ python -c \
# "import secrets; print secrets.radius_eap_key"`
SECRET=e4hmraqw6Yps
NAS_NAME=atree.wifi.crans.org
#SERVER=127.0.0.1
SERVER=pea.v6.wifi.crans.org
SERVER=138.231.136.35
SERVER=[2a01:240:fe3d:c04:0:70ff:fe65:6103]
SERVER=localhost
radtest -t mschap -x -4 $MAC $PASSWORD $SERVER 18 $SECRET $SECRET $NAS_NAME

View file

@ -94,6 +94,10 @@ def dialog(backtitle, arg, dialogrc=''):
# Récupération du contenu du pipe
_, sortie = processus.communicate()
# On décode la sortie du programme dialog (et on le fait ici parce que
# c'est ici l'interface).
sortie = to_unicode(sortie)
resultat = sortie.splitlines()
# Récupération du code d'erreur

View file

@ -8,6 +8,10 @@
# Contenu :
# ---------
#
# Décorateur :
# static_var([(name, val)]), un décorateur pour créer des variables
# statiques dans une fonction
#
# Fonctions :
# getTerminalSize(), une fonction qui récupère le couple
# largeur, hauteur du terminal courant.
@ -35,13 +39,12 @@ import os
import fcntl
import termios
import struct
import functools
import time
import re
from locale import getpreferredencoding
from cranslib.decorators import static_var
OCT_NAMES = ["Pio", "Tio", "Gio", "Mio", "Kio"]
OCT_SIZES = [1024**(len(OCT_NAMES) - i) for i in xrange(0, len(OCT_NAMES))]
TERM_FORMAT = '\x1b\[[0-1];([0-9]|[0-9][0-9])m'
@ -53,9 +56,7 @@ def try_decode(string):
avoir en réception.
"""
if isinstance(string, unicode):
return string
unicode_str = ""
try:
return string.decode("UTF-8")
except UnicodeDecodeError:
@ -87,6 +88,21 @@ def guess_preferred_encoding():
return encoding
def static_var(couples):
"""Decorator setting static variable
to a function.
"""
# Using setattr magic, we set static
# variable on function. This avoid
# computing stuff again.
def decorate(fun):
functools.wraps(fun)
for (name, val) in couples:
setattr(fun, name, val)
return fun
return decorate
def getTerminalSize():
"""Dummy function to get term dimensions.
Thanks to http://stackoverflow.com/questions/566746/how-to-get-console-window-width-in-python
@ -260,7 +276,7 @@ def nostyle(dialog=False):
return "\Zn"
return "\033[1;0m"
@static_var(("styles", {}))
@static_var([("styles", {})])
def style(texte, what=None, dialog=False):
"""Pretty text is pretty
On peut appliquer plusieurs styles d'affilée, ils seront alors traités
@ -558,26 +574,7 @@ if __name__ == "__main__":
time.sleep(1)
prettyDoin("Les carottes sont cuites." , "Ok")
data = [
[
style("Durand", "rouge"),
"Toto",
"40",
"50 rue Döp"
],
[
"Dupont",
"Robert",
"50",
"42" + style(" avenue ", "vert") + style("dumotel", 'rouge')
],
[
style("znvuzbvzruobouzb", ["gras", "vert"]),
"pppoe",
"1",
"poiodur 50 pepe"
]
]
data = [[style("Durand", "rouge"), "Toto", "40", "50 rue Döp"], ["Dupont", "Robert", "50", "42" + style(" avenue ", "vert") + style("dumotel", 'rouge')], [style("znvuzbvzruobouzb", ["gras", "vert"]), "pppoe", "1", "poiodur 50 pepe"]]
titres = ("Nom", "Prénom", "Âge", "Adresse")
longueurs = [25, 25, '*', '*']
print tableau(data, titres, longueurs).encode(guess_preferred_encoding())

View file

@ -2,11 +2,8 @@
import os
import psycopg2
from functools import wraps
import time
import socket
conn = None
# : échec définitif, on raise une exception direct
@ -22,19 +19,17 @@ def _need_conn(f):
raise NameError("La connexion à la pase postgresql ne peut être établie.")
attempts = 0
while not conn or not attempts:
host = os.getenv('DBG_ANNUAIRE', 'pgsql.v4.adm.crans.org')
# Test habituel sur vo:
if host == '1' or __name__.endswith('annuaires_pg_test'):
if __name__.endswith('annuaires_pg_test') or os.getenv('DBG_ANNUAIRE', False):
host='localhost'
else:
host='pgsql.v4.adm.crans.org'
# "connecting …"
try:
if not conn:
if attempts:
# Attend un peu avant de reessayer
time.sleep(delay)
conn = psycopg2.connect(user='crans_ro', database='django',
conn = psycopg2.connect(user='crans', database='switchs',
host=host)
return f(*args, **kwargs)
except psycopg2.OperationalError:
@ -47,8 +42,7 @@ def _need_conn(f):
# backend pgsql. On utilise donc une exception plus standard
return first_connect
# Le v est virtuel.
bat_switchs = ["a", "b", "c", "g", "h", "i", "j", "m", "o", "p", "v"]
bat_switchs = ["a", "b", "c", "g", "h", "i", "j", "m", "o", "p"]
class ChbreNotFound(ValueError):
"""Lorsqu'une chambre n'existe pas"""
@ -61,14 +55,14 @@ def chbre_prises(batiment, chambre = None):
if chambre:
chambre = chambre.lower()
cur = conn.cursor()
cur.execute("SELECT prise_crans FROM prises_prise WHERE (batiment, chambre) = (%s, %s)", (batiment, chambre))
cur.execute("SELECT prise_crans FROM prises WHERE (batiment, chambre) = (%s, %s)", (batiment, chambre))
try:
return "%03d" % cur.fetchone()[0]
except TypeError:
raise ChbreNotFound("Chambre inexistante bat %r, chbre %r" % (batiment, chambre))
else:
cur = conn.cursor()
cur.execute("SELECT chambre, prise_crans FROM prises_prise WHERE batiment = %s", batiment)
cur.execute("SELECT chambre, prise_crans FROM prises WHERE batiment = %s", batiment)
ret = {}
for chambre, prise_crans in cur.fetchall():
ret[chambre] = "%03d" % prise_crans
@ -82,7 +76,7 @@ def chbre_commentaire(batiment, chambre):
global conn
batiment = batiment.lower()
cur = conn.cursor()
cur.execute("SELECT commentaire FROM prises_prise WHERE (batiment, chambre) = (%s,%s)", (batiment, chambre))
cur.execute("SELECT commentaire FROM prises WHERE (batiment, chambre) = (%s,%s)", (batiment, chambre))
try:
return cur.fetchone()[0]
except TypeError:
@ -94,14 +88,14 @@ def reverse(batiment, prise = None):
batiment = batiment.lower()
if prise:
cur = conn.cursor()
cur.execute("SELECT chambre FROM prises_prise WHERE (batiment, prise_crans) = (%s, %s)", (batiment, int(prise)))
cur.execute("SELECT chambre FROM prises WHERE (batiment, prise_crans) = (%s, %s)", (batiment, int(prise)))
try:
return [chbre for (chbre,) in cur.fetchall()]
except TypeError:
raise ValueError("Prise %s inexistante" % prise)
else:
cur = conn.cursor()
cur.execute("SELECT chambre, prise_crans FROM prises_prise WHERE batiment = %s", batiment)
cur.execute("SELECT chambre, prise_crans FROM prises WHERE batiment = %s", batiment)
ret = {}
for chambre, prise_crans in cur.fetchall():
try:
@ -112,15 +106,37 @@ def reverse(batiment, prise = None):
if not ret:
raise ValueError("Batiment %s inexistant" % batiment)
return ret
@_need_conn
def is_crans(batiment, chambre):
"""Chambre cablee au Cr@ns ?"""
batiment = batiment.lower()
chambre = chambre.lower()
cur = conn.cursor()
cur.execute("SELECT crans FROM prises WHERE (batiment, chambre) = (%s, %s)", (batiment, chambre))
return cur.fetchone()[0]
@_need_conn
def is_connected(batiment, chambre):
"""Cablage physique effectue ?"""
batiment = batiment.lower()
chambre = chambre.lower()
cur = conn.cursor()
cur.execute("SELECT cablage_effectue FROM prises_prise WHERE (batiment, chambre) = (%s, %s)", (batiment, chambre))
cur.execute("SELECT cablage_effectue FROM prises WHERE (batiment, chambre) = (%s, %s)", (batiment, chambre))
return cur.fetchone()[0]
@_need_conn
def crous_to_crans(batiment, chambre):
"""Passage d'une chambre de CROUS a Cr@ns"""
batiment = batiment.lower()
chambre = chambre.lower()
if is_crans(batiment, chambre):
return
cur = conn.cursor()
cur.execute("UPDATE prises SET (crans, crous, cablage_effectue) = (TRUE, FALSE, not cablage_effectue) WHERE (batiment, chambre) = (%s, %s)", (batiment, chambre))
conn.commit()
cur.close()
# Prises d'uplink, de machines du crans / Prises d'utilité CRANS
uplink_prises={ 'a' :
{ 49 : 'uplink->bata-4', 50 : 'libre-service',
@ -137,8 +153,7 @@ uplink_prises={ 'a' :
349 : 'uplink->batb-4', 350 : 'libre-service',
401 : 'uplink->batb-0', 402 : 'uplink->batb-1',
403 : 'uplink->batb-2', 404 : 'uplink->batb-3',
405 : 'uplink->backbone', 523 : 'uplink->batb-4',
},
405 : 'uplink->backbone' },
'c' :
{ 49 : 'uplink->batc-3', 50 : 'libre-service',
149 : 'uplink->batc-3', 150 : 'libre-service',
@ -271,38 +286,14 @@ uplink_prises={ 'a' :
},
}
_SPECIAL_SWITCHES = ['backbone.adm.crans.org',
'multiprise-v6.adm.crans.org',
'batk-0.crans.org',
'minigiga.adm.crans.org',
'batb-5.crans.org',
]
_HIDDEN_SWITCHES = [
'batp-4.adm.crans.org',
'batv-0.adm.crans.org',
]
_SPECIAL_SWITCHES=['backbone.adm.crans.org',
'multiprise-v6.adm.crans.org',
'batk-0.crans.org',
'batp-4.adm.crans.org',
'minigiga.adm.crans.org',
]
def guess_switch_fqdn(switch_name):
"""Retourne le FQDN d'un switch à partir de son nom"""
try:
return socket.gethostbyname_ex(switch_name)[0]
except socket.gaierror:
pass
try:
return socket.gethostbyname_ex(switch_name + ".adm.crans.org")[0]
except socket.gaierror:
pass
try:
return socket.gethostbyname_ex(switch_name + ".crans.org")[0]
except socket.gaierror:
pass
raise socket.gaierror
def all_switchs(bat=None, hide=_SPECIAL_SWITCHES + _HIDDEN_SWITCHES):
def all_switchs(bat=None, hide=_SPECIAL_SWITCHES):
"""Retourne la liste des switchs pour un batiment.
Si bat est donné, seulement pour le bâtiment demandé, sinon pour
@ -311,19 +302,14 @@ def all_switchs(bat=None, hide=_SPECIAL_SWITCHES + _HIDDEN_SWITCHES):
simplement batx"""
if bat == None:
bat = list(bat_switchs)
bat = bat_switchs
if type(bat) not in [ tuple, list ] :
bat = [bat]
switchs = []
for b in bat:
indexes = set(n/100 for n in uplink_prises[b])
for i in indexes:
switch_name = "bat%s-%s" % (b, i)
try:
hostname = guess_switch_fqdn(switch_name)
except socket.gaierror:
print "Le switch %s ne semble pas exister." % (switch_name,)
continue
hostname = "bat%s-%s.adm.crans.org" % (b, i)
if hostname not in hide:
switchs.append(hostname)
# on ajoute quand-même le backbone et/ou multiprise-v6 si demandé

View file

@ -18,10 +18,11 @@ import re
import affichage
import lc_ldap.shortcuts
from lc_ldap.crans_utils import to_generalized_time_format as to_gtf
import mail as mail_module
from config import demenagement_delai as delai, \
gtf_debut_periode_transitoire, periode_transitoire
debut_periode_transitoire, periode_transitoire
ERASE_DAY = { 'second': 0, 'minute': 0, 'microsecond': 0, 'hour': 0, }
DAY = datetime.timedelta(days=1)
@ -71,28 +72,16 @@ def warn_or_delete(smtp, clandestin, fail, done):
mail_addr = clandestin.get_mail()
if not clandestin.machines() or not mail_addr:
return # Si pas de machine, on s'en fout. Si pas de mail, inutile
try:
data = {
'dn': clandestin.dn.split(',')[0],
'when': now.strftime('%Y/%M/%D %H:%m:%S:%s'),
'chbre' : exchambre,
}
chbre_url = mail_module.validation_url('demenagement', data, True)
chbre_url_error = u""
except Exception as error:
chbre_url_error = u"[[erreur de génération: %r]]" % error
chbre_url = u""
data = {
"from" : RESP,
"chambre" : exchambre,
"jours" : (date_suppr - now).days+1,
"to" : mail_addr,
"adh": clandestin,
"chbre_url" : chbre_url,
"chbre_url_error" : chbre_url_error,
"lang_info": "English version below",
}
smtp.send_template('demenagement', data)
mail = mail_module.generate('demenagement', data)
smtp.sendmail(RESP, [mail_addr], mail.as_string())
def format_entry(m):
"""Renvoie une ligne de tableau, pour une machine"""
@ -112,7 +101,7 @@ if __name__ == '__main__':
conn = lc_ldap.shortcuts.lc_ldap_admin()
if periode_transitoire:
date = gtf_debut_periode_transitoire
date = to_gtf(debut_periode_transitoire)
else:
date = now.strftime(FORMAT_LDAP) + 'Z'

View file

@ -25,6 +25,7 @@ import lc_ldap.attributs
import lc_ldap.objets
import gestion.mail as mail_module
encoding = getattr(sys.stdout, 'encoding', "UTF-8")
current_user = os.getenv("SUDO_USER") or os.getenv("USER") or os.getenv("LOGNAME") or getpass.getuser()
def check_password(password, no_cracklib=False, dialog=False):
@ -38,16 +39,15 @@ def check_password(password, no_cracklib=False, dialog=False):
password.decode('ascii')
except UnicodeDecodeError:
problem = True
msg += u"Le mot de passe ne doit contenir que des caractères ascii.\n"
if len(password) >= 64:
problem = True
msg += u"Le mot de passe doit faire strictement moins de 64 caractères\n"
if not dialog:
affich_tools.cprint(u'Le mot de passe ne doit contenir que des caractères ascii.', "rouge")
else:
msg += affich_tools.coul(u'Le mot de passe ne doit contenir que des caractères ascii.\n', "rouge", dialog=dialog)
# Nounou mode
if no_cracklib:
if len(password) >= config.password.root_min_len:
return True, msg
return True
else:
upp = 0
low = 0
@ -67,22 +67,37 @@ def check_password(password, no_cracklib=False, dialog=False):
# Recherche de manque de caractères
if cif < config.password.min_cif:
msg += u'Le mot de passe doit contenir plus de chiffres.\n'
if not dialog:
affich_tools.cprint(u'Le mot de passe doit contenir plus de chiffres.', "rouge")
else:
msg += affich_tools.coul(u'Le mot de passe doit contenir plus de chiffres.\n', "rouge", dialog=dialog)
problem = True
if upp < config.password.min_upp:
msg += u'Le mot de passe doit contenir plus de majuscules.\n'
if not dialog:
affich_tools.cprint(u'Le mot de passe doit contenir plus de majuscules.', "rouge")
else:
msg += affich_tools.coul(u'Le mot de passe doit contenir plus de majuscules.\n', "rouge", dialog=dialog)
problem = True
if low < config.password.min_low:
msg += u'Le mot de passe doit contenir plus de minuscules.\n'
if not dialog:
affich_tools.cprint(u'Le mot de passe doit contenir plus de minuscules.', "rouge")
else:
msg += affich_tools.coul(u'Le mot de passe doit contenir plus de minuscules.\n', "rouge", dialog=dialog)
problem = True
if oth < config.password.min_oth:
msg += u'Le mot de passe doit contenir plus de caractères qui ne sont ni des chiffres, ni des majuscules, ni des minuscules.\n'
if not dialog:
affich_tools.cprint(u'Le mot de passe doit contenir plus de caractères qui ne sont ni des chiffres, ni des majuscules, ni des minuscules.', "rouge")
else:
msg += affich_tools.coul(u'Le mot de passe doit contenir plus de caractères qui ne sont ni des chiffres, ni des majuscules, ni des minuscules.\n', "rouge", dialog=dialog)
problem = True
# Scores sur la longueur
longueur = config.password.upp_value*upp + config.password.low_value*low + config.password.cif_value*cif + config.password.oth_value*oth
if longueur < config.password.min_len:
msg += u'Le mot de passe devrait être plus long, ou plus difficile.\n'
if not dialog:
affich_tools.cprint(u'Le mot de passe devrait être plus long, ou plus difficile.', "rouge")
else:
msg += affich_tools.coul(u'Le mot de passe devrait être plus long, ou plus difficile.\n', "rouge", dialog=dialog)
problem = True
if not problem:
@ -96,46 +111,31 @@ def check_password(password, no_cracklib=False, dialog=False):
# Le mot vient-il du dico (à améliorer, on voudrait pouvoir préciser
# la rigueur du test) ?
password = cracklib.VeryFascistCheck(password)
if dialog:
msg = affich_tools.coul(msg, 'rouge', dialog=dialog)
return True, msg
except ValueError as e:
msg += str(e).decode(config.in_encoding)
if dialog:
msg = affich_tools.coul(msg, 'rouge', dialog=dialog)
if not dialog:
affich_tools.cprint(e.message, "rouge")
else:
msg += affich_tools.coul(str(e).decode(), "rouge", dialog=dialog)
return False, msg
else:
if dialog:
msg = affich_tools.coul(msg, 'rouge', dialog=dialog)
return True, msg
else:
if dialog:
msg = affich_tools.coul(msg, 'rouge', dialog=dialog)
return False, msg
if dialog:
msg = affich_tools.coul(msg, 'rouge', dialog=dialog)
return False, msg
@lc_ldap.shortcuts.with_ldap_conn(retries=2, delay=5, constructor=lc_ldap.shortcuts.lc_ldap_admin)
def change_password(ldap, login=None, verbose=False, no_cracklib=False, **kwargs):
def change_password(ldap, login=None, verbose=False, no_cracklib=False, **args):
"""
Change le mot de passe en fonction des arguments
"""
if login is None:
login = current_user
if type(login) == str:
login = login.decode(config.in_encoding)
if no_cracklib:
if not lc_ldap.attributs.nounou in ldap.droits:
no_cracklib = False
login = login.decode(encoding)
login = lc_ldap.crans_utils.escape(login)
query = ldap.search(u"(uid=%s)" % login, mode="w")
if not query:
affich_tools.cprint('Utilisateur introuvable dans la base de données, modification de l\'utilisateur local.', "rouge")
sys.exit(2)
@ -145,7 +145,7 @@ def change_password(ldap, login=None, verbose=False, no_cracklib=False, **kwargs
user['userPassword'] = [lc_ldap.crans_utils.hash_password("test").decode('ascii')]
user.cancel()
except EnvironmentError as e:
affich_tools.cprint(str(e).decode(config.in_encoding), "rouge")
affich_tools.cprint(str(e).decode(encoding), "rouge")
# Génération d'un mail
From = 'roots@crans.org'
@ -155,11 +155,11 @@ To: %s
Subject: Tentative de changement de mot de passe !
Tentative de changement du mot de passe de %s par %s.
""" % (From, To, login.encode(config.out_encoding), current_user)
""" % (From, To , login.encode(encoding), current_user)
# Envoi mail
with mail_module.ServerConnection() as conn:
conn.sendmail(From, To, mail)
conn.sendmail(From, To , mail )
sys.exit(1)
# On peut modifier le MDP
@ -167,63 +167,54 @@ Tentative de changement du mot de passe de %s par %s.
prenom = "Club"
else:
prenom = user['prenom'][0]
affich_tools.cprint(
"Changement du mot de passe de %s %s." % (
prenom,
user['nom'][0]
),
"vert",
)
affich_tools.cprint("Changement du mot de passe de %s %s." %
(prenom, user['nom'][0]),
"vert")
# Règles du jeu
# (J'ai perdu)
if verbose:
affich_tools.cprint(
u"""Règles :
affich_tools.cprint(u"""Règles :
Longueur standard : %s, root : %s,
Minimums : chiffres : %s, minuscules : %s, majuscules : %s, autres : %s,
Scores de longueur : chiffres : %s, minuscules : %s, majuscules : %s, autres : %s,
Cracklib : %s.""" % (config.password.min_len,
config.password.root_min_len,
config.password.min_cif,
config.password.min_low,
config.password.min_upp,
config.password.min_oth,
config.password.cif_value,
config.password.low_value,
config.password.upp_value,
config.password.oth_value,
"Oui" * (not no_cracklib) + "Non" * (no_cracklib),
),
'jaune',
)
Cracklib : %s.""" % (
config.password.min_len,
config.password.root_min_len,
config.password.min_cif,
config.password.min_low,
config.password.min_upp,
config.password.min_oth,
config.password.cif_value,
config.password.low_value,
config.password.upp_value,
config.password.oth_value,
"Oui" * (not no_cracklib) + "Non" * (no_cracklib)
),
'jaune')
else:
affich_tools.cprint(
u"""Le nouveau mot de passe doit comporter au minimum %s caractères.
affich_tools.cprint(u"""Le nouveau mot de passe doit comporter au minimum %s caractères.
Il ne doit pas être basé sur un mot du dictionnaire.
Il doit contenir au moins %s chiffre(s), %s minuscule(s),
%s majuscule(s) et au moins %s autre(s) caractère(s).
CTRL+D ou CTRL+C provoquent un abandon.""" % (config.password.min_len,
config.password.min_cif,
config.password.min_low,
config.password.min_upp,
config.password.min_oth
),
'jaune',
)
CTRL+D ou CTRL+C provoquent un abandon.""" %
(
config.password.min_len,
config.password.min_cif,
config.password.min_low,
config.password.min_upp,
config.password.min_oth
), 'jaune')
try:
while True:
mdp = getpass.getpass("Nouveau mot de passe: ")
(ret, msg) = check_password(mdp, no_cracklib)
if ret:
if check_password(mdp, no_cracklib)[0]:
mdp2 = getpass.getpass("Retaper le mot de passe: ")
if mdp != mdp2:
affich_tools.cprint(u"Les deux mots de passe diffèrent.", "rouge")
else:
break
else:
affich_tools.cprint(msg, 'rouge')
except KeyboardInterrupt:
affich_tools.cprint(u'\nAbandon', 'rouge')
@ -240,35 +231,29 @@ CTRL+D ou CTRL+C provoquent un abandon.""" % (config.password.min_len,
affich_tools.cprint(u"Mot de passe de %s changé." % (user['uid'][0]), "vert")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Recherche dans la base des adhérents",
add_help=False,
)
parser.add_argument('-h',
'--help',
help="Affiche ce message et quitte.",
action="store_true",
)
parser.add_argument('-n',
'--no-cracklib',
help="Permet de contourner les règles de choix du mot de passe" +
"(réservé aux nounous).",
action="store_true",
)
parser.add_argument('-v',
'--verbose',
help="Permet de contourner les règles de choix du mot de passe" +
"(réservé aux nounous).",
action="store_true",
)
parser.add_argument('login',
type=str,
nargs="?",
help="L'utilisateur dont on veut changer le mot de passe.",
)
parser = argparse.ArgumentParser(
description="Recherche dans la base des adhérents",
add_help=False)
parser.add_argument('-h', '--help',
help="Affiche ce message et quitte.",
action="store_true")
parser.add_argument('-n', '--no-cracklib',
help="Permet de contourner les règles de choix du mot de passe" +
"(réservé aux nounous).",
action="store_true")
parser.add_argument('-v', '--verbose',
help="Permet de contourner les règles de choix du mot de passe" +
"(réservé aux nounous).",
action="store_true")
parser.add_argument('login', type=str, nargs="?",
help="L'utilisateur dont on veut changer le mot de passe.")
args = parser.parse_args()
if args.help:
parser.print_help()
sys.exit(0)
if args.no_cracklib:
if not lc_ldap.attributs.nounou in ldap.droits:
args.no_cracklib = False
change_password(**vars(args))

View file

@ -10,28 +10,29 @@
import os, sys
from gestion.affich_tools import prompt
from gestion.ldap_crans import crans_ldap
from lc_ldap import shortcuts
ldap = shortcuts.lc_ldap_admin()
db = crans_ldap()
uid = os.getenv('SUDO_UID')
if not uid :
print "Impossible de déterminer l'utilisateur"
sys.exit(1)
adh = ldap.search(u'uidNumber=%s' % uid,mode='w')
try:
adh = adh[0]
except IndexError:
print 'Erreur fatale lors de la consultation de la base LDAP'
sys.exit(3)
s = db.search('uidNumber=%s' % os.getenv('SUDO_UID'),'w')
# On vérifie que c'est pas un club
if unicode(adh.ldap_name)!=u"adherent":
club = s['club']
if len(club) == 1 :
print 'Pas de changement de shell pour les clubs'
sys.exit(2)
# On regarde si on a des résultats dans les adhérents
adh = s['adherent']
if len(adh) != 1 :
print 'Erreur fatale lors de la consultation de la base LDAP'
sys.exit(3)
adh = adh[0]
shell = prompt(u'Nouveau shell :')
fd=open('/etc/shells')
lines=fd.readlines()
@ -44,9 +45,7 @@ if not shell in shells:
print '\n'.join(shells)
sys.exit(4)
with adh as ad:
ad['loginShell']=shell
ad.save()
adh.chsh(shell)
adh.save()
# A cause de nscd
print "La modification sera prise en compte dans l'heure suivante."

View file

@ -4,4 +4,3 @@
from config import *
from encoding import *
import dns

View file

@ -9,35 +9,42 @@ import datetime
# Fichier généré à partir de bcfg2
from config_srv import adm_only, role
# Valeur par défaut pour les champs d'études
etudes_defaults = [
u"Établissement inconnu",
u"Année inconnue",
u"Domaine d'études inconnu"
]
gtfepoch = "19700101000000Z"
##### Gestion des câblages
# Selon la date, on met :
# -ann_scol : Année scolaire en cours
# -periode_transitoire : on accepte ceux qui ont payé l'année dernière
# On récupère l'année scolaire à tout besoin
__annee = time.localtime()[0]
# Ne modifier que les dates !
dat = time.localtime()
if dat[1] < 8 or dat[1] == 8 and dat[2] < 16:
# Si pas encore début août, on est dans l'année précédente
ann_scol = dat[0]-1
periode_transitoire = False
# sinon on change d'année
elif dat[1] < 10:
# Si pas encore octobre, les gens ayant payé l'année précédente sont
# acceptés
ann_scol = dat[0]
periode_transitoire = True
else:
# Seulement ceux qui ont payé cette année sont acceptés
ann_scol = dat[0]
periode_transitoire = False
# Prochaine période transitoire de l'année version generalizedTimeFormat
gtf_debut_periode_transitoire = "%s0816000000+0200" % (__annee,)
gtf_fin_periode_transitoire = "%s0930235959+0200" % (__annee,)
debut_periode_transitoire = time.mktime(time.strptime("%s/08/16 00:00:00" % (ann_scol,), "%Y/%m/%d %H:%M:%S"))
fin_periode_transitoire = time.mktime(time.strptime("%s/09/30 23:59:59" % (ann_scol,), "%Y/%m/%d %H:%M:%S"))
# Version timestampées timezone-naïves
debut_periode_transitoire = time.mktime(time.strptime("%s/08/16 00:00:00" % (__annee,), "%Y/%m/%d %H:%M:%S"))
fin_periode_transitoire = time.mktime(time.strptime("%s/09/30 23:59:59" % (__annee,), "%Y/%m/%d %H:%M:%S"))
## Bloquage si carte d'étudiants manquante pour l'année en cours
# /!\ Par sécurité, ces valeurs sont considérées comme False si
# periode_transitoire est True
# Soft (au niveau du Squid)
bl_carte_et_actif = not (dat[1] in [9, 10] or dat[1] == 11 and dat[2] < 7)
# Hard (l'adhérent est considéré comme paiement pas ok)
bl_carte_et_definitif = not(dat[1] == 11 and dat[2] < 17)
# On est en période transitoire si on est dans le bon intervale
periode_transitoire = (debut_periode_transitoire <= time.time() <= fin_periode_transitoire)
ann_scol = __annee
if time.time() <= debut_periode_transitoire:
ann_scol -= 1
#Sursis pour les inscription après le 1/11 pour fournir la carte étudiant
sursis_carte=8*24*3600
# Gel des cableurs pas a jour de cotisation
# Les droits ne sont pas retires mais il n'y a plus de sudo
@ -55,210 +62,95 @@ quota_hard = 10000000
fquota_soft = 0
fquota_hard = 0
# Shell
login_shell = '/bin/zsh'
club_login_shell = '/usr/bin/rssh'
login_shell='/bin/zsh'
club_login_shell='/usr/bin/rssh'
# Longueur maximale d'un login
maxlen_login = 25
maxlen_login=25
shells_possibles = [
u'/bin/csh',
u'/bin/sh', # tout caca
u'/bin/dash', # un bash light
u'/usr/bin/rc',
u'/usr/bin/ksh', # symlink vers zsh
u'/bin/ksh', # symlink vers zsh
u'/usr/bin/tcsh', # TENEX C Shell (csh++)
u'/bin/tcsh', # TENEX C Shell (csh++)
u'/bin/bash', # the Bourne-Again SHell
u'/bin/zsh', # the Z shell
u'/usr/bin/zsh', # the Z shell
u'/usr/bin/screen',
u'/bin/rbash', # Bash restreint
u'/usr/bin/rssh', # restricted secure shell allowing only scp and/or sftp
u'/usr/local/bin/badPassSh', # demande de changer de mot de passe
u'/usr/bin/passwd', # idem
u'/usr/local/bin/disconnect_shell', # déconnexion crans
u'/usr/scripts/surveillance/disconnect_shell', # idem
u'/usr/sbin/nologin', # This account is currently not available.
u'/bin/false', # vraiement méchant
u'/usr/bin/es', # n'exsite plus
u'/usr/bin/esh', # n'existe plus
u'', # le shell vide pour pouvoir les punis
]
shells_gest_crans_order = [
"zsh",
"bash",
"tcsh",
"screen",
"rbash",
"rssh",
"badPassSh",
"disconnect_shell"
shells_possibles = [u'/bin/csh',
u'/bin/sh', # tout caca
u'/bin/dash', # un bash light
u'/usr/bin/rc',
u'/usr/bin/ksh', # symlink vers zsh
u'/bin/ksh', # symlink vers zsh
u'/usr/bin/tcsh', # TENEX C Shell (csh++)
u'/bin/tcsh', # TENEX C Shell (csh++)
u'/bin/bash', # the Bourne-Again SHell
u'/bin/zsh', # the Z shell
u'/usr/bin/zsh', # the Z shell
u'/usr/bin/screen',
u'/bin/rbash', # Bash restreint
u'/usr/bin/rssh', # restricted secure shell allowing only scp and/or sftp
u'/usr/local/bin/badPassSh', # demande de changer de mot de passe
u'/usr/bin/passwd', # idem
u'/usr/local/bin/disconnect_shell', # déconnexion crans
u'/usr/scripts/surveillance/disconnect_shell', # idem
u'/usr/sbin/nologin', # This account is currently not available.
u'/bin/false', # vraiement méchant
u'/usr/bin/es', # n'exsite plus
u'/usr/bin/esh', # n'existe plus
u'', # le shell vide pour pouvoir les punis
]
shells_gest_crans_order = ["zsh", "bash", "tcsh", "screen", "rbash", "rssh",
"badPassSh", "disconnect_shell"]
shells_gest_crans = {
"zsh" : {
"path" : "/bin/zsh",
"desc" : "Le Z SHell, shell par defaut sur zamok"
},
"bash" : {
"path" : "/bin/bash",
"desc" : "Le Boune-Again SHell, shell par defaut de la plupart des linux"
},
"tcsh" : {
"path" : "/bin/tcsh",
"desc" : "C SHell ++"
},
"screen" : {
"path" : '/usr/bin/screen',
"desc" : "Un gestionnaire de fenêtre dans un terminal"
},
"rbash" : {
"path" : "/bin/rbash",
"desc" : "Un bash très restreint, voir man rbash"
},
"rssh" : {
"path" : "/usr/bin/rssh",
"desc" : "Shell ne permetant que les transferts de fichiers via scp ou sftp"
},
"badPassSh" : {
"path" : "/usr/local/bin/badPassSh",
"desc" : "Demande de changer de mot de passe à la connexion"
},
"disconnect_shell" : {
"path" : "/usr/local/bin/disconnect_shell",
"desc" : "Shell pour les suspensions de compte avec message explicatif"
},
"zsh": {"path":"/bin/zsh", "desc":"Le Z SHell, shell par defaut sur zamok"},
"bash": {"path":"/bin/bash", "desc":"Le Boune-Again SHell, shell par defaut de la plupart des linux"},
"tcsh": {"path":"/bin/tcsh", "desc":"C SHell ++"},
"screen":{"path":'/usr/bin/screen', "desc":"Un gestionnaire de fenêtre dans un terminal"},
"rbash": {"path":"/bin/rbash", "desc":"Un bash très restreint, voir man rbash"},
"rssh": {"path":"/usr/bin/rssh", "desc":"Shell ne permetant que les transferts de fichiers via scp ou sftp"},
"badPassSh":{"path":"/usr/local/bin/badPassSh", "desc":"Demande de changer de mot de passe à la connexion"},
"disconnect_shell":{"path":"/usr/local/bin/disconnect_shell", "desc":"Shell pour les suspensions de compte avec message explicatif"},
}
# Quels droits donnent l'appartenance à quel groupe Unix ?
droits_groupes = {
'adm' : [
u'Nounou',
],
'respbats' : [
u'Imprimeur',
u'Cableur',
u'Nounou',
],
'apprentis' : [
u'Apprenti',
],
'moderateurs' : [
u'Moderateur',
],
'disconnect' : [
u'Bureau',
],
'imprimeurs' : [
u'Imprimeur',
u'Nounou',
u'Tresorier',
],
'bureau' : [
u'Bureau',
],
'webadm' : [
u'Webmaster',
],
'webradio' : [
u'Webradio',
],
}
droits_groupes = {'adm' : [u'Nounou'],
'respbats' : [u'Imprimeur', u'Cableur', u'Nounou'],
'apprentis' : [u'Apprenti'],
'moderateurs' : [u'Moderateur'],
'disconnect' : [u'Bureau'],
'imprimeurs' : [u'Imprimeur', u'Nounou', u'Tresorier'],
'bureau' : [u'Bureau'],
'webadm' : [u'Webmaster'],
'webradio' : [u'Webradio'],
}
####### Les modes de paiement accepté par le crans
modePaiement = [
'liquide',
'paypal',
'solde',
'cheque',
'carte',
'comnpay',
'arbitraire',
'note',
]
modePaiement = ['liquide', 'paypal', 'solde', 'cheque', 'carte']
####### Les ML
# Le + devant un nom de ML indique une synchronisation
# ML <-> fonction partielle : il n'y a pas d'effacement automatique
# des abonnés si le droit est retiré
droits_mailing_listes = {
'roots' : [
u'Nounou',
u'Apprenti',
],
'mailman' : [
u'Nounou',
],
'+nounou' : [
u'Nounou',
u'Apprenti',
],
'respbats' : [
u'Cableur',
u'Nounou',
u'Bureau',
],
'moderateurs' : [
u'Moderateur',
u'Bureau',
],
'disconnect' : [
u'Nounou',
u'Bureau',
],
'impression' : [
u'Imprimeur',
],
'bureau' : [
u'Bureau',
],
'tresorier' : [
u'Tresorier',
],
'apprentis' : [
u'Apprenti',
],
'+ca' : [
u'Bureau',
u'Apprenti',
u'Nounou',
u'Cableur',
],
'+federez' : [
u'Bureau',
u'Apprenti',
u'Nounou',
],
'+install-party' : [
u'Bureau',
u'Apprenti',
u'Nounou',
],
droits_mailing_listes = {'roots' : [ u'Nounou', u'Apprenti'],
'mailman' : [ u'Nounou'],
'+nounou' : [ u'Nounou', u'Apprenti'],
'respbats' : [ u'Cableur', u'Nounou', u'Bureau'],
'moderateurs' : [ u'Moderateur', u'Bureau'],
'disconnect' : [ u'Nounou', u'Bureau'],
'impression' : [ u'Imprimeur'],
'bureau' : [u'Bureau'],
'tresorier' : [u'Tresorier'],
'apprentis' : [u'Apprenti'],
'+ca' : [u'Bureau', u'Apprenti', u'Nounou', u'Cableur'],
# Correspondance partielle nécessaire... Des adresses non-crans sont inscrites à ces ML.
'+dsi-crans' : [
u'Nounou',
u'Bureau',
],
'+crous-crans' : [
u'Nounou',
u'Bureau',
],
'+wrc' : [
u'Webradio',
],
}
'+federez' : [u'Bureau', u'Apprenti', u'Nounou'],
'+install-party' : [u'Bureau', u'Apprenti', u'Nounou'],
# Correspondance partielle nécessaire... Des adresses non-crans sont inscrites à ces ML.
'+dsi-crans' : [u'Nounou', u'Bureau'],
'+crous-crans' : [u'Nounou', u'Bureau'],
'+wrc' : [u'Webradio'],
}
#: Répertoire de stockage des objets détruits
cimetiere = '/home/cimetiere'
#: Adresses mac utiles
# Mac du routeur est la mac du routeur du crans (actuellement odlyd)
# Utilisé par ra2.py, à changer si le routeur est remplacé
mac_komaz = '00:19:bb:31:3b:80'
mac_du_routeur = 'a0:d3:c1:00:f4:04'
mac_komaz = 'a0:d3:c1:00:f4:04'
mac_titanic = 'aa:73:65:63:6f:76'
#: Serveur principal de bcfg2
@ -273,24 +165,24 @@ ISCSI_MAP_FILE = "/usr/scripts/var/iscsi_names_%s.py"
# IANA_id correspond à l'entier attribué par l'IANA pour l'algorithm dans les champs DNS SSHFP
# ssh_algo correspond a la première chaine de caractères donnant le nom de l'algorithme de chiffrement lorsque la clef ssh est dans le format openssh (algo key comment)
sshfp_algo = {
"rsa" : (1, "ssh-rsa"),
"dsa" : (2, "ssh-dss"),
"ecdsa-256" : (3, "ecdsa-sha2-nistp256"),
"ecdsa-384" : (3, "ecdsa-sha2-nistp384"),
"ecdsa-521" : (3, "ecdsa-sha2-nistp521"),
"ecdsa" : (3, "ecdsa-sha2-nistp521"),
}
"rsa" : (1, "ssh-rsa"),
"dsa" : (2, "ssh-dss"),
"ecdsa-256" : (3, "ecdsa-sha2-nistp256"),
"ecdsa-384" : (3, "ecdsa-sha2-nistp384"),
"ecdsa-521" : (3, "ecdsa-sha2-nistp521"),
"ecdsa" : (3, "ecdsa-sha2-nistp521"),
}
sshfs_ralgo = {}
for key, value in sshfp_algo.items():
for key,value in sshfp_algo.items():
sshfs_ralgo[value[1]] = (value[0], key)
sshfp_hash = {
"sha1" : 1,
"sha256" : 2,
"sha1" : 1,
"sha256" : 2,
}
sshkey_max_age = int(9.869604401089358 * (365.25 * 24 * 3600))
sshkey_max_age=2*(365.25*24*3600)
sshkey_size = {
'rsa':4096,
@ -325,90 +217,46 @@ plage_ens = '138.231.0.0/16'
# clefs qui cassent la bijectivité, mais qui peuvent servir.
# NETs est l'union des deux
NETs_primaires = {
'serveurs' : [
'138.231.136.0/24',
],
'adherents' : [
'138.231.137.0/24',
'138.231.138.0/23',
'138.231.140.0/22',
],
'wifi-adh' : [
'138.231.144.0/22',
'138.231.148.32/27',
'138.231.148.64/26',
'138.231.148.128/25',
'138.231.149.0/24',
'138.231.150.0/23',
],
'bornes' : [
'138.231.148.0/27',
],
'adm' : [
'10.231.136.0/24'
],
'personnel-ens' : [
'10.2.9.0/24'
],
'gratuit' : [
'10.42.0.0/16'
],
'accueil' : [
'10.51.0.0/16'
],
'federez' : [
'10.53.0.0/16'
],
'isolement' : [
'10.52.0.0/16'
],
'evenementiel' : [
'10.231.137.0/24'
],
'multicast' : [
'239.0.0.0/8'
],
'ens' : [
'138.231.135.0/24'
],
}
'serveurs' : ['138.231.136.0/24'],
'adherents' : ['138.231.137.0/24', '138.231.138.0/23', '138.231.140.0/22'],
'wifi-adh' : ['138.231.144.0/22', '138.231.148.128/25', '138.231.149.0/24', '138.231.150.0/23'],
'bornes' : ['138.231.148.0/25'],
'adm' : ['10.231.136.0/24'],
'personnel-ens' : ['10.2.9.0/24'],
'gratuit' : ['10.42.0.0/16'],
'accueil' : ['10.51.0.0/16'],
'isolement' : ['10.52.0.0/16'],
'evenementiel' : ['10.231.137.0/24'],
'multicast' : ['239.0.0.0/8'],
'ens' : ['138.231.135.0/24'],
}
NETs_secondaires = {
'all' : [
'138.231.136.0/21',
'138.231.144.0/21',
],
'wifi': [
'138.231.144.0/21',
],
'fil' : [
'138.231.136.0/21',
],
}
'all' : ['138.231.136.0/21', '138.231.144.0/21'],
'wifi': ['138.231.144.0/21'],
'fil' : ['138.231.136.0/21'],
}
NETs = {}
NETs.update(NETs_primaires)
NETs.update(NETs_secondaires)
NETs_regexp = {
'all' : r'^138\.231\.1(3[6789]|4[0123456789]|5[01])\.\d+$'
}
NETs_regexp = { 'all' : '^138\.231\.1(3[6789]|4[0123456789]|5[01])\.\d+$' }
# Classes de rid
# Merci d'essayer de les faire correspondre avec les réseaux
# ci-dessus...
# De même que pout NETs, primaires c'est pour la bijectivité, et secondaires
# pour les trucs pratiques
# https://wiki.crans.org/CransTechnique/PlanAdressage#Machines
rid_primaires = {
# Rid pour les serveurs
'serveurs' : [(0, 255),],
# Rid pour les machines fixes
'adherents' : [(256, 2047),],
# Rid pour les machines wifi
'wifi-adh' : [(2048, 3071), (3104, 4095),],
'wifi-adh' : [(2048, 3071), (3200, 4095),],
# Rid pour les bornes
'bornes' : [(3072, 3103), (34816, 35071),],
'bornes' : [(3072, 3199),],
# Rid pour machines spéciales
'special' : [(4096, 6143),],
# Rid pour les serveurs v6-only
@ -417,8 +265,8 @@ rid_primaires = {
'adherents-v6' : [(16384, 24575),],
# Rid pour les wifi v6-only
'wifi-adh-v6' : [(24576, 32767),],
# Bornes-v6
'bornes-v6' : [(34816, 35071),],
# Bornes-v6 ?
'bornes-v6' : [(32768, 33791),],
# Rid pour les machines du vlan adm
'adm-v6' : [(49152, 51199),],
# Rid pour les machines du vlan adm
@ -429,13 +277,13 @@ rid_primaires = {
'personnel-ens' : [(55296, 55551),],
# Un unique rid pour les machines multicast
'multicast' : [(65535, 65535),],
}
}
rid_secondaires = {
# Rid pour les machines filaire ipv4
'fil' : [(0, 2047),],
'wifi' : [(2048, 4095), (34816, 35071),],
}
'wifi' : [(2048, 4095),],
}
rid = {}
rid.update(rid_primaires)
@ -461,59 +309,24 @@ ipv6_machines_speciales = {
}
# Les préfixes ipv6 publics
prefix = {
'subnet' : [
'2a01:240:fe3d::/48',
],
'serveurs' : [
'2a01:240:fe3d:4::/64',
],
'adherents' : [
'2a01:240:fe3d:4::/64',
],
'fil' : [
'2a01:240:fe3d:4::/64',
],
'adm' : [
'2a01:240:fe3d:c804::/64',
],
'adm-v6' : [
'2a01:240:fe3d:c804::/64',
],
'wifi' : [
'2a01:240:fe3d:c04::/64',
],
'serveurs-v6' : [
'2a01:240:fe3d:c04::/64',
],
'adherents-v6' : [
'2a01:240:fe3d:4::/64',
],
'wifi-adh-v6' : [
'2a01:240:fe3d:c04::/64',
],
'personnel-ens' : [
'2a01:240:fe3d:4::/64',
],
'sixxs2' : [
'2a01:240:fe00:68::/64',
],
'evenementiel' : [
'2a01:240:fe3d:d2::/64',
],
'bornes' : [
'2a01:240:fe3d:c04::/64',
],
'bornes-v6' : [
'2a01:240:fe3d:c04::/64',
],
'wifi-adh' : [
'2a01:240:fe3d:c04::/64',
],
'v6only' : [
'2001:470:c8b9:a4::/64',
],
}
prefix = { 'subnet' : [ '2a01:240:fe3d::/48' ],
'serveurs' : [ '2a01:240:fe3d:4::/64' ],
'adherents' : [ '2a01:240:fe3d:4::/64' ],
'fil' : [ '2a01:240:fe3d:4::/64' ],
'adm' : [ '2a01:240:fe3d:c804::/64' ],
'adm-v6' : [ '2a01:240:fe3d:c804::/64' ],
'wifi' : [ '2a01:240:fe3d:c04::/64' ],
'serveurs-v6' : [ '2a01:240:fe3d:c04::/64' ],
'adherents-v6' : [ '2a01:240:fe3d:4::/64' ],
'wifi-adh-v6' : [ '2a01:240:fe3d:c04::/64' ],
'personnel-ens' : [ '2a01:240:fe3d:4::/64' ],
'sixxs2' : [ '2a01:240:fe00:68::/64' ],
'evenementiel' : [ '2a01:240:fe3d:d2::/64' ],
'bornes' : [ '2a01:240:fe3d:c04::/64' ],
'bornes-v6' : [ '2a01:240:fe3d:c04::/64' ],
'wifi-adh' : [ '2a01:240:fe3d:c04::/64' ],
'v6only' : [ '2001:470:c8b9:a4::/64' ],
}
# Préfixes ipv6 internes (ula)
int_prefix = {
@ -522,12 +335,10 @@ int_prefix = {
}
# Domaines dans lesquels les machines sont placées suivant leur type
domains = {
'machineFixe': 'crans.org',
'machineCrans': 'crans.org',
'machineWifi': 'wifi.crans.org',
'borneWifi': 'wifi.crans.org',
}
domains = { 'machineFixe': 'crans.org',
'machineCrans': 'crans.org',
'machineWifi': 'wifi.crans.org',
'borneWifi': 'wifi.crans.org' }
# VLans
vlans = {
@ -547,10 +358,10 @@ vlans = {
'v6only': 6,
# Vlan isolement
'isolement' : 9,
# Vlan de tests de chiffrement DSI
'chiffrement': 11,
# VLan des appartements de l'ENS
'appts': 21,
# Vlan federez-wifi
'federez': 22,
# Vlan evenementiel (install-party, etc)
'event': 10,
# Vlan zone routeur ens (zrt)
@ -559,100 +370,68 @@ vlans = {
'iscsi': 42,
# freebox (pour faire descendre la connexion au 0B)
'freebox': 8,
}
filter_policy = {
'komaz' : {
'policy_input' : 'ACCEPT',
'policy_forward' : 'ACCEPT',
'policy_output' : 'ACCEPT',
},
'zamok' : {
'policy_input' : 'ACCEPT',
'policy_forward' : 'DROP',
'policy_output' : 'ACCEPT',
},
'default' : {
'policy_input' : 'ACCEPT',
'policy_forward' : 'ACCEPT',
'policy_output' : 'ACCEPT',
}
}
filter_policy = { 'komaz' : { 'policy_input' : 'ACCEPT',
'policy_forward' : 'ACCEPT',
'policy_output' : 'ACCEPT'
},
'zamok' : { 'policy_input' : 'ACCEPT',
'policy_forward' : 'DROP',
'policy_output' : 'ACCEPT'
},
'default' : { 'policy_input' : 'ACCEPT',
'policy_forward' : 'ACCEPT',
'policy_output' : 'ACCEPT'
}
}
# Cf RFC 4890
authorized_icmpv6 = [
'echo-request',
'echo-reply',
'destination-unreachable',
'packet-too-big',
'ttl-zero-during-transit',
'parameter-problem',
]
authorized_icmpv6 = ['echo-request', 'echo-reply', 'destination-unreachable',
'packet-too-big', 'ttl-zero-during-transit', 'parameter-problem']
output_file = {
4 : '/tmp/ipt_rules',
6 : '/tmp/ip6t_rules',
}
output_file = { 4 : '/tmp/ipt_rules',
6 : '/tmp/ip6t_rules'
}
file_pickle = {
4 : '/tmp/ipt_pickle',
6 : '/tmp/ip6t_pickle',
}
file_pickle = { 4 : '/tmp/ipt_pickle',
6 : '/tmp/ip6t_pickle'
}
##################################################################################
#: Items de la blackliste
blacklist_items = {
u'bloq': u'Blocage total de tous les services',
u'paiement': u'Paiement manquant cette année',
u'virus': u'Passage en VLAN isolement',
u'upload': u"Bridage du débit montant vers l'extérieur",
u'autodisc_upload': u'Autodisconnect pour upload',
u'ipv6_ra': u'Isolement pour RA',
u'mail_invalide': u'Blocage pour mail invalide',
u'warez' : u"Présence de contenu violant de droit d'auteur sur zamok",
}
blacklist_items = { u'bloq': u'Blocage total de tous les services',
u'carte_etudiant': u'Carte etudiant manquante',
u'paiement': u'Paiement manquant cette année',
u'virus': u'Passage en VLAN isolement',
u'upload': u"Bridage du débit montant vers l'extérieur",
u'p2p': u"Blocage 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'Blocage pour mail invalide',
u'warez' : u"Présence de contenu violant de droit d'auteur sur zamok",
}
#: Blacklistes entrainant une déconnexion complète
blacklist_sanctions = [
'warez',
'virus',
'bloq',
blacklist_sanctions = ['warez', 'p2p', 'autodisc_p2p','autodisc_virus','virus', 'bloq',
'paiement',
]
if bl_carte_et_definitif:
blacklist_sanctions.append('carte_etudiant')
#: Blacklistes redirigeant le port 80 en http vers le portail captif (avec des explications)
blacklist_sanctions_soft = [
'ipv6_ra',
'mail_invalide',
'virus',
'warez',
'bloq',
'chambre_invalide',
]
blacklist_sanctions_soft = ['autodisc_virus','ipv6_ra','mail_invalide','virus',
'warez', 'p2p', 'autodisc_p2p', 'bloq','carte_etudiant','chambre_invalide']
#: Blacklistes entrainant un bridage de la connexion pour upload
blacklist_bridage_upload = ['autodisc_upload', 'upload']
##################################################################################
adm_users = [
'root',
'identd',
'daemon',
'postfix',
'freerad',
'amavis',
'nut',
'respbats',
'list',
'sqlgrey',
'ntpd',
'lp',
]
adm_users = [ 'root', 'identd', 'daemon', 'postfix', 'freerad', 'amavis',
'nut', 'respbats', 'list', 'sqlgrey', 'ntpd', 'lp' ]
open_ports = {
'tcp' : '22',
}
open_ports = { 'tcp' : '22' }
# Debit max sur le vlan de la connexion gratuite
debit_max_radin = 1000000
@ -662,90 +441,13 @@ debit_max_gratuit = 1000000
## Vlan accueil et isolement ##
###############################
accueil_route = {
'138.231.136.1' : {
'tcp' : [
'80',
'443',
'22'
],
'hosts' : [
'ssh.crans.org',
'zamok.crans.org',
],
},
'138.231.136.67' : {
'tcp' : [
'80',
'443',
],
'hosts' : [
'www.crans.org',
'wiki.crans.org',
'wifi.crans.org',
],
},
'138.231.136.98' : {
'tcp' : [
'20',
'21',
'80',
'111',
'1024:65535',
],
'udp' : [
'69',
'1024:65535',
],
'hosts' : [
'ftp.crans.org',
],
},
'138.231.136.130' : {
'tcp' : [
'80',
'443',
],
'hosts' : [
'intranet2.crans.org',
'intranet.crans.org',
],
},
'138.231.136.18' : {
'tcp' : [
'80',
'443',
],
'hosts' : [
'cas.crans.org',
'login.crans.org',
'auth.crans.org',
],
},
'213.154.225.236' : {
'tcp' : [
'80',
'443',
],
'hosts' : [
'crl.cacert.org',
],
},
'213.154.225.237' : {
'tcp' : [
'80',
'443',
],
'hosts' : [
'ocsp.cacert.org',
],
},
'138.231.136.1':{'tcp':['80','443', '22'],'hosts':['intranet.crans.org', 'ssh.crans.org', 'zamok.crans.org']},
'138.231.136.67':{'tcp':['80','443'],'hosts':['www.crans.org', 'wiki.crans.org', 'wifi.crans.org']},
'138.231.136.98':{'tcp':['20','21','80','111','1024:65535'],'udp':['69','1024:65535'], 'hosts':['ftp.crans.org']},
'138.231.136.130':{'tcp':['80','443'],'hosts':['intranet2.crans.org']},
'138.231.136.18':{'tcp':['80','443'],'hosts':['cas.crans.org', 'login.crans.org', 'auth.crans.org']},
'213.154.225.236':{'tcp':['80','443'], 'hosts':['crl.cacert.org']},
'213.154.225.237':{'tcp':['80','443'], 'hosts':['ocsp.cacert.org']},
}
dhcp_servers = ['dhcp.adm.crans.org', 'isc.adm.crans.org']
# Le bâtiment virtuel dans lequel on place des chambres qui n'existent pas, pour faire
# des tests.
bats_virtuels = ['v']
# Liste des batiments
liste_bats = ['a', 'b', 'c', 'h', 'i', 'j', 'm', 'g', 'p', 'k']

View file

@ -10,17 +10,11 @@
# Délai minimal avant de pouvoir réadhérer.
# Ne tient pas compte de la période transitoire, qui est un confort
# pour l'administration.
delai_readh_jour = 32
delai_readh_jour = 15
delai_readh = delai_readh_jour * 86400
duree_adh_an = 1
# Un compte avec une adhésion valide ne peut être détruit que lorsque celle-ci
# est expirée depuis plus que le délai indiqué ici. (secondes)
# Ici, on choisit 90 jours.
del_post_adh_jours = 90
del_post_adh = del_post_adh_jours * 86400
# Cotisation pour adhérer à l'association. Les services autres que l'accès à
# Internet sont offerts une et une fois pour toute aux personnes qui adhèrent,
# et ce dès leur première fois. (comprendre : le compte Crans et cie ne sont pas

View file

@ -3,10 +3,8 @@
""" Variables de configuration pour la gestion du DNS """
import os
# import des variables génériques
import __init__ as config
import config
#: ariane et ariane2 pour la zone parente
parents = [
@ -30,125 +28,38 @@ slaves_tv = slaves
zone_tv = 'tv.crans.org'
#: DNS en connexion de secours
secours_relay = '10.231.136.14';
secours_relay='10.231.136.14';
#: Serveurs autoritaires pour les zones crans, le master doit être le premier
DNSs = [
'sable.crans.org',
'freebox.crans.org',
'soyouz.crans.org',
]
MXs = {
'redisdead.crans.org': {
'prio': 10,
},
'freebox.crans.org': {
'prio': 25,
},
'soyouz.crans.org': {
'prio': 15,
},
}
DNSs = ['sable.crans.org', 'freebox.crans.org', 'soyouz.crans.org']
#: Résolution DNS directe, liste de toutes les zones crans hors reverse
zones_direct = [
'crans.org',
'crans.ens-cachan.fr',
'wifi.crans.org',
'clubs.ens-cachan.fr',
'adm.crans.org',
'crans.eu',
'wifi.crans.eu',
'tv.crans.org',
'ap.crans.org',
]
zones_direct = [ 'crans.org', 'crans.ens-cachan.fr', 'wifi.crans.org', 'clubs.ens-cachan.fr', 'adm.crans.org','crans.eu','wifi.crans.eu', 'tv.crans.org', 'ap.crans.org' ]
#: Les zones apparaissant dans des objets lc_ldap
zones_ldap = [
'crans.org',
'crans.ens-cachan.fr',
'wifi.crans.org',
'clubs.ens-cachan.fr',
'adm.crans.org',
'tv.crans.org',
]
zones_ldap = [ 'crans.org', 'crans.ens-cachan.fr', 'wifi.crans.org', 'clubs.ens-cachan.fr', 'adm.crans.org', 'tv.crans.org' ]
#: Zones signée par opendnssec sur le serveur master
zones_dnssec = [
'crans.org',
'wifi.crans.org',
'adm.crans.org',
'tv.crans.org',
'crans.eu',
]
zones_dnssec = ['crans.org', 'wifi.crans.org', 'adm.crans.org', 'tv.crans.org', 'crans.eu']
#: Zones alias : copie les valeur des enregistrement pour la racine de la zone et utilise un enregistemenr DNAME pour les sous domaines
zone_alias = {
'crans.org' : [
'crans.eu',
],
'crans.org' : ['crans.eu'],
}
#: Résolution inverse v4
zones_reverse = config.NETs["all"] + config.NETs["adm"] + config.NETs["personnel-ens"] + config.NETs['multicast']
#: Résolution inverse v6
zones_reverse_v6 = config.prefix['fil'] + config.prefix['wifi'] + config.prefix['adm'] + config.prefix['personnel-ens'] # à modifier aussi dans bind.py
zones_reverse_v6 = config.prefix['fil'] + config.prefix['wifi'] + config.prefix ['adm'] + config.prefix['personnel-ens'] # à modifier aussi dans bind.py
#: Serveurs DNS récursifs :
recursiv = {
'fil' : [
'138.231.136.98',
'138.231.136.152',
],
'wifi' : [
'138.231.136.98',
'138.231.136.152',
],
'evenementiel' : [
'138.231.136.98',
'138.231.136.152',
],
'adm' : [
'10.231.136.98',
'10.231.136.152',
],
'gratuit' : [
'10.42.0.164',
],
'accueil' : [
'10.51.0.10',
],
'isolement' : [
'10.52.0.10',
],
'personnel-ens' : [
'10.2.9.10',
'138.231.136.98',
'138.231.136.152',
],
'federez' : [
'138.231.136.98',
'138.231.136.152',
],
'fil' : ['138.231.136.98', '138.231.136.152'],
'wifi' : ['138.231.136.98', '138.231.136.152'],
'evenementiel' : ['138.231.136.98', '138.231.136.152'],
'adm' : ['10.231.136.98', '10.231.136.152'],
'gratuit' : ['10.42.0.164'],
'accueil' : ['10.51.0.10'],
'isolement' : ['10.52.0.10'],
'personnel-ens' : ['10.2.9.10', '138.231.136.98', '138.231.136.152'],
}
#: Domaines correspondant à des mails crans
mail_crans = [
'crans.org',
'crans.fr',
'crans.eu',
'crans.ens-cachan.fr',
]
#: Les ip/net des vlans limité vue par les récursifs
menteur_clients = [
"138.231.136.210",
"138.231.136.10",
] + config.prefix['evenementiel']
# Chemins de fichiers/dossiers utiles.
DNS_DIR = '/etc/bind/generated/'
DNSSEC_DIR = '/etc/bind/signed/'
# Fichier de définition des zones pour le maître
DNS_CONF = os.path.join(DNS_DIR, 'zones_crans')
# Fichier de définition des zones pour les esclaves géré par BCfg2
DNS_CONF_BCFG2 = "/var/lib/bcfg2/Cfg/etc/bind/generated/zones_crans/zones_crans"
menteur_clients = [ "138.231.136.210", "138.231.136.10" ] + config.prefix['evenementiel']

View file

@ -4,4 +4,3 @@ import sys
in_encoding = getattr(sys.stdin, 'encoding', None) or "UTF-8"
out_encoding = getattr(sys.stdout, 'encoding', None) or "UTF-8"
ldap_encoding = "UTF-8"

View file

@ -1,67 +1,10 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Déclaration des items accessibles à la vente (prix coûtant) et générant
une facture.
items est un dictionnaire, dont chaque entrée est composée d'un dictionnaire
ayant une désignation, un prix unitaire, et indique si l'item n'est accessible
qu'aux imprimeurs (par défaut, non)."""
# Les clef sont un code article
ITEMS = {
'CABLE': {
'designation': u'Cable Ethernet 5m',
'pu': 3.,
},
'ADAPTATEUR_TrendNet': {
'designation': u'Adaptateur 10/100 Ethernet/USB-2',
'pu': 17.,
},
'ADAPTATEUR_UGreen': {
'designation': u'Adaptateur 10/100/1000 Ethernet/USB-3',
'pu': 14.,
},
'RELIURE': {
'designation': u'Reliure plastique',
'pu': 0.12,
},
'PULL_ZIP_MARK': {
'designation': u'Zipper marqué',
'pu': 39.18,
},
'PULL_ZIP': {
'designation': u'Zipper non marqué',
'pu': 35.8,
},
'PULL_MARK': {
'designation': u'Capuche marqué',
'pu': 32.28,
},
'PULL': {
'designation': u'Capuche non marqué',
'pu': 28.92,
},
}
# Utilisé par gest_crans_lc, contient également le rachargement de solde
ITEM_SOLDE = {'SOLDE': {'designation': u'Rechargement de solde', 'pu': u'*'}}
# Dico avec les modes de paiement pour modification du solde
SOLDE = {
'liquide' : u'Espèces',
'cheque' : u'Chèque',
'carte': u'Carte bancaire',
'note': u'Note Kfet',
'arbitraire': u'Modification arbitraire du solde',
}
# Dico avec les modes de paiement pour la vente
VENTE = {
'liquide' : u'Espèces',
'cheque' : u'Chèque',
'carte': u'Carte bancaire',
'note': u'Note Kfet',
'solde': u'Vente à partir du Solde',
items = {
'CABLE' : {'designation': u'Cable Ethernet 5m', 'pu': 3, 'imprimeur': False},
'ADAPTATEUR' : {'designation': u'Adaptateur Ethernet/USB', 'pu': 17, 'imprimeur': False},
'RELIURE': {'designation': u'Reliure plastique', 'pu': 0.12, 'imprimeur': False},
'SOLDE':{'designation': u'Rechargement du solde', 'pu':'*', 'imprimeur': False},
}

View file

@ -21,7 +21,6 @@ dev = {
'wifi' : 'crans.3',
'fil' : 'crans',
'app' : 'crans.21',
'federez' : 'crans.22',
'adm' : 'crans.2',
'tun-soyouz' : 'tun-soyouz'
},
@ -53,20 +52,12 @@ mask = [24]
now=datetime.datetime.now()
if now.hour >= 6 and now.hour < 19 and now.weekday() < 5 and not is_ferie():
#: Débit maximal autorisé
debit_max = { 'total' : 250,
'out' : 250,
'wifi' : 100,
'fil' : 150 }
# mbits per second en connexion de jour
debit_max = 150 # mbits per second en connexion de jour
#: Est-ce qu'on est en connexion de jour ou de nuit/week-end ?
debit_jour = True
else:
#: Débit maximal autorisé
debit_max = { 'total' : 600,
'out' : 600,
'wifi' : 150,
'fil' : 450 }
# mbits per second en conn de nuit et du week-end
debit_max = 500 # mbits per second en conn de nuit et du week-end
#: Est-ce qu'on est en connexion de jour ou de nuit/week-end ?
debit_jour = False
@ -76,12 +67,9 @@ bl_upload_debit_max = 60 #kbytes per second
# Débit pour upload des gens en appartement ens
appt_upload_max = 1 # mbytes per second
# Debit pour l'upload de federez-wifi
federez_upload_max = 10 #mbytes per second
# Debit appartement down max
# TODO : mettre en place dans komaz.py
appt_download_max = debit_max['total']/10
appt_download_max = debit_max/10
#: Liste des réseaux non routables
reseaux_non_routables = [ '10.0.0.0/8', '172.16.0.0/12','198.18.0.0/15',

View file

@ -0,0 +1,32 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
""" Templates des mails envoyés en cas d'upload.
TODO: à migrer dans /usr/scripts/gestion/mail/templates
"""
#: Envoyé à la ML disconnect@ en cas de dépassement de la limite soft (désactivé)
message_disconnect_soft = u"""From: %(from)s
To: %(to)s
Subject: %(proprio)s uploade
Content-Type: text/plain; charset="utf-8"
%(proprio)s uploade actuellement %(upload)s Mio.
--\u0020
Message créé par deconnexion.py"""
#: Envoyé à la ML disconnect@ en cas de dépassement de la limite hard plusieurs fois
message_disconnect_multi = u"""From: %(from)s
To: %(to)s
Subject: %(proprio)s a =?utf-8?q?=C3=A9t=C3=A9_brid=C3=A9?= %(nbdeco)d fois pour upload en un mois !
Content-Type: text/plain; charset="utf-8"
L'adhérent %(proprio)s a été bridé %(nbdeco)d fois pour upload en un mois !
Le PS a été généré et se trouve sur zamok :
%(ps)s
--\u0020
Message créé par deconnexion.py"""

View file

@ -1,11 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import print_function
import sys
try:
from .services_etc import services
except ImportError:
print("Cannot import /etc/crans/services.py, continuing empty", file=sys.stderr)
services = {}

1
gestion/config/services.py Symbolic link
View file

@ -0,0 +1 @@
/etc/crans/services.py

View file

@ -1 +0,0 @@
/etc/crans/services.py

View file

@ -1,10 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
PRELOAD_MIBS = (
"STATISTICS-MIB",
"SNMPv2-SMI",
"SNMPv2-MIB",
"IF-MIB",
"CONFIG-MIB",
)

View file

@ -5,9 +5,8 @@
# License : GPLv3
import itertools
import os
debug = (int(os.environ.get('DBG_TRIGGER', 0)) == 1) or True
debug = True
log_level = "info"
# Serveur maître
@ -16,30 +15,24 @@ user = "trigger"
port = 5671
ssl = True
# TTL en secondes pour les messages en attente.
# Une suite d'opérations a faire a un ob_id, qui est un hash.
# Quand cette suite traîne depuis trop longtemps en attente sans que rien
# ne se passe, on la jette.
MSG_TTL = 3600
# Liste des services associés aux hôtes
# useradd : Envoie le mail de bienvenue, et crée le home
# userdel : Détruit le home, déconnecte l'utilisateur sur zamok, détruit les indexes dovecot, désinscrit l'adresse crans des mailing listes associées
services = {
'civet' : ["event", "ack"],
'civet' : ["event"],
'dhcp' : ["dhcp"],
'dyson' : ["autostatus"],
'isc' : ["dhcp"],
'odlyd' : ["firewall", "secours"],
'owl' : ["users"],
'redisdead' : ["mailman", "modif_ldap", "solde", "users", "secours"],
'komaz' : ["firewall", "secours"],
'owl' : ["userdel"],
'redisdead' : ["mailman", "modif_ldap", "solde", "userdel", "secours"],
'sable' : ["dns"],
'titanic' : ["secours"],
'zamok' : ["users"],
'zbee' : ["users"],
'zamok' : ["userdel"],
'zbee' : ["useradd", "userdel"],
}
# XXX - Uncomment this when in prod
#all_services = set([service for service in itertools.chain(*services.values())])
all_services = ['dhcp', 'firewall', 'secours']
all_services = ['dhcp', 'firewall']

View file

@ -3,22 +3,10 @@
""" Définitions des variables pour le contrôle d'upload. """
#: Intervalle en heures pour le comptage
interval = 24
#: liste des exemptions générales
exempt = [ ['138.231.136.0/21', '138.231.0.0/16'],
['138.231.148.0/22', '138.231.0.0/16'] ]
#: Limite en nombre de lignes pour analyse2
analyse_limit = "3000"
#: Template fichier d'analyse
analyse_file_tpl = "/usr/scripts/var/analyse/%s_%s_%s.txt"
#: Période de surveillance pour le max de décos
periode_watch = 30 * 86400
#: limite soft
soft = 1024 # Mio/24h glissantes
@ -28,6 +16,11 @@ hard = 8192 # Mio/24h glissantes
#: max déconnexions
max_decos = 7
#: envoyer des mails à disconnect@ en cas de dépassement soft ?
disconnect_mail_soft = False
#: envoyer des mails à disconnect@ en cas de dépassement hard ?
disconnect_mail_hard = True
#: expéditeur des mails de déconnexion
expediteur = "disconnect@crans.org"

View file

@ -19,7 +19,7 @@ if '/usr/scripts' not in sys.path:
from pythondialog import Dialog as PythonDialog
from pythondialog import DialogTerminatedBySignal, PythonDialogErrorBeforeExecInChildProcess
from pythondialog import error as DialogError
from gestion import affichage
from gestion.affich_tools import get_screen_size, coul
debug_enable = False
debugf = None
@ -203,7 +203,7 @@ class Dialog(object):
setattr(self, attr, ret)
return ret
def __init__(self, debug_enable=False, dialogrc=False):
def __init__(self, debug_enable=False):
signal.signal(signal.SIGINT, signal.SIG_IGN)
self.debug_enable = debug_enable
@ -211,7 +211,6 @@ class Dialog(object):
# On met un timeout à 10min d'innactivité sur dialog
self.timeout = 600
self.error_to_raise = (Continue, DialogError, ldap.SERVER_DOWN)
self.dialogrc = dialogrc
_dialog = None
@property
@ -219,9 +218,7 @@ class Dialog(object):
"""
Renvois l'objet dialog.
"""
if self.dialogrc:
self._dialog = PythonDialog(DIALOGRC=self.dialogrc)
else:
if self._dialog is None:
self._dialog = PythonDialog()
self.dialog_last_access = time.time()
return self._dialog
@ -230,7 +227,7 @@ class Dialog(object):
"""
Nyan nyan nyan nyan nyan nyan nyan nyan nyan nyan nyan nyan nyan nyan nyan nyan
"""
(cols, lines) = affichage.getTerminalSize()
(lines, cols) = get_screen_size()
print "\033[48;5;17m"
print " "*(lines * cols)
cols = int(min(cols/2, 65))

View file

@ -9,8 +9,6 @@ Licence : GPLv3
import sys
import time
import datetime
import subprocess
import pytz
import dateutil.relativedelta
if '/usr/scripts' not in sys.path:
sys.path.append('/usr/scripts')
@ -20,7 +18,6 @@ import config.cotisation
import lc_ldap.objets as objets
import lc_ldap.attributs as attributs
from lc_ldap.attributs import UniquenessError
from lc_ldap import crans_utils
import proprio
from CPS import TailCall, tailcaller, Continue
@ -48,20 +45,20 @@ class Dialog(proprio.Dialog):
'GPGFingerprint' : [a.nounou, a.soi],
'Remarques' : [a.cableur, a.nounou],
'Droits':[a.nounou, a.bureau],
'Blackliste':[a.bureau, a.nounou],
'Blackliste':[a.cableur, a.nounou],
'Vente':[a.cableur, a.nounou],
'Supprimer':[a.nounou, a.bureau],
}
menu = {
'Administratif' : {'text' : "Adhésion, chartes", "callback":self.adherent_administratif},
'Personnel' : {'text' : "Nom, prénom, téléphone, et mail de contact", 'callback':self.adherent_personnel},
'Administratif' : {'text' : "Adhésion, carte étudiant, chartes", "callback":self.adherent_administratif},
'Personnel' : {'text' : "Nom, prénom, téléphone... (ajouter l'age ?)", 'callback':self.adherent_personnel},
'Études' : {'text' : "Étude en cours", "callback":self.adherent_etudes},
'Chambre' : {'text' : 'Déménagement', "callback":self.adherent_chambre},
'Compte' : {'text' : "Gestion du compte crans", "adherent":"proprio", "callback":TailCall(self.proprio_compte, update_obj='adherent'), 'help':"Création/Suppression/Activation/Désactivation du compte, gestion des alias mails crans du compte"},
'GPGFingerprint' : {'text':'Ajouter ou supprimer une empeinte GPG', 'attribut':attributs.gpgFingerprint},
'Remarques' : {'text':'Ajouter ou supprimer une remarque à cet adhérent', 'attribut':attributs.info},
'Remarques' : {'text':'Ajouter ou supprimer une remarque de la machine', 'attribut':attributs.info},
'Droits' : {'text':"Modifier les droits alloués à cet adhérent", "callback":self.adherent_droits},
'Blackliste' : {'text': 'Modifier les blacklist de cet adhérent', 'callback':self.modif_adherent_blacklist},
'Blackliste' : {'text': 'Modifier les blacklist de la machine', 'callback':self.modif_adherent_blacklist},
'Vente' : {'text':"Chargement solde crans, vente de cable ou adaptateur ethernet ou autre", "adherent":"proprio", "callback":self.proprio_vente},
'Supprimer' : {'text':"Supprimer l'adhérent de la base de donnée", 'callback':TailCall(self.delete_adherent, del_cont=cont(proprio=None))},
}
@ -114,10 +111,6 @@ class Dialog(proprio.Dialog):
codes_todo=[([self.dialog.DIALOG_OK], todo, [tag, menu, adherent, cont_ret])]
)
def modif_adherent_attributs(self, adherent, attr, cont):
"""Juste un raccourci vers edit_attributs spécifique aux adherents"""
return self.edit_attributs(obj=adherent, update_obj='adherent', attr=attr, title="Modification de %s %s" % (adherent['prenom'][0], adherent['nom'][0]), cont=cont)
def adherent_administratif(self, cont, adherent, default_item=None):
"""Menu de gestion du compte crans d'un proprio"""
@ -126,13 +119,17 @@ class Dialog(proprio.Dialog):
"Adhésion": [a.cableur, a.nounou],
'Connexion': [a.cableur, a.nounou],
"Charte MA" : [a.nounou, a.bureau],
"Carte Étudiant" : [a.nounou, a.cableur, a.tresorier],
}
menu = {
"Adhésion" : {"text":"Pour toute réadhésion *sans* connexion.", "help":"", "callback":self.adherent_adhesion},
'Connexion' : {'text': "Mise à jour de l'accès Internet (effectue la réadhésion si besoin)", "help":"", 'callback':self.adherent_connexion},
"Carte Étudiant" : {"text" : "Validation de la carte étudiant", "help":"", "callback":self.adherent_carte_etudiant},
"Charte MA" : {"text" : "Signature de la charte des membres actifs", "help":'', "callback":self.adherent_charte},
}
menu_order = ["Adhésion", 'Connexion']
if self.has_right(a.tresorier, adherent) or not adherent.carte_controle():
menu_order.append("Carte Étudiant")
menu_order.append("Charte MA")
def box(default_item=None):
return self.dialog.menu(
@ -220,7 +217,8 @@ class Dialog(proprio.Dialog):
# Boite si on ne peux pas réahdérer
def box_already(end):
self.dialog.msgbox("Actuellement adhérent jusqu'au %s.\nMerci de revenir lorsqu'il restera moins de %s jours avant la fin." % (end, config.cotisation.delai_readh_jour),
t_end = time.strftime('%d/%m/%Y %H:%M:%S', time.localtime(end))
self.dialog.msgbox("Actuellement adhérent jusqu'au %s.\nMerci de revenir lorsqu'il restera moins de %s jours avant la fin." % (t_end, config.cotisation.delai_readh_jour),
width=0,
height=0,
timeout=self.timeout,
@ -228,8 +226,9 @@ class Dialog(proprio.Dialog):
# Boite de confirmation à l'ahésion
def box_adherer(end=None):
if end != crans_utils.localized_datetime():
adherer = self.confirm(text="Adhésion jusqu'au %s. Réadhérer ?" % end, title="Adhésion de %s %s" % (adherent.get("prenom", [''])[0], adherent["nom"][0]))
if end:
t_end = time.strftime('%d/%m/%Y %H:%M:%S', time.localtime(end))
adherer = self.confirm(text="Adhésion jusqu'au %s. Réadhérer ?" % t_end, title="Adhésion de %s %s" % (adherent.get("prenom", [''])[0], adherent["nom"][0]))
else:
adherer = self.confirm(text="Adhésion pour un an, continuer ?", title="Adhésion de %s %s" % (adherent.get("prenom", [''])[0], adherent["nom"][0]))
return adherer
@ -243,8 +242,9 @@ class Dialog(proprio.Dialog):
# Génération de la facture pour adhésion
def paiement(tag_paiement, adherent, finadhesion, comment, facture, cancel_cont, cont):
now = crans_utils.localized_datetime()
new_finadhesion = max(finadhesion, now).replace(year=max(finadhesion, now).year + 1)
now = time.time()
new_finadhesion = datetime.datetime.fromtimestamp(max(finadhesion, now))
new_finadhesion = time.mktime(new_finadhesion.replace(year=new_finadhesion.year + config.cotisation.duree_adh_an).timetuple()) + 86400
new_debutadhesion = now
if facture:
facture = self.conn.search(dn=facture.dn, scope=0, mode='rw')[0]
@ -256,8 +256,8 @@ class Dialog(proprio.Dialog):
facture['modePaiement'] = unicode(tag_paiement, 'utf-8')
facture['info'] = unicode(comment, 'utf-8')
facture['article'].append(config.cotisation.dico_adh)
facture["finAdhesion"] = new_finadhesion
facture["debutAdhesion"] = new_debutadhesion
facture["finAdhesion"] = unicode(new_finadhesion)
facture["debutAdhesion"] = unicode(new_debutadhesion)
# On peut retarder le credit pour ajouter des contribution pour la connexion internet à la facture
if crediter:
if self.confirm_item(item=facture,
@ -280,13 +280,9 @@ class Dialog(proprio.Dialog):
raise Continue(cont(adherent=adherent))
now = crans_utils.localized_datetime()
try:
finadhesion = adherent.fin_adhesion().value
except AttributeError:
finadhesion = now
finadhesion = adherent.fin_adhesion()
# Si fin de l'adhésion trop loin dans le futur, rien a faire
if finadhesion and (finadhesion - now).days > config.cotisation.delai_readh_jour:
if finadhesion and finadhesion - config.cotisation.delai_readh > time.time():
self.handle_dialog(cancel_cont if cancel_cont else cont, box_already, finadhesion)
raise Continue(cancel_cont if cancel_cont else cont)
@ -340,9 +336,9 @@ class Dialog(proprio.Dialog):
# Une boite pour choisir un nombre de mois pour prolonger la connexion
def box(finconnexion, default_item=None):
t_end = finconnexion
t_end = time.strftime('%d/%m/%Y %H:%M:%S', time.localtime(finconnexion))
return self.dialog.menu(
"Connexion jusqu'au %s" % t_end if finconnexion != datetime.datetime.fromtimestamp(0, tz=pytz.utc) else "N'a jamais été connecté",
"Connexion jusqu'au %s" % t_end if finconnexion else "N'a jamais été connecté",
width=0,
height=0,
menu_height=0,
@ -357,16 +353,17 @@ class Dialog(proprio.Dialog):
# Génération et crédit de la facture
def todo(adherent, mois, finadhesion, finconnexion, cancel_cont, cont, facture=None, tag_paiment=None, comment=None):
now = crans_utils.localized_datetime()
now = time.time()
new_finconnexion = datetime.datetime.fromtimestamp(max(finconnexion, now))
# On ajoute 3600 secondes sur suggestion de Raphaël Bonaque (<bonaque@crans.org>), pour tenir compte des malheureux qui
# pourraient subir le changement d'heure.
new_finconnexion = time.mktime((new_finconnexion + dateutil.relativedelta.relativedelta(months=mois)).timetuple()) + 3600
new_debutconnexion = max(now, finconnexion)
con_month = new_debutconnexion.month
con_year = new_debutconnexion.year
new_finconnexion = max(finconnexion, now).replace(year=con_year + ((con_month + mois) // 13), month= (con_month + mois - 1) % 12 + 1)
if (new_finconnexion - finadhesion.value).days > 0:
t_end_adh = finadhesion.value
t_end_conn = finconnexion
if (new_finconnexion - finadhesion.value).days > 30:
if new_finconnexion > finadhesion:
t_end_adh = time.strftime('%d/%m/%Y %H:%M:%S', time.localtime(finadhesion))
t_end_conn = time.strftime('%d/%m/%Y %H:%M:%S', time.localtime(new_finconnexion))
if new_finconnexion - finadhesion > 30 * 3600 * 24:
raise ValueError("Impossible de prolonger la connexion jusqu'au %s plus d'un mois après la fin de l'adhésion au %s" % (t_end_conn, t_end_adh))
else:
if not self.confirm("La fin de la connexion de l'adhérent (%s) tombera après la fin de son adhésion (%s).\n" \
@ -376,8 +373,8 @@ class Dialog(proprio.Dialog):
if facture:
with self.conn.search(dn=facture.dn, scope=0, mode='rw')[0] as facture:
if mois:
facture["finConnexion"] = new_finconnexion
facture["debutConnexion"] = new_debutconnexion
facture["finConnexion"] = unicode(new_finconnexion)
facture["debutConnexion"] = unicode(new_debutconnexion)
facture["article"].append(config.cotisation.dico_cotis(mois))
if self.confirm_item(item=facture,
text=u"Le paiement de %sEUR a-t-il bien été reçu (mode : %s) ?\n" % (facture.total(), facture['modePaiement'][0]),
@ -400,8 +397,8 @@ class Dialog(proprio.Dialog):
facture['modePaiement'] = unicode(tag_paiment, 'utf-8')
facture['article'].append(config.cotisation.dico_cotis(mois))
facture['info'] = unicode(comment, 'utf-8')
facture["finConnexion"] = new_finconnexion
facture["debutConnexion"] = new_debutconnexion
facture["finConnexion"] = unicode(new_finconnexion)
facture["debutConnexion"] = unicode(new_debutconnexion)
if self.confirm_item(item=facture,
text=u"Le paiement de %sEUR a-t-il bien été reçu (mode : %s) ?\n" % (facture.total(), tag_paiment),
title=u"Validation du paiement",
@ -411,7 +408,7 @@ class Dialog(proprio.Dialog):
else:
if not self.confirm(text=u"Le paiement n'a pas été reçue.\n Annuler ?", title="Annulation de l'adhésion", defaultno=True):
raise Continue(cancel_cont)
raise Continue(cont)
raise Continue(cont(adherent=adherent))
def todo_mois(tag, self_cont):
if tag == 'An':
@ -433,18 +430,16 @@ class Dialog(proprio.Dialog):
finconnexion = adherent.fin_connexion()
# Si l'adhésion fini avant la connexion
if finadhesion <= crans_utils.localized_datetime() or finadhesion <= finconnexion:
if finadhesion <= time.time() or finadhesion <= finconnexion:
if finadhesion:
t_end_adh = time.strftime('%d/%m/%Y %H:%M:%S', time.localtime(finadhesion))
# Si l'adhésion est déjà fini
if finadhesion <= crans_utils.localized_datetime():
if finadhesion == datetime.datetime.fromtimestamp(0, tz=pytz.utc):
self.dialog.msgbox(text=u"L'adhérent n'a jamais adhéré à l'association, on va d'abord le faire adhérer (10€)", title="Adhésion nécessaire", width=0, height=0, timeout=self.timeout)
else:
self.dialog.msgbox(text=u"L'adhésion a expiré le %s, il va falloir réadhérer d'abord (10€)" % finadhesion, title="Réadhésion nécessaire", width=0, height=0, timeout=self.timeout)
if finadhesion <= time.time():
self.dialog.msgbox(text=u"L'adhésion a expiré le %s, il va falloir réadhérer d'abord" % t_end_adh, title="Réadhésion nécessaire", width=0, height=0, timeout=self.timeout)
# Sinon si elle fini avant la fin de la connexion courante
elif finadhesion < finconnexion:
t_end_conn = finconnexion
self.dialog.msgbox(text=u"L'adhésion de termine le %s, avant la fin de la connexion le %s, il va falloir réadhérer d'abord (10€)" % (finadhesion, t_end_conn), title="Réadhésion nécessaire", width=0, height=0, timeout=self.timeout)
t_end_conn = time.strftime('%d/%m/%Y %H:%M:%S', time.localtime(finconnexion))
self.dialog.msgbox(text=u"L'adhésion de termine le %s, avant la fin de la connexion le %s, il va falloir réadhérer d'abord" % (t_end_adh, t_end_conn), title="Réadhésion nécessaire", width=0, height=0, timeout=self.timeout)
# Échouera si on essaie de prolonger la connexion au dela de l'adhésion et que l'adhésion est encore valable plus de quinze jours
return self.adherent_adhesion(cont=self_cont, cancel_cont=cont, adherent=adherent, crediter=False)
@ -484,6 +479,76 @@ class Dialog(proprio.Dialog):
return self.proprio_choose_paiement(proprio=adherent, cont=self_cont, cancel_cont=lcont)
return cont
def adherent_carte_etudiant(self, cont, adherent, values={}, cancel_cont=None):
# Dictionnaire décrivant quelle est la valeur booléenne à donner à l'absence de l'attribut
a = attributs
choices = []
if self.has_right(a.tresorier, adherent) or not adherent.carte_controle():
choices.append((a.carteEtudiant.ldap_name, "Carte étudiant présentée", 1 if adherent[a.carteEtudiant.ldap_name] or values.get(a.carteEtudiant.ldap_name, False) else 0))
if self.has_right(a.tresorier, adherent):
choices.append(("controleCarte", "La carte a-t-elle été controlée", 1 if adherent.carte_controle() or values.get("controleCarte", False) else 0))
if not choices:
self.dialog.msgbox("Carte d'étudiant déjà validée et non modifiable", title="Gestion de la carte étudiant", width=0, height=0)
if cancel_cont:
cancel_cont(cont=cont)
try:
cont(cancel_cont=cancel_cont)
except TypeError:
pass
raise Continue(cont)
def box():
return self.dialog.checklist("Gestion de la carte étudiant",
height=0,
width=0,
timeout=self.timeout,
list_height=7,
choices=choices,
title="Gestion de la carte étudiant")
def todo(values, adherent, cont):
# On met à jour chaque attribut si sa valeur à changé
with self.conn.search(dn=adherent.dn, scope=0, mode='rw')[0] as adherent:
# Si on est trésorier et que controleCarte a changer on enregistre le changement
if self.has_right(a.tresorier, adherent) and values["controleCarte"] and not adherent.carte_controle():
if adherent["controle"]:
adherent["controle"] = u"c%s" % adherent["controle"][0]
else:
adherent["controle"] = u"c"
elif self.has_right(a.tresorier, adherent) and not values["controleCarte"] and adherent.carte_controle():
adherent["controle"] = unicode(adherent["controle"][0]).replace('c','')
if not adherent["controle"][0]:
adherent["controle"] = []
# Si la carte n'est pas validé ou qu'on est trésorier, on sauvegarde les changements
if values[a.carteEtudiant.ldap_name] and not adherent[a.carteEtudiant.ldap_name] and (not adherent.carte_controle() or self.has_right(a.tresorier, adherent)):
adherent[a.carteEtudiant.ldap_name] = u"TRUE"
elif not values[a.carteEtudiant.ldap_name] and adherent[a.carteEtudiant.ldap_name] and (not adherent.carte_controle() or self.has_right(a.tresorier, adherent)):
adherent[a.carteEtudiant.ldap_name] = []
if adherent["controle"]:
adherent["controle"] = unicode(adherent["controle"][0]).replace('c','')
if not adherent["controle"][0]:
adherent["controle"] = []
adherent.validate_changes()
adherent.history_gen()
adherent.save()
# On s'en va en mettant à jour dans la continuation la valeur de obj
raise Continue(cont(adherent=adherent))
(code, output) = self.handle_dialog(cont, box)
# On transforme la liste des cases dialog cochée en dictionnnaire
values = dict((a[0], a[0] in output) for a in choices)
# Une continuation que l'on suivra si quelque chose se passe mal
retry_cont = TailCall(self.adherent_carte_etudiant, adherent=adherent, cont=cont, values=values)
return self.handle_dialog_result(
code=code,
output=output,
cancel_cont=cancel_cont if cancel_cont else cont,
error_cont=retry_cont,
codes_todo=[([self.dialog.DIALOG_OK], todo, [values, adherent, cont])]
)
def adherent_charte(self, cont, adherent):
a = attributs
attribs = [a.charteMA]
@ -547,15 +612,6 @@ class Dialog(proprio.Dialog):
with self.conn.search(dn=adherent.dn, scope=0, mode='rw')[0] as adherent:
for (key, values) in attrs.items():
adherent[key] = values
# On retire les éventuelle bl mail invalide
if key == u'mailExt' or key == u'mail':
for bl in adherent['blacklist']:
now = int(time.time())
if bl['type'] == u'mail_invalide' and bl['fin'] > now:
bl['fin'] = now
if bl['debut'] > now:
bl['debut'] = now
bl['comm'] += u'- mail rectifié'
adherent.validate_changes()
adherent.history_gen()
adherent.save()
@ -593,13 +649,6 @@ class Dialog(proprio.Dialog):
if self.confirm_item(adherent, title="Créer l'adhérent suivant ?"):
adherent.validate_changes()
adherent.create()
if make_compte_crans:
if self.dialog.yesno("Imprimer un ticket avec un mot de passe attribué automatiquement ?",
title="Impression de ticket pour %s %s" % (adherent.get('prenom', [''])[0], adherent["nom"][0]),
timeout=self.timeout
) == self.dialog.DIALOG_OK:
subprocess.call(['/usr/scripts/cransticket/dump_creds.py', '--forced', '--pass', 'aid=%s' % adherent['aid'][0]])
self.display_item(adherent, "Impression du ticket en cours ...")
else:
adherent = None
return adherent
@ -863,11 +912,13 @@ class Dialog(proprio.Dialog):
"""Crée un adhérent et potentiellement son compte crans avec lui"""
def mycont(adherent=None, **kwargs):
if adherent:
# Une fois l'adhérent créé, on vois s'il adhére/prend la connexion internet
# Une fois l'adhérent créé, on vois s'il donne sa carte étudiant et s'il adhére/prend la connexion internet
#adh_cont = TailCall(self.modif_adherent, cont=cont, adherent=adherent)
conn_cont = TailCall(self.adherent_connexion, cont=cont(proprio=adherent), adherent=adherent)
etude_cont = TailCall(self.adherent_etudes, cont=conn_cont, adherent=adherent)
carte_cont = TailCall(self.adherent_carte_etudiant, cont=conn_cont, adherent=adherent)
etude_cont = TailCall(self.adherent_etudes, cont=carte_cont, adherent=adherent)
etude_cont(cancel_cont=etude_cont)
carte_cont(cancel_cont=etude_cont)
# Comme on crée une facture, pas de retour possible
conn_cont(cancel_cont=conn_cont)
raise Continue(etude_cont)

View file

@ -13,7 +13,7 @@ import traceback
if '/usr/scripts' not in sys.path:
sys.path.append('/usr/scripts')
from gestion import affichage
from gestion.affich_tools import coul
import gestion.config as config
import lc_ldap.objets as objets
@ -37,14 +37,10 @@ class Dialog(lc.Dialog):
index = 0
for bl in obj['blacklist']:
choices.append(
(
str(index),
affichage.style(
"%s [%s]" % (bl['type'], bl['comm']),
'rouge' if bl['actif'] else None,
dialog=True
)
)
(str(index),
coul("%s [%s]" % (bl['type'], bl['comm']), 'rouge' if bl['actif'] else None,
dialog=True)
)
)
index+=1
return self.dialog.menu(
@ -149,7 +145,7 @@ class Dialog(lc.Dialog):
fin_tuple = self.get_timestamp(title=title, text="Choisir la date de fin :",
cont=self_cont(bl=bl, tag=tag, bl_type=bl_type,
debut=None, fin=None, comm=None))
fin = int(time.mktime(time.struct_time(fin_tuple + (0, 0, -1))))
fin = int(time.mktime(time.struct_time(debut_tuple + (0, 0, -1))))
else:
fin = '-'
bl['debut']=debut

View file

@ -6,18 +6,16 @@ Copyright (C) Valentin Samir
Licence : GPLv3
"""
import os
import sys
import time
import ldap
import traceback
import locale
if '/usr/scripts' not in sys.path:
sys.path.append('/usr/scripts')
from pythondialog import Dialog
from pythondialog import error as DialogError
from gestion import affichage
from gestion.affich_tools import get_screen_size, coul
import lc_ldap.shortcuts
import lc_ldap.objets as objets
@ -29,17 +27,12 @@ from CPS import TailCall, tailcaller, Continue, TailCaller
class Dialog(CPS.Dialog):
def __init__(self, debug_enable=False, ldap_test=False, custom_user=None):
super(Dialog, self).__init__()
super(Dialog, self).__init__(debug_enable=debug_enable)
# On initialise le moteur de rendu en spécifiant qu'on va faire du dialog
printing.template(dialog=True)
self.ldap_test = ldap_test
if custom_user:
custom_user = custom_user.decode(locale.getdefaultlocale()[1] or "ascii")
self.custom_user = custom_user
self.check_ldap()
login = self.conn.current_login
dialogrc='/home/%s/.dialogrc' % login
super(Dialog, self).__init__(debug_enable=debug_enable, dialogrc=dialogrc)
def has_right(self, liste, obj=None):
"""Vérifie que l'un des droits de l'utilisateur courant est inclus dans list"""
@ -347,7 +340,7 @@ class Dialog(CPS.Dialog):
# pour prendre en compte la largeur du widget dialog
del items[:] # On vide la liste pour la modifier en place
items_id = {}
(col, line) = affichage.getTerminalSize()
(line, col) = get_screen_size()
for c in classes:
items.extend(olist[c])
items_s = printing.sprint_list(olist[c], col-20).encode('utf-8').split('\n')

View file

@ -12,7 +12,6 @@ if '/usr/scripts' not in sys.path:
import lc_ldap.objets as objets
import lc_ldap.attributs as attributs
import subprocess
import certificat
import blacklist
@ -35,12 +34,10 @@ class Dialog(certificat.Dialog, blacklist.Dialog):
"""
a = attributs
# Quel sont les attributs ldap dont on veut afficher et la taille du champs d'édition correspondant
to_display = [(a.host, 30), (a.macAddress, 17), (a.ipHostNumber, 15)]
to_display_port = [(a.portTCPout, 50), (a.portTCPin, 50), (a.portUDPout, 50),
(a.portUDPin, 50)]
to_display_borne = [(a.canal, 10), (a.hotspot, 10), (a.puissance, 10), (a.positionBorne, 50), (a.nvram, 10)]
to_display = [(a.host, 30), (a.macAddress, 17), (a.ipHostNumber, 15),
(a.portTCPout, 50), (a.portTCPin, 50), (a.portUDPout, 50),
(a.portUDPin, 50)
]
# Quel séparateur on utilise pour les champs multivalué
separateur = ' '
@ -61,19 +58,15 @@ class Dialog(certificat.Dialog, blacklist.Dialog):
title="Paramètres machine",
backtitle="Gestion des machines du Crans")
def check_host(host, objectClass, realm):
def check_host(host, objectClass):
# Si c'est une machine wifi, host doit finir par wifi.crans.org
if "machineWifi" == objectClass or 'borneWifi' == objectClass or realm == 'bornes':
if "machineWifi" == objectClass or 'borneWifi' == objectClass:
hostend = ".wifi.crans.org"
# Si c'est une machine wifi, host doit finir par crans.org
elif "machineFixe" == objectClass or realm == 'serveurs':
elif "machineFixe" == objectClass:
hostend = ".crans.org"
# Si l'object class est machineCrans, pas de vérification
elif "machineCrans" == objectClass:
if realm == 'adm':
hostend = ".adm.crans.org"
if not '.' in host:
host = host + hostend
return host
# Sinon, libre à chachun d'ajouter d'autres objectClass ou de filtrer
# plus finement fonction des droits de self.conn.droits
@ -81,7 +74,7 @@ class Dialog(certificat.Dialog, blacklist.Dialog):
raise ValueError("La machine n'est ni une machine fixe, ni une machine wifi mais %s ?!?" % objectClass)
if not host.endswith(hostend) and not '.' in host:
host = host + hostend
host = "%s.wifi.crans.org" % host
elif host.endswith(hostend) and '.' in host[:-len(hostend)]:
raise ValueError("Nom d'hôte invalide, devrait finir par %s et être sans point dans la première partie" % hostend)
elif not host.endswith(hostend) and '.' in host:
@ -92,8 +85,7 @@ class Dialog(certificat.Dialog, blacklist.Dialog):
def modif_machine(machine, attrs):
with self.conn.search(dn=machine.dn, scope=0, mode='rw')[0] as machine:
for (key, values) in attrs.items():
if values!=u'<automatique>' or key != 'ipHostNumber':
machine[key]=values
machine[key]=values
machine.validate_changes()
machine.history_gen()
machine.save()
@ -109,18 +101,13 @@ class Dialog(certificat.Dialog, blacklist.Dialog):
}
with self.conn.newMachine(proprio.dn, realm, ldif) as machine:
for (key, values) in attrs.items():
if values!=u'<automatique>' or key != u'ipHostNumber':
machine[key]=values
machine[key]=values
if attributs.ipsec in machine.attribs:
machine[attributs.ipsec.ldap_name]=attributs.ipsec.default
machine.validate_changes()
if self.confirm_item(machine, "Voulez vous vraiement créer cette machine ?"):
machine.create()
self.display_item(machine, "La machine a bien été créée", ipsec=True)
if realm == 'wifi-adh':
if self.dialog.yesno("Imprimer un ticket pour la machine ?", timeout=self.timeout, title="Impression de ticket", width=50) == self.dialog.DIALOG_OK:
subprocess.call(['/usr/scripts/cransticket/dump_creds.py', '--forced', 'mid=%s' % machine['mid'][0]])
self.display_item(machine, "Impression du ticket ...", ipsec=True)
self.display_item(machine, "La machine à bien été créée", ipsec=True)
return machine
else:
raise Continue(cont)
@ -136,7 +123,7 @@ class Dialog(certificat.Dialog, blacklist.Dialog):
values = [v for v in values.split(separateur) if v]
# Pour host, on fait quelques vérification de syntaxe
if a.ldap_name == 'host':
attrs[a.ldap_name]=check_host(values, objectClass, realm)
attrs[a.ldap_name]=check_host(values, objectClass)
else:
attrs[a.ldap_name]=values
# Soit on édite une machine existante
@ -147,16 +134,10 @@ class Dialog(certificat.Dialog, blacklist.Dialog):
machine = create_machine(proprio, realm, attrs)
raise Continue(cont(machine=machine))
if machine:
objectClass = machine["objectClass"][0]
if self.has_right(a.nounou, proprio):
to_display += to_display_port
# Les bornes wifi ont un to_display différent
if objectClass == 'borneWifi':
to_display += to_display_borne
(code, tags) = self.handle_dialog(cont, box)
# On prépare les fiels à afficher à l'utilisateur si une erreure à lieu
@ -183,6 +164,11 @@ class Dialog(certificat.Dialog, blacklist.Dialog):
"""Juste un raccourci vers edit_attributs spécifique aux machines"""
return self.edit_attributs(obj=machine, update_obj='machine', attr=attr, title="Modification de la machine %s" % machine['host'][0], cont=cont)
def modif_adherent_attributs(self, adherent, attr, cont):
"""Juste un raccourci vers edit_attributs spécifique aux adherents"""
return self.edit_attributs(obj=adherent, update_obj='adherent', attr=attr, title="Modification de %s %s" % (adherent['prenom'][0], adherent['nom'][0]), cont=cont)
def modif_machine_boolean(self, machine, cont):
"""Juste un raccourci vers edit_boolean_attributs spécifique aux machines"""
a = attributs
@ -207,8 +193,8 @@ class Dialog(certificat.Dialog, blacklist.Dialog):
menu_droits = {
'Information' : [a.parent, a.cableur, a.nounou],
'Autre': [a.parent, a.cableur, a.nounou],
'Blackliste':[a.nounou],
'Certificat': [a.parent, a.nounou],
'Blackliste':[a.cableur, a.nounou],
'Certificat': [a.parent, a.cableur, a.nounou],
'Exemption' : [a.nounou],
'Alias' : [a.parent, a.cableur, a.nounou],
'Remarques' : [a.cableur, a.nounou],
@ -270,41 +256,24 @@ class Dialog(certificat.Dialog, blacklist.Dialog):
menu_droits = {
'Fixe' : [a.soi, a.cableur, a.nounou],
'Wifi' : [a.soi, a.cableur, a.nounou],
'Appartements': [a.soi, a.cableur, a.nounou],
}
menu = {
'Fixe' : {'text' : "Machine filaire", 'objectClass':'machineFixe', 'realm':'adherents'},
'Appartements' : {'text' : "Machine filaire de personnel ENS", 'objectClass':'machineFixe', 'realm':'personnel-ens'},
'Wifi' : {'text': 'Machine sans fil', 'objectClass':'machineWifi', 'realm':'wifi-adh'},
}
menu_order = ['Wifi']
# Machine appartement pour les personnels, fixe pour les autres
if proprio.get('etudes', [False])[0] == u'Personnel ENS':
menu_order.append('Appartements')
else:
# On vérifie que un non MA a qu'une machine fixe
menu_order.append('Fixe')
if not bool(proprio.get('droits', False)) and isinstance(proprio, objets.adherent):
for machine in proprio.machines():
if isinstance(machine, objets.machineFixe):
menu_order.remove('Fixe')
break
menu_order = ['Fixe', 'Wifi']
if isinstance(proprio, objets.AssociationCrans):
menu_droits.update({
'Fixe' : [a.nounou],
'Wifi' : [a.nounou],
'Wifi-v6' : [a.nounou],
'Adm' : [a.nounou],
})
menu.update({
'Fixe' : {'text' : "Ajouter un serveur sur le vlan adherent", 'objectClass':'machineCrans', 'realm':'serveurs'},
'Wifi' : {'text': 'Ajouter une borne WiFi sur le vlan wifi', 'objectClass':'borneWifi', 'realm':'bornes'},
'Wifi-v6' : {'text': 'Ajouter une borne WiFi sur le vlan wifi en ipv6 only', 'objectClass':'borneWifi', 'realm':'bornes-v6'},
'Adm' : {'text' : "Ajouter un serveur sur le vlan adm", "objectClass":"machineCrans", 'realm':'adm'},
})
menu_order += ['Adm', 'Wifi-v6']
menu_order.append('Adm')
def box(default_item=None):
return self.dialog.menu(
"Type de Machine ?",

View file

@ -85,10 +85,11 @@ class Dialog(machine.Dialog, blacklist.Dialog):
@tailcaller
def set_password(proprio, update_obj, cont):
if self.dialog.yesno("Attribuer un mot de passe maintenant ? (Vous aurez la possibilité d'imprimer un ticket plus tard également ...)",
if self.dialog.yesno("Attribuer un mot de passe maintenant ?",
title="Création du compte de %s %s" % (proprio.get('prenom', [''])[0], proprio["nom"][0]),
timeout=self.timeout
) == self.dialog.DIALOG_OK:
#return self.proprio_compte_password(proprio=proprio, return_obj=return_obj, cont=cont(**{update_obj:proprio}))
proprio = self.proprio_compte_password(proprio=proprio, return_obj=True, cont=TailCall(set_password, proprio, update_obj, cont))
if return_obj:
return proprio
@ -167,7 +168,7 @@ class Dialog(machine.Dialog, blacklist.Dialog):
raise Continue(cont(proprio=proprio))
#(code, passwords) = self.handle_dialog(cont, box)
(code, passwords) = (self.dialog.DIALOG_OK, "")
self_cont = TailCall(self.proprio_compte_password, proprio=proprio, cont=cont, return_obj=return_obj)
self_cont = TailCall(self.proprio_compte_password, proprio=proprio, cont=cont)
return self.handle_dialog_result(
code=code,
output=passwords,
@ -410,19 +411,14 @@ class Dialog(machine.Dialog, blacklist.Dialog):
"cheque" : "Chèque",
"carte" : "Par carte bancaire",
"solde" : "Solde Crans (actuel : %s€)",
"note" : "Note kfet (attention, moins tracable...)",
"arbitraire" : "Création ou destruction magique d'argent.",
}
def box_choose_paiment(tag, articles):
box_paiement_order = ["liquide", "cheque", "carte","note"]
box_paiement_order = ["liquide", "cheque", "carte"]
if "cransAccount" in proprio['objectClass']:
if not "SOLDE" in [art['code'] for art in articles] and proprio["solde"]:
box_paiement_order.append("solde")
box_paiement["solde"] = box_paiement["solde"] % proprio["solde"][0]
if len(articles) == 1 and "SOLDE" in [art['code'] for art in articles]:
box_paiement_order.append("arbitraire")
choices = []
for key in box_paiement_order:
choices.append((key, box_paiement[key], 1 if key == tag else 0))
@ -461,8 +457,7 @@ class Dialog(machine.Dialog, blacklist.Dialog):
def box_choose_item(tags):
choices = []
gestion.config.factures.ITEMS.update(gestion.config.factures.ITEM_SOLDE)
for code, article in gestion.config.factures.ITEMS.items():
for code, article in gestion.config.factures.items.items():
choices.append((code, u"%s%s" % (article['designation'], (u' (%s€)' % article['pu']) if article['pu'] != '*' else ""), 1 if code in tags else 0))
return self.dialog.checklist(
text="",
@ -504,19 +499,11 @@ class Dialog(machine.Dialog, blacklist.Dialog):
def paiement(have_set, tag, proprio, comment, cancel_cont, cont):
articles = copy.deepcopy(have_set)
# On formate les articles
for article in articles:
if article['pu'] == '*':
article['pu'] = article['nombre']
article['nombre'] = 1
# En arbitraire, on accepte que le solde
if tag == u"arbitraire":
if len(articles) > 1 or "SOLDE" not in [art['code'] for art in articles]:
raise ValueError("Il n'est possible que de faire une opération de solde en mode arbitraire")
# Les articles classiques on facture
with self.conn.newFacture(proprio.dn, {}) as facture:
facture['modePaiement']=unicode(tag, 'utf-8')
facture['article']=articles
@ -530,13 +517,13 @@ class Dialog(machine.Dialog, blacklist.Dialog):
arts = ["%s %s" % (art['nombre'], art['designation']) for art in facture['article'] if art['code'] != 'SOLDE']
if arts:
self.dialog.msgbox(
text=u"Vous pouvez remettre à l'adherent les articles (si ce sont des articles) suivant :\n * %s" % '\n * '.join(arts),
title=u"Vente terminée",
width=0, height=0, timeout=self.timeout)
text=u"Vous pouvez remettre à l'adherent les articles (si se sont des articles) suivant :\n * %s" % '\n * '.join(arts),
title=u"Vente terminée",
width=0, height=0, timeout=self.timeout)
if tag == "solde":
self.dialog.msgbox(text=u"Le solde de l'adhérent a bien été débité", title="Solde débité", width=0, height=0, timeout=self.timeout)
self.dialog.msgbox(text=u"Le solde de l'adhérent à bien été débité", title="Solde débité", width=0, height=0, timeout=self.timeout)
if [a for a in facture['article'] if art['code'] == 'SOLDE']:
self.dialog.msgbox(text=u"Le solde de l'adhérent a bien été crédité", title="Solde crédité", width=0, height=0, timeout=self.timeout)
self.dialog.msgbox(text=u"Le solde de l'adhérent à bien été crédité", title="Solde crédité", width=0, height=0, timeout=self.timeout)
else:
if not self.confirm(text=u"Le paiement n'a pas été reçue.\n Annuler la vente ?", title="Annulation de la vente", defaultno=True):
raise Continue(cancel_cont)
@ -572,11 +559,10 @@ class Dialog(machine.Dialog, blacklist.Dialog):
else:
(code, tags) = self.handle_dialog(cont, box_choose_item, tags)
self_cont=self_cont(tags=tags, have_set=[], to_set=[], tag_paiment=None)
gestion.config.factures.ITEMS.update(gestion.config.factures.ITEM_SOLDE)
return self.handle_dialog_result(
code=code,
output=tags,
cancel_cont=cont,
error_cont=self_cont,
codes_todo=[([self.dialog.DIALOG_OK], choose_item, [proprio, tags, copy.deepcopy(gestion.config.factures.ITEMS), self_cont])]
codes_todo=[([self.dialog.DIALOG_OK], choose_item, [proprio, tags, copy.deepcopy(gestion.config.factures.items), self_cont])]
)

File diff suppressed because it is too large Load diff

View file

@ -25,7 +25,6 @@ except:
# Machine sans mailman, les ML ne seront pas reconfigurées
pass
CONN = crans_ldap()
class del_user:
""" Suppression des fichiers d'un compte utilisateur """
@ -115,28 +114,18 @@ class home:
for args in self.args:
anim('\t' + args)
try:
login, oldLogin, oldHome = args.split(",")
if login:
res = CONN.search("login=%s" % (login,))
if res['adherent']:
adh = res['adherent'][0]
gid = config.gid
elif res['club']:
adh = res['club'][0]
gid = config.club_gid
home = adh.home()
uid = adh.uidNumber()
mail_redirect = adh.email_exterieur()
if oldHome and hostname == "zbee":
home_service = del_user(["%s,%s" % (oldLogin, oldHome)])
home_service.delete_zbee()
try:
home, uid, login, mail_redirect = args.split(',')
except ValueError:
home, uid, login = args.split(',')
mail_redirect = None
homesplit = home.split("/")
symlink = "/home-adh/%s" % (homesplit[-1],)
### Home
if not os.path.exists(home):
# Le home n'existe pas
os.mkdir(home, 0755)
os.chown(home, int(uid), gid)
os.chown(home, int(uid), config.gid)
if homesplit[-2] != "club":
if os.path.exists(symlink) and os.path.islink(symlink):
os.unlink(symlink)
@ -147,7 +136,7 @@ class home:
# Il y un répertoire existant
# Bon UID ?
stat = os.stat(home)
if stat[4] != int(uid) or stat[5] != gid:
if stat[4] != int(uid) or stat[5] != config.gid:
# Le home n'est pas pas à la bonne personne
raise OSError('home existant')
if homesplit[-2] != "club":
@ -169,29 +158,21 @@ class home:
### Mail
if not os.path.exists(home + '/Mail'):
os.mkdir(home + '/Mail', 0700)
os.chown(home + '/Mail', int(uid), gid)
os.chown(home + '/Mail', int(uid), config.gid)
if not os.path.exists('/home-adh/mail/' + login):
os.mkdir('/home-adh/mail/' + login, 0700)
os.chown('/home-adh/mail/' + login, int(uid), 8)
### Redirection
if mail_redirect:
write_in_forward = True
# On vérifie s'il y a déjà un .forward
if os.path.exists(os.path.join(home, ".forward")):
write_in_forward = False
if write_in_forward:
with open(os.path.join(home, '.forward'), 'w') as forward_file:
forward_file.write(mail_redirect + '\n')
os.chown(home + '/.forward', int(uid), gid)
os.chmod(home + '/.forward', 0600)
file(home + '/.forward', 'w').write(mail_redirect + '\n')
os.chown(home + '/.forward', int(uid), config.gid)
os.chmod(home + '/.forward', 0604)
### Owncloud dans le home
if not os.path.exists(home + '/OwnCloud'):
os.mkdir(home + '/OwnCloud')
os.chown(home + '/OwnCloud', int(uid), grp.getgrnam('www-data').gr_gid)
os.chmod(home + '/OwnCloud', 0770)
os.chmod(home + '/OwnCloud',0770)
except:
print ERREUR
if self.debug:

View file

@ -43,7 +43,6 @@ class autostatus(gen_config) :
"obm.crans.org",
"obm.adm.crans.org",
"batv-3.adm.crans.org",
"batv-1.adm.crans.org",
# Config par défaut
"non-configure.wifi.crans.org",
@ -71,7 +70,6 @@ class autostatus(gen_config) :
"ragnarok.crans.org", # RIP contrôleur disque...
"zamok.crans.org", # c'est en fait fx
"bati-2.adm.crans.org", # N'est plus en place
"batv-1.crans.org",
# Bornes wifi de test
"bullet5.wifi.crans.org",
@ -143,11 +141,9 @@ class autostatus(gen_config) :
# quelque descriptions de routeurs triés par IP (pour la route vers l'extérieur)
infos_routeurs = {}
infos_routeurs [ '138.231.136.4' ] = ['Odlyd', u'Routeur principal du CRANS']
infos_routeurs [ '138.231.136.3' ] = ['Komaz', u'Routeur secondaire du CRANS']
infos_routeurs [ '138.231.136.4' ] = ['Komaz', u'Routeur principal du CRANS']
infos_routeurs [ '138.231.132.1' ] = ['Pioneer.zrt', u'Routeur principal de l\'ENS (interne)']
infos_routeurs [ '138.231.132.101' ] = ['Pioneer1.zrt.ens-cachan', u'Routeur principal de l\'ENS (interne)']
infos_routeurs [ '138.231.132.102' ] = ['Pioneer2.zrt.ens-cachan', u'Routeur principal de l\'ENS (interne)']
infos_routeurs [ '138.231.132.102' ] = ['Pioneer', u'Routeur principal de l\'ENS (interne)']
infos_routeurs [ '138.231.176.1' ] = ['Pioneer', u'Routeur principal de l\'ENS']
infos_routeurs [ '193.49.65.1' ] = ['RenaterCachan1' , u'Routeur Renater' ]
infos_routeurs [ '193.51.181.186' ] = ['RenaterCachan2', u'Routeur Renater']
@ -161,7 +157,7 @@ class autostatus(gen_config) :
services_exterieurs ['Free'] = [ 'Free', '212.27.60.27', 21, 'Le serveur FTP de free. (France)', 'nobody' ]
services_exterieurs ['Monde'] = [ 'Monde', '195.154.120.129', 80, 'Est-ce que LeMonde.fr fonctionne ? (France)', 'nobody' ]
services_exterieurs ['Yahoo!'] = [ 'Yahoo!', '206.190.36.45', 80, 'Est-ce que Yahoo! fonctionne ? (USA)', 'nobody' ]
services_exterieurs ['Google'] = [ 'Google', '74.125.71.138', 80, 'Est-ce que Google fonctionne ? (USA)', 'nobody' ]
services_exterieurs ['Google'] = [ 'Google', '173.194.34.20', 80, 'Est-ce que Google fonctionne ? (USA)', 'nobody' ]
# personnes à informer pour l'indiponibilité de certains serveurs
contact = {}
@ -295,14 +291,10 @@ class autostatus(gen_config) :
# ajout du routeur
# ip
try:
tmp_ip = routeur.split(' ')[1]
except IndexError:
print "Skipping %r" % routeur
continue
tmp_ip = routeur.split(' ')[1]
# nom & desciption
if tmp_ip in self.infos_routeurs.keys() :
if routeur.split(' ')[1] in self.infos_routeurs.keys() :
tmp_name = self.infos_routeurs[tmp_ip][0]
tmp_desc = self.infos_routeurs[tmp_ip][1]
else :

View file

@ -6,7 +6,6 @@ Copyright (C) Valentin Samir
Licence : GPLv3
"""
import os
import sys
import ssl
import time
@ -38,31 +37,24 @@ def short_name(fullhostname):
return fullhostname.split(".")[0]
class ResourceRecord(object):
"""Classe standard définissant une ressource DNS"""
def __init__(self, r_type, name, value, ttl=None):
"""Affecte les valeurs de base de l'enregistrement"""
self.r_type = r_type
self.name = name
self.value = value
self._ttl = ttl
def __init__(self, type, name, value, ttl=None):
self._type=type
self._name=name
self._value=value
self._ttl=ttl
def __str__(self):
"""Retourne une chaîne printable dans un fichier bind"""
if self._ttl:
return "%s\t%s\tIN\t%s\t%s" % (self.name, self._ttl, self.r_type, self.value)
return "%s\t%s\tIN\t%s\t%s" % (self._name, self._ttl, self._type, self._value)
else:
return "%s\tIN\t%s\t%s" % (self.name, self.r_type, self.value)
return "%s\tIN\t%s\t%s" % (self._name, self._type, self._value)
def __repr__(self):
"""__repr__ == __str__"""
return str(self)
class TLSA(ResourceRecord):
"""Enregistrement TLSA pour stocker des certifs dans un enregistrement DNS"""
def __init__(self, name, port, proto, cert, certtype, reftype, selector=0, compat=True, r_format='pem', ttl=None):
""" name: nom du domaine du certificat
def __init__(self, name, port, proto, cert, certtype, reftype, selector=0, compat=True, format='pem', ttl=None):
"""
name: nom du domaine du certificat
port: port écoute le service utilisant le certificat
proto: udp ou tcp
cert: le certificat au format ``format`` (pem ou der) (selector est donc toujours à 0)
@ -70,12 +62,8 @@ class TLSA(ResourceRecord):
reftype: 0 = plain cert, 1 = sha256, 2 = sha512
compat: on génère un enregistement compris même par les serveurs dns n'implémentant pas TLSA
"""
if not r_format in ['pem', 'der']:
if not format in ['pem', 'der']:
raise ValueError("format should be pem or der")
if selector != 0:
raise NotImplementedError("selector different form 0 not implemented")
if cert is None and proto == 'tcp' and name[-1] == '.':
try:
cert = ssl.get_server_certificate((name[:-1], port), ca_certs='/etc/ssl/certs/ca-certificates.crt')
@ -83,17 +71,13 @@ class TLSA(ResourceRecord):
raise ValueError("Unable de retrieve cert dynamically: %s" % e)
elif cert is None:
raise ValueError("cert can only be retrive if proto is tcp and name fqdn")
if r_format is not 'der':
if format is not 'der':
dercert = ssl.PEM_cert_to_DER_cert(cert)
else:
dercert = cert
if not dercert:
raise ValueError("Impossible de convertir le certificat au format DER %s %s %s\n%s" % (name, port, proto, cert))
certhex = TLSA.hashCert(reftype, str(dercert))
self.certhex = certhex
if compat:
super(TLSA, self).__init__(
'TYPE52',
@ -103,15 +87,15 @@ class TLSA(ResourceRecord):
)
else:
super(TLSA, self).__init__(
'TLSA',
'_%s._%s%s' % (port, proto, '.' + name if name else ''),
"%s %s %s %s"% (certtype, selector, reftype, certhex),
ttl
'TLSA',
'_%s._%s%s' % (port, proto, '.' + name if name else ''),
"%s %s %s %s"% (certtype, selector, reftype, certhex),
ttl
)
@staticmethod
def hashCert(reftype, certblob):
"""Retourne un hash d'un certif DER en MAJUSCULES.
"""
certblob: un certificat au format DER
"""
if reftype == 0:
@ -127,170 +111,98 @@ class TLSA(ResourceRecord):
return hashobj.hexdigest().upper()
class SOA(ResourceRecord):
"""Ressource pour une entrée DNS SOA"""
def __init__(self, master, email, serial, refresh, retry, expire, ttl):
super(SOA, self).__init__('SOA', '@', '%s. %s. (\n %s ; numero de serie\n %s ; refresh (s)\n %s ; retry (s)\n %s ; expire (s)\n %s ; TTL (s)\n )' % (master, email, serial, refresh, retry, expire, ttl))
class A(ResourceRecord):
"""Entrée DNS pour une IPv4"""
def __init__(self, name, value, ttl=None):
super(A, self).__init__('A', name, value, ttl)
class DS(ResourceRecord):
"""Entrée DNS pour l'empreinte d'une clef DNSSEC"""
def __init__(self, name, value, ttl=None):
super(DS, self).__init__('DS', name, value, ttl)
class PTR(ResourceRecord):
"""Entrée DNS inverse (pour obtenir l'IP à partir du NDD"""
def __init__(self, name, value, ttl=None):
super(PTR, self).__init__('PTR', name, value, ttl)
class AAAA(ResourceRecord):
"""Entrée DNS pour une IPv6"""
def __init__(self, name, value, ttl=None):
super(AAAA, self).__init__('AAAA', name, value, ttl)
class TXT(ResourceRecord):
"""Entrée DNS pour un champ TXT"""
def __init__(self, name, value, ttl=None):
super(TXT, self).__init__('TXT', name, value, ttl)
if len(self.value) > 200:
self.value = '( "' + '"\n\t\t\t\t"'.join([self.value[x:x+200] for x in xrange(0, len(self.value), 200)]) + '" )'
else:
self.value = '"%s"' % (self.value,)
def __str__(self):
"""Retourne une chaîne printable dans un fichier bind"""
if self._ttl:
return '%s\t%s\tIN\t%s\t%s' % (self.name, self._ttl, self.r_type, self.value)
else:
return '%s\tIN\t%s\t%s' % (self.name, self.r_type, self.value)
class CNAME(ResourceRecord):
"""Entrée DNS pour un alias (toto -> redisdead)"""
def __init__(self, name, value, ttl=None):
super(CNAME, self).__init__('CNAME', name, value, ttl)
class DNAME(ResourceRecord):
"""Entrée DNS pour un alias de domaine (crans.eu -> crans.org)"""
def __init__(self, name, value, ttl=None):
super(DNAME, self).__init__('DNAME', name, value, ttl)
class MX(ResourceRecord):
"""Entrée DNS pour un serveur mail. crans.org IN MX 5 redisdead.crans.org veut dire
que redisdead est responsable de recevoir les mails destinés à toto@crans.org avec
une priorité 5 (plus c'est faible, plus c'est prioritaire.
"""
def __init__(self, name, priority, value, ttl=None):
super(MX, self).__init__('MX', name, '%s\t%s' % (priority, value), ttl)
class NS(ResourceRecord):
"""Entrée DNS pour donner les serveurs autoritaires pour un nom de domaine"""
def __init__(self, name, value, ttl=None):
super(NS, self).__init__('NS', name, value, ttl)
class SPF(ResourceRecord):
def __init__(self, name, value, ttl=None):
super(SPF, self).__init__('SPF', name, value, ttl)
class SRV(ResourceRecord):
"""Entrée DNS pour les champs SRV"""
def __init__(self, service, proto, priority, weight, port, target, ttl=None, subdomain=None):
super(SRV, self).__init__('SRV', '_%s._%s' % (service, proto) + ('.%s' % subdomain if subdomain else ''), '%s\t%s\t%s\t%s' % (priority, weight, port, target), ttl)
def __init__(self, service, proto, priority, weight, port, target, ttl=None):
super(SRV, self).__init__('SRV', '_%s._%s' % (service, proto), '%s\t%s\t%s\t%s' % (priority, weight, port, target), ttl)
class NAPTR(ResourceRecord):
"""Entrée DNS pour les NAPTR"""
def __init__(self, name, order, preference, flag, service, replace_regexpr, value, ttl=None):
super(NAPTR, self).__init__('NAPTR', name, '%s\t%s\t"%s"\t"%s"\t"%s"\t%s' % (order, preference, flag, service, replace_regexpr, value), ttl)
class SSHFP(ResourceRecord):
"""Entrée DNS stockant une fingerprint SSH"""
def __init__(self, name, r_hash, algo, key, ttl=None):
"""Vérifie que hash/algo sont supportés dans la config"""
if not r_hash in config.sshfp_hash.keys():
raise ValueError('Hash %s invalid, valid hash are %s' % (r_hash, ', '.join(config.sshfp_hash.keys())))
def __init__(self, name, hash, algo, key, ttl=None):
if not hash in config.sshfp_hash.keys():
raise ValueError('Hash %s invalid, valid hash are %s' % (hash, ', '.join(config.sshfp_host.keys())))
if not algo in config.sshfp_algo.keys():
raise ValueError('Algo %s unknown, valid values are %s' % (algo, ', '.join(config.sshfp_algo.keys())))
super(SSHFP, self).__init__('SSHFP', name, '%s\t%s\t%s' % (config.sshfp_algo[algo][0], config.sshfp_hash[hash], getattr(hashlib, hash)(base64.b64decode(key)).hexdigest()), ttl)
super(SSHFP, self).__init__('SSHFP', name, '%s\t%s\t%s' % (config.sshfp_algo[algo][0], config.sshfp_hash[r_hash], getattr(hashlib, r_hash)(base64.b64decode(key)).hexdigest()), ttl)
class ZoneBase(list):
"""Classe abstraite décrivant une zone.
Elle surcharge une liste, car l'ensemble des enregistrements de cette
zone sera contenu en elle-même."""
class ZoneBase(object):
def __init__(self, zone_name):
"""Affecte un nom de zone"""
super(ZoneBase, self).__init__()
self._rrlist=[]
self.zone_name = zone_name
self.ttl = 3600
def __repr__(self):
return "<%s %s>" % (self.__class__.__name__, self.zone_name)
def __str__(self):
"""Version enregistrable en fichier d'une zone."""
_ret = "%s\n$ORIGIN %s.\n$TTL %s\n" % (disclamer.replace('//', ';'), self.zone_name, self.ttl)
for rr in self:
_ret += "%s\n" % rr
return _ret
ret="%s\n$ORIGIN %s.\n$TTL %s\n" % (disclamer.replace('//', ';'), self.zone_name, self.ttl)
for rr in self._rrlist:
ret+="%s\n" % rr
return ret
def add(self, rr):
"""Ajout d'un enregistrement DNS"""
if isinstance(rr, ResourceRecord):
self.append(rr)
self._rrlist.append(rr)
else:
raise ValueError("You can only add ResourceRecords to a Zone")
def extend(self, rr_list):
for rr in rr_list:
self.add(rr)
def write(self, path):
"""Pour dumper le tout dans le fichier idoine."""
with open(path, 'w') as f:
f.write("%s" % self)
class ZoneClone(ZoneBase):
"""Zone clone d'une autre zone."""
def __init__(self, zone_name, zone_clone, soa):
"""La zone clone possède, outre son nom, un pointeur vers
la zone qu'elle duplique.
Le SOA est fourni manuellement, et la première entrée de la zone clonée
est ignorée. (c'est a priori le SOA de celle-ci)
"""
class ZoneClone(ZoneBase):
def __init__(self, zone_name, zone_clone, soa):
super(ZoneClone, self).__init__(zone_name)
self.zone_clone = zone_clone
self.ttl = zone_clone.ttl
# On met un SOA custom.
self.add(soa)
# On ajoute un DNAME, qui indique que la zone est un clone.
self.add(DNAME('', "%s." % self.zone_clone.zone_name))
# Et on extrait les données nécessaires de la zone clônée
# à savoir, celles de l'apex (la base du domaine, qui elle
# n'est pas clônée, seuls les sous-domaines le sont)
for rr in self.zone_clone[1:]:
# Si pas de nom ou si le nom est @, on duplique bêtement l'enregistrement
if rr.name in ['', '@']:
for rr in self.zone_clone._rrlist[1:]:
if rr._name in ['', '@']:
self.add(rr)
# Si le nom de domaine concerné est celui de la zone clonée, pareil, on
# "duplique", en créant un enregistrement idoine.
if rr.name in ["%s." % self.zone_clone.zone_name]:
self.add(ResourceRecord(rr.r_type, "%s." % self.zone_name, rr.value))
if rr._name in ["%s." % self.zone_clone.zone_name]:
self.add(ResourceRecord(rr._type, "%s." % self.zone_name, rr._value))
class Zone(ZoneBase):
"""Une zone standard"""
def __init__(self, zone_name, ttl, soa, ns_list, ipv6=True, ipv4=True, other_zones=None):
"""Héritage, plus quelques propriétés en plus
On définit ici si la zone comporte des ipv4/ipv6,
ainsi que des données utiles pour le comportement de celles-ci.
other_zones contient la liste de sous-zones "indépendantes".
(exemple avec wifi.crans.org qui est une sous-zone de crans.org)"""
if other_zones is None:
other_zones = []
def __init__(self, zone_name, ttl, soa, ns_list, ipv6=True, ipv4=True, other_zones=[]):
super(Zone, self).__init__(zone_name)
self.ttl = ttl
self.ipv4 = ipv4
@ -303,26 +215,15 @@ class Zone(ZoneBase):
self.add(NS('@', '%s.' % ns))
def name_in_subzone(self, hostname):
"""Teste si le nom qu'on observe est dans une
sous-zone (toto.wifi.crans.org. est dans wifi.crans.org., et non
dans crans.org..
"""
for zone in self.subzones:
if str(hostname).endswith(".%s" % zone):
return True
return False
def get_name(self, hostname):
"""Retourne la base du nom d'un hôte. Teste si celui-ci appartient bien
à la zone courante et s'il n'est pas lié à une sous-zone.
Si tout est bon, le nom peut valoir "", auquel cas, l'entrée concerne le domaine
courant, donc @.
Dans le cas ce nom ne devrait pas être , on retourne None.
"""
# le hostname fini bien par la zone courante, et il n'appartient pas à une sous-zone
if str(hostname) == self.zone_name or str(hostname).endswith(".%s" % self.zone_name) and not self.name_in_subzone(hostname):
ret = str(hostname)[0:-len(self.zone_name)-1]
ret=str(hostname)[0:- len(self.zone_name) -1]
if ret == "":
return "@"
else:
@ -331,91 +232,65 @@ class Zone(ZoneBase):
return None
def get_name_vi(self, nom, i):
"""Kludge foireux pour retourner toto.v4.crans.org à partir
de toto.crans.org (sous-zones v4/v6)."""
if not i in [4, 6]:
raise ValueError("i should be 4 or 6")
if nom == '@':
return 'v%s' % i
# On considère que le "vrai" nom est la partie avant le premier .
elif '.' in nom:
nom_1, nom_2 = nom.split('.', 1)
return "%s.v%s.%s" % (nom_1, i, nom_2)
else:
return "%s.v%s" % (nom, i)
def add_delegation(self, zone, server):
"""Lorsqu'on veut offrir une délégation DNS à une machine
pour un nom de domaine"""
zone = self.get_name(zone)
def add_delegation(zone, server):
zone = self.het_name(zone)
if zone:
self.add(NS('@', '%s.' % server))
def add_a_record(self, nom, machine):
"""Ajout d'une entrée A."""
# Fait-on de l'IPv4 dans cette zone ?
if self.ipv4:
for ip in machine.get('ipHostNumber', []):
self.add(A(nom, ip))
# Fait-on aussi de l'IPv6 ?
if self.ipv6:
# Bon bah alors on ajoute nom.v4.crans.org en plus.
self.add(A(self.get_name_vi(nom, 4), ip))
def add_aaaa_record(self, nom, machine):
"""Ajout d'une entrée AAAA (for the AAAAAAAAwesome)."""
# Fait-on de l'IPv6 dans cette zone ?
if self.ipv6:
for ip in machine.get('ip6HostNumber', []):
# Si dnsIpv6 est à True dans la base LDAP, on ajoute l'entrée.
# On l'ajoute quand même si la zone ne fait pas d'IPv4, parce que
# ça semble assez dommage d'avoir une machine qui a une IPv6, pas
# d'IPv4, et pas d'entrée DNS pour la contacter, non mais oh.
dnsipv6 = machine.get('dnsIpv6', [True])[0]
if dnsipv6 or not self.ipv4:
if machine.get('dnsIpv6', [True])[0]:
self.add(AAAA(nom, ip))
# Si on fait aussi de l'IPv4...
if self.ipv4:
self.add(AAAA(self.get_name_vi(nom, 6), ip))
def add_sshfp_record(self, nom, machine):
"""Ajoute une fingerprint SSH"""
for sshkey in machine.get('sshFingerprint', []):
try:
algo_txt, key = str(sshkey).split()[:2]
algo = config.sshfs_ralgo[algo_txt][1]
for r_hash in config.sshfp_hash.keys():
self.add(SSHFP(nom, r_hash, algo, key))
if self.ipv4:
self.add(SSHFP(self.get_name_vi(nom, 4), r_hash, algo, key))
if self.ipv6:
self.add(SSHFP(self.get_name_vi(nom, 6), r_hash, algo, key))
algo=config.sshfs_ralgo[algo_txt][1]
for hash in config.sshfp_hash.keys():
self.add(SSHFP(nom, hash, algo, key))
if self.ipv4 and self.ipv6:
self.add(SSHFP(self.get_name_vi(nom, 4), hash, algo, key))
self.add(SSHFP(self.get_name_vi(nom, 6), hash, algo, key))
# KeyError is l'algo dans ldap n'est pas connu
# TypeError si la clef n'est pas bien en base64
except (KeyError, TypeError):
pass
def add_tlsa_record(self, cert):
"""Ajout d'un certif dans le DNS"""
if 'TLSACert' in cert['objectClass']:
if not cert.get('revocked', [False])[0]:
for host in cert['hostCert']:
nom = self.get_name(host)
if nom is None: continue
for port in cert['portTCPin']:
self.add(TLSA(nom, port, 'tcp', cert['certificat'][0], cert['certificatUsage'][0], cert['matchingType'][0], cert['selector'][0], r_format='der'))
for port in cert['portUDPin']:
self.add(TLSA(nom, port, 'udp', cert['certificat'][0], cert['certificatUsage'][0], cert['matchingType'][0], cert['selector'][0], r_format='der'))
for host in cert['hostCert']:
nom=self.get_name(host)
if nom is None: continue
for port in cert['portTCPin']:
self.add(TLSA(nom, port, 'tcp', cert['certificat'][0], cert['certificatUsage'][0], cert['matchingType'][0], cert['selector'][0], format='der'))
for port in cert['portUDPin']:
self.add(TLSA(nom, port, 'udp', cert['certificat'][0], cert['certificatUsage'][0], cert['matchingType'][0], cert['selector'][0], format='der'))
def add_machine(self, machine):
"""Ajout d'une machine, à savoir chaînage d'ajout
d'IP, d'IPv6, de fingerprint et de TLSA, pour chaque
entrée "host" dans la base LDAP."""
for host in machine['host']:
# Le nom peut être None (machine appartenant à une sous-zone, ou à une autre zone)
nom = self.get_name(host)
if nom is None:
continue
nom=self.get_name(host)
if nom is None: continue
self.add_a_record(nom, machine)
self.add_aaaa_record(nom, machine)
@ -423,23 +298,14 @@ class Zone(ZoneBase):
for cert in machine.certificats():
self.add_tlsa_record(cert)
# Si la machine a bien un nom en "host", on lui ajoute aussi
# les aliases, sous forme de CNAME vers le premier nom.
if machine['host']:
for alias in machine.get('hostAlias', []):
# Si l'alias pointe dans une autre zone, on passe. (ça sera fait quand on refera le add_machine
# en toutnant dans la sous-zone
if str(alias) in self.other_zones and str(alias) != self.zone_name:
continue
alias = self.get_name(alias)
if alias is None:
continue
if alias is None: continue
to_nom = self.get_name(machine['host'][0])
# Si l'alias est sur le nom de la zone, il faut ajouter
# des entrées standard.
if alias in ['@', '%s.' % self.zone_name]:
self.add_a_record(alias, machine)
self.add_aaaa_record(alias, machine)
@ -449,34 +315,24 @@ class Zone(ZoneBase):
if self.ipv4 and self.ipv6:
self.add(CNAME(self.get_name_vi(alias, 6), self.get_name_vi(to_nom, 6)))
self.add(CNAME(self.get_name_vi(alias, 4), self.get_name_vi(to_nom, 4)))
# Ne devrait pas arriver.
else:
self.add(CNAME(alias, "%s." % machine['host'][0]))
class ZoneReverse(Zone):
"""Zone inverse, listant des PTR (toto.crans.org IN PTR 138.231...)"""
def __init__(self, net, ttl, soa, ns_list):
"""Initialise une zone reverse.
net est un truc de la forme fe80::/64, ou 138.231.136.0/24
En v4, il faut que net soit un /32, un /24, un /16 ou un /8
En gros, il faut que network_to_arpanets retourne une liste à un élément."""
# Comme dit, liste à un élément.
if len(ZoneReverse.network_to_arpanets(net)) != 1:
if len(ZoneReverse.network_to_arpanets(net))!=1:
raise ValueError("%s n'est pas un réseau valide pour une zone de reverse dns" % net)
self.net = net
zone_name = ZoneReverse.reverse(net)[0]
if '.' in net:
ipv6 = False
ipv4 = True
ipv6=False
ipv4=True
elif ':' in net:
ipv6 = True
ipv4 = False
ipv6=True
ipv4=False
else:
raise ValueError("net should be an ipv4 ou ipv6 network")
super(ZoneReverse, self).__init__(zone_name, ttl, soa, ns_list, ipv6=ipv6, ipv4=ipv4)
@staticmethod
@ -485,43 +341,29 @@ class ZoneReverse(Zone):
l'adresse donnés, ainsi que le nombre d'éléments de l'ip a
mettre dans le fichier de zone si elle est fournie, n'importe
quoi sinon."""
# Initialise la plage d'IP à partir de net
_network = netaddr.IPNetwork(net)
# Prend la première adresse ip de la plage, sauf si une est fournie
_address = netaddr.IPAddress(ip if ip else _network.ip)
# retourne le reverse splitté. (un reverse ressemble à 0.136.231.138.in-addr.arpa.)
rev_dns_a = _address.reverse_dns.split('.')[:-1]
# Si la config est foireuse (donc si on a fourni une IP hors de la plage, ça
# va planter ici.
assert _address in _network
# En v4, le reverse étant de la forme 0.136.231.138.in-addr.arpa., soit
# on a un /8, soit un /16, soit un /24.
if _network.version == 4:
if _network.prefixlen == 8:
n = netaddr.IPNetwork(net)
a = netaddr.IPAddress(ip if ip else n.ip)
rev_dns_a = a.reverse_dns.split('.')[:-1]
assert a in n
if n.version == 4:
if n.prefixlen == 8:
return ('.'.join(rev_dns_a[3:]), 3)
elif _network.prefixlen == 16:
elif n.prefixlen == 16:
return ('.'.join(rev_dns_a[2:]), 2)
elif _network.prefixlen == 24:
elif n.prefixlen == 24:
return ('.'.join(rev_dns_a[1:]), 1)
else:
raise ValueError("Bad network %s" % _network)
# En v6 c'est plus calme.
# Le reverse a cette tronche : 1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.e.f.ip6.arpa.
# Du coup c'est aussi fin qu'on le souhaite.
elif _network.version == 6:
return ('.'.join(rev_dns_a[(128 - _network.prefixlen)/4:]), (128 - _network.prefixlen)/4)
raise ValueError("Bad network %s" % n)
elif n.version == 6:
return ('.'.join(rev_dns_a[(128-n.prefixlen)/4:]), (128-n.prefixlen)/4)
@staticmethod
def network_to_arpanets(nets):
"""Dans reverse(net, ip), on a constaté qu'en v4, on ne pouvait définir
que des plages reverse en /24, /16 ou /8. Cette fonction vise à retourner
une liste des plages en tenant compte de ce critère (donc de taille
32/24/16/8)
Ne touche à rien pour l'IPv6.
"""
retourne une liste de reseaux ne contenant que
des préfixes de taille 32, 24, 16 ou 8 en ipv4
et laisse inchangé les réseaux ipv6.
"""
if not isinstance(nets, list):
nets = [nets]
@ -529,8 +371,6 @@ class ZoneReverse(Zone):
for net in nets:
if not isinstance(net, netaddr.IPNetwork):
net = netaddr.IPNetwork(net)
# Si on est en v4, on fragmente les subnets
# dans les tailles qui vont bien.
if net.version == 4:
if net.prefixlen > 24:
subnets.extend(net.subnet(32))
@ -540,13 +380,12 @@ class ZoneReverse(Zone):
subnets.extend(net.subnet(16))
else:
subnets.extend(net.subnet(8))
# En v6 c'est tout pété.
elif net.version == 6:
subnets.append(net)
return subnets
def add_machine(self, machine):
"""Ajout d'un reverse pour une machine."""
if machine['host']:
if self.ipv4:
attr = 'ipHostNumber'
@ -554,42 +393,33 @@ class ZoneReverse(Zone):
attr = 'ip6HostNumber'
else:
raise ValueError("A reverse zone should be ipv6 or ipv6")
for ip in machine[attr]:
try:
zone, length = ZoneReverse.reverse(self.net, str(ip))
nom = '.'.join(ip.value.reverse_dns.split('.')[:length])
# La zone retournée n'est pas le nom de la zone. A priori
# on aurait dû tomber en AssertionError.
if zone != self.zone_name:
continue
if attr != 'ip6HostNumber' or machine.get('dnsIpv6', [True])[0]:
self.add(PTR(nom, '%s.' % machine['host'][0]))
# Gros kludge pour ajouter le reverse vers le .v6 quand on est sur
# une reverse v6 et que dnsIpv6 est faux.
if attr != 'ip6HostNumber' or machine.get('dnsIpv6', [True])[0]: # Hack pour envoyer le reverse vers l'adresse .v6 dans le cas où dnsIpv6 = False
self.add(PTR(nom, '%s.' % machine['host'][0]))
else:
rev_nom, rev_zone = str(machine['host'][0]).split('.', 1)
self.add(PTR(nom, '%s.v6.%s.' % (rev_nom, rev_zone)))
except AssertionError:
# L'ip n'est pas dans la zone reverse, donc on continue silencieusement.
pass
class dns(gen_config):
"""Classe de configuration du DNS (les services, generate, toussa)"""
class dns(gen_config) :
######################################PARTIE DE CONFIGURATION
### Fichiers à écrire
# Répertoire d'écriture des fichiers de zone
DNS_DIR = config.dns.DNS_DIR
DNSSEC_DIR = config.dns.DNSSEC_DIR
DNS_DIR = '/etc/bind/generated/' # Avec un / à la fin
DNSSEC_DIR = '/etc/bind/signed/' # Avec un / à la fin
# Fichier de définition des zones pour le maître
DNS_CONF = config.dns.DNS_CONF
DNS_CONF = DNS_DIR + 'zones_crans'
# Fichier de définition des zones pour les esclaves géré par BCfg2
DNS_CONF_BCFG2 = config.dns.DNS_CONF_BCFG2
DNS_CONF_BCFG2 = "/var/lib/bcfg2/Cfg/etc/bind/generated/zones_crans/zones_crans"
### Liste DNS
# Le premier doit être le maitre
@ -602,10 +432,13 @@ class dns(gen_config):
### Serveurs de mail
# format : [ priorité serveur , .... ]
MXs = [MX('@', config.dns.MXs[_mx].get('prio', 25), "%s." %_mx) for _mx in config.dns.MXs]
MXs = [
MX('@',10, 'redisdead.crans.org.'),
MX('@',15, 'soyouz.crans.org.'),
MX('@',25, 'freebox.crans.org.'),
]
SRVs = {
'crans.org': [
'crans.org': [
SRV('jabber', 'tcp', 5, 0, 5269, 'xmpp'),
SRV('xmpp-server', 'tcp', 5, 0, 5269, 'xmpp'),
SRV('xmpp-client', 'tcp', 5, 0, 5222, 'xmpp'),
@ -613,41 +446,14 @@ class dns(gen_config):
SRV('sip', 'tcp', 5, 0, 5060, 'asterisk'),
SRV('sips', 'tcp', 5, 0, 5061, 'asterisk'),
SRV('stun', 'udp', 5, 0, 3478, 'asterisk'),
# Quelques ancien utilisent le server XMPP avec des addresses de la forme
# login@jabber.crans.org, aussi les clients XMPP et autres serveurs de la
# fédération veulent ces ResourceRecord
SRV('jabber', 'tcp', 5, 0, 5269, 'xmpp', subdomain="jabber"),
SRV('xmpp-server', 'tcp', 5, 0, 5269, 'xmpp', subdomain="jabber"),
SRV('xmpp-client', 'tcp', 5, 0, 5222, 'xmpp', subdomain="jabber"),
],
]
}
SPFs = {
'crans.org': [
TXT('@', 'v=spf1 mx ~all'),
],
}
DKIM = {
'crans.org': [
TXT('mail._domainkey', 'v=DKIM1; k=rsa; p=MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtwkNVd9Mmz8S4WcfuPk0X2drG39gS8+uxAv8igRILgzWeN8j2hjeZesl8pm/1UTVU87bYcdfUgXiGfQy9nR5p/Vmt2kS7sXk9nsJ/VYENgb3IJQ6paWupSTFMyeKycJ4ZHCEZB/bVvifoG6vLKqW5jpsfCiOcfdcgXATn0UPuVx9t93yRrhoEMntMv9TSodjqd3FKCtJUoh5cNQHo0T6dWKtxoIgNi/mvZ92D/IACwu/XOU+Rq9fnoEI8GukBQUR5AkP0B/JrvwWXWX/3EjY8X37ljEX0XUdq/ShzTl5iK+CM83stgkFUQh/rpww5mnxYEW3X4uirJ7VJHmY4KPoIU+2DPjLQj9Hz63CMWY3Ks2pXWzxD3V+GI1aJTMFOv2LeHnI3ScqFaKj9FR4ZKMb0OW2BEFBIY3J3aeo/paRwdbVCMM7twDtZY9uInR/NhVa1v9hlOxwp4/2pGSKQYoN2CkAZ1Alzwf8M3EONLKeiC43JLYwKH1uBB1oikSVhMnLjG0219XvfG/tphyoOqJR/bCc2rdv5pLwKUl4wVuygfpvOw12bcvnTfYuk/BXzVHg9t4H8k/DJR6GAoeNAapXIS8AfAScF8QdKfplhKLJyQGJ6lQ75YD9IwRAN0oV+8NTjl46lI/C+b7mpfXCew+p6YPwfNvV2shiR0Ez8ZGUQIcCAwEAAQ==')
],
}
NON_CLONABLE_SPFs = {
'crans.org': [
TXT(short_name(_mx), 'v=spf1 mx:crans.org ~all') for _mx in config.dns.MXs
],
}
NAPTRs = {
'crans.org' : [
NAPTR('@', 5, 100, "S", "SIPS+D2T", "", '_sips._tcp.crans.org.', ttl=86400),
NAPTR('@', 10, 100, "S", "SIP+D2U", "", '_sip._udp.crans.org.', ttl=86400),
NAPTR('@', 15, 100, "S", "SIP+D2T", "", '_sip._tcp.crans.org.', ttl=86400),
],
NATPRs = {
'crans.org' : [
NAPTR('@', 5, 100, "S", "SIPS+D2T", "", '_sips._tcp.crans.org.', ttl=86400),
NAPTR('@', 10, 100, "S", "SIP+D2U", "", '_sip._udp.crans.org.', ttl=86400),
NAPTR('@', 15, 100, "S", "SIP+D2T", "", '_sip._tcp.crans.org.', ttl=86400),
]
}
# DS à publier dans zone parentes : { parent : [ zone. TTL IN DS key_id algo_id 1 hash ] }
@ -655,10 +461,10 @@ class dns(gen_config):
# /!\ Il faut faire attention au rollback des keys, il faudrait faire quelque chose d'automatique avec opendnssec
DSs = {
'crans.org': [
DS('adm', '64649 8 2 9c45f0fef063672d96c983d5a3813a08a649c72d357f41ddece73ae8872d60cf'),
DS('wifi', '5531 8 2 daf30a647566234edc1617546fd74abbbaf965b17389248f72fc66a33d6f5063'),
DS('tv', '18199 8 2 d3cc2f5f81b830cbb8894ffd32c236e968edd3b0c0305112b6eb970aa763418e'),
],
DS('adm', '64649 8 2 9c45f0fef063672d96c983d5a3813a08a649c72d357f41ddece73ae8872d60cf'),
DS('wifi', '5531 8 2 daf30a647566234edc1617546fd74abbbaf965b17389248f72fc66a33d6f5063'),
DS('tv', '18199 8 2 d3cc2f5f81b830cbb8894ffd32c236e968edd3b0c0305112b6eb970aa763418e'),
],
}
@ -666,29 +472,24 @@ class dns(gen_config):
serial = int(time.time()) + 1000000000
TTL = 3600
if hostname == short_name(config.dns.DNSs[0]):
if hostname == short_name(config.dns.DNSs[0]):
restart_cmd = '/usr/sbin/ods-signer sign --all && /etc/init.d/bind9 reload'
else:
restart_cmd = '/etc/init.d/bind9 reload'
def __init__(self, *args, **kwargs):
"""Surcharge pour affecter EXTRAS"""
self.EXTRAS = {}
self.anim = None
super(dns, self).__init__(*args, **kwargs)
def gen_soa(self, ns_list, serial, ttl):
"""Génère l'enregistrement SOA pour le domaine"""
return SOA(ns_list[0], 'root.crans.org', serial, 21600, 3600, 1209600, ttl)
def populate_zones(self, zones, machines):
"""On peuple les fichiers de zones"""
self.anim.iter = len(zones.values())
self.anim.iter=len(zones.values())
for zone in zones.values():
# On met les mêmes MX pour toutes les zones.
zone.extend(self.MXs)
# Les RR définis ici sont ajoutés aux zones idoines, de façon à se simplifier la vie.
for rr_type in [self.SRVs, self.NAPTRs, self.DSs, self.EXTRAS, self.SPFs, self.NON_CLONABLE_SPFs, self.DKIM]:
for rr_type in [self.SRVs, self.NATPRs, self.DSs, self.EXTRAS]:
if zone.zone_name in rr_type.keys():
zone.extend(rr_type[zone.zone_name])
for m in machines:
@ -697,43 +498,31 @@ class dns(gen_config):
return zones
def gen_zones_ldap(self, ttl, ns_list, serial, zones={}, zones_ldap=config.dns.zones_ldap):
"""On génère la liste des zones ldap, à partir de config.dns. C'est un peu ici que tout commence.
Le dico zones passé en argument est modifié en place."""
for zone in zones_ldap:
# On crée la zone et on l'ajoute au dico.
zones[zone] = Zone(zone, ttl, self.gen_soa(ns_list, serial, ttl), ns_list, other_zones=config.dns.zones_direct)
zones[zone]=Zone(zone, ttl, self.gen_soa(ns_list, serial, ttl), ns_list, other_zones=config.dns.zones_direct)
return zones
def gen_zones_reverse(self, ttl, ns_list, serial, zones={},
zones_reverse_v4=config.dns.zones_reverse,
zones_reverse_v6=config.dns.zones_reverse_v6):
"""Deuxième gros morceau, les reverses, pareil, on peuple depuis config.dns, et on crée toutes les zones
idoines. Pareil, ici, le dico zones est modifié en place"""
zones_reverse_v4=config.dns.zones_reverse, zones_reverse_v6=config.dns.zones_reverse_v6):
for net in ZoneReverse.network_to_arpanets(zones_reverse_v4 + zones_reverse_v6):
# On crée la zone et on l'ajoute au dico.
zones[str(net)] = ZoneReverse(str(net), ttl, self.gen_soa(ns_list, serial, ttl), ns_list)
zones[str(net)]=ZoneReverse(str(net), ttl, self.gen_soa(ns_list, serial, ttl), ns_list)
return zones
def gen_zones_clone(self, ttl, ns_list, serial, zones={}):
"""Les clônes, à savoir crans.eu et cie, dico zones modifié en place."""
for zone_clone, zones_alias in config.dns.zone_alias.iteritems():
for zone_clone, zones_alias in config.dns.zone_alias.items():
for zone in zones_alias:
# On crée la zone et on l'ajoute au dico.
zones[zone] = ZoneClone(zone, zones[zone_clone], self.gen_soa(ns_list, serial, ttl))
# Et on ajoute les enregistrements concernant la zone clône (pas la clônée, ça
# a déjà été fait à l'init) à la main.
for rr_type in [self.SRVs, self.NAPTRs, self.DSs, self.SPFs]:
zones[zone]=ZoneClone(zone, zones[zone_clone], self.gen_soa(ns_list, serial, ttl))
for rr_type in [self.SRVs, self.NATPRs, self.DSs]:
if zones[zone].zone_name in rr_type.keys():
zones[zone].extend(rr_type[zones[zone].zone_name])
return zones
def gen_zones(self, ttl, serial, ns_list, populate=True):
"""On chaîne les différents gen_zones_*"""
zones = {}
self.gen_zones_ldap(ttl, ns_list, serial, zones)
self.gen_zones_reverse(ttl, ns_list, serial, zones)
# Si populate, on remplit les zones avec les enregistrements \o/
if populate:
conn = lc_ldap.shortcuts.lc_ldap_admin()
machines = conn.search(u"mid=*", sizelimit=10000)
@ -745,34 +534,30 @@ class dns(gen_config):
self.gen_zones_clone(ttl, ns_list, serial, zones)
return zones
def gen_tv(self, populate=True):
"""Génération de la TV, un peu à part."""
self.anim = affich_tools.anim('\tgénération de la zone tv')
zones = {}
serial = self.serial
self.gen_zones_reverse(self.TTL, config.dns.DNSs, serial, zones, zones_reverse_v4=config.NETs['multicast'], zones_reverse_v6=[])
self.gen_zones_ldap(self.TTL, config.dns.DNSs, serial, zones, zones_ldap=[config.dns.zone_tv])
# Pareil, si on doit peupler on ajoute ce qu'il faut niveau machines.
if populate:
conn = lc_ldap.shortcuts.lc_ldap_admin()
machines = conn.machinesMulticast()
machines=conn.machinesMulticast()
machines.extend(conn.search(u'(|(host=%s)(host=*.%s)(hostAlias=%s)(hostAlias=*.%s))' % ((config.dns.zone_tv,)*4)))
self.populate_zones(zones, machines)
for zone in zones.values():
zone.write(os.path.join(self.DNS_DIR, 'db.%s' % (zone.zone_name,)))
zone.write(self.DNS_DIR + 'db.' + zone.zone_name)
self.anim.reinit()
print affich_tools.OK
return zones
def gen_master(self):
"""Pour le serveur maître.
Appelle gen_zones puis écrit les fichiers."""
# Syntaxe utilisée dans le fichier DNS_CONF pour définir une zone sur le maître
zone_template = """
zone_template="""
zone "%(zone_name)s" {
type master;
file "%(zone_path)s";
@ -782,17 +567,15 @@ zone "%(zone_name)s" {
with open(self.DNS_CONF, 'w') as f:
f.write(disclamer)
for zone in zones.values():
zone.write(os.path.join(self.DNS_DIR, 'db.%s' % (zone.zone_name,)))
zone.write(self.DNS_DIR + 'db.' + zone.zone_name)
if zone.zone_name in config.dns.zones_dnssec:
zone_path = os.path.join(self.DNSSEC_DIR, 'db.%s' % (zone.zone_name,))
zone_path = self.DNSSEC_DIR + 'db.' + zone.zone_name
else:
zone_path = os.path.join(self.DNS_DIR, 'db.%s' % (zone.zone_name,))
zone_path = self.DNS_DIR + 'db.' + zone.zone_name
f.write(zone_template % {'zone_name' : zone.zone_name, 'zone_path' : zone_path})
def gen_slave(self):
"""Pour les slaves, fait l'écriture de la conf dans bcfg2, mais on ne peuple rien !
On ne fait qu'écrire le fichier zone_crans."""
zone_template = """
zone_template="""
zone "%(zone_name)s" {
type slave;
file "%(zone_path)s";
@ -804,9 +587,9 @@ zone "%(zone_name)s" {
f.write(disclamer)
for zone in zones.values():
if zone.zone_name in config.dns.zones_dnssec:
zone_path = os.path.join(self.DNSSEC_DIR, 'db.%s' % (zone.zone_name,))
zone_path = self.DNSSEC_DIR + 'db.' + zone.zone_name
else:
zone_path = os.path.join(self.DNS_DIR, 'db.%s' % (zone.zone_name,))
zone_path = self.DNS_DIR + 'db.' + zone.zone_name
f.write(zone_template % {'zone_name' : zone.zone_name, 'zone_path' : zone_path, 'master_ip' : config.dns.master})
def _gen(self):
@ -816,28 +599,28 @@ zone "%(zone_name)s" {
return "DNS"
if __name__ == '__main__':
HOSTNAME = short_name(gethostname())
if HOSTNAME == short_name(config.bcfg2_main):
if __name__ == '__main__' :
hostname = short_name(gethostname())
if hostname == short_name(config.bcfg2_main):
print "Reconfiguration du fichier de BCfg2 pour configurer le bind d'un serveur en esclave (pensez à lancer bcfg2 sur les esclaves)."
CONFIG = dns()
CONFIG.gen_slave()
elif HOSTNAME == short_name(config.dns.DNSs[0]):
c = dns()
c.gen_slave()
elif hostname == short_name(config.dns.DNSs[0]):
print "Serveur maître :"
CONFIG = dns()
ZONES = CONFIG.gen_tv()
c = dns()
zones = c.gen_tv()
import subprocess
for ZONE in ZONES.values():
if ZONE.zone_name in config.dns.zones_dnssec:
ARGS = ("/usr/sbin/ods-signer sign %s" % ZONE.zone_name).split()
PROCESS = subprocess.Popen(ARGS, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
RET = PROCESS.communicate()
print RET[0].strip()
if RET[1].strip():
print RET[1].strip()
for zone in zones.values():
if zone.zone_name in config.dns.zones_dnssec:
args=("/usr/sbin/ods-signer sign %s" % zone.zone_name).split()
p=subprocess.Popen(args,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
ret=p.communicate()
print ret[0].strip()
if ret[1].strip():
print ret[1].strip()
print "Ce serveur est également serveur maitre pour les autres zones dns, mais leur reconfiguration se fait par generate."
elif HOSTNAME in [short_name(FULLHOSTNAME) for FULLHOSTNAME in config.dns.DNSs[1:]]:
print "Ce serveur est esclave! Lancez ce script sur %s, puis lancez bcfg2 ici" % (config.bcfg2_main,)
elif hostname in map(lambda fullhostname : short_name(fullhostname),config.dns.DNSs[1:]):
print "Ce serveur est esclave! Lancez ce script sur %s, puis lancez bcfg2 ici" % bcfg2_main
else:
print "Ce serveur ne correspond à rien pour la configuration DNS."

View file

@ -48,15 +48,11 @@ class dydhcp:
msg.obj.append((b"hardware-address", pack_mac(mac)))
msg.obj.append((b"hardware-type", struct.pack("!I", 1)))
msg.obj.append((b"ip-address", pack_ip(ip)))
# See patch for hostnames at
# http://jpmens.net/2011/07/20/dynamically-add-static-leases-to-dhcpd/
if name:
statem = b'supersede host-name "%s";' % bytes(name)
msg.obj.append((b"name", bytes(name)))
msg.obj.append((b"statements", statem))
msg.obj.append((b"client-hostname", bytes(name)))
conn=Omapi(self.server, 9991,self.dhcp_omapi_keyname, self.dhcp_omapi_key)
response = conn.query_server(msg)
# print response.dump() # DEBUG purpose (repr() marche po)
conn.close()
def del_host(self, ip,mac):

View file

@ -45,13 +45,9 @@ class exemptions(gen_config):
for machine in machines:
for destination in machine["exempt"]:
if destination.value.version == 4:
if not machine['ipHostNumber']:
continue
source = str(machine["ipHostNumber"][0])
requete = "INSERT INTO exemptes (ip_crans, ip_dest) VALUES ('%s','%s')" % (source, destination)
else:
if not machine['macAddress']:
continue
source = str(machine["macAddress"][0])
requete = "INSERT INTO exemptes6 (mac_crans, ip_dest) VALUES ('%s','%s')" % (source, destination)
# Si ip vide, passons au suivant
@ -90,8 +86,7 @@ class machines(gen_config):
if not m['macAddress'][0].value == '<automatique>':
curseur.execute("INSERT INTO machines (mac_addr, type, id) VALUES ('%s','adherent',%s);" % (m['macAddress'][0], m.proprio()['aid'][0].value))
elif m.proprio().__class__ == lc_ldap.objets.AssociationCrans:
if not m['macAddress'][0].value == '<automatique>':
curseur.execute("INSERT INTO machines (mac_addr, type, id) VALUES ('%s','crans',%s);" % (m['macAddress'][0], m['mid'][0].value))
curseur.execute("INSERT INTO machines (mac_addr, type, id) VALUES ('%s','crans',%s);" % (m['macAddress'][0], m['mid'][0].value))
# on commit
pgsql.commit()

View file

@ -34,13 +34,13 @@ class firewall(utils.firewall_tools) :
self.use_ipset = [self.blacklist_hard, self.test_mac_ip, self.blacklists]
self.ipset['mac_ip']={
'adh' : Ipset("MAC-IP-ADH", "bitmap:ip,mac", "range 138.231.136.0-138.231.151.255"),
'adm' : Ipset("MAC-IP-ADM", "bitmap:ip,mac", "range 10.231.136.0-10.231.136.255"),
'app' : Ipset("MAC-IP-APP", "bitmap:ip,mac", "range 10.2.9.0-10.2.9.255"),
'adh' : Ipset("MAC-IP-ADH","macipmap","--from 138.231.136.0 --to 138.231.151.255"),
'adm' : Ipset("MAC-IP-ADM","macipmap","--from 10.231.136.0 --to 10.231.136.255"),
'app' : Ipset("MAC-IP-APP","macipmap","--from 10.2.9.0 --to 10.2.9.255"),
}
self.ipset['blacklist']={
'hard' : Ipset("BLACKLIST-HARD", "hash:ip"),
'hard' : Ipset("BLACKLIST-HARD","ipmap","--from 138.231.136.0 --to 138.231.151.255"),
}
@ -110,7 +110,7 @@ class firewall(utils.firewall_tools) :
if fill_ipset:
# On récupère la liste de toutes les ips blacklistés hard
bl_hard_ips = self.blacklisted_ips(config.blacklist_sanctions)
bl_hard_ips = self.blacklisted_ips(config.blacklist_sanctions, config.NETs['all'])
anim('\tRestoration de l\'ipset %s' % self.ipset['blacklist']['hard'])
self.ipset['blacklist']['hard'].restore(bl_hard_ips)
print OK
@ -131,7 +131,7 @@ class firewall(utils.firewall_tools) :
def mac_ip_remove(self, mac, ip):
machine = {'macAddress':[mac], 'ipHostNumber': [ip]}
self.test_mac_ip_dispatch(lambda set, data: self.ipset['mac_ip'][set].delete(data), machine)
self.test_mac_ip_dispatch(lambda set, data: set.ipset['mac_ip'][set].delete(data), machine)
def test_mac_ip_dispatch(self, func, machine):
"""Détermine à quel set de mac-ip appliquer la fonction ``func`` (add, delete, append, ...)"""
@ -142,7 +142,7 @@ class firewall(utils.firewall_tools) :
# Si la machines est sur le réseau des adhérents
if utils.AddrInNet(str(ip), config.NETs['wifi']):
# Les machines wifi sont vues à travers komaz
func('adh', "%s,%s" % (ip, config.mac_du_routeur))
func('adh', "%s,%s" % (ip, config.mac_komaz))
elif utils.AddrInNet(str(ip), config.NETs['fil']):
func('adh', "%s,%s" % (ip, machine['macAddress'][0]))
# Si la machine est sur le réseau admin
@ -180,7 +180,7 @@ class firewall(utils.firewall_tools) :
# Proxy ARP de Komaz et Titanic pour OVH
ip_soyouz = self.conn.search(u"host=soyouz.adm.crans.org")[0]['ipHostNumber'][0]
self.add(table, chain, '-m mac -s %s --mac-source %s -j RETURN' % (ip_soyouz, config.mac_du_routeur))
self.add(table, chain, '-m mac -s %s --mac-source %s -j RETURN' % (ip_soyouz, config.mac_komaz))
self.add(table, chain, '-m mac -s %s --mac-source %s -j RETURN' % (ip_soyouz, config.mac_titanic))
self.add(table, chain, '-j REJECT')
@ -238,7 +238,7 @@ class firewall_wifionly(firewall):
if utils.AddrInNet(str(ip), config.NETs['wifi']):
func('adh', "%s,%s" % (ip, machine['macAddress'][0]))
elif utils.AddrInNet(str(ip), config.NETs['fil']):
func('adh', "%s,%s" % (ip, config.mac_du_routeur))
func('adh', "%s,%s" % (ip, config.mac_komaz))
# Si la machine est sur le réseau admin
elif utils.AddrInNet(str(ip), config.NETs['adm']):
func('adm', "%s,%s" % (ip, machine['macAddress'][0]))

View file

@ -21,9 +21,8 @@ firewall = {
'odlyd' : komaz.firewall,
'zamok' : zamok.firewall,
'routeur' : routeur.firewall,
'gordon' : base.firewall_routeur,
'eap' : base.firewall_wifionly,
'pea' : base.firewall_wifionly,
'radius' : base.firewall_wifionly
}
if hostname in firewall.keys():

View file

@ -19,7 +19,6 @@ class firewall(base.firewall_routeur):
'ssh_on_https' : self.ssh_on_https,
'connexion_secours' : self.connexion_secours,
'connexion_appartement' : self.connexion_appartement,
'connexion_wififederez' : self.connexion_wififederez,
'blacklist_soft' : self.blacklist_soft,
'blacklist_upload' : self.blacklist_upload,
'reseaux_non_routable' : self.reseaux_non_routable,
@ -33,21 +32,18 @@ class firewall(base.firewall_routeur):
self.use_tc.extend([self.limitation_debit])
self.ipset['reseaux_non_routable'] = {
'deny' : base.Ipset("RESEAUX-NON-ROUTABLE-DENY", "hash:net"),
'allow' : base.Ipset("RESEAUX-NON-ROUTABLE-ALLOW", "hash:net"),
'deny' : base.Ipset("RESEAUX-NON-ROUTABLE-DENY","nethash"),
'allow' : base.Ipset("RESEAUX-NON-ROUTABLE-ALLOW","nethash"),
}
self.ipset['blacklist'].update({
'soft' : base.Ipset("BLACKLIST-SOFT", "hash:ip"),
'upload' : base.Ipset("BLACKLIST-UPLOAD", "hash:ip"),
'soft' : base.Ipset("BLACKLIST-SOFT","ipmap","--from 138.231.136.0 --to 138.231.151.255"),
'upload' : base.Ipset("BLACKLIST-UPLOAD","ipmap","--from 138.231.136.0 --to 138.231.151.255"),
})
# Portail captif/blacklist soft: ipset des gens ayant cliqué pour continuer à naviguer
self.ipset['confirmation'] = base.Ipset("CONFIRMATION", "hash:ip", "")
# Ouvertures de ports temporaires
self.ipset['ip_port_tmp'] = base.Ipset("IP-PORT-TMP", "hash:ip,port", "timeout 3600")
def blacklist_maj(self, ips):
"""Mise à jour des blacklistes"""
self.blacklist_hard_maj(ips)
@ -97,7 +93,6 @@ class firewall(base.firewall_routeur):
self.add(table, chain, '-p icmp -j ACCEPT')
self.add(table, chain, '-m state --state RELATED,ESTABLISHED -j ACCEPT')
self.add(table, chain, '-j %s' % blacklist_soft_chain)
self.add(table, chain, '-j %s' % self.limit_ssh_connexion(table))
for net in base.config.NETs['all'] + base.config.NETs['adm'] + base.config.NETs['personnel-ens']:
self.add(table, chain, '-s %s -j %s' % (net, mac_ip_chain))
self.add(table, chain, '-j %s' % blacklist_hard_chain)
@ -117,9 +112,8 @@ class firewall(base.firewall_routeur):
self.add(table, chain, '-s %s -j %s' % (net, mac_ip_chain))
self.add(table, chain, '-j %s' % self.connexion_secours(table))
self.add(table, chain, '-j %s' % self.connexion_appartement(table))
self.add(table, chain, '-j %s' % self.connexion_wififederez(table))
self.add(table, chain, '-j %s' % self.ingress_filtering(table))
self.add(table, chain, '-j %s' % self.limit_ssh_connexion(table, ttl=30, counter_name="SSH2"))
self.add(table, chain, '-j %s' % self.limit_ssh_connexion(table))
self.add(table, chain, '-i %s -j %s' % (dev['out'], self.filtrage_ports(table)))
self.add(table, chain, '-o %s -j %s' % (dev['out'], self.filtrage_ports(table)))
return
@ -132,10 +126,8 @@ class firewall(base.firewall_routeur):
self.add(table, chain, '-j %s' % self.ssh_on_https(table))
self.add(table, chain, '-j %s' % self.connexion_secours(table))
self.add(table, chain, '-j %s' % self.blacklist_soft(table))
self.add(table, chain, '-j %s' % self.blacklist_hard(table))
chain = 'POSTROUTING'
self.add(table, chain, '-j %s' % self.connexion_wififederez(table))
self.add(table, chain, '-j %s' % self.connexion_appartement(table))
return
@ -155,13 +147,13 @@ class firewall(base.firewall_routeur):
self.apply(table, chain)
return chain
def limit_ssh_connexion(self, table=None, apply=False, ttl=120, counter_name="SSH"):
chain = 'LIMIT-%s-CONNEXION' % (counter_name,)
def limit_ssh_connexion(self, table=None, apply=False):
chain = 'LIMIT-SSH-CONNEXION'
if table == 'filter':
pretty_print(table, chain)
self.add(table, chain, '-i %s -p tcp --dport ssh -m state --state NEW -m recent --name %s --set' % (dev['out'], counter_name))
self.add(table, chain, '-i %s -p tcp --dport ssh -m state --state NEW -m recent --name %s --update --seconds %s --hitcount 10 --rttl -j DROP' % (dev['out'], counter_name, ttl))
self.add(table, chain, '-i %s -p tcp --dport ssh -m state --state NEW -m recent --name SSH --set' % dev['out'])
self.add(table, chain, '-i %s -p tcp --dport ssh -m state --state NEW -m recent --name SSH --update --seconds 30 --hitcount 10 --rttl -j DROP' % dev['out'])
print OK
if apply:
@ -251,7 +243,6 @@ class firewall(base.firewall_routeur):
if table == 'nat':
pretty_print(table, chain)
self.add(table, chain, '-p tcp -d 138.231.136.2 --dport 22 -j DNAT --to-destination 138.231.136.1:22') # redirection du ssh vers zamok
self.add(table, chain, '-p tcp -d 138.231.136.2 --dport 80 -j DNAT --to-destination 138.231.136.1:81') # redirection du ssh vers zamok a travers httptunnel
self.add(table, chain, '-p tcp -d 138.231.136.2 --dport 443 -j DNAT --to-destination 138.231.136.1:22') # redirection du ssh vers zamok (pour passer dans un proxy, avec corkscrew)
print OK
@ -306,29 +297,6 @@ class firewall(base.firewall_routeur):
self.apply(table, chain)
return chain
def connexion_wififederez(self, table=None, apply=False):
"""PNAT le vlan wififederez derrière wififederez.crans.org"""
chain = 'CONNEXION-WIFIFEDEREZ'
if table == 'nat':
pretty_print(table, chain)
for dev_key in ['out', 'fil', 'wifi']:
for net in base.config.NETs['federez']:
self.add(table, chain, '-o %s -s %s -j SNAT --to 138.231.136.77' % (dev[dev_key], net))
print OK
if table == 'filter':
pretty_print(table, chain)
for net in base.config.NETs['federez']:
self.add(table, chain, '-s %s -j ACCEPT' % net)
self.add(table, chain, '-d %s -j ACCEPT' % net)
print OK
if apply:
self.apply(table, chain)
return chain
def blacklist_soft_maj(self, ip_list):
self.blacklist_soft(fill_ipset=True)
# for ip in ip_list:
@ -347,7 +315,7 @@ class firewall(base.firewall_routeur):
if fill_ipset:
# On récupère la liste de toutes les ips blacklistés soft
bl_soft_ips = self.blacklisted_ips(base.config.blacklist_sanctions_soft)
bl_soft_ips = self.blacklisted_ips(base.config.blacklist_sanctions_soft, base.config.NETs['all'])
anim('\tRestoration de l\'ipset %s' % self.ipset['blacklist']['soft'])
self.ipset['blacklist']['soft'].restore(bl_soft_ips)
print OK
@ -372,41 +340,6 @@ class firewall(base.firewall_routeur):
self.apply(table, chain)
return chain
def blacklist_hard(self, table=None, fill_ipset=False, apply=False):
"""Bloque tout, sauf le 80 pour afficher le portail captif"""
chain = 'BLACKLIST_HARD'
if fill_ipset:
# On récupère la liste de toutes les ips blacklistés hard
bl_hard_ips = self.blacklisted_ips(base.config.blacklist_sanctions)
anim('\tRestoration de l\'ipset %s' % self.ipset['blacklist']['hard'])
self.ipset['blacklist']['hard'].restore(bl_hard_ips)
print OK
if table == 'filter':
pretty_print(table, chain)
# Same as blacklist_soft: autorise le port 80 et 3128 vers soi-même
self.add(table, chain, '-p tcp --dport 80 -m set --match-set %s src -j ACCEPT' % self.ipset['blacklist']['hard'] )
self.add(table, chain, '-p tcp --sport 80 -m set --match-set %s dst -j ACCEPT' % self.ipset['blacklist']['hard'] )
self.add(table, chain, '-p tcp -d 10.231.136.4 --dport 3128 -m set --match-set %s src -j ACCEPT' % self.ipset['blacklist']['hard'] )
self.add(table, chain, '-p tcp -s 10.231.136.4 --sport 3128 -m set --match-set %s dst -j ACCEPT' % self.ipset['blacklist']['hard'] )
# Mais on continue en refusant le reste
self.add(table, chain, '-m set --match-set %s src -j REJECT' % self.ipset['blacklist']['hard'] )
self.add(table, chain, '-m set --match-set %s dst -j REJECT' % self.ipset['blacklist']['hard'] )
print OK
if table == 'nat':
pretty_print(table, chain)
for net in base.config.NETs['all']:
self.add(table, chain, '-d %s -j RETURN' % net)
self.add(table, chain, '-p tcp --dport 80 -m set --match-set %s src -j RETURN' % self.ipset['confirmation'] ) # Les gens qui ont cliqué -> fine !
self.add(table, chain, '-p tcp --dport 80 -m set --match-set %s src -j DNAT --to-destination 10.231.136.4:3128' % self.ipset['blacklist']['hard'] )
print OK
if apply:
self.apply(table, chain)
return chain
def blacklist_upload_maj(self, ip_list):
self.blacklist_upload(fill_ipset=True)
# for ip in ip_list:
@ -425,7 +358,7 @@ class firewall(base.firewall_routeur):
if fill_ipset:
# On récupère la liste de toutes les ips blacklistés pour upload
bl_upload_ips = self.blacklisted_ips(base.config.blacklist_bridage_upload)
bl_upload_ips = self.blacklisted_ips(base.config.blacklist_bridage_upload, base.config.NETs['all'])
anim('\tRestoration de l\'ipset %s' % self.ipset['blacklist']['upload'])
self.ipset['blacklist']['upload'].restore(bl_upload_ips)
print OK
@ -493,7 +426,6 @@ class firewall(base.firewall_routeur):
if table == 'filter':
pretty_print(table, chain)
self.add(table, chain, '-m set --match-set %s dst,dst -j ACCEPT' % self.ipset['ip_port_tmp'] )
for net in base.config.NETs['serveurs']:
for proto in base.config.firewall.srv_ports_default.keys():
if base.config.firewall.srv_ports_default[proto]['output']:
@ -532,10 +464,8 @@ class firewall(base.firewall_routeur):
debit_max = base.config.firewall.debit_max
bl_upload_debit_max = base.config.firewall.bl_upload_debit_max
appt_upload_max = base.config.firewall.appt_upload_max
federez_upload_max = base.config.firewall.federez_upload_max
uplink_speed = '1024mbit'
if table == 'mangle':
pretty_print(table, chain)
# Pas de QoS vers/depuis la zone ENS
@ -563,11 +493,6 @@ class firewall(base.firewall_routeur):
self.add(table, chain, '-o %s -d %s -j CLASSIFY --set-class 1:3' % (dev['app'], net))
self.add(table, chain, '-o %s -s %s -j CLASSIFY --set-class 1:2' % (dev['out'], net))
# Classification pour federez wifi
for net in base.config.NETs['federez']:
self.add(table, chain, '-o %s -d %s -j CLASSIFY --set-class 1:5' % (dev['federez'], net))
self.add(table, chain, '-o %s -s %s -j CLASSIFY --set-class 1:4' % (dev['out'], net))
# Classification pour la voip
self.add(table, chain, '-d sip.crans.org -j CLASSIFY --set-class 1:12')
self.add(table, chain, '-s sip.crans.org -j CLASSIFY --set-class 1:12')
@ -584,17 +509,17 @@ class firewall(base.firewall_routeur):
utils.tc("class add dev %s parent 1: classid 1:1 "
"htb rate %s ceil %s" % (dev[int_key], uplink_speed, uplink_speed))
utils.tc("class add dev %s parent 1:1 classid 1:2 "
"htb rate %smbit ceil %smbit" % (dev[int_key], debit_max[int_key], debit_max[int_key]))
"htb rate %smbit ceil %smbit" % (dev[int_key], debit_max, debit_max))
# Classe par defaut
utils.tc('class add dev %s parent 1:2 classid 1:10 '
'htb rate %smbit ceil %smbit prio 1' % (dev[int_key], debit_max[int_key], debit_max[int_key]))
'htb rate %smbit ceil %smbit prio 1' % (dev[int_key], debit_max, debit_max))
utils.tc('qdisc add dev %s parent 1:10 '
'handle 10: sfq perturb 10' % dev[int_key])
# Classe par pour la voip
utils.tc('class add dev %s parent 1:2 classid 1:12 '
'htb rate %smbit ceil %smbit prio 0' % (dev[int_key], debit_max[int_key], debit_max[int_key]))
'htb rate %smbit ceil %smbit prio 0' % (dev[int_key], debit_max, debit_max))
utils.tc('qdisc add dev %s parent 1:12 '
'handle 12: sfq perturb 10' % dev[int_key])
@ -622,34 +547,10 @@ class firewall(base.firewall_routeur):
# Classe pour le download des apparetments
utils.tc("class add dev %s parent 1: classid 1:3 "
"htb rate %smbit ceil %smbit" % (dev[int_key], debit_max['total']/10, debit_max['total']/2))
"htb rate %smbit ceil %smbit" % (dev[int_key], debit_max/10, debit_max/2))
utils.tc('qdisc add dev %s parent 1:3 '
'handle 3: sfq perturb 10' % dev[int_key])
# Class du vlan wifi federez, on bride l'upload/download, à 10 mbytes/sec
for int_key in ['federez']:
try:
utils.tc('qdisc del dev %s root' % dev[int_key])
except utils.TcError:
pass
utils.tc('qdisc add dev %s root handle 1: htb r2q 1' % dev[int_key])
utils.tc("class add dev %s parent 1: classid 1:1 "
"htb rate %smbps ceil %smbps" % (dev[int_key], federez_upload_max, federez_upload_max))
# Classe pour l'upload wifi federez
utils.tc("class add dev %s parent 1:1 classid 1:4 "
"htb rate %smbps ceil %smbps" % (dev[int_key], federez_upload_max, federez_upload_max))
utils.tc('qdisc add dev %s parent 1:4 '
'handle 2: sfq perturb 10' % dev[int_key])
# Classe pour le download wifi federez
utils.tc("class add dev %s parent 1: classid 1:5 "
"htb rate %smbit ceil %smbit" % (dev[int_key], debit_max['total']/10, debit_max['total']/2))
utils.tc('qdisc add dev %s parent 1:5 '
'handle 3: sfq perturb 10' % dev[int_key])
print OK
if apply:

View file

@ -1,10 +1,11 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import sys
import netaddr
if '/usr/scripts' not in sys.path:
sys.path.append('/usr/scripts')
if '/usr/scripts/' not in sys.path:
sys.path.append('/usr/scripts/')
import syslog
import subprocess
@ -55,7 +56,7 @@ class firewall_tools(object) :
"""Classe de base du pare-feu implémentant l'association mac-ip (pour les machines filaires) et les blacklists hard"""
def machines(self):
"""Renvoit la liste de toutes les machines"""
"""Renvois la liste de toutes les machines"""
if self._machines:
return self._machines
# On utilise allMachinesAdherents car on a besoin que
@ -63,37 +64,48 @@ class firewall_tools(object) :
# les blacklistes d'un proprio lorsque l'on regarde les blacklistes
# d'une machine
anim('\tChargement des machines')
# On prend toutes les machines y compris celles de ceux qui n'ont pas payé
# elles seront ajoutées dans mac_ip mais blacklistées du fait du non paiement ensuite
self._machines = self.conn.allMachines()
self._machines, self._adherents = self.conn.allMachinesAdherents()
self._adherents = [ adh for adh in self._adherents if adh.paiement_ok() ]
print OK
return self._machines
def adherents(self):
"""
Renvois la liste de tous les adhérents à jour de paiement
(car on suppose que la blackliste paiement est hard)
"""
if self._adherents:
return self._adherents
self._machines, self._adherents = self.conn.allMachinesAdherents()
self._adherents = [ adh for adh in self._adherents if adh.paiement_ok() ]
return self._adherents
def blacklisted_machines(self):
"""Renvoit la liste de toutes les machines ayant une blackliste actives"""
"""Renvois la liste de toutes les machines ayant une blackliste actives"""
if self._blacklisted_machines:
return self._blacklisted_machines
self._blacklisted_machines = [ machine for machine in self.machines() if machine.blacklist_actif() ]
return self._blacklisted_machines
def blacklisted_ips(self, blacklist_sanctions=None):
"""Renvoit l'ensemble des ips des machines ayant une blacklist dans blacklist_sanctions et étant dans nets si spécifié"""
def blacklisted_ips(self, blacklist_sanctions=None, nets=None):
"""Renvois l'ensemble des ips des machines ayant une blacklist dans blacklist_sanctions et étant dans nets si spécifié"""
bl_ips = set()
for machine in self.blacklisted_machines():
if blacklist_sanctions is None or set(bl['type'] for bl in machine.blacklist_actif()).intersection(blacklist_sanctions):
for ip in machine['ipHostNumber']:
bl_ips.add(str(ip))
if nets is None:
bl_ips.add(str(ip))
else:
for net in nets:
if ip in netaddr.IPNetwork(net):
bl_ips.add(str(ip))
return bl_ips
def blacklisted_adherents(self, excepts=[]):
"""Renvoit la liste de tous les adhérents ayant une blackliste active en ignorant les blacklist de excepts"""
if not self._adherents:
self._adherents = self.conn.allAdherents()
"""Renvois la liste de tous les adhérents ayant une blackliste active en ignorant les blacklist de excepts"""
if self._blacklisted_adherents and self._blacklisted_adherents_type == set(excepts):
return self._blacklisted_adherents
self._blacklisted_adherents = filter(lambda adh: adh.blacklist_actif(excepts), self._adherents)
self._blacklisted_adherents = filter(lambda adh: adh.blacklist_actif(excepts), self.adherents())
self._blacklisted_adherents_type = set(excepts)
return self._blacklisted_adherents

View file

@ -102,7 +102,7 @@ class firewall(base.firewall):
self.add(table, chain, '-d 127.0.0.1/8 -j RETURN')
for net in base.config.NETs['all']:
self.add(table, chain, '-d %s -j RETURN' % net)
for adh in self.blacklisted_adherents(excepts=['paiement']):
for adh in self.blacklisted_adherents():
if 'uidNumber' in adh:
self.add(table, chain, '-m owner --uid-owner %s -j REJECT' % adh['uidNumber'][0])
print OK

View file

@ -20,10 +20,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import sys
import re
import os
import pwd
import sys, re, os, pwd
sys.path.append('/usr/scripts/gestion')
@ -60,11 +57,13 @@ def ports(dev_ip6, dev_list):
# Il semble qu'il faille un kernel >= .29 et iptables >= 1.4.3
# http://netfilter.org/projects/iptables/files/changes-iptables-1.4.3.txt
ip6tables.filter.forward('-i %s -p tcp --dport ssh -m state --state NEW -m recent --name SSH2 --set ' % dev_ip6)
ip6tables.filter.forward('-i %s -p tcp --dport ssh -m state --state NEW -m recent --name SSH2 --update --seconds 30 --hitcount 10 --rttl -j DROP' % dev_ip6)
ip6tables.filter.input('-i %s -p tcp --dport ssh -m state --state NEW -m recent --name SSH --set ' % dev_ip6)
ip6tables.filter.input('-i %s -p tcp --dport ssh -m state --state NEW -m recent --name SSH --update --seconds 120 --hitcount 10 --rttl -j DROP' % dev_ip6)
#ip6tables.filter.forward('-i %s -p tcp --dport ssh -m state --state NEW -j ACCEPT' % dev_ip6)
# ip6tables.filter.forward('-i %s -p tcp --dport ssh -m state --state NEW -m \
#recent --name SSH --set ' % dev_ip6)
# ip6tables.filter.forward('-i %s -p tcp --dport ssh -m state --state NEW -m \
#recent --name SSH --update --seconds 60 --hitcount 4 --rttl -j DROP' %
# dev_ip6)
# ip6tables.filter.forward('-i %s -p tcp --dport ssh -m state --state NEW \
#-j ACCEPT' % dev_ip6)
for proto in open_ports.keys():
ip6tables.filter.forward('-i %s -p %s -m multiport --dports %s -j ACCEPT' % (dev_ip6, proto, open_ports[proto]))
@ -136,7 +135,7 @@ def main_router():
ip6tables.mangle.prerouting('-i %s -m state --state NEW -j LOG --log-prefix "LOG_ALL "' % dev_ip6 )
# On force le /32 de google à passer en ipv4 pour tester si ça soulage le tunnel ipv6
ip6tables.filter.forward('-o %s -p tcp -d 2a00:1450:4006::/32 -j REJECT --reject-with icmp6-addr-unreachable' % dev_ip6)
ip6tables.filter.forward('-o %s -p tcp -d 2a00:1450:4006::/32 -j REJECT' % dev_ip6)
# Ipv6 sur évènementiel, on ne laisse sortir que si ça vient de la mac d'ytrap-llatsni
ip6tables.filter.forward('-o %s -d 2a01:240:fe3d:d2::/64 -j ACCEPT' % dev_crans)

View file

@ -45,13 +45,17 @@ class base_reconfigure:
'macip': [ _s + '-macip' for _s in __firewalled_servers ],
# 'droits': [ 'rouge-droits', 'ragnarok-droits' ],
'blacklist': __blacklist_servers,
'bl_carte_etudiant': __blacklist_servers,
'bl_chbre_invalide': __blacklist_servers,
'blacklist_mail_invalide': __blacklist_servers,
'blacklist_virus': __blacklist_servers,
'blacklist_warez': __blacklist_servers,
'blacklist_ipv6_ra': __blacklist_servers,
'blacklist_upload': __blacklist_servers,
'blacklist_p2p': __blacklist_servers,
'blacklist_autodisc_virus': __blacklist_servers,
'blacklist_autodisc_upload': __blacklist_servers,
'blacklist_autodisc_p2p': __blacklist_servers,
'blacklist_bloq': __blacklist_servers,
'del_user': [ 'zbee-del_user', 'owl-del_user', 'zamok-del_user' ],
'port': ['%s-port' % _s for _s in __services.get('connection-main', [])],
@ -64,7 +68,10 @@ class base_reconfigure:
'warez':__service_develop['blacklist_warez'],
'ipv6_ra':__service_develop['blacklist_ipv6_ra'],
'upload': __service_develop['blacklist_upload'],
'p2p': __service_develop['blacklist_p2p'],
'autodisc_virus':__service_develop['blacklist_autodisc_virus'],
'autodisc_upload': __service_develop['blacklist_autodisc_upload'],
'autodisc_p2p': __service_develop['blacklist_autodisc_p2p'],
'bloq': __service_develop['blacklist_bloq'],
})
except ImportError:
@ -229,7 +236,7 @@ class odlyd(base_reconfigure):
class zamok(base_reconfigure):
def del_user(self, args):
# Suppression des fichiers d'impression
# Suppression des fichies d'impression
from adherents import del_user
self._do(del_user(args))

View file

@ -16,37 +16,31 @@
import sys
if '/usr/scripts' not in sys.path:
sys.path.append('/usr/scripts')
sys.path.append('/usr/scripts/gestion')
import commands
import os
IPSET_PATH = '/sbin/ipset'
# Avant jessie: ipset était dans /usr/sbin
if not os.path.exists(IPSET_PATH):
IPSET_PATH = '/usr' + IPSET_PATH
class IpsetError(Exception):
# Gestion des erreurs d'ipset
def __init__(self, cmd, err_code, output):
self.cmd = cmd
self.err_code = err_code
self.output = output
def __init__(self,cmd,err_code,output):
self.cmd=cmd
self.err_code=err_code
self.output=output
def __str__(self):
return "%s\n status : %s\n %s" % (self.cmd, self.err_code, self.output)
return "%s\n status : %s\n %s" % (self.cmd,self.err_code,self.output)
class Ipset(object):
ipset = IPSET_PATH
ipset="/usr/sbin/ipset"
def __str__(self):
return self.set
def __init__(self, set, type, typeopt=''):
self.set = set
self.type = type
self.typeopt = typeopt
def __init__(self,set,type,typeopt=''):
self.set=set
self.type=type
self.typeopt=typeopt
self.squeeze = os.uname()[2] < '3'
try:
self.create()
except IpsetError as error:
@ -56,58 +50,62 @@ class Ipset(object):
raise
pass
def call(self, cmd, arg=''):
def call(self,cmd,arg=''):
"""Appel système à ipset"""
cmd_line = "%s %s %s %s" % (self.ipset, cmd, self.set, arg)
status, output = commands.getstatusoutput(cmd_line)
cmd_line="%s %s %s %s" % (self.ipset,cmd,self.set,arg)
status,output=commands.getstatusoutput(cmd_line)
if status:
raise IpsetError(cmd_line, status, output)
raise IpsetError(cmd_line,status,output)
return output
def create(self, opt=''):
self.call("create", "%s %s" % (self.type, self.typeopt))
def create(self,opt=''):
self.call("-N","%s %s" % (self.type, self.typeopt))
def add(self, arg):
self.call("add", arg)
def add(self,arg):
self.call("-A",arg)
def list(self):
output = self.call("list").splitlines()
list = []
output=self.call("-L").splitlines()
list=[]
for line in output[6:]:
if line == 'Bindings:':
if line=='Bindings:':
break
list.append(line)
return list
def delete(self, ip):
def delete(self,ip):
"""Delete an IP"""
self.call("del", ip)
self.call("-D",ip)
def restore(self, rules):
def restore(self,rules):
""" restore le set courrant"""
rules_str = self.restore_format(rules)
str = "%s\nCOMMIT\n" % rules_str
path = '/tmp/ipset_%s' % self.set
f = open(path, 'w+')
rules_str=self.restore_format(rules)
if self.squeeze:
create_str="-N %s %s %s" % (self.set,self.type,self.typeopt)
str="%s\n%s\nCOMMIT\n" % (create_str,rules_str)
else:
str="%s\nCOMMIT\n" % rules_str
path='/tmp/ipset_%s' % self.set
f=open(path, 'w+')
f.write(str)
f.close()
try:
self.flush()
except IpsetError as error:
sys.stderr.write("%s\n" % error)
cmd = "cat %s | %s -R" % (path, self.ipset)
status, output = commands.getstatusoutput(cmd)
if self.squeeze:
self.destroy()
except IpsetError as error: sys.stderr.write("%s\n" % error)
cmd="cat %s | %s -R" % (path,self.ipset)
status,output=commands.getstatusoutput(cmd)
if status:
raise IpsetError(cmd, status, output)
raise IpsetError(cmd,status,output)
return output
def flush(self):
self.call("flush")
self.call("-F")
def destroy(self):
self.call("destroy")
self.call("-X")
def restore_format(self, rules):
return '\n'.join(["add %s %s" % (self.set, data) for data in rules])
def restore_format(self,rules):
return '\n'.join(["-A %s %s" % (self.set,data) for data in rules])

View file

@ -31,11 +31,11 @@ DEBIAN_BACKPORT_ARCHS="i386 amd64"
DEBIAN_BACKPORT_FTP="ftp://cdimage.debian.org/cdimage/unofficial/backports/"
# Définitions spécifiques à Ubuntu
UBUNTU_DISTS="precise trusty utopic vivid"
UBUNTU_DISTS="precise saucy trusty utopic"
UBUNTU_ARCHS="i386 amd64"
UBUNTU_FTP="ftp://ftp.crans.org/ubuntu/dists"
UBUNTU_LIVE="12.04 14.04 14.10 15.04"
UBUNTU_LIVE="12.04 12.10 13.04 13.10 14.04 14.10"
# il faut modifier le nfs (ajouter la sortie de export_ubuntu_live
# à /etc/exports) et mettre les images dans $ISODIR/ubuntu/ puis
# les monter dans $TFTPROOT/livecd/ubuntu/$dist-$arch avec
@ -54,7 +54,7 @@ CENTOS_ARCHS="i386 x86_64"
CENTOS_FTP="ftp://mirror.in2p3.fr/pub/linux/CentOS"
# Définitions spécifiques à Fedora
FEDORA_DISTS="20 21 22"
FEDORA_DISTS="19 20"
FEDORA_ARCHS="i386 x86_64"
FEDORA_FTP="ftp://ftp.free.fr/mirrors/fedora.redhat.com/fedora/linux/"

View file

@ -123,11 +123,11 @@ for dist in $DEBIAN_DISTS; do
#~ mkdir -p $TFTPROOT/debian-gtk-$dist/$arch
#~ cp $TMPDIR/netboot-debian-gtk-$dist-$arch/debian-installer/$arch/initrd.gz $TFTPROOT/debian-gtk-$dist/$arch
#~ cp $TMPDIR/netboot-debian-gtk-$dist-$arch/debian-installer/$arch/linux $TFTPROOT/debian-gtk-$dist/$arch
#wget $WGETOPT -c $DEBIAN_FTP/$dist/main/installer-kfreebsd-$arch/current/images/netboot/netboot.tar.gz -O $TMPDIR/netboot-debian-kfreebsd-$dist-$arch.tar.gz
#mkdir -p $TMPDIR/netboot-debian-$dist-kfreebsd-$arch/
#tar zxf $TMPDIR/netboot-debian-kfreebsd-$dist-$arch.tar.gz -C $TMPDIR/netboot-debian-$dist-kfreebsd-$arch/
#mkdir -p $TFTPROOT/debian-$dist/kfreebsd-$arch/
# cp -r $TMPDIR/netboot-debian-$dist-kfreebsd-$arch/* $TFTPROOT/debian-$dist/kfreebsd-$arch/
wget $WGETOPT -c $DEBIAN_FTP/$dist/main/installer-kfreebsd-$arch/current/images/netboot/netboot.tar.gz -O $TMPDIR/netboot-debian-kfreebsd-$dist-$arch.tar.gz
mkdir -p $TMPDIR/netboot-debian-$dist-kfreebsd-$arch/
tar zxf $TMPDIR/netboot-debian-kfreebsd-$dist-$arch.tar.gz -C $TMPDIR/netboot-debian-$dist-kfreebsd-$arch/
mkdir -p $TFTPROOT/debian-$dist/kfreebsd-$arch/
cp -r $TMPDIR/netboot-debian-$dist-kfreebsd-$arch/* $TFTPROOT/debian-$dist/kfreebsd-$arch/
done
done
@ -170,12 +170,12 @@ cat >> $TFTPROOT/boot-screens/menu.cfg << EOF
menu end
EOF
done
#for arch in $DEBIAN_ARCHS; do
#cat >> $TFTPROOT/boot-screens/menu.cfg <<EOF
# LABEL Debian $dist kfreebsd-$arch
# kernel debian-$dist/kfreebsd-$arch/grub2pxe
#EOF
#done
for arch in $DEBIAN_ARCHS; do
cat >> $TFTPROOT/boot-screens/menu.cfg <<EOF
LABEL Debian $dist kfreebsd-$arch
kernel debian-$dist/kfreebsd-$arch/grub2pxe
EOF
done
cat >> $TFTPROOT/boot-screens/menu.cfg << EOF
menu end
@ -573,10 +573,8 @@ for dist in $FEDORA_DISTS; do
for arch in $FEDORA_ARCHS; do
mkdir -p $TMPDIR/fedora-$dist/$arch/
wget $WGETOPT -c $FEDORA_FTP/releases/$dist/Fedora/$arch/os/images/pxeboot/initrd.img -O $TMPDIR/fedora-$dist/$arch/initrd.img ||\
wget $WGETOPT -c $FEDORA_FTP/releases/$dist/Server/$arch/os/images/pxeboot/initrd.img -O $TMPDIR/fedora-$dist/$arch/initrd.img ||\
wget $WGETOPT -c $FEDORA_FTP/development/$dist/$arch/os/images/pxeboot/initrd.img -O $TMPDIR/fedora-$dist/$arch/initrd.img
wget $WGETOPT -c $FEDORA_FTP/releases/$dist/Fedora/$arch/os/images/pxeboot/vmlinuz -O $TMPDIR/fedora-$dist/$arch/vmlinuz ||\
wget $WGETOPT -c $FEDORA_FTP/releases/$dist/Server/$arch/os/images/pxeboot/vmlinuz -O $TMPDIR/fedora-$dist/$arch/vmlinuz ||\
wget $WGETOPT -c $FEDORA_FTP/development/$dist/$arch/os/images/pxeboot/vmlinuz -O $TMPDIR/fedora-$dist/$arch/vmlinuz
done
done

View file

@ -28,7 +28,7 @@ console inactivity-timer 30
logging {{ s }}
{%- endfor %}
;--- IP du switch ---
ip default-gateway {{ gateway }}
ip default-gateway 10.231.136.4
{%- for vlan in vlans %}
vlan {{ vlan.id }}
name "{{ vlan.name|capitalize }}"
@ -54,13 +54,12 @@ no web-management
aaa authentication ssh login public-key none
aaa authentication ssh enable public-key none
ip ssh
ip authorized-managers {{ network_id }} {{ subnet }}
ip authorized-managers 10.231.136.0 255.255.255.0
ip ssh filetransfer
;--- Protection contre les boucles ---
loop-protect disable-timer 30
loop-protect transmit-interval 3
loop-protect {{ non_trusted }}
{%- if not public %}
;--- Serveurs radius ---
radius-server dead-time 2
radius-server key {{ radius_key }}
@ -69,26 +68,24 @@ radius-server host {{ s }}
{%- endfor %}
;--- Filtrage mac ---
aaa port-access mac-based addr-format multi-colon
{%- endif %}
;--- Bricoles ---
no cdp run
no stack
;--- DHCP Snooping ---
{%- if dhcp_snooping_vlan_names %}
dhcp-snooping vlan{% for n in dhcp_snooping_vlan_names %} {{ n|vlan_id }}{% endfor %}
dhcp-snooping trust {{ trusted }}
no dhcp-snooping trust {{ non_trusted }}
{%- for s in dhcp_servers %}
dhcp-snooping authorized-server {{ s }}
{%- endfor %}
; Activation
dhcp-snooping
{%- endif %}
{% if ra_filter %};--- RA guards ---
ipv6 ra-guard ports {{ non_trusted }}
no ipv6 ra-guard ports {{ trusted }}
{% endif %}
;--- Config des prises ---
{%- for port in ports %}
{%- if port.radius_auth() and not public %}
{%- if port.radius_auth() %}
aaa port-access mac-based {{ port|int }}
aaa port-access mac-based {{ port|int }} addr-limit {{ port.num_mac() }}
aaa port-access mac-based {{ port|int }} logoff-period 3600
@ -98,9 +95,6 @@ interface {{ port|int }}
enable
name "{{ port }}"
{{ port.flowcontrol() }}
{%- if port.is_trusted() %}
dhcp-snooping trust
{%- endif %}
{%- if gigabit %}
{{ port.speed() }}
{%- endif %}

View file

@ -3,6 +3,8 @@
"""
Génération de la configuration d'un switch.
Attention, cette version n'a pas encore été totalement testée.
procédure de configuration initiale :
* mot de passe admin (password manager user-name <username>)
* activation du ssh (crypto key generate ssh)
@ -51,7 +53,7 @@ V_NO = 3
# Vlans disponibles
ENABLED_VLANS = ['adherent', 'adm', 'wifi', 'v6only', 'accueil', 'isolement',
'appts', 'event', 'federez']
'appts', 'event']
def vlan_id(name):
"""Vlan id of a name (filtre jinja)"""
@ -176,7 +178,7 @@ class Port(object):
return V_NO
elif self.bornes:
if vlan in ['wifi', 'accueil', 'isolement', 'v6only', 'appts',
'event', 'federez']:
'event']:
return V_TAGGED
# Cas d'une borne dans une chambre: l'adherent doit pouvoir
# se connecter
@ -441,11 +443,7 @@ def format_prises_group(data, first, last):
def pretty_print(hostname):
"""Affiche joliement le plan de connexion d'un switch"""
bat, sw_num = get_bat_num(hostname)
try:
switch = ldap.search(u'host=bat%s-%d.adm.crans.org' % (bat, sw_num))[0]
except IndexError:
switch = ldap.search(u'host=bat%s-%d.crans.org' % (bat, sw_num))[0]
switch = ldap.search(u'host=bat%s-%d.adm.crans.org' % (bat, sw_num))[0]
port_dict = get_port_dict(switch)
total = max(port_dict.keys())
@ -465,11 +463,7 @@ def conf_switch(hostname):
"""Affiche la configuration d'un switch"""
bat, sw_num = get_bat_num(hostname)
try:
switch = ldap.search(u'host=bat%s-%d.adm.crans.org' % (bat, sw_num))[0]
except IndexError:
switch = ldap.search(u'host=bat%s-%d.crans.org' % (bat, sw_num))[0]
switch = ldap.search(u'host=bat%s-%d.adm.crans.org' % (bat, sw_num))[0]
tpl_env = jinja2.Environment(loader=jinja2.FileSystemLoader(os.path.dirname(__file__)))
##for info:
@ -482,12 +476,12 @@ def conf_switch(hostname):
'date_gen': datetime.datetime.now(),
# TODO fill that depuis bcfg2 ou whatever
'radius_servers': [
'10.231.136.72',
'10.231.136.11',
],
'radius_servers': ['10.231.136.72', '10.231.136.9' ],
'radius_key': secrets.get('radius_key'),
'ntp_servers': ['10.231.136.98'],
'log_servers': ['10.231.136.38'],
# dhcp et isc (secondaire) sont les deux seuls serveurs
'dhcp_rid_servers': [34, 160],
@ -496,7 +490,7 @@ def conf_switch(hostname):
# réseaux où on fait du dhcp snooping (cf data.NETs)
'dhcp_snooping_vlan_names': ['adherent', 'wifi', 'accueil',
'isolement', 'v6only', 'appts', 'federez'],
'isolement', 'v6only', 'appts'],
}
for com in switch['info']:
@ -523,30 +517,6 @@ def conf_switch(hostname):
first = netaddr.IPNetwork(net_of_vlan_name(vname)[0]).first
data['dhcp_servers'].append(str(netaddr.IPAddress(first + rid)))
# Si le switch n'est pas en .adm, il n'est pas publique (ex : batk-0)
# (désactivation de radius etc)
# On règle les logs, ntp, suivant si le switch est public ou privé (adm)
if u"adm" in unicode(switch['host']):
data['public'] = False
data['ntp_servers'] = ['10.231.136.98']
data['log_servers'] = ['10.231.136.38']
data['gateway'] = '10.231.136.4'
data['network_id'] = '10.231.136.0'
data['subnet'] = '255.255.255.0'
else:
data['public'] = True
data['ntp_servers'] = ['138.231.136.98']
data['log_servers'] = ['138.231.136.38']
data['gateway'] = '138.231.136.4'
data['network_id'] = '138.231.136.0'
data['subnet'] = '255.255.248.0'
# Ra gards ne concerne que les 2620
if "2620" in switch['info'][0].value:
data['ra_filter'] = True
else:
data['ra_filter'] = False
# Switch avec des ports gigabit uniquement
if imodel in GIGABIT_MODELS:
data['gigabit'] = True
@ -569,14 +539,9 @@ def conf_switch(hostname):
V_NO: 'no'}[assign]
vlan.setdefault(attr, PortList())
vlan[attr].extend(p)
if name == 'adm' and not data['public']:
if name == 'adm':
vlan['ip_cfg'] = (gethostbyname(hostname), '255.255.255.0')
if name == 'adherent':
# TODO : proprifier cela
# Si le switch est publique, adh en non tagué partout
if data['public']:
vlan['untagged'] = u'1-' + unicode(switch['nombrePrises'][0])
vlan['ip_cfg'] = (gethostbyname(hostname), '255.255.248.0')
# igmp snooping (multicast) mais nous ne sommes pas querier
vlan['extra'] = 'ip igmp\nno ip igmp querier'
vlans[name] = vlan

File diff suppressed because it is too large Load diff

View file

@ -35,7 +35,7 @@ def handle_exit_code(d, code):
os.system('clear')
sys.exit(0)
else:
msg = "Vous avez appuyé sur ESC ou CTRL+C dans la dernière fenêtre de dialogue.\n\n" \
msg = "Vous avez appuyer sur ESC ou CTRL+C dans la dernière fenêtre de dialogue.\n\n" \
"Voulez vous quitter le programme ?"
if d.yesno(msg, width=60) == d.DIALOG_OK:
os.system('clear')

View file

@ -25,7 +25,7 @@ import netsnmp
if '/usr/scripts' not in sys.path:
path.append('/usr/scripts')
import gestion.secrets_new as secrets
from gestion.config import vlans, bats_virtuels
from gestion.config import vlans
from gestion.annuaires_pg import chbre_prises, all_switchs
try:
@ -390,27 +390,17 @@ class hpswitch :
prise = prise.replace('-','')
return self.get(oid + '.' + prise) == 'up'
def is_fake(self, prise=0):
"""Retourne True ou False selon que le switch est virtuel."""
if self.switch.split('-')[0].replace('bat', '') in bats_virtuels:
return True
return False
def is_enable(self,prise=0):
def is_enable(self,prise=0) :
""" Retoune True ou False suivant si la prise est activée ou non
Si prise=all retourne le nombre de prises activées sur le switch """
if self.switch.split('-')[0].replace('bat', '') in bats_virtuels:
return False
if prise != 'all': prise = int(prise)
return self.__is('IF-MIB::ifAdminStatus',prise)
def is_up(self,prise=0) :
""" Retoune True ou False suivant si la prise est up
Si prise=all retourne le nombre de prises up sur le switch """
if self.switch.split('-')[0].replace('bat', '') in bats_virtuels:
return False
if prise != 'all': prise = int(prise)
return self.__is('IF-MIB::ifOperStatus', prise)
return self.__is('IF-MIB::ifOperStatus',prise)
def nom(self,nom=None,prise=0) :
""" Retourne ou attribue le nom à la prise fournie """
@ -449,15 +439,13 @@ class hpswitch :
def vlans(self, prise = None):
"""Récupère les vlans activés sur la prise 'prise'"""
result = []
if self.switch.split('-')[0].replace('bat', '') in bats_virtuels:
return result
if not prise:
prise = self.prise
prise = int(prise)
oid_base = 'SNMPv2-SMI::enterprises.11.2.14.11.5.1.7.1.15.3.1.1'
oid_format = oid_base + '.%(vlan)d.%(prise)d'
oids = self.walk(oid_base)
result = []
for vlan_name, vlan in vlans.iteritems():
if oid_format % {'vlan': vlan, 'prise': prise} in oids:
result.append(vlan_name)

View file

@ -1,25 +0,0 @@
#!/usr/bin/env python2.7
# -*- coding: utf-8 -*-
#
# This file is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This file is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Street #330, Boston, MA 02111-1307, USA.
import os
from .switch import HPSwitch, SwitchNotFound
from .tools import trace_mac
import gestion.config.snmp as config_snmp
os.environ["MIBS"] = ":".join([mib for mib in config_snmp.PRELOAD_MIBS])

View file

@ -1,67 +0,0 @@
#!/bin/bash /usr/scripts/python.sh
# -*- coding: utf-8 -*-
#
# This file is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This file is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Street #330, Boston, MA 02111-1307, USA.
"""Ce sont les variables utiles pour les autres scripts du
module"""
OPERSTATUS = {
1: 'up',
2: 'down',
3: 'testing',
4: 'unknown',
5: 'dormant',
6: 'notPresent',
7: 'lowerLayerDown',
}
ADMINSTATUS = {
1: 'up',
2: 'down',
3: 'testing',
}
ETHSPEED = {
'HD': {
0: '5',
10: '1',
100: '2',
1000: '5',
},
'FD': {
0: '5',
10: '3',
100: '4',
1000: '6',
},
'AUTO': {
0: '5',
10: '7',
100: '8',
1000: '9',
},
}
REV_ETHSPEED = {
'1': '10 Mbs Half Duplex',
'2': '100 Mbs Half Duplex',
'3': '10 Mbs Full Duplex',
'4': '100 Mbs Full Duplex',
'6': '1000 Mbs Full Duplex',
'5': 'auto',
'7': '10 Mbs auto',
'8': '100 Mbs auto',
'9': '1000 Mbs auto',
}

View file

@ -1,93 +0,0 @@
#!/bin/bash /usr/scripts/python.sh
# -*- coding: utf-8 -*-
#
# This file is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This file is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Street #330, Boston, MA 02111-1307, USA.
"""Contient les outils pour manipuler des adresses MAC
dans le module hptools"""
import binascii
import netaddr
def bin_to_mac(raw):
"""Convertit une OctetString en une MAC"""
return format_mac(binascii.hexlify(raw))
def format_mac(raw):
"""Formatte la mac en aa:bb:cc:dd:ee:ff"""
return str(netaddr.EUI(raw)).replace('-', ':').lower()
class MACFactory(object):
"""Factory stockant les macs"""
__macs = {}
@classmethod
def register_mac(cls, mac, parent=None):
"""Enregistre une mac dans la factory et
retourne une instance de MACAddress si besoin."""
if cls.__macs.get(mac, None) is None:
cls.__macs[mac] = MACAddress(mac, parent)
else:
cls.__macs[mac].append_parent(parent)
return cls.__macs[mac]
@classmethod
def get_mac(cls, mac):
"""Récupère une mac dans la factory"""
return cls.__macs.get(mac, None)
@classmethod
def get_macs(cls):
"""Récupère l'ensemble des MACS de la factory"""
return cls.__macs
class MACAddress(object):
"""Classe représentant une adresse MAC"""
def __init__(self, value, parent=None):
"""Stocke l'adresse mac quelque part et le parent"""
self.__value = value
if parent is not None:
self.__parents = {parent.name() : parent}
@property
def value(self):
"""Property pour lire la valeur d'une MAC"""
return self.__value
@property
def parents(self):
"""Retourne les parents"""
return self.__parents
def append_parent(self, parent):
"""Ajoute un parent à la MAC si parent n'est pas None"""
if parent is not None:
if self.__parents.get(parent.name(), None) is None:
self.__parents[parent.name()] = parent
def remove_parent(self, parent):
"""Retire le parent référencé à la MAC"""
if parent is not None:
if self.__parents.get(parent.name(), None) is not None:
_ = self.__parents.pop(parent.name())

View file

@ -1,153 +0,0 @@
#!/bin/bash /usr/scripts/python.sh
# -*- coding: utf-8 -*-
#
# This file is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This file is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Street #330, Boston, MA 02111-1307, USA.
"""Contient la définition et les outils pour bosser avec
les ports.
C'est essentiellement une couche d'abstraction, les fonctions
utiles sont appelées avec les switches."""
import netaddr
from .mac import MACFactory, bin_to_mac
class HPSwitchPort(object):
"""Classe représentant le port d'un switch"""
def __init__(self, num, parent, ptype=None):
"""Permet de lier un port au switch parent."""
self.__num = num
self.__ptype = ptype
self.__parent = parent
self.__macs = []
self.__multicast = []
self.__vlans = []
self.__oper = False
self.__admin = False
self.__alias = None
self.__eth = None
def name(self):
"""Retourne le nom du port"""
return "%s%02d" % (self.__parent.name(), self.__num)
def get_vlans(self):
"""Retourne les vlans du port"""
return self.__vlans
def add_vlan(self, vlan):
"""Ajoute le vlan à la liste"""
self.__vlans.append(vlan)
def purge_vlans(self):
"""Purge la liste des vlans connus du port"""
self.__vlans = []
def get_eth(self):
"""Récupère l'alias du port"""
if self.__eth is None:
self.__parent.update_eth_speed()
return self.__eth
def set_eth(self, val):
"""Affecte le nom"""
self.__eth = val
def get_alias(self):
"""Récupère l'alias du port"""
if self.__alias is None:
self.__parent.update_ports_aliases()
return self.__alias
def set_alias(self, alias):
"""Affecte le nom"""
self.__alias = alias
def append_mac(self, mac):
"""Ajoute une mac au port"""
self.__macs.append(MACFactory.register_mac(bin_to_mac(mac), self))
def get_macs(self, update=False):
"""Récupère les adresses mac depuis le parent"""
if not self.__macs or update:
# On boucle sur les macs et on les sépare du parent actuel (vu
# qu'on va régénérer sa liste de macs).
self.flush_macs()
__ret = self.__parent.client.walk('hpSwitchPortFdbAddress.%d' % (self.__num,))
self.__macs = [MACFactory.register_mac(bin_to_mac(ret['val']), self) for ret in __ret]
return self.__macs
def flush_macs(self):
"""Vire les macs"""
if not self.__macs:
return True
for mac in self.__macs:
mac.remove_parent(self)
self.__macs = []
return True
def append_multicast(self, multi_ip):
"""Ajoute l'IP aux multicasts"""
self.__multicast.append(netaddr.IPAddress(multi_ip))
@property
def multicast(self):
"""Retourne les ip multicast liées au port."""
return self.__multicast
def flush_multicast(self):
"""Vire les infos sur le multicast."""
self.__multicast = []
@property
def parent(self):
"""Property sur __parent"""
return self.__parent
@property
def oper(self):
"""Retourne l'oper status"""
return self.__oper
@property
def admin(self):
"""Retourne l'admin status"""
return self.__admin
@admin.setter
def admin(self, stat):
"""Met à jour l'admin status. Si stat n'est pas bon, met 3 (testing)"""
try:
stat = int(stat)
except TypeError:
stat = 3
self.__admin = stat
@oper.setter
def oper(self, stat):
"""Met à jour l'oper status. Si stat n'est pas bon, met 4 (unknown)"""
try:
stat = int(stat)
except TypeError:
stat = 4
self.__oper = stat

View file

@ -1,138 +0,0 @@
#!/bin/bash /usr/scripts/python.sh
# -*- coding: utf-8 -*-
#
# This file is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This file is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Street #330, Boston, MA 02111-1307, USA.
"""Ce fichier propose un client snmp basique"""
import netsnmp
import socket
import gestion.secrets_new as secrets_new
class SNMPClient(object):
"""Classe de base définissant un client SNMP."""
def __init__(self, host):
"""Crée une session pointant vers le serveur SNMP, et
peuple les variables utiles."""
# Le fait se gérer si c'est .adm.crans.org, .crans.org, ou
# si le nom est un fqdn ou pas est du ressort du DNS (dans la
# mesure où de toute façon, si on a pas de dns, contacter les
# switches dont on doit résoudre l'IP va être tendu).
try:
self.host = socket.gethostbyname_ex(host)[0]
except socket.gaierror:
self.host = host
self.__session = None
self.__version3 = False
self.__snmp_community = None
self.__snmp_version = None
self.__snmp_seclevel = None
self.__snmp_authprotocol = None
self.__snmp_authpassword = None
self.__snmp_secname = None
self.__snmp_privprotocol = None
self.__snmp_privpassword = None
def __get_session(self, version3=False):
"""Crée une session en cas de besoin, en vérifiant qu'une
session répondant aux besoins n'existe pas déjà."""
if version3 and self.__version3 and self.__session:
return self.__session
if not version3 and not self.__version3 and self.__session:
return self.__session
if version3 and (not self.__version3 or not self.__session):
self.__snmp_community = 'private'
self.__snmp_version = 3
self.__snmp_seclevel = 'authPriv'
self.__snmp_authprotocol = 'SHA'
self.__snmp_authpassword = secrets_new.get('snmp_authentication_pass')
self.__snmp_secname = 'crans'
self.__snmp_privprotocol = 'DES'
self.__snmp_privpassword = secrets_new.get('snmp_privacy_pass')
if not version3 and (self.__version3 or not self.__session):
self.__snmp_community = 'public'
self.__snmp_version = 1
self.__snmp_seclevel = 'noAuthNoPriv'
self.__snmp_authprotocol = 'DEFAULT'
self.__snmp_authpassword = ''
self.__snmp_secname = 'initial'
self.__snmp_privprotocol = 'DEFAULT'
self.__snmp_privpassword = ''
self.__version3 = version3
session = netsnmp.Session(Version=self.__snmp_version, DestHost=self.host,
Community=self.__snmp_community, SecLevel=self.__snmp_seclevel,
SecName=self.__snmp_secname, PrivProto=self.__snmp_privprotocol,
PrivPass=self.__snmp_privpassword, AuthProto=self.__snmp_authprotocol,
AuthPass=self.__snmp_authpassword)
return session
def walk(self, attribute):
"""Fait un walk.
Exemple:
Si je demande hpSwitchPortFdbAddress, le retour contiendra
des entrées ayant pour tag hpSwitchPortFdbAddress, pour iid
une éventuelle valeur (si pertinent), et pour val la valeur
associée."""
self.__session = self.__get_session()
# Crée une variable netsnmp exploitable pour walk.
__varbind = netsnmp.Varbind(attribute)
# La stocke dans une liste.
__varlist = netsnmp.VarList(__varbind)
# __varlist est modifiée en place par la méthode walk.
_ = self.__session.walk(__varlist)
return [
{
'tag': ret.tag,
'iid': ret.iid,
'val': ret.val,
}
for ret in __varlist
]
def set(self, list_of_vars):
"""Met à jour un attribut"""
# On passe en SNMPv3
self.__session = self.__get_session(True)
# On construit la varlist à balancer en SNMP
__varlist = [
netsnmp.Varbind(
tag=res['tag'],
iid=res['iid'],
val=res['val']
)
for res in list_of_vars
]
# Oui, c'est moche
__varlist = netsnmp.VarList(*__varlist)
__ret = self.__session.set(__varlist)
return __ret

View file

@ -1,413 +0,0 @@
#!/bin/bash /usr/scripts/python.sh
# -*- coding: utf-8 -*-
#
# This file is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This file is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Street #330, Boston, MA 02111-1307, USA.
"""Outils principaux pour la description d'un switch"""
import socket
import netaddr
from .port import HPSwitchPort
from .snmp import SNMPClient
from .mac import format_mac, MACFactory
from .defaults import OPERSTATUS, ADMINSTATUS, ETHSPEED, REV_ETHSPEED
class SwitchNotFound(Exception):
"""Erreur basique quand le switch n'est pas trouvé"""
pass
class HPSwitchFactory(object):
"""Factory stockant les switches"""
switches = {}
@classmethod
def get_switch(cls, switch):
"""Récupère un switch dans la factory"""
return cls.switches.get(switch, None)
@classmethod
def register_switch(cls, switch, switch_object):
"""Enregistre un switch dans la factory"""
cls.switches[switch] = switch_object
@classmethod
def get_switches(cls):
"""Récupère l'ensemble des switches dans la Factory"""
return cls.switches
class HPSwitch(object):
"""Classe décrivant un switch HP."""
def __new__(cls, switch):
"""Vérifie d'abord si un switch n'existe pas déjà.
L'idée est d'éviter de manipuler en parallèle des objets
dont les données deviendraient incohérentes. Donc,
lorsqu'on instancie un switch, celui-ci est référencé
par son hostname s'il existe, ou par le nom donné sinon."""
try:
__switch = socket.gethostbyname_ex(switch)[0]
except socket.gaierror:
try:
netaddr.IPAddress(switch)
except netaddr.AddrFormatError:
raise SwitchNotFound("Switch %r non trouvé." % (switch,))
switch_object = HPSwitchFactory.get_switch(__switch)
if switch_object is None:
switch_object = super(HPSwitch, cls).__new__(cls)
HPSwitchFactory.register_switch(switch, switch_object)
return switch_object
def __init__(self, switch):
"""Récupère le nom, et un client snmp"""
self.switch = switch
self.client = SNMPClient(self.switch)
self.ports = {}
self.__build_ports_list()
def version(self):
"""Retourne les données relatives à la version du firmware"""
return self.client.walk('sysDescr')[0]['val']
def name(self):
"""Retourne un nom "standardisé" du switch
(aka g0, c4, )"""
return self.switch.split(".", 1)[0].replace('bat', '').replace('-', '').lower()
def __build_ports_list(self):
"""Construit via une requête SNMP la liste des ports pour le switch."""
__ret = self.client.walk('hpSwitchPhysicalPortEntry.2')
for entry in __ret:
self.ports[int(entry['iid'])] = HPSwitchPort(int(entry['iid']), self, int(entry['val']))
def flush_port(self, num):
"""Vide un port de ses MACs"""
if self.ports.get(num, None) is not None:
self.ports[num].flush_macs()
def flush_ports(self):
"""Vide un port de ses MACs"""
for port in self.ports.itervalues():
port.flush_macs()
def fetch_all_ports(self):
"""Récupère les données depuis les ports du switch, et
renvoie ce qui a un intérêt"""
self.flush_ports()
__ret = self.client.walk('hpSwitchPortFdbAddress')
__ret = [
(int(ret['iid'].split('.')[0]), ret['val'])
for ret in __ret
]
return __ret
def __populate_port(self, num):
"""Peuple le port numéro num"""
if self.ports.get(num, None) is not None:
_ = self.ports[num].get_macs(update=True)
def __populate_all_ports(self):
"""Peuple tous les ports."""
__ret = self.fetch_all_ports()
for (iid, val) in __ret:
if self.ports.get(iid, None) is not None:
self.ports[iid].append_mac(val)
def show_port_macs(self, num, populate=False):
"""Affiche les macs d'un port donné.
Si populate vaut True, fait le populate et affiche le
bousin."""
if populate:
self.__populate_port(num)
return (
[
mac.value
for mac in self.ports[num].get_macs()
],
self.ports[num].name()
)
def show_ports_macs(self, populate=False):
"""Affiche les ports et macs associées.
Si populate vaut True, fait le populate et affiche le
bousin."""
if populate:
self.__populate_all_ports()
return {
port.name(): [
mac.value
for mac in port.get_macs()
]
for port in self.ports.itervalues()
}
def find_mac(self, mac, populate=False):
"""Cherche une mac sur le switch"""
mac = format_mac(mac)
if populate:
self.__populate_all_ports()
# On boucle sur les macs dans la factory
__mac = MACFactory.get_mac(mac)
if __mac is not None:
# On boucle sur les parents (des ports) à la recherche
# de ceux qui appartiennent au switch courant.
__parents = []
for parent in __mac.parents.itervalues():
if parent.parent == self:
__parents.append(parent)
# Si on en a trouvé, on les retourne avec la mac.
if __parents:
return (__mac, __parents)
return None
def __flush_multicast(self):
"""Vide les infos de multicast sur les ports"""
for port in self.ports.itervalues():
port.flush_multicast()
def __update_multicast(self):
"""Fait la mise à jour des infos de multicast sur chaque port"""
# On commence par vider.
self.__flush_multicast()
# On fait un walk.
data = self.client.walk('hpIgmpStatsPortIndex2')
# Le dico est du format standard. L'ip est au milieu du champ iid, et
# le port est dans val.
for data_dict in data:
# En gros, le champ iid ressemble à 1.239.255.255.255.6, où 1 est un truc
# que je connais pas, et 6 le numéro du port.
data_ip = ".".join(data_dict['iid'].split('.')[1:5])
igmp_ip, igmp_port = netaddr.IPAddress(data_ip), int(data_dict['val'])
# Y a plus qu'à stocker
if self.ports.get(igmp_port, None) is not None:
self.ports[igmp_port].append_multicast(igmp_ip)
def get_multicast(self, multi_ip=None, update=True):
"""Permet de récupérer les informations sur les ports
pour lesquels le multicast est actif."""
__output = {}
if multi_ip is not None:
multi_ip = netaddr.IPAddress(multi_ip)
# En cas d'update
if update:
self.__update_multicast()
# On construit le résultat de façon identique dans le cas
# update ou non.
for port in self.ports.itervalues():
for multicast_ip in port.multicast:
__output.setdefault(multicast_ip, []).append(port)
# On filtre par l'ip si besoin.
if multi_ip is not None:
__output = __output[multi_ip]
return __output
def nb_prises(self):
"""Retourne le nombre de prises du switch.
On pourrait aussi faire un self.client.walk('mib-2.17.1.2')
et récupérer la clef "val" du premier élément de la liste
retournée."""
return len(self.ports)
def __update_oper_status(self):
"""Récupère le statut des ports du switch."""
__oper = self.client.walk('ifOperStatus')
for dico in __oper:
port, state = dico['iid'], dico['val']
if self.ports.get(int(port), None) is not None:
self.ports[int(port)].oper = state
def __update_admin_status(self):
"""Récupère l'état d'un port du switch."""
__admin = self.client.walk('ifAdminStatus')
for dico in __admin:
port, state = dico['iid'], dico['val']
if self.ports.get(int(port), None) is not None:
self.ports[int(port)].admin = state
def is_enabled(self, prise, update=True):
"""Vérifie si la prise est activée"""
if update:
self.__update_admin_status()
if self.ports.get(prise, None) is not None:
return ADMINSTATUS[self.ports[prise].admin]
else:
return {
port : ADMINSTATUS[port.admin]
for port in self.ports.itervalues()
}
def is_up(self, prise, update=True):
"""Vérifie si la prise est allumée actuellement
(en gros, s'il y a une mac dessus)"""
if update:
self.__update_oper_status()
if self.ports.get(prise, None) is not None:
return OPERSTATUS[self.ports[prise].oper]
else:
return {
port : OPERSTATUS[port.oper]
for port in self.ports.itervalues()
}
def set_enabled(self, prise, enabled=True):
"""Met le port à enabled/disabled"""
if enabled:
val = '1'
else:
val = '2'
command_dict = {
'iid': str(prise),
'tag': 'ifAdminStatus',
'val': val,
}
return self.client.set([
command_dict
])
def toggle_enabled(self, prise):
"""Alterne up/down"""
if self.is_enabled(prise) == ADMINSTATUS[1]:
enabled = False
else:
enabled = True
return self.set_enabled(prise, enabled)
def get_port_alias(self, prise):
"""Retourne le nom du port"""
if self.ports.get(prise, None) is None:
return ""
return self.ports[prise].get_alias()
def update_ports_aliases(self):
"""Récupère les aliases des ports et les affecte"""
data = self.client.walk('ifAlias')
for data_dict in data:
if self.ports.get(int(data_dict['iid']), None) is not None:
self.ports[int(data_dict['iid'])].set_alias(data_dict['val'])
def set_port_alias(self, prise, alias):
"""Affecte un nom au port"""
if self.ports.get(prise, None) is not None:
self.client.set([
{
'iid': str(prise),
'tag': 'ifAlias',
'val': alias,
}
])
self.ports[prise].set_alias(alias)
def get_eth_speed(self, prise):
"""Retourne le nom du port"""
if self.ports.get(prise, None) is None:
return ""
return REV_ETHSPEED.get(self.ports[prise].get_eth(), 'unknown')
def update_eth_speed(self):
"""Met à jour la vitesse de tous les ports"""
data = self.client.walk('hpSwitchPortFastEtherMode')
for data_dict in data:
if self.ports.get(int(data_dict['iid']), None) is not None:
self.ports[int(data_dict['iid'])].set_eth(data_dict['val'])
def set_eth_speed(self, prise, rate=0, dtype='AUTO'):
"""Affecte un nom au port"""
# On affecte une config spécifique en vitesse
if self.ports.get(prise, None) is not None:
self.client.set([
{
'iid': str(prise),
'tag': 'hpSwitchPortFastEtherMode',
'val': ETHSPEED[dtype][int(rate)],
}
])
self.ports[prise].set_eth(ETHSPEED[dtype][int(rate)])
def get_vlans(self, prise=None, update=True):
"""Récupère les vlans actifs sur une prise"""
# Si mise à jour
if update:
# On fait le ménage
for port in self.ports.itervalues():
port.purge_vlans()
# Et on recommence
data = self.client.walk('enterprises.11.2.14.11.5.1.7.1.15.3.1.1')
for data_dict in data:
vlan, iid = [int(res) for res in data_dict['iid'].split('.')]
if self.ports.get(iid, None) is not None:
self.ports[iid].add_vlan(vlan)
# Si la prise vaut none, on file tout, sinon juste elle.
if prise is not None:
if self.ports.get(prise, None) is not None:
return self.ports[prise].get_vlans()
else:
return {
iid: port.get_vlans()
for (iid, port) in self.ports.iteritems()
}

View file

@ -1,144 +0,0 @@
#!/bin/bash /usr/scripts/python.sh
# -*- coding: utf-8 -*-
#
# This file is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This file is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Street #330, Boston, MA 02111-1307, USA.
"""Fournit des outils et fonctions appelables au besoin"""
from gestion import annuaires_pg
from multiprocessing import Process, Manager
from .switch import HPSwitch
from .mac import MACFactory, format_mac
def filter_uplink(switch, stuff):
"""Filtre les prises uplink d'un retour.
stuff est une liste de la forme
[(port_id, mac), ...]"""
sortie = []
# Retourne "batg", "4.adm.crans.org", par exemple.
bat, num = switch.split('-', 1)
# On filtre ce qui n'est pas utile.
bat = bat[-1]
num = int(num[0])
# On récupère les infos utiles.
uplink = annuaires_pg.uplink_prises[bat]
gen_num_prise = 100 * num
for (port_id, mac) in stuff:
num_prise = gen_num_prise + port_id
if not num_prise in uplink:
sortie.append((port_id, mac))
return sortie
# +--------------------------------------------------------+
# | Mac Tracking Functions |
# +--------------------------------------------------------+
"""Ces fonctions servent à tracker une mac sur le réseau.
La fonction trace_mac s'occupe de ce travail. Elle utilise
la librairie multiprocessing pour spawner un process par
switch, et aller récupérer auprès de ceux-ci la liste des
MACs connectées, et les ports allant bien.
Multiprocessing ne mettant pas en place du partage de variable
par défaut, les objets retournés le sont via un Manager, dans
un dico, sans structure complexe.
Une solution dans laquelle les switches seraient renvoyés dans
leur structure python existe, mais elle est plus coûteuse,
et peu utile dans notre cas. (l'overhead engendré par la méthode
à base de dicos et régénération dans le processus parent est
epsilonesque)"""
def fetch_all_ports(switch, output):
"""Récupère l'ensemble des ports d'un switch, avec les MACS
dessus."""
sw = HPSwitch(switch)
# output est un Manager().dict()
__stuff = sw.fetch_all_ports()
__stuff = filter_uplink(switch, __stuff)
output[switch] = __stuff
def populate_all_switches(switches=None):
"""Remplit l'ensemble des switches avec les MACS qui sont
présentes sur leurs ports.
Peut également ne remplir qu'une liste spécifique si fournie
en argument."""
if switches == None:
switches = annuaires_pg.all_switchs()
hp_switches = {
switch : HPSwitch(switch)
for switch in switches
}
processes = {}
# La sortie des appels de fetch_all_ports sera écrite dans ce dico.
# On évitera la concurrence en utilisant le nom du switch comme
# séparateur
output = Manager().dict()
# Dans une première boucle, on crée les switches. Et on met
# les processes en mode actif.
for switch in switches:
hp_switches[switch].flush_ports()
processes[switch] = Process(target=fetch_all_ports, args=(switch, output), name=switch)
processes[switch].start()
# On fait la jointure des processes dans une seconde
# boucle, pour s'assurer que les processes ont bien
# tous été lancés avant de commencer à les sonder.
for switch in switches:
processes[switch].join()
for switch in switches:
if output[switch] is not None:
for (iid, val) in output[switch]:
if hp_switches[switch].ports.get(iid, None) is not None:
hp_switches[switch].ports[iid].append_mac(val)
else:
print "Output for switch %s is None." % (switch,)
def trace_mac(mac, in_all_switches=False):
"""Cherche une MAC. Si in_all_switches est à True, commence
par instancier tous les switches, et à les peupler.
Cette méthode est assez agressive, il faut l'utiliser avec
précaution."""
if in_all_switches:
populate_all_switches()
mac = format_mac(mac)
# On boucle sur les macs dans la factory
__mac = MACFactory.get_mac(mac)
if __mac is not None:
# On boucle sur les parents (des ports) à la recherche
# de ceux qui appartiennent au switch courant.
__parents = []
for parent in __mac.parents.itervalues():
__parents.append(parent)
# Si on en a trouvé, on les retourne avec la mac.
if __parents:
return (__mac, __parents)
return None

View file

@ -21,10 +21,10 @@
import sys
import os, re, syslog, cPickle, socket
from ldap_crans import crans_ldap, hostname, generalizedTimeFormat
from ldap_crans import crans_ldap, hostname
from commands import getstatusoutput
from config import NETs, role, prefix, rid, output_file, filter_policy, rid_primaires
from config import blacklist_sanctions, blacklist_sanctions_soft, blacklist_bridage_upload, file_pickle, periode_transitoire, gtf_debut_periode_transitoire
from config import blacklist_sanctions, blacklist_sanctions_soft, blacklist_bridage_upload, file_pickle, ann_scol, periode_transitoire
from iptools import AddrInNet
from ridtools import Rid, find_rid_plage
import subprocess
@ -768,13 +768,9 @@ def blacklist(ipt):
if [x for x in sanctions if x in blacklist_sanctions_ipv6]:
blcklst.extend(target.machines())
s = db.search('mblacklist=*&finConnexion>=%(fin)s&finAdhesion>=%(fin)s' % {
'fin': generalizedTimeFormat(),
})
s = db.search('mblacklist=*&paiement=%s' % ann_scol)
if periode_transitoire:
s['machine'].extend(db.search('mblacklist=*&finConnexion>=%(fin)s&finAdhsion>=%(fin)s' % {
'fin': gtf_debut_periode_transitoire,
})['machine'])
s['machine'].extend(db.search('mblacklist=*&paiement=%s' % (ann_scol-1))['machine'])
for target in s['machine']:
sanctions = target.blacklist_actif()

File diff suppressed because it is too large Load diff

Some files were not shown because too many files have changed in this diff Show more