diff --git a/bird.py b/bird.py new file mode 100644 index 0000000..9cee93c --- /dev/null +++ b/bird.py @@ -0,0 +1,148 @@ + +import socket +import sys +import traceback + +BUFSIZE = 4096 + +SUCCESS_CODES = { + "0000" : "OK", + "0001" : "Welcome", + "0002" : "Reading configuration", + "0003" : "Reconfigured", + "0004" : "Reconfiguration in progress", + "0005" : "Reconfiguration already in progress, queueing", + "0006" : "Reconfiguration ignored, shutting down", + "0007" : "Shutdown ordered", + "0008" : "Already disabled", + "0009" : "Disabled", + "0010" : "Already enabled", + "0011" : "Enabled", + "0012" : "Restarted", + "0013" : "Status report", + "0014" : "Route count", + "0015" : "Reloading", + "0016" : "Access restricted", +} + +TABLES_ENTRY_CODES = { + "1000" : "BIRD version", + "1001" : "Interface list", + "1002" : "Protocol list", + "1003" : "Interface address", + "1004" : "Interface flags", + "1005" : "Interface summary", + "1006" : "Protocol details", + "1007" : "Route list", + "1008" : "Route details", + "1009" : "Static route list", + "1010" : "Symbol list", + "1011" : "Uptime", + "1012" : "Route extended attribute list", + "1013" : "Show ospf neighbors", + "1014" : "Show ospf", + "1015" : "Show ospf interface", + "1016" : "Show ospf state/topology", + "1017" : "Show ospf lsadb", + "1018" : "Show memory", +} + +ERROR_CODES = { + "8000" : "Reply too long", + "8001" : "Route not found", + "8002" : "Configuration file error", + "8003" : "No protocols match", + "8004" : "Stopped due to reconfiguration", + "8005" : "Protocol is down => cannot dump", + "8006" : "Reload failed", + "8007" : "Access denied", + + "9000" : "Command too long", + "9001" : "Parse error", + "9002" : "Invalid symbol type", +} + +END_CODES = ERROR_CODES.keys() + SUCCESS_CODES.keys() + +global bird_sockets +bird_sockets = {} + +def BirdSocketSingleton(host, port): + global bird_sockets + s = bird_sockets.get((host,port), None) + if not s: + s = BirdSocket(host,port) + bird_sockets[(host,port)] = s + return s + +class BirdSocket: + + def __init__(self, host, port): + self.__host = host + self.__port = port + self.__sock = None + + def __connect(self): + if self.__sock: return + + self.__sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.__sock.settimeout(3.0) + self.__sock.connect((self.__host, self.__port)) + + def close(self): + if self.__sock: + try: self.__sock.close() + except: pass + self.__sock = None + + def cmd(self, cmd): + try: + self.__connect() + self.__sock.send(cmd + "\n") + return self.__read() + except socket.error: + why = sys.exc_info()[1] + self.close() + return False, "Bird connection problem: %s" % why + + def __read(self): + + code = "7000" # Not used in bird + parsed_string = "" + lastline = "" + + while code not in END_CODES: + data = self.__sock.recv(BUFSIZE) + + lines = (lastline + data).split("\n") + if len(data) == BUFSIZE: + lastline = lines[-1] + lines = lines[:-1] + + for line in lines: + code = line[0:4] + + if not line.strip(): + continue + elif code == "0000": + return True, parsed_string + elif code in SUCCESS_CODES.keys(): + return True, SUCCESS_CODES.get(code) + elif code in ERROR_CODES.keys(): + return False, ERROR_CODES.get(code) + elif code[0] in [ "1", "2"] : + parsed_string += line[5:] + "\n" + elif code[0] == " ": + parsed_string += line[1:] + "\n" + elif code[0] == "+": + parsed_string += line[1:] + else: + parsed_string += "<<>>\n"%line + + return True, parsed_string + + +__all__ = ['BirdSocketSingleton' , 'BirdSocket' ] + + + diff --git a/lg.cfg b/lg.cfg new file mode 100644 index 0000000..7aafb8f --- /dev/null +++ b/lg.cfg @@ -0,0 +1,8 @@ + + +HOST_MAPPING={ + "gw": { "ipv4" : 9994, "ipv6": 9996 }, + "h3": { "ipv4" : 9994, "ipv6": 9996 }, +} + +SESSION_KEY = '\xd77\xf9\xfa\xc2\xb5\xcd\x85)`+H\x9d\xeeW\\%\xbe/\xbaT\x89\xe8\xa7' diff --git a/lg.py b/lg.py index 0b0eca2..d08ff82 100755 --- a/lg.py +++ b/lg.py @@ -1,211 +1,177 @@ #!/usr/bin/python -import sys, os, subprocess, re -from dns import resolver,reversename -from flask import Flask, render_template, jsonify, redirect +import sys +import os +import subprocess +import re + +from toolbox import mask_is_valid, ipv6_is_valid, ipv4_is_valid, resolve + +from bird import BirdSocketSingleton +from flask import Flask, render_template, jsonify, redirect, session, request + app = Flask(__name__) - -import socket - -def get_ip(n, q): - return str(resolver.query(n,q)[0]) - -def check_mask(n): - if not n: - return True - try: - mask = int(n) - return ( mask > 1 and mask < 128) - except: - return False - -def check_ipv4(n): - try: - socket.inet_pton(socket.AF_INET, n) - return True - except socket.error: - return False - -def check_ipv6(n): - try: - socket.inet_pton(socket.AF_INET6, n) - return True - except socket.error: - return False +app.config.from_pyfile('lg.cfg') -def cleanup_output(text): - return "\n".join([ add_link(re.sub(r'^[0-9]*-', r' ', line)) for line in text.split("\n") if not line.startswith("0000") ]) +def add_links(text): + if type(text) in [ str, unicode ]: + text = text.split("\n") -def add_link(text): - if text.strip().startswith("BGP.as_path:") or \ - text.strip().startswith("Neighbor AS:") : - return re.sub(r'([0-9]*)',r'\1',text) + ret_text = [] + for line in text: + if line.strip().startswith("BGP.as_path:") or \ + line.strip().startswith("Neighbor AS:") : + ret_text.append(re.sub(r'(\d+)',r'\1',line)) + else: + line = re.sub(r'AS(\d+)', r'AS\1',line) + line = re.sub(r'(\d+\.\d+\.\d+\.\d+)', r'\1',line) + ret_text.append(line) + return "\n".join(ret_text) + +def set_session(req_type, hosts, proto, request_args): + session.update( { + "req_type": req_type, + "hosts": hosts, + "proto": proto, + "request_args": request_args, + }) + +def bird_command(host, proto, command): + conf = app.config["HOST_MAPPING"].get(host, None) + port = conf.get(proto) + if not conf or not port: + return False, "Host/Proto not allowed" else: - return text - -#@app.errorhandler(404) -#def notfound(error): -# return redirect("/") + b = BirdSocketSingleton(host, port) + return b.cmd(command) + +@app.context_processor +def inject_all_host(): + return dict(all_hosts="+".join(app.config["HOST_MAPPING"].keys())) @app.route("/") def hello(): return render_template('index.html') -@app.route("/whois/") -def whois(asnum): - output = "

