diff --git a/gestion/gen_confs/switch_conf.tpl b/gestion/gen_confs/switch_conf.tpl new file mode 100644 index 00000000..4053cc9b --- /dev/null +++ b/gestion/gen_confs/switch_conf.tpl @@ -0,0 +1,103 @@ +{{ config_header }} + +hostname "{{ hostname }}" +; Generated on {{ date_gen }} by switchs2.py +{{ module_type }} +;--- Snmp --- +snmp-server contact "root@crans.org" +snmp-server location "Batiment {{ bat }}" +;A faire à la main +snmpv3 enable +snmpv3 restricted-access +;snmpv3 user "initial" +snmpv3 user "crans" +snmpv3 group ManagerPriv user "crans" sec-model ver3 +snmp-server community "public" Operator +;--- Heure/date --- +time timezone 60 +time daylight-time-rule Western-Europe +{%- for s in ntp_servers %} +sntp server {{ s }} +{%- endfor %} +timesync sntp +sntp unicast +;--- Misc --- +console inactivity-timer 30 +;--- Logs --- +{%- for s in log_servers %} +logging {{ s }} +{%- endfor %} +;--- IP du switch --- +ip default-gateway 10.231.136.4 +{%- for vlan in vlans %} +vlan {{ vlan.id }} + name "{{ vlan.name|capitalize }}" + {%- if vlan.tagged %} + tagged {{ vlan.tagged }} + {%- endif %} + {%- if vlan.untagged %} + untagged {{ vlan.untagged }} + {%- endif %} + {%- if vlan.ip_cfg %} + ip address {{ vlan.ip_cfg.0 }} {{ vlan.ip_cfg.1 }} + {%- else %} + no ip address + {%- endif %} + {%- if vlan.extra %} + {{ vlan.extra|indent(3, false) }} + {%- endif %} +exit +{%- endfor %} +;--- Accès d'administration --- +no telnet-server +no web-management +aaa authentication ssh login public-key none +aaa authentication ssh enable public-key none +ip ssh +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 }} +;--- Serveurs radius --- +radius-server dead-time 2 +radius-server key {{ radius_key }} +{%- for s in radius_servers %} +radius-server host {{ s }} +{%- endfor %} +;--- Filtrage mac --- +aaa port-access mac-based addr-format multi-colon +;--- 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 %} + +;--- Config des prises --- +{%- for port in ports %} +{%- 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 +aaa port-access mac-based {{ port|int }} unauth-vid 1 +{%- endif %} +interface {{ port|int }} + enable + name "{{ port }}" + {{ port.flowcontrol() }} + {%- if gigabit %} + {{ port.speed() }} + {%- endif %} + no lacp +exit +{%- endfor %} diff --git a/gestion/gen_confs/switchs2.py b/gestion/gen_confs/switchs2.py new file mode 100755 index 00000000..a4ff86d6 --- /dev/null +++ b/gestion/gen_confs/switchs2.py @@ -0,0 +1,345 @@ +#!/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.uplink or self.servers: + # 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['data_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['data_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))