#!/usr/bin/env python # -*- coding: utf-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 ) * activation du ssh (crypto key generate ssh) * copie fichier de conf pour les reconfiguration copier le fichier de conf dans /cfg/startup-config Dans tous les cas FAIRE LE SNMP A LA MAIN (script hptools) """ from __future__ import print_function import sys, os import datetime import re import jinja2 import itertools from socket import gethostbyname import netaddr import argparse sys.path.append('/usr/scripts/') import gestion.secrets_new as secrets from lc_ldap.shortcuts import lc_ldap_readonly as make_ldap_conn import gestion.annuaires_pg as annuaire import gestion.config as config capture_model = re.compile(r'\((.*)\)') GIGABIT_MODELS = ['J9021A', 'J9145A'] ldap = make_ldap_conn() # états possibles V_TAGGED = 1 V_UNTAGGED = 2 V_NO = 3 def vlan_id(name): """Vlan id of a name (filtre jinja)""" return config.vlans[name] def net_of_vlan_name(name): net_name = { 'adherent': 'fil', 'appts': 'personnel-ens', 'v6only': 'gratuit', 'wifi': 'bornes', }.get(name, name) return config.NETs[net_name] class Port(object): """Un port de switch""" num = None # : uplink: None ou str uplink = None # : Liste de serveurs servers = None # : Liste de bornes bornes = None # : Liste de noms de chambres chambres = None def __init__(self, num): self.num = num self.servers = list() self.bornes = list() self.chambres = list() def __str__(self): if self.uplink: return self.uplink else: labels = [] if self.servers: labels.append('Srv_' + ','.join(s['host'][0].value.split('.',1)[0] for s in self.servers)) if self.chambres: labels.append('Ch_' + ','.join(self.chambres)) if self.bornes: labels.append('Wifi_' + ','.join(b['host'][0].value.split('.',1)[0] for b in self.bornes)) return ",".join(labels) or "Inconnu" def __int__(self): return self.num def speed(self): """Full speed or 100Mb ?""" if any("cl" in nom for nom in self.chambres) or self.uplink or \ self.servers: return '' if any( adh['droits'] for adh in self.adherents()): return '' return 'speed-duplex auto-10-100' def flowcontrol(self): if self.uplink or self.servers: return 'no flow-control' else: return 'flow-control' def is_trusted(self): """Est-ce une prise que l'on maîtrise ?""" return self.uplink or self.servers def vlan_member(self, vlan): """Renvoie V_TAGGED, V_UNTAGGED ou V_NO suivant le ``vlan`` (str) demandé""" if self.servers: if vlan == 'adm': return V_UNTAGGED else: return V_NO if self.uplink: # TODO retirer ce hack dégueux: tous les switchs devraient tout # tagguer, même le vlan adhérent if vlan == 'adherent': return V_UNTAGGED else: return V_TAGGED # Précisons tout de suite qu'adm ne va pas plus loin elif vlan == 'adm': return V_NO elif self.bornes: if vlan in ['wifi', 'accueil', 'isolement', 'v6only', 'appts', 'event']: return V_TAGGED # Cas d'une borne dans une chambre: l'adherent doit pouvoir # se connecter elif vlan == 'adherent' and self.chambres: return V_UNTAGGED else: return V_NO # C'est donc une chambre d'adherent sans borne, il y aura # l'auth radius else: return V_NO def radius_auth(self): """Doit-on faire de l'auth radius ?""" return not self.uplink and not self.servers and not self.bornes def adherents(self): """Adhérents sur la prise""" filtre = u'(|%s)' % (''.join('(chbre=%s)' % c for c in self.chambres)) return ldap.search(filtre) def num_mac(self): """Renvoie le nombre de macs autorisées. Ne devrait pas être appelée si c'est une prise d'uplink ou de bornes """ assert(not self.bornes and not self.uplink) num = 2 # Si c'est un club, on peut supposer qu'il a besoin de beaucoup # de machines (krobot etc) if any("cl" in nom for nom in self.chambres): num += 6 else: # Si c'est une chambre d'adhérent, on rajoute le nombre de machines # filaires # TODO cette config serait à faire régulièrement # TODO serait-ce plus rapide de tout chercher d'abord ? for adh in self.adherents(): num += len(adh.machines()) return num class PortList(list): """Liste de ports""" def __str__(self): """ transforme une liste de prises en une chaine pour le switch exemple : 1, 3, 4, 5, 6, 7, 9, 10, 11, 12 => 1,3-7,9-12 """ liste = list(int(x) for x in self) liste.sort() sortie = [] groupe = [-99999,-99999] for x in itertools.chain(liste, [99999]): if x > groupe[1]+1: # Nouveau groupe ! if groupe[0] == groupe[1]: sortie.append('%d' % groupe[1]) else: sortie.append('%d-%d' % tuple(groupe)) groupe[0] = x groupe[1] = x return ','.join(sortie[1:]) def filter_vlan(self, vlan): """Prend un ``vlan`` et renvoie deux PortList, la première avec les ports taggués, la seconde pour les ports untaggués""" tagged = PortList() untagged = PortList() for port in self: assign = port.vlan_member(vlan) if assign == V_TAGGED: tagged.append(port) elif assign == V_UNTAGGED: untagged.append(port) return (tagged, untagged) def conf_switch(hostname): """Affiche la configuration d'un switch""" bat = hostname[3].lower() sw_num = int(hostname[5]) 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: tpl_env.filters['vlan_id'] = vlan_id data = { 'switch': switch, 'hostname': 'bat%s-%d' % (bat, sw_num), 'bat': bat.upper(), 'date_gen': datetime.datetime.now(), # TODO fill that depuis bcfg2 ou whatever '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], # vlans activés (cf data.vlans) 'vlan_names': ['adherent', 'adm', 'wifi', 'v6only', 'accueil', 'isolement', 'appts', 'event'], # réseaux où on fait du dhcp snooping (cf data.NETs) 'dhcp_snooping_vlan_names': ['adherent', 'wifi', 'accueil', 'isolement', 'v6only', 'appts'], } for com in switch['info']: if com.value.startswith(';'): data['config_header'] = com.value.encode('utf-8') break else: print(((u'Impossible de déterminer le header à utiliser pour %s;' + u"Utilisation d'une valeur par défaut (remplir ldap)") % switch).encode('utf-8'), file=sys.stderr) data['config_header']= '; J4899A Configuration Editor; Created on release #H.10.50' imodel = data['config_header'].split(' ', 2)[1] if imodel == "J9145A": data['module_type'] = 'module 1 type J9145A' # Pas de snooping pour les 2810 if "2810" in switch['info'][0].value: data['dhcp_snooping_vlan_names'] = [] else: data['dhcp_servers'] = [] for vname in data['dhcp_snooping_vlan_names']: for rid in data['dhcp_rid_servers']: first = netaddr.IPNetwork(net_of_vlan_name(vname)[0]).first data['dhcp_servers'].append(str(netaddr.IPAddress(first + rid))) # Switch avec des ports gigabit uniquement if imodel in GIGABIT_MODELS: data['gigabit'] = True # Build ports ! ports = {} for x in xrange(1, 1+int(switch['nombrePrises'][0].value)): ports[x] = Port(x) # Remplit les machines ayant une prise spécifiée # (normalement uniquement les serveurs et les bornes) for machine in ldap.search(u'prise=%s%i*' % (bat.upper(), sw_num)): port = ports[int(machine['prise'][0].value[2:])] classe = machine['objectClass'][0].value if classe == 'machineCrans': port.servers.append(machine) elif classe == 'borneWifi': port.bornes.append(machine) # On remplit les chambres for prise, chbres in annuaire.reverse(bat).iteritems(): # TODO rajouter un arg à reverse if int(prise[0]) != int(sw_num): continue port = ports[int(prise[1:])] # (below) beware: type(num) == str (ex: 302g) port.chambres += [bat.upper() + num for num in chbres] # Remplit les uplinks for num_prise, label in annuaire.uplink_prises[bat].iteritems(): if num_prise/100 != int(sw_num): continue port = ports[num_prise % 100] port.uplink = label ports_list = PortList(ports.itervalues()) data['ports'] = ports_list data['vlans'] = [] # On remplit les objets vlans (nom, id, tagged, untagged etc) vlans = {} for name in data['vlan_names']: vlan = { 'name': name, 'id': config.vlans[name], } for assign, p in itertools.groupby(ports_list, lambda p: p.vlan_member(name)): attr = { V_TAGGED: 'tagged', V_UNTAGGED: 'untagged', V_NO: 'no'}[assign] vlan.setdefault(attr, PortList()) vlan[attr].extend(p) if name == 'adm': vlan['ip_cfg'] = (gethostbyname(hostname), '255.255.255.0') if name == 'adherent': # igmp snooping (multicast) mais nous ne sommes pas querier vlan['extra'] = 'ip igmp\nno ip igmp querier' vlans[name] = vlan data['vlans'].append(vlan) # Quelles sont les prises de confiance ? data['trusted'] = str(PortList(p for p in ports_list if p.is_trusted())) data['non_trusted'] = str(PortList(p for p in ports_list if not p.is_trusted())) # On render : return tpl_env.get_template('switch_conf.tpl').render(**data).encode('utf-8') if __name__ == "__main__": parser = argparse.ArgumentParser(description="Génération de la conf d'un "+ "switch.") parser.add_argument('hostname', help="Nom du switch à regénérer " + "(ex: batg-4)") options = parser.parse_args(sys.argv[1:]) print(conf_switch(options.hostname))