Whois as" + asnum + "

"
+def error_page(text):
+	return render_template('error.html', data = { "error": text } ), 500
+
+
+@app.route("/whois/")
+def whois(query):
 	try:
-		asnum = int(asnum)
+		asnum = int(query)
+		query = "as%d"%asnum
 	except:
-		output += "Failed to parse as%s"%asnum
-	else:
-		output += subprocess.Popen(['whois', 'as%d'%asnum], stdout=subprocess.PIPE).communicate()[0].decode('utf-8', 'ignore')
-	output += "
" - return render_template('index.html', output=output, typ="whois", asnum=asnum) + m = re.match(r"[\w\d-]*\.(?P[\d\w-]+\.[\d\w-]+)$", query) + if m: query = query.groupdict()["domain"] + output = subprocess.Popen( [ 'whois', query], stdout=subprocess.PIPE).communicate()[0].decode('utf-8', 'ignore').replace("\n","
") + return jsonify(output=output, title=query) -@app.route("/prefix_detail///") -@app.route("/prefix_detail///") -@app.route("/prefix_detail////") -def show_route_for_prefix_detail(host, proto, prefix="", mask=""): - return show_route_for_prefix(host, proto=proto, prefix=prefix, mask=mask, all = True) +SUMMARY_UNWANTED_PROTOS = ["Kernel", "Static", "Device"] +SUMMARY_RE_MATCH = r"(?P[\w_]+)\s+(?P\w+)\s+(?P\w+)\s+(?P\w+)\s+(?P((|\d\d\d\d-\d\d-\d\d\s)(|\d\d:)\d\d:\d\d|\w\w\w\d\d))(\s+(?P.+)|)" -@app.route("/prefix///") -@app.route("/prefix///") -@app.route("/prefix////") -def show_route_for_prefix(host, proto, prefix="", mask="", all=False): - qprefix = prefix +@app.route("/summary/") +@app.route("/summary//") +def summary(hosts, proto="ipv4"): + set_session("summary", hosts, proto, "") + command = "show protocols" + + summary = {} + for host in hosts.split("+"): + ret, res = bird_command(host, proto, command) + res = res.split("\n") + if ret: + data = [] + for line in res[1:]: + line = line.strip() + if line and ( line.split() + [""] )[1] not in SUMMARY_UNWANTED_PROTOS: + m = re.match(SUMMARY_RE_MATCH ,line) + if m: + data.append(m.groupdict()) + else: + app.logger.warning("couldn't parse: %s" , line) - - # security check - allowed = True - if not prefix: - allowed = False - elif not check_mask(mask): - allowed = False - elif proto == "ipv6": - if not check_ipv6(prefix): - try: - qprefix = get_ip(prefix, "AAAA") - except: - qprefix = "unresolvable" - allowed = False - - - elif proto == "ipv4": - if not check_ipv4(prefix): - try: - qprefix = get_ip(prefix, "A") - except: - qprefix = "unresolvable" - allowed = False - else: - allowed = False - - output = '

