From 4c768af92f73dda8d48cef6b0d6f35c4401de026 Mon Sep 17 00:00:00 2001 From: zmegolaz Date: Thu, 21 Nov 2019 15:54:25 +0100 Subject: [PATCH 1/3] Added support for dist config. Moved common http/tftp code to separate file. Fixed syntax error in dhcp-hook.py. --- dhcp-hook.py | 2 +- dhcpd.conf | 2 +- swcommon.py | 131 +++++++++++++++++++++++++++++++++++++++++++++ swhttpd.py | 53 ++++++------------- swtftpd.py | 147 ++------------------------------------------------- 5 files changed, 153 insertions(+), 182 deletions(-) create mode 100644 swcommon.py 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..42c30ef --- /dev/null +++ b/swcommon.py @@ -0,0 +1,131 @@ + +#!/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]-[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 + + 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', ip) + return None + + 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 + + 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 + + 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) + + 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..44acb1e 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,25 @@ 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: + # 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: log("Switch not found:", self.client_address[0]) 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 @@ -68,6 +46,7 @@ def log_error(self, format, *args): class swbootTCPServer(SocketServer.ForkingTCPServer): def server_bind(self): + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.socket.bind(self.server_address) log("swhttpd started") diff --git a/swtftpd.py b/swtftpd.py index d3d1284..6167162 100755 --- a/swtftpd.py +++ b/swtftpd.py @@ -11,6 +11,7 @@ import time import config +import swcommon db = redis.Redis() @@ -22,155 +23,15 @@ 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, 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 - - f.seek(0) - return f +def file_callback(file_to_transfer, raddress, rport): + return swcommon.select_file(file_to_transfer, raddress) 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) From 54546f2c30b7f7378b8905f62397a3421f0b043c Mon Sep 17 00:00:00 2001 From: zmegolaz Date: Thu, 21 Nov 2019 16:08:00 +0100 Subject: [PATCH 2/3] Better switch name validation. Added some comments. Moved some log messages when starting swhttpd and swtftpd. --- swcommon.py | 9 +++++++-- swhttpd.py | 3 +-- swtftpd.py | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/swcommon.py b/swcommon.py index 42c30ef..d39f93e 100644 --- a/swcommon.py +++ b/swcommon.py @@ -83,11 +83,12 @@ def select_file(file_to_transfer, ip): model = db.get('client-{}'.format(ip)) - if not re.match('^([A-Z]{1,2}[0-9][0-9]-[A-C]|DIST:[A-Z]-[A-Z]-[A-Z]+-S[TW])$', switch): + 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") + 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) @@ -96,6 +97,7 @@ def select_file(file_to_transfer, ip): error('Dist config not found', ip) return None + # Juniper config. if file_to_transfer == "juniper-confg": log("Generating Juniper config for", ip, "name =", switch) f = tempfile.TemporaryFile() @@ -103,6 +105,7 @@ def select_file(file_to_transfer, ip): 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) @@ -111,12 +114,14 @@ def select_file(file_to_transfer, ip): 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) diff --git a/swhttpd.py b/swhttpd.py index 44acb1e..941aa5e 100755 --- a/swhttpd.py +++ b/swhttpd.py @@ -49,10 +49,9 @@ def server_bind(self): self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 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 6167162..29cb3aa 100755 --- a/swtftpd.py +++ b/swtftpd.py @@ -26,10 +26,10 @@ def error(*args): def file_callback(file_to_transfer, raddress, rport): return swcommon.select_file(file_to_transfer, raddress) -log("swtftpd started") server = tftpy.TftpServer('/scripts/swboot/ios', file_callback) tftplog = logging.getLogger('tftpy.TftpClient') tftplog.setLevel(logging.WARN) +log("swtftpd started") try: server.listen("", 69) except tftpy.TftpException, err: From 23c29bec20ef432067d0f83b8abefb051c8dbb53 Mon Sep 17 00:00:00 2001 From: zmegolaz Date: Thu, 21 Nov 2019 19:26:00 +0100 Subject: [PATCH 3/3] Minor logging changes. Cleaned franken net stuff. Removed unused variables and modules. Removed reuse_addr. --- config-sample.py | 9 --------- swcommon.py | 6 +++--- swhttpd.py | 2 -- swtftpd.py | 15 +++++---------- 4 files changed, 8 insertions(+), 24 deletions(-) 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/swcommon.py b/swcommon.py index d39f93e..67f8789 100644 --- a/swcommon.py +++ b/swcommon.py @@ -47,7 +47,7 @@ def generate(out, ip, switch): if not model in config.models: sw_reload(ip) - error("Template for model " + model_id + " not found") + error("Template for model", model_id, "not found") return # Throws exception if something bad happens @@ -78,7 +78,7 @@ def select_file(file_to_transfer, ip): error('No record of switch', ip, 'in Redis, ignoring ..') return None - log('Switch is ', switch) + log('Switch is', switch) db.set('switchname-%s' % ip, switch) model = db.get('client-{}'.format(ip)) @@ -94,7 +94,7 @@ def select_file(file_to_transfer, ip): log("Sending config to", switch) f = open('distconfig/%s' % switch[5:]) return f - error('Dist config not found', ip) + error('Dist config not found for', ip) return None # Juniper config. diff --git a/swhttpd.py b/swhttpd.py index 941aa5e..cd73cd1 100755 --- a/swhttpd.py +++ b/swhttpd.py @@ -23,7 +23,6 @@ def do_GET(self): file_handle = swcommon.select_file(self.path.lstrip("/"), self.client_address[0]) if file_handle == None: - log("Switch not found:", self.client_address[0]) self.send_error(404, "File not found") return None @@ -46,7 +45,6 @@ def log_error(self, format, *args): class swbootTCPServer(SocketServer.ForkingTCPServer): def server_bind(self): - self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.socket.bind(self.server_address) try: diff --git a/swtftpd.py b/swtftpd.py index 29cb3aa..380c0b3 100755 --- a/swtftpd.py +++ b/swtftpd.py @@ -1,20 +1,12 @@ #!/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 import swcommon -db = redis.Redis() - def log(*args): print time.strftime("%Y-%m-%d %H:%M:%S") + ':', ' '.join(args) syslog.syslog(syslog.LOG_INFO, ' '.join(args)) @@ -27,8 +19,11 @@ def file_callback(file_to_transfer, raddress, rport): return swcommon.select_file(file_to_transfer, raddress) server = tftpy.TftpServer('/scripts/swboot/ios', file_callback) -tftplog = logging.getLogger('tftpy.TftpClient') -tftplog.setLevel(logging.WARN) + +# TFTPD logging not needed in production, we have our own functions. +tftplog = logging.getLogger('tftpy') +tftplog.addHandler(logging.NullHandler()) + log("swtftpd started") try: server.listen("", 69)