diff --git a/config-sample.py b/config-sample.py index 70efc24..025b6df 100644 --- a/config-sample.py +++ b/config-sample.py @@ -27,15 +27,6 @@ #snmp_priv = "" wifi_vlanid = yaml_conf['wifi']['vlan_id'] -# Enable this if we cannot set special option82 tags -franken_net_switches = [ ] - -# If you have franken net, you need snmpv3 credentials to dist -# NOTE: THERE IS NO NEED TO USE THIS IF is_franken_net == False -snmpv3_username = '' -snmpv3_auth = '' -snmpv3_priv = '' - models = {} for model in yaml_conf['models']: data = bunch(template=model['path'],eth=model['ports']) diff --git a/dhcp-hook.py b/dhcp-hook.py index 1e3a111..13bcd60 100755 --- a/dhcp-hook.py +++ b/dhcp-hook.py @@ -26,6 +26,6 @@ sql = "SELECT short_name FROM network WHERE ipv4_gateway_txt = ?" networkname = cursor.execute(sql, (swRelay, )).fetchone()[0] db.set('networkname-{}'.format(swIp), networkname) - if "Juniper" not in :swClient + if "Juniper" not in swClient: # We don't need any SNMP or base config for Juniper. os.system("/scripts/swboot/configure " + swIp + " &") diff --git a/dhcpd.conf b/dhcpd.conf index ae220ba..b98dd6f 100644 --- a/dhcpd.conf +++ b/dhcpd.conf @@ -20,7 +20,7 @@ on commit { if agentType = "0" { set swName = pick-first-value(binary-to-ascii(16, 8, ":", substring(option agent.circuit-id, 4, 2)), "None"); } else { - set swName = pick-first-value(substring(option agent.circuit-id, 2, 10), "None"); + set swName = pick-first-value(substring(option agent.circuit-id, 2, 50), "None"); } set swMac = binary-to-ascii(16, 8, ":", substring(hardware, 1, 6)); set swIp = binary-to-ascii(10, 8, ".", leased-address); diff --git a/swcommon.py b/swcommon.py new file mode 100644 index 0000000..67f8789 --- /dev/null +++ b/swcommon.py @@ -0,0 +1,136 @@ + +#!/usr/bin/env python +import tempfile +import syslog +import redis +import netsnmp +import os +import re +import traceback +import time + +import config + +db = redis.Redis() + +def log(*args): + print time.strftime("%Y-%m-%d %H:%M:%S") + ':', ' '.join(args) + syslog.syslog(syslog.LOG_INFO, ' '.join(args)) + +def error(*args): + print time.strftime("%Y-%m-%d %H:%M:%S") + ': ERROR:', ' '.join(args) + syslog.syslog(syslog.LOG_ERR, ' '.join(args)) + +def sw_reload(ip): + error("Reloading switch") + try: + os.system("/scripts/swboot/reload " + ip + " &") + except: + error("Exception in reload:", traceback.format_exc()) + +def generate(out, ip, switch): + model = db.get('client-{}'.format(ip)) + if model == None: + # Get Cisco model name (two tries) + for i in xrange(2): + var = netsnmp.Varbind('.1.3.6.1.2.1.47.1.1.1.1.13.1') + model = netsnmp.snmpget(var, Version=2, DestHost=ip, Community='private')[0] + + if model == None: + var = netsnmp.Varbind('.1.3.6.1.2.1.47.1.1.1.1.13.1001') + model = netsnmp.snmpget(var, Version=2, DestHost=ip, Community='private')[0] + + if model == None: + sw_reload(ip) + error("Could not get model for switch" , ip) + return + + if not model in config.models: + sw_reload(ip) + error("Template for model", model_id, "not found") + return + + # Throws exception if something bad happens + try: + txt = config.generate(switch, model) + out.write(txt) + except: + sw_reload(ip) + error("Exception in generation for %s :" % switch, traceback.format_exc()) + out.close() + return None + + return out + +def base(out, switch): + out.write("snmp-server community private rw\n") + out.write("hostname BASE\n") + out.write("no vlan 2-4094\n") + out.write("end\n\n") + +def select_file(file_to_transfer, ip): + if file_to_transfer in config.static_files: + return file(config.static_files[file_to_transfer]) + + global db + switch = db.get(ip) + if switch is None: + error('No record of switch', ip, 'in Redis, ignoring ..') + return None + + log('Switch is', switch) + db.set('switchname-%s' % ip, switch) + + model = db.get('client-{}'.format(ip)) + + if not re.match('^([A-Z]{1,2}[0-9][0-9]-[A-C]|DIST:[A-Z]{1,2}-[A-Z]-[A-Z]+-S[TW])$', switch): + sw_reload(ip) + error("Switch", ip, "does not match regexp, invalid option 82? Received '", switch, "' as option 82") + return None + + # Dist config. + if "DIST:" in switch and file_to_transfer.lower().endswith("-confg"): + if re.match(r'^[a-zA-Z0-9:-]+$', switch) and os.path.isfile('distconfig/%s' % switch[5:]): + log("Sending config to", switch) + f = open('distconfig/%s' % switch[5:]) + return f + error('Dist config not found for', ip) + return None + + # Juniper config. + if file_to_transfer == "juniper-confg": + log("Generating Juniper config for", ip, "name =", switch) + f = tempfile.TemporaryFile() + f.write(config.generate(switch, model)) + f.seek(0) + return f + + # Switch base config. + if (file_to_transfer == "network-confg" or + file_to_transfer == "Switch-confg"): + log("Generating config for", ip, "name =", switch) + f = tempfile.TemporaryFile() + base(f, switch) + f.seek(0) + return f + + # Juniper image. + if file_to_transfer == "juniper.tgz": + if (model in config.models) and ('image' in config.models[model]): + log("Sending JunOS image to ", ip, "name =", switch) + return file(config.models[model]['image']) + log("Missing image file for", ip, "name =", switch) + + # Final config for non-Juniper switches. + if file_to_transfer.lower().endswith("-confg"): + f = tempfile.TemporaryFile() + log("Generating config for", ip,"config =", switch) + if generate(f, ip, switch) == None: + return None + f.seek(0) + return f + + error("Switch", ip, "config =", switch, "tried to get file", + file_to_transfer) + return None + diff --git a/swhttpd.py b/swhttpd.py index e053d30..cd73cd1 100755 --- a/swhttpd.py +++ b/swhttpd.py @@ -1,15 +1,13 @@ #!/usr/bin/env python import sys, logging -import redis import syslog import socket -import re -import tempfile import SimpleHTTPServer import SocketServer import time import config +import swcommon def log(*args): print time.strftime("%Y-%m-%d %H:%M:%S") + ':', ' '.join(args) @@ -21,45 +19,24 @@ def error(*args): class swbootHttpHandler(SimpleHTTPServer.SimpleHTTPRequestHandler): def do_GET(self): - db = redis.Redis() - switch = db.get(self.client_address[0]) - model = db.get('client-{}'.format(self.client_address[0])) - if switch == None or model == None: - log("Switch not found:", self.client_address[0]) + # self.path is the path of the requested file. + file_handle = swcommon.select_file(self.path.lstrip("/"), self.client_address[0]) + + if file_handle == None: self.send_error(404, "File not found") return None - if self.path == "/juniper-confg": - log("Generating Juniper config for", - self.client_address[0], "name =", switch) - f = tempfile.TemporaryFile() - f.write(config.generate(switch, model)) - content_length = f.tell() - f.seek(0) - self.send_response(200) - self.send_header("Content-type", "application/octet-stream") - self.send_header("Content-Length", content_length) - self.end_headers() - self.copyfile(f, self.wfile) - log("Config sent to", self.client_address[0], "name =", switch) + # Go to the end of the file to get the length of it. + file_handle.seek(0, 2) + content_length = file_handle.tell() + file_handle.seek(0) + + self.send_response(200) + self.send_header("Content-type", "application/octet-stream") + self.send_header("Content-Length", content_length) + self.end_headers() + self.copyfile(file_handle, self.wfile) - f.close() - return - elif self.path == "/juniper.tgz": - log("Sending JunOS file", config.models[model]['image'], "to", - self.client_address[0], "name =", switch) - if (model in config.models) and ('image' in config.models[model]): - # All good! Overwrite the requested file path and send our own. - self.path = config.models[model]['image'] - f = self.send_head() - if f: - self.copyfile(f, self.wfile) - log("Sent JunOS to", self.client_address[0], "name =", switch) - f.close() - else: - log("Unknown file:", self.path) - self.send_error(404, "File not found") - # We write our own logs. def log_request(self, code='-', size='-'): pass @@ -70,10 +47,9 @@ class swbootTCPServer(SocketServer.ForkingTCPServer): def server_bind(self): self.socket.bind(self.server_address) -log("swhttpd started") - try: httpd = swbootTCPServer(("", 80), swbootHttpHandler) + log("swhttpd started") httpd.serve_forever() except socket.error, err: sys.stderr.write("Socket error: %s\n" % str(err)) diff --git a/swtftpd.py b/swtftpd.py index d3d1284..380c0b3 100755 --- a/swtftpd.py +++ b/swtftpd.py @@ -1,18 +1,11 @@ #!/usr/bin/env python import sys, logging import tftpy -import tempfile -import redis import syslog -import netsnmp -import traceback -import re -import os import time import config - -db = redis.Redis() +import swcommon def log(*args): print time.strftime("%Y-%m-%d %H:%M:%S") + ':', ' '.join(args) @@ -22,155 +15,18 @@ def error(*args): print time.strftime("%Y-%m-%d %H:%M:%S") + ': ERROR:', ' '.join(args) syslog.syslog(syslog.LOG_ERR, ' '.join(args)) -def sw_reload(ip): - error("Reloading switch") - try: - os.system("/scripts/swboot/reload " + ip + " &") - except: - error("Exception in reload:", traceback.format_exc()) - -def generate(out, ip, switch): - model = db.get('client-{}'.format(ip)) - if model == None: - # Get Cisco model name (two tries) - for i in xrange(2): - var = netsnmp.Varbind('.1.3.6.1.2.1.47.1.1.1.1.13.1') - model = netsnmp.snmpget(var, Version=2, DestHost=ip, Community='private')[0] - - if model == None: - var = netsnmp.Varbind('.1.3.6.1.2.1.47.1.1.1.1.13.1001') - model = netsnmp.snmpget(var, Version=2, DestHost=ip, Community='private')[0] - - if model == None: - sw_reload(ip) - error("Could not get model for switch" , ip) - return - - if not model in config.models: - sw_reload(ip) - error("Template for model " + model_id + " not found") - return - - # Throws exception if something bad happens - try: - txt = config.generate(switch, model) - out.write(txt) - except: - sw_reload(ip) - error("Exception in generation for %s :" % switch, traceback.format_exc()) - out.close() - return None - - return out - -def base(out, switch): - out.write("snmp-server community private rw\n") - out.write("hostname BASE\n") - out.write("no vlan 2-4094\n") - out.write("end\n\n") - -def snmpv3_command(var, host, cmd): - return cmd(var, Version=3, DestHost=host, - SecName=config.snmpv3_username, SecLevel='authPriv', - AuthProto='SHA', AuthPass=config.snmpv3_auth, - PrivProto='AES128', PrivPass=config.snmpv3_priv) - -def resolve_option82(relay, option82): - module = int(option82[0], 16) - port = int(option82[1], 16) - print 'Switch on "%s" attached to module "%s" and port "%s"' % ( - relay, module, port) - var = netsnmp.VarList(netsnmp.Varbind('.1.3.6.1.2.1.31.1.1.1.1')) - if snmpv3_command(var, relay, netsnmp.snmpwalk) is None: - print 'ERROR: Unable to talk to relay "%s" for description lookup' % relay - return None - - for result in var: - iface = result.tag.split('.')[-1] - name = result.val - if (name == 'Gi%d/%d' % (module, port) or - name == 'Gi%d/0/%d' % (module, port)): - print 'Found switch on interface "%s"' % name - var = netsnmp.Varbind( - '.1.3.6.1.4.1.9.2.2.1.1.28.%d' % int(iface)) - return snmpv3_command(var, relay, netsnmp.snmpget)[0][6:] +def file_callback(file_to_transfer, raddress, rport): + return swcommon.select_file(file_to_transfer, raddress) -def file_callback(file_to_transfer, ip, rport): - if file_to_transfer in config.static_files: - return file(config.static_files[file_to_transfer]) - - global db - option82 = db.get(ip) - if option82 is None: - error('No record of switch', ip, 'in Redis, ignoring ..') - return None - - # If we do not have any franken switches, do not execute this horrible code path - if not config.franken_net_switches: - switch = option82 - else: - # In this sad universe we have switches with different capabilities, so we - # need to figure out who sent the request. We use the Gateway Address - # (a.k.a. relay address) for this. - relay = db.get('relay-%s' % ip) - if relay not in config.franken_net_switches: - # Puh, cris averted - not a franken switch. - switch = option82 - else: - # If the relay is set to 0.0.0.0 something is wrong - this shouldn't - # be the case anymore, but used to happen when dhcp-hook didn't filter - # this. - if relay == '0.0.0.0': - error('Ignoring non-relayed DHCP request from', ip) - return None - switch = resolve_option82(relay, option82.split(':')) - - if switch is None: - error('Unable to identifiy switch', ip) - return None - - print 'Switch is "%s"' % switch - db.set('switchname-%s' % ip, switch) - - if (file_to_transfer == "network-confg" or - file_to_transfer == "Switch-confg"): - f = tempfile.TemporaryFile() - log("Generating base config", file_to_transfer, - "for", ip,"config =", switch) - base(f, switch) - f.seek(0) - return f - - if file_to_transfer == "juniper.tgz": - model = db.get('client-{}'.format(ip)) - if (model in config.models) and ('image' in config.models[model]): - return file(config.models[model]['image']) - - if not re.match('[A-Z]{1,2}[0-9][0-9]-[A-C]', switch): - sw_reload(ip) - error("Switch", ip, "does not match regexp, invalid option 82? Received ", option82, " as option 82") - return None - - f = tempfile.TemporaryFile() - if file_to_transfer.lower().endswith("-confg"): - log("Generating config for", ip,"config =", switch) - if generate(f, ip, switch) == None: - return None - else: - error("Switch", ip, "config =", switch, "tried to get file", - file_to_transfer) - f.close() - return None +server = tftpy.TftpServer('/scripts/swboot/ios', file_callback) - f.seek(0) - return f +# TFTPD logging not needed in production, we have our own functions. +tftplog = logging.getLogger('tftpy') +tftplog.addHandler(logging.NullHandler()) log("swtftpd started") -server = tftpy.TftpServer('/scripts/swboot/ios', file_callback) -tftplog = logging.getLogger('tftpy.TftpClient') -tftplog.setLevel(logging.WARN) try: - server.listen("192.168.40.10", 69) + server.listen("", 69) except tftpy.TftpException, err: sys.stderr.write("%s\n" % str(err)) sys.exit(1)