' + host + ' (' + proto + ') show route for ' + prefix + (prefix != qprefix and " (%s)"%qprefix or "") + (mask and '/' + mask or '' ) + (all and " all" or "") + '

' - - if allowed: - if mask: qprefix = qprefix +"/"+mask - if mask: prefix = prefix +"/"+mask - ok, string = get_cmd_result(host , proto, "show route for " + qprefix + (all and " all" or "")) - if ok: - output += '
' + cleanup_output(string) + '
' + summary[host] = data else: - output += string - else: - if prefix and qprefix != "unresolvable": - output += prefix + ' not valid' - elif prefix: - output += prefix + ' unresolvable' + summary[host] = { "error" : res } + + return render_template('summary.html', summary=summary, command=command) + +@app.route("/detail//") +def detail(hosts, proto): + name = request.args.get('q', '') + set_session("detail", hosts, proto, name) + command = "show protocols all %s" % name + + detail = {} + for host in hosts.split("+"): + ret, res = bird_command(host, proto, command) + res = res.split("\n") + if ret: + detail[host] = { "status": res[1], "description": add_links(res[2:]) } else: - output += 'prefix missing' + detail[host] = { "status": "bird error: %s" % "\n".join(res), "description": "" } + + return render_template('detail.html', detail=detail, command=command) - return render_template('index.html', output=output, typ="prefix" + (all and "_detail" or ""), host=host+"/"+proto, prefix=prefix) +@app.route("/where//") +def show_route_where(hosts, proto): + return show_route("where", hosts, proto) -@app.route("/detail///") -@app.route("/detail///") -def detail(host, proto, name=""): - output = '

' + host + ' (' + proto + ') show protocols all ' + name + '

' +@app.route("/where_detail//") +def show_route_where_detail(hosts, proto): + return show_route("where_detail", hosts, proto) - if not name: - output += "name missing" +@app.route("/prefix//") +def show_route_for(hosts, proto): + return show_route("prefix", hosts, proto) + +@app.route("/prefix_detail//") +def show_route_for_detail(hosts, proto): + return show_route("prefix_detail", hosts, proto) + +def show_route(req_type, hosts, proto): + expression = request.args.get('q', '') + set_session(req_type, hosts, proto, expression) + + all = (req_type.endswith("detail") and " all" or "" ) + + if req_type.startswith("where"): + command = "show route where net ~ [ " + expression + " ]" + all else: - ok, string = get_cmd_result(host , proto, "show protocols all " + name) - if ok: - output += '
'
-			output += "\n".join([ add_link(s.strip()) for s in string.split("\n") if s.startswith(" ")])
-			output += '
' + mask = "" + if len(expression.split("/")) > 1: + expression, mask = (expression.split("/")) + + if not mask and proto == "ipv4" : mask = "32" + if not mask and proto == "ipv6" : mask = "128" + if not mask_is_valid(mask): + return error_page("mask %s invalid" % mask) + if proto == "ipv6" and not ipv6_is_valid(expression): + try: expression = resolve(expression, "AAAA") + except: return error_page("%s unresolvable/invalid" % expression) + if proto == "ipv4" and not ipv4_is_valid(expression): + try: expression = resolve(expression, "A") + except: return error_page("%s unresolvable/invalid" % expression) + + if mask: expression += "/" + mask + command = "show route for " + expression + all + + detail = {} + for host in hosts.split("+"): + ret, res = bird_command(host, proto, command) + + res = res.split("\n") + if ret: + detail[host] = add_links(res) else: - output += string - - return render_template('index.html', output=output, typ="detail", host=host+"/"+proto, name=name) - -@app.route("/summary/") -@app.route("/summary//") -def summary(host, proto="ipv4"): - output = '

' + host + ' (' + proto + ') show protocols

' - - ok, string = get_cmd_result(host , proto, "show protocols") - if ok: - output += '
' - for infos in string.split("\n"): - if not infos.startswith(" "): continue - d = infos.split() - name = d[0] - typ = d[1] - if typ == "BGP": - output += ''%(host,proto,name,name,infos.replace(name,"").strip()) - output += '
%s%s
' - else: - output += string - return render_template('index.html', output=output, typ="summary", host=host+"/"+proto) - -@app.route("///status") -def status(host, proto): - string = get_cmd_result(host, proto, "show status") - output = '
' + string + '
' - return render_template('index.html', output=output, host=host+"/"+proto) - -def get_cmd_result(host, proto, cmd): - - ret = True - if proto == "ipv4": - port = 9994 - else: - port = 9996 - - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.settimeout(3.0) -# sock.setblocking(0) - try: - sock.connect((host, port)) - app.logger.debug("open socket on %s:%d", host, port) - - sock.send(cmd + "\n") - app.logger.debug("send %s socket on %s:%d", cmd, host, port) - - bufsize = 4096 - data = sock.recv(bufsize) - string = data - app.logger.debug("read %s (%d)", data, len(data)) - code = string.split("\n")[-2][0:4] - while not code[0] in ["0", "9", "8"]: - data = sock.recv(bufsize) - string = string + data - app.logger.debug("read %s (%d)", data, len(data)) - code = string.strip()[len(string.strip())-4:] - - if code[0] in [ "9", "8" ]: - ret = False - - app.logger.debug("return %s",string) - except Exception as detail: - ret = False - string = "Failed connect to %s:%d (%s)"%(host, port, detail) - app.logger.error(string) - sock.close() - return (ret, string) + detail[host] = "bird error: %s" % "\n".join(res) + + return render_template('route.html', detail=detail, command=command, expression=expression ) +app.secret_key = app.config["SESSION_KEY"] app.debug = True if __name__ == "__main__": app.run("0.0.0.0") diff --git a/lg.wsgi b/lg.wsgi new file mode 100644 index 0000000..2faa6bf --- /dev/null +++ b/lg.wsgi @@ -0,0 +1,5 @@ + +import sys +sys.path.insert(0,"/var/www/lg.tetaneutral.net/") + +from lg import app as application diff --git a/static/jquery-impromptu.3.2.min.js b/static/jquery-impromptu.3.2.min.js new file mode 100644 index 0000000..d629fb8 --- /dev/null +++ b/static/jquery-impromptu.3.2.min.js @@ -0,0 +1,13 @@ +/* + * jQuery Impromptu + * By: Trent Richardson [http://trentrichardson.com] + * Version 3.2 + * Last Modified: 10/31/2011 + * + * Copyright 2011 Trent Richardson + * Dual licensed under the MIT and GPL licenses. + * http://trentrichardson.com/Impromptu/GPL-LICENSE.txt + * http://trentrichardson.com/Impromptu/MIT-LICENSE.txt + * + */ + (function($){$.prompt=function(message,options){options=$.extend({},$.prompt.defaults,options);$.prompt.currentPrefix=options.prefix;var ie6=($.browser.msie&&$.browser.version<7);var $body=$(document.body);var $window=$(window);options.classes=$.trim(options.classes);if(options.classes!='')options.classes=' '+options.classes;var msgbox='
';if(options.useiframe&&(($('object, applet').length>0)||ie6)){msgbox+='';}else{if(ie6){$('select').css('visibility','hidden');}msgbox+='
';}msgbox+='
X
';msgbox+='
';var $jqib=$(msgbox).appendTo($body);var $jqi=$jqib.children('#'+options.prefix);var $jqif=$jqib.children('#'+options.prefix+'fade');if(message.constructor==String){message={state0:{html:message,buttons:options.buttons,focus:options.focus,submit:options.submit}};}var states="";$.each(message,function(statename,stateobj){stateobj=$.extend({},$.prompt.defaults.state,stateobj);message[statename]=stateobj;states+='';});$jqi.find('#'+options.prefix+'states').html(states).children('.'+options.prefix+'_state:first').css('display','block');$jqi.find('.'+options.prefix+'buttons:empty').css('display','none');$.each(message,function(statename,stateobj){var $state=$jqi.find('#'+options.prefix+'_state_'+statename);$state.children('.'+options.prefix+'buttons').children('button').click(function(){var msg=$state.children('.'+options.prefix+'message');var clicked=stateobj.buttons[$(this).text()];if(clicked==undefined){for(var i in stateobj.buttons)if(stateobj.buttons[i].title==$(this).text())clicked=stateobj.buttons[i].value;}if(typeof clicked=='object')clicked=clicked.value;var forminputs={};$.each($jqi.find('#'+options.prefix+'states :input').serializeArray(),function(i,obj){if(forminputs[obj.name]===undefined){forminputs[obj.name]=obj.value;}else if(typeof forminputs[obj.name]==Array||typeof forminputs[obj.name]=='object'){forminputs[obj.name].push(obj.value);}else{forminputs[obj.name]=[forminputs[obj.name],obj.value];}});var close=stateobj.submit(clicked,msg,forminputs);if(close===undefined||close){removePrompt(true,clicked,msg,forminputs);}});$state.find('.'+options.prefix+'buttons button:eq('+stateobj.focus+')').addClass(options.prefix+'defaultbutton');});var fadeClicked=function(){if(options.persistent){var offset=(options.top.toString().indexOf('%')>=0?($window.height()*(parseInt(options.top,10)/100)):parseInt(options.top,10)),top=parseInt($jqi.css('top').replace('px',''),10)-offset;$('html,body').animate({scrollTop:top},'fast',function(){var i=0;$jqib.addClass(options.prefix+'warning');var intervalid=setInterval(function(){$jqib.toggleClass(options.prefix+'warning');if(i++>1){clearInterval(intervalid);$jqib.removeClass(options.prefix+'warning');}},100);});}else{removePrompt();}};var keyPressEventHandler=function(e){var key=(window.event)?event.keyCode:e.keyCode;if(key==27){fadeClicked();}if(key==9){var $inputels=$(':input:enabled:visible',$jqib);var fwd=!e.shiftKey&&e.target==$inputels[$inputels.length-1];var back=e.shiftKey&&e.target==$inputels[0];if(fwd||back){setTimeout(function(){if(!$inputels)return;var el=$inputels[back===true?$inputels.length-1:0];if(el)el.focus();},10);return false;}}};var positionPrompt=function(){var bodyHeight=$body.outerHeight(true),windowHeight=$window.height(),documentHeight=$(document).height(),height=bodyHeight>windowHeight?bodyHeight:windowHeight,top=parseInt($window.scrollTop(),10)+(options.top.toString().indexOf('%')>=0?(windowHeight*(parseInt(options.top,10)/100)):parseInt(options.top,10));height=height>documentHeight?height:documentHeight;$jqib.css({position:"absolute",height:height,width:"100%",top:0,left:0,right:0,bottom:0});$jqif.css({position:"absolute",height:height,width:"100%",top:0,left:0,right:0,bottom:0});$jqi.css({position:"absolute",top:top,left:"50%",marginLeft:(($jqi.outerWidth()/2)*-1)});};var stylePrompt=function(){$jqif.css({zIndex:options.zIndex,display:"none",opacity:options.opacity});$jqi.css({zIndex:options.zIndex+1,display:"none"});$jqib.css({zIndex:options.zIndex});};var removePrompt=function(callCallback,clicked,msg,formvals){$jqi.remove();$window.unbind('resize',positionPrompt);$jqif.fadeOut(options.overlayspeed,function(){$jqif.unbind('click',fadeClicked);$jqif.remove();if(callCallback){options.callback(clicked,msg,formvals);}$jqib.unbind('keypress',keyPressEventHandler);$jqib.remove();if(ie6&&!options.useiframe){$('select').css('visibility','visible');}});};positionPrompt();stylePrompt();$jqif.click(fadeClicked);$window.resize(positionPrompt);$jqib.bind("keydown keypress",keyPressEventHandler);$jqi.find('.'+options.prefix+'close').click(removePrompt);$jqif.fadeIn(options.overlayspeed);$jqi[options.show](options.promptspeed,options.loaded);$jqi.find('#'+options.prefix+'states .'+options.prefix+'_state:first .'+options.prefix+'defaultbutton').focus();if(options.timeout>0)setTimeout($.prompt.close,options.timeout);return $jqib;};$.prompt.defaults={prefix:'jqi',classes:'',buttons:{Ok:true},loaded:function(){},submit:function(){return true;},callback:function(){},opacity:0.6,zIndex:999,overlayspeed:'slow',promptspeed:'fast',show:'promptDropIn',focus:0,useiframe:false,top:'15%',persistent:true,timeout:0,state:{html:'',buttons:{Ok:true},focus:0,submit:function(){return true;}}};$.prompt.currentPrefix=$.prompt.defaults.prefix;$.prompt.setDefaults=function(o){$.prompt.defaults=$.extend({},$.prompt.defaults,o);};$.prompt.setStateDefaults=function(o){$.prompt.defaults.state=$.extend({},$.prompt.defaults.state,o);};$.prompt.getStateContent=function(state){return $('#'+$.prompt.currentPrefix+'_state_'+state);};$.prompt.getCurrentState=function(){return $('.'+$.prompt.currentPrefix+'_state:visible');};$.prompt.getCurrentStateName=function(){var stateid=$.prompt.getCurrentState().attr('id');return stateid.replace($.prompt.currentPrefix+'_state_','');};$.prompt.goToState=function(state,callback){$('.'+$.prompt.currentPrefix+'_state').slideUp('slow');$('#'+$.prompt.currentPrefix+'_state_'+state).slideDown('slow',function(){$(this).find('.'+$.prompt.currentPrefix+'defaultbutton').focus();if(typeof callback=='function')callback();});};$.prompt.nextState=function(callback){var $next=$('.'+$.prompt.currentPrefix+'_state:visible').next();$('.'+$.prompt.currentPrefix+'_state').slideUp('slow');$next.slideDown('slow',function(){$next.find('.'+$.prompt.currentPrefix+'defaultbutton').focus();if(typeof callback=='function')callback();});};$.prompt.prevState=function(callback){var $next=$('.'+$.prompt.currentPrefix+'_state:visible').prev();$('.'+$.prompt.currentPrefix+'_state').slideUp('slow');$next.slideDown('slow',function(){$next.find('.'+$.prompt.currentPrefix+'defaultbutton').focus();if(typeof callback=='function')callback();});};$.prompt.close=function(){$('#'+$.prompt.currentPrefix+'box').fadeOut('fast',function(){$(this).remove();});};$.fn.extend({prompt:function(options){if(options==undefined)options={};if(options.withDataAndEvents==undefined)options.withDataAndEvents=false;$.prompt($(this).clone(options.withDataAndEvents).html(),options);},promptDropIn:function(speed,callback){var $t=$(this);if($t.css("display")=="none"){var eltop=$t.css('top');$t.css({top:$(window).scrollTop(),display:'block'}).animate({top:eltop},speed,'swing',callback);}}});})(jQuery); diff --git a/static/style.css b/static/style.css index e69de29..4a525db 100644 --- a/static/style.css +++ b/static/style.css @@ -0,0 +1,154 @@ +* { + font-family: Arial, Helvetica, Sans-Serif; +} + +html, body, ul, li { padding: 0 ; margin: 0; } +html, body { + height:100% ; +} +a { text-decoration: none; color:blue; } + +h1, h2, h3 { } +h1 { font-size: 18px; } +h2 { font-size: 16px; } +h3 { font-size: 14px; } + +td, th { + padding:2px 5px; + border-left: 1px solid #AFAFAF; +} +tr.odd td{ + background: #EFEFEF; +} +th { + border-bottom: 1px solid #AFAFAF; + background: #AFAFAF; +} +table { + border-collapse: collapse; + border-top: 1px solid #AFAFAF; + border-right: 1px solid #AFAFAF; + border-bottom: 1px solid #AFAFAF; +} +#header { + float: left; + width: 180px; + padding:10px 20px; + height: 100%; + border-right: 1px solid #AFAFAF; + position:fixed; +} +#header h1{ + text-align: center; +} +#content { + padding: 10px 20px; + margin-left: 230px; +} +div.summary, +div.detail { + font-size:12px; +} +ul { + /* 140px */ + border-top: 1px solid #AFAFAF; + margin: 0px; + padding: 0px; + margin-bottom: 10px; +} + +ul li{ + display: block; + margin: 0px; + padding: 2px 5px; + list-style-type: none; + background: #FFF; + border-bottom: 1px solid #AFAFAF; +} +ul li.current{ + font-weight: bold; +} +ul li.selected{ + background: #EFEFEF; +} +ul li:hover{ + background: #AFAFAF; + cursor: pointer; +} +ul.hosts{ + border-bottom: 1px solid #AFAFAF; +} +ul.hosts li{ + width: 47px; + border-bottom: 0px solid #AFAFAF; + text-align: center; + display:inline-block; +} +ul.proto li{ + text-align: center; + width: 78px; + display:inline-block; +} + +select, input, { width: 100%;} +input[type=text],textarea { width: 97%; font-size:10px; } + + +/* POPUP */ +.jqifade{ + position: absolute; + background-color: #aaaaaa; +} +div.jqi{ + width: 600px; + position: absolute; + background-color: #ffffff; + font-size: 11px; + text-align: left; + border: solid 1px #eeeeee; + border-radius: 10px; + -moz-border-radius: 10px; + -webkit-border-radius: 10px; + padding: 7px; +} +div.jqi .jqicontainer{ + font-weight: bold; +} +div.jqi .jqiclose{ + position: absolute; + top: 4px; right: -2px; + width: 18px; + cursor: default; + color: #bbbbbb; + font-weight: bold; +} +div.jqi .jqimessage{ + padding: 10px; + color: #444444; + height: 400px; + overflow: auto; +} +div.jqi .jqibuttons{ + text-align: right; + padding: 5px 0 5px 0; + border: solid 1px #eeeeee; + background-color: #f4f4f4; +} +div.jqi button{ + padding: 3px 10px; + margin: 0 10px; + background-color: #2F6073; + border: solid 1px #f4f4f4; + color: #ffffff; + font-weight: bold; + font-size: 12px; +} +div.jqi button:hover{ + background-color: #728A8C; +} +div.jqi button.jqidefaultbutton{ + background-color: #BF5E26; +} +.jqiwarning .jqi .jqibuttons{ + background-color: #BF5E26; +} diff --git a/templates/detail.html b/templates/detail.html new file mode 100644 index 0000000..593cf8d --- /dev/null +++ b/templates/detail.html @@ -0,0 +1,12 @@ +{% extends "layout.html" %} +{% block body %} +{% for host in detail %} +
+

{{host}}/{{session.proto}}: {{command}}

+{{ detail[host].status }} +
+{{ detail[host].description|safe }}
+
+
+{% endfor %} +{% endblock %} diff --git a/templates/error.html b/templates/error.html new file mode 100644 index 0000000..76e76f5 --- /dev/null +++ b/templates/error.html @@ -0,0 +1,4 @@ +{% extends "layout.html" %} +{% block body %} +{{ data.error|safe }} +{% endblock %} diff --git a/templates/layout.html b/templates/layout.html index 6a421fb..c5a92dc 100644 --- a/templates/layout.html +++ b/templates/layout.html @@ -1,119 +1,132 @@ Tetaneutral.net looking glass + +
-

Tetaneutral.net Looking Glass

+ +
+ {% if errors %} +
{{errors}}
+ {% endif %} {% block body %}{% endblock %} +
diff --git a/templates/route.html b/templates/route.html new file mode 100644 index 0000000..9989493 --- /dev/null +++ b/templates/route.html @@ -0,0 +1,14 @@ +{% extends "layout.html" %} +{% block body %} +{% for host in detail %} +
+

{{host}}/{{session.proto}}: {{command}}

+{% if session.request_args != expression %} +DNS: {{session.request_args}} => {{expression|replace("/32","")}} +{% endif %} +
+{{ detail[host]|safe }}
+
+
+{% endfor %} +{% endblock %} diff --git a/templates/summary.html b/templates/summary.html new file mode 100644 index 0000000..bdf7848 --- /dev/null +++ b/templates/summary.html @@ -0,0 +1,16 @@ +{% extends "layout.html" %} +{% block body %} +{% for host in summary %} +
+

{{host}}/{{session.proto}}: {{command}}

+ + + {% for row in summary[host] %} + + {% else %} + + {% endfor %} +
Nameprotocoltablestatesinceinfo
{{row.name}}{{row.proto}}{{row.table}}{{row.state}}{{row.since}}{{row.info}}
{{summary[host].error}}
+
+{% endfor %} +{% endblock %} diff --git a/test.py b/test.py new file mode 100644 index 0000000..5bf9068 --- /dev/null +++ b/test.py @@ -0,0 +1,6 @@ + +from bird import BirdSocketSingleton + +s = BirdSocketSingleton("h3", 9994) +print s.cmd("show protocols") +print s.cmd("show protocols all TETANEUTRAL") diff --git a/toolbox.py b/toolbox.py new file mode 100644 index 0000000..155c95e --- /dev/null +++ b/toolbox.py @@ -0,0 +1,33 @@ + +from dns import resolver,reversename +import socket + + +def resolve(n, q): + return str(resolver.query(n,q)[0]) + +def mask_is_valid(n): + if not n: + return True + try: + mask = int(n) + return ( mask >= 1 and mask <= 128) + except: + return False + +def ipv4_is_valid(n): + try: + socket.inet_pton(socket.AF_INET, n) + return True + except socket.error: + return False + +def ipv6_is_valid(n): + try: + socket.inet_pton(socket.AF_INET6, n) + return True + except socket.error: + return False + + +