Browse Source

update files, add map and few more scripts

Signed-off-by: r4sas <r4sas@i2pmail.org>
master
R4SAS 1 year ago
parent
commit
8897b48fc9
  1. 4
      README.md
  2. 14
      api/addresses.py
  3. 24
      api/config.py.example
  4. 21
      api/crawler.py
  5. 112
      api/graphPlotter.py
  6. 43
      api/importer.py
  7. 15
      api/max-min.py
  8. 32
      api/pk2addr.py
  9. 6
      api/static/map/jquery-2.0.3.min.js
  10. 29
      api/static/map/jquery.autocomplete.min.js
  11. 426
      api/static/map/network.js
  12. 202
      api/static/map/style.css
  13. 16
      api/templates/index.html
  14. 17
      api/templates/map/about.html
  15. 25
      api/templates/map/base.html
  16. 22
      api/templates/map/network.html
  17. 78
      api/updateGraph.py
  18. 7
      api/web.fcgi
  19. 89
      api/web.py
  20. 36
      ygg-crawl.sh
  21. 2
      yggapi.service

4
README.md

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
# niflheim-api.py
# Yggdrasil-monitor
Niflheim-api provides both a web interface and an api. The web interface is used to see some basic stats on the data vserv.py has collected and the API provides raw data in JSON format.
Yggdrasil-monitor provides both a web interface and an api. The web interface is used to see some basic stats on the data has collected by crawler and the API provides raw data in JSON format.
## Install & Setup

14
api/addresses.py

@ -0,0 +1,14 @@ @@ -0,0 +1,14 @@
#!/usr/bin/env python
import json
from pk2addr import keyTo128BitAddress
with open('api/result.json', 'r') as f:
data = json.load(f)
addlist = open("api/addresses.txt", "w")
for key, _ in data['yggnodes'].items():
addlist.write(keyTo128BitAddress(key) + "\n")
addlist.close()

24
api/config.py.example

@ -0,0 +1,24 @@ @@ -0,0 +1,24 @@
### Configuration file ###
### Database configuration ###
DB_PASS = "password"
DB_USER = "yggindex"
DB_NAME = "yggindex"
DB_HOST = "localhost"
DB_RETRIES = 3
DB_RECONNIDLE = 2
# count peer alive if it was available not more that amount of seconds ago
# I'm using 1 hour beause of running crawler every 30 minutes
ALIVE_SECONDS = 3600 # 1 hour
### Built-in crawler configuration ###
# Configuration to use TCP connection or unix domain socket for admin connection to yggdrasil
useAdminSock = True
yggAdminTCP = ('localhost', 9001)
yggAdminSock = ('/var/run/yggdrasil.sock')
# Save in database node info fields like buildname, buildarch, etc. (True/False)?
saveDefaultNodeInfo = False
removableFileds = ['buildname', 'buildarch', 'buildplatform', 'buildversion', 'board_name', 'kernel', 'model', 'system']

21
api/crawler.py

@ -12,22 +12,7 @@ import ipaddress @@ -12,22 +12,7 @@ import ipaddress
import traceback
from threading import Lock, Thread
from queue import Queue
#####
# Configuration to use TCP connection or unix domain socket for admin connection to yggdrasil
useAdminSock = True
yggAdminTCP = ('localhost', 9001)
yggAdminSock = ('/var/run/yggdrasil.sock')
DB_PASS = "password"
DB_USER = "yggindex"
DB_NAME = "yggindex"
DB_HOST = "localhost"
## Save in database node info fields like buildname, buildarch, etc. (True/False)?
saveDefaultNodeInfo = False
removableFileds = ['buildname', 'buildarch', 'buildplatform', 'buildversion', 'board_name', 'kernel', 'model', 'system']
from config import DB_PASS, DB_USER, DB_NAME, DB_HOST, useAdminSock, yggAdminTCP, yggAdminSock, saveDefaultNodeInfo, removableFileds
#####
@ -210,11 +195,11 @@ for k,v in selfInfo['response']['self'].items(): @@ -210,11 +195,11 @@ for k,v in selfInfo['response']['self'].items():
# Loop over rumored nodes and ping them, adding to visited if they respond
while len(rumored) > 0:
for k,v in rumored.items():
#print("Processing", v['coords'])
# print("Processing", v['coords'])
handleResponse(k, v, doRequest(getDHTPingRequest(v['box_pub_key'], v['coords'])))
break
del rumored[k]
#End
# End
nodeinfopool.wait()

112
api/graphPlotter.py

@ -0,0 +1,112 @@ @@ -0,0 +1,112 @@
import pygraphviz as pgv
import time
import json
import networkx as nx
from networkx.algorithms import centrality
import urllib.request
def position_nodes(nodes, edges):
G = pgv.AGraph(strict=True, directed=False, size='10!')
for n in nodes.values():
G.add_node(n.ip, label=n.label, coords=n.coords)
for e in edges:
G.add_edge(e.a.ip, e.b.ip, len=1.0)
G.layout(prog='neato', args='-Gepsilon=0.0001 -Gmaxiter=100000')
return G
def compute_betweenness(G):
ng = nx.Graph()
for start in G.iternodes():
others = G.neighbors(start)
for other in others:
ng.add_edge(start, other)
c = centrality.betweenness_centrality(ng)
for k, v in c.items():
c[k] = v
return c
def canonalize_ip(ip):
return ':'.join( i.rjust(4, '0') for i in ip.split(':') )
def load_db():
url = "http://[316:c51a:62a3:8b9::2]/result.json"
f = urllib.request.urlopen(url)
return dict(
[
(canonalize_ip(v[0]), v[1]) for v in
[
l.split(None)[:2] for l in
json.loads(f.read())["yggnodes"].keys()
]
if len(v) > 1
]
)
def get_graph_json(G):
max_neighbors = 1
for n in G.iternodes():
neighbors = len(G.neighbors(n))
if neighbors > max_neighbors:
max_neighbors = neighbors
print('Max neighbors: %d' % max_neighbors)
out_data = {
'created': int(time.time()),
'nodes': [],
'edges': []
}
centralities = compute_betweenness(G)
db = load_db()
for n in G.iternodes():
neighbor_ratio = len(G.neighbors(n)) / float(max_neighbors)
pos = n.attr['pos'].split(',', 1)
centrality = centralities.get(n.name, 0)
size = 5*(1 + 1*centrality)
name = db.get(canonalize_ip(n.name))
# If label isn't the default value, set name to that instead
if n.attr['label'] != n.name.split(':')[-1]: name = n.attr['label']
out_data['nodes'].append({
'id': n.name,
'label': name if name else n.attr['label'],
'name': name,
'coords': n.attr['coords'],
'x': float(pos[0]),
'y': float(pos[1]),
'color': _gradient_color(neighbor_ratio, [(100, 100, 100), (0, 0, 0)]),
'size': size,
'centrality': '%.4f' % centrality
})
for e in G.iteredges():
out_data['edges'].append({
'sourceID': e[0],
'targetID': e[1]
})
return json.dumps(out_data)
def _gradient_color(ratio, colors):
jump = 1.0 / (len(colors) - 1)
gap_num = int(ratio / (jump + 0.0000001))
a = colors[gap_num]
b = colors[gap_num + 1]
ratio = (ratio - gap_num * jump) * (len(colors) - 1)
r = int(a[0] + (b[0] - a[0]) * ratio)
g = int(a[1] + (b[1] - a[1]) * ratio)
b = int(a[2] + (b[2] - a[2]) * ratio)
return '#%02x%02x%02x' % (r, g, b)

43
api/importer.py

@ -1,39 +1,37 @@ @@ -1,39 +1,37 @@
#!/usr/bin/env python
import psycopg2, json, traceback
from config import DB_PASS, DB_USER, DB_NAME, DB_HOST, saveDefaultNodeInfo, removableFileds
from pk2addr import keyTo128BitAddress
#####
# Configuration to use TCP connection or unix domain socket for admin connection to yggdrasil
DB_PASS = "password"
DB_USER = "yggindex"
DB_NAME = "yggindex"
DB_HOST = "localhost"
## Save in database node info fields like buildname, buildarch, etc. (True/False)?
saveDefaultNodeInfo = False
removableFileds = ['buildname', 'buildarch', 'buildplatform', 'buildversion', 'board_name', 'kernel', 'model', 'system']
#####
with open('api/results.json', 'r') as f:
with open('api/result.json', 'r') as f:
data = json.load(f)
timestamp = data['meta']['generated_at_utc']
# connect to database
dbconn = psycopg2.connect(host=DB_HOST, database=DB_NAME, user=DB_USER, password=DB_PASS)
cur = dbconn.cursor()
# start importing
for node in data['topology']:
for key, node in data['yggnodes'].items():
nodename = ""
nodeinfo = {}
ipv6 = data['topology'][node]['ipv6_addr']
coords = '[%s]' % (' '.join(str(e) for e in data['topology'][node]['coords']))
if node in data['nodeinfo']:
nodeinfo = data['nodeinfo'][node]
if "address" in node:
ipv6 = keyTo128BitAddress(node['address']) if len(node['address']) == 64 else node['address']
else:
ipv6 = keyTo128BitAddress(key)
if "coords" in node:
coords = node['coords']
else:
continue
timestamp = node['time']
if "nodeinfo" in node:
nodeinfo = node['nodeinfo']
if not saveDefaultNodeInfo:
# remove default Node info fields
@ -42,10 +40,8 @@ for node in data['topology']: @@ -42,10 +40,8 @@ for node in data['topology']:
if "name" in nodeinfo:
nodename = nodeinfo['name']
elif data['topology'][node]['found'] == False:
nodename = '? %s' % coords
else:
nodename = ipv6
nodename = '? %s' % coords
nodeinfo = json.dumps(nodeinfo)
@ -66,4 +62,3 @@ for node in data['topology']: @@ -66,4 +62,3 @@ for node in data['topology']:
dbconn.commit()
cur.close()
dbconn.close()

15
api/max-min.py

@ -1,21 +1,12 @@ @@ -1,21 +1,12 @@
#!/usr/bin/env python
#max/min for the day with nodes
# max/min for the day with nodes
import psycopg2
import time
from config import DB_PASS, DB_USER, DB_NAME, DB_HOST, ALIVE_SECONDS
#run every hour
DB_PASS = "password"
DB_USER = "yggindex"
DB_NAME = "yggindex"
DB_HOST = "localhost"
# count peer alive if it was available not more that amount of seconds ago
# I'm using 1 hour beause of running crawler every 15 minutes
ALIVE_SECONDS = 3600 # 1 hour
# run every hour
def age_calc(ustamp):
if (time.time() - ustamp) <= ALIVE_SECONDS :

32
api/pk2addr.py

@ -0,0 +1,32 @@ @@ -0,0 +1,32 @@
# Author: silentfamiliar@matrix
def keyTo128BitAddress(key):
key260bits = int("1" + key, 16) # "1" to avoid trimming leading 0s
source_cursor = 4 # skip the "1"
# loop over each bit while NOT(bit) is 1
while (1 & ~(key260bits >> (260 - source_cursor - 1))) == 1:
source_cursor = source_cursor + 1
ones_count = source_cursor - 4 # 1s to count minus 4 which was our initial offset
source_cursor = source_cursor + 1 # skipping trailing 0
dest = (0x2 << 8) | ones_count # set header
bitsToAdd = 128 - 16 # header was 2 bytes which is 16 bit
# append needed amount of NOT key starting from source_cursor
dest = (dest << bitsToAdd) | ((2**bitsToAdd - 1) & ~(key260bits >> (260 - source_cursor - bitsToAdd)))
# the long addr
dest_hex = "0" + hex(dest)[2:]
# format ipv6 128bit addr
addr = ""
for i in range(8):
piece = int(dest_hex[i*4:i*4+4], 16)
if (len(addr) != 0) and not (addr[len(addr)-2:len(addr)] == "::"):
addr += ":"
if (piece != 0):
addr += hex(piece)[2:]
return addr

6
api/static/map/jquery-2.0.3.min.js vendored

File diff suppressed because one or more lines are too long

29
api/static/map/jquery.autocomplete.min.js vendored

@ -0,0 +1,29 @@ @@ -0,0 +1,29 @@
/**
* Ajax Autocomplete for jQuery, version 1.2.9
* (c) 2013 Tomas Kirda
*
* Ajax Autocomplete for jQuery is freely distributable under the terms of an MIT-style license.
* For details, see the web site: https://github.com/devbridge/jQuery-Autocomplete
*
*/
(function(d){"function"===typeof define&&define.amd?define(["jquery"],d):d(jQuery)})(function(d){function g(a,b){var c=function(){},c={autoSelectFirst:!1,appendTo:"body",serviceUrl:null,lookup:null,onSelect:null,width:"auto",minChars:1,maxHeight:300,deferRequestBy:0,params:{},formatResult:g.formatResult,delimiter:null,zIndex:9999,type:"GET",noCache:!1,onSearchStart:c,onSearchComplete:c,onSearchError:c,containerClass:"autocomplete-suggestions",tabDisabled:!1,dataType:"text",currentRequest:null,triggerSelectOnValidInput:!0,
lookupFilter:function(a,b,c){return-1!==a.value.toLowerCase().indexOf(c)},paramName:"query",transformResult:function(a){return"string"===typeof a?d.parseJSON(a):a}};this.element=a;this.el=d(a);this.suggestions=[];this.badQueries=[];this.selectedIndex=-1;this.currentValue=this.element.value;this.intervalId=0;this.cachedResponse={};this.onChange=this.onChangeInterval=null;this.isLocal=!1;this.suggestionsContainer=null;this.options=d.extend({},c,b);this.classes={selected:"autocomplete-selected",suggestion:"autocomplete-suggestion"};
this.hint=null;this.hintValue="";this.selection=null;this.initialize();this.setOptions(b)}var k=function(){return{escapeRegExChars:function(a){return a.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&")},createNode:function(a){var b=document.createElement("div");b.className=a;b.style.position="absolute";b.style.display="none";return b}}}();g.utils=k;d.Autocomplete=g;g.formatResult=function(a,b){var c="("+k.escapeRegExChars(b)+")";return a.value.replace(RegExp(c,"gi"),"<strong>$1</strong>")};g.prototype=
{killerFn:null,initialize:function(){var a=this,b="."+a.classes.suggestion,c=a.classes.selected,e=a.options,f;a.element.setAttribute("autocomplete","off");a.killerFn=function(b){0===d(b.target).closest("."+a.options.containerClass).length&&(a.killSuggestions(),a.disableKillerFn())};a.suggestionsContainer=g.utils.createNode(e.containerClass);f=d(a.suggestionsContainer);f.appendTo(e.appendTo);"auto"!==e.width&&f.width(e.width);f.on("mouseover.autocomplete",b,function(){a.activate(d(this).data("index"))});
f.on("mouseout.autocomplete",function(){a.selectedIndex=-1;f.children("."+c).removeClass(c)});f.on("click.autocomplete",b,function(){a.select(d(this).data("index"))});a.fixPosition();a.fixPositionCapture=function(){a.visible&&a.fixPosition()};d(window).on("resize.autocomplete",a.fixPositionCapture);a.el.on("keydown.autocomplete",function(b){a.onKeyPress(b)});a.el.on("keyup.autocomplete",function(b){a.onKeyUp(b)});a.el.on("blur.autocomplete",function(){a.onBlur()});a.el.on("focus.autocomplete",function(){a.onFocus()});
a.el.on("change.autocomplete",function(b){a.onKeyUp(b)})},onFocus:function(){this.fixPosition();if(this.options.minChars<=this.el.val().length)this.onValueChange()},onBlur:function(){this.enableKillerFn()},setOptions:function(a){var b=this.options;d.extend(b,a);if(this.isLocal=d.isArray(b.lookup))b.lookup=this.verifySuggestionsFormat(b.lookup);d(this.suggestionsContainer).css({"max-height":b.maxHeight+"px",width:b.width+"px","z-index":b.zIndex})},clearCache:function(){this.cachedResponse={};this.badQueries=
[]},clear:function(){this.clearCache();this.currentValue="";this.suggestions=[]},disable:function(){this.disabled=!0;this.currentRequest&&this.currentRequest.abort()},enable:function(){this.disabled=!1},fixPosition:function(){var a;"body"===this.options.appendTo&&(a=this.el.offset(),a={top:a.top+this.el.outerHeight()+"px",left:a.left+"px"},"auto"===this.options.width&&(a.width=this.el.outerWidth()-2+"px"),d(this.suggestionsContainer).css(a))},enableKillerFn:function(){d(document).on("click.autocomplete",
this.killerFn)},disableKillerFn:function(){d(document).off("click.autocomplete",this.killerFn)},killSuggestions:function(){var a=this;a.stopKillSuggestions();a.intervalId=window.setInterval(function(){a.hide();a.stopKillSuggestions()},50)},stopKillSuggestions:function(){window.clearInterval(this.intervalId)},isCursorAtEnd:function(){var a=this.el.val().length,b=this.element.selectionStart;return"number"===typeof b?b===a:document.selection?(b=document.selection.createRange(),b.moveStart("character",
-a),a===b.text.length):!0},onKeyPress:function(a){if(!this.disabled&&!this.visible&&40===a.which&&this.currentValue)this.suggest();else if(!this.disabled&&this.visible){switch(a.which){case 27:this.el.val(this.currentValue);this.hide();break;case 39:if(this.hint&&this.options.onHint&&this.isCursorAtEnd()){this.selectHint();break}return;case 9:if(this.hint&&this.options.onHint){this.selectHint();return}case 13:if(-1===this.selectedIndex){this.hide();return}this.select(this.selectedIndex);if(9===a.which&&
!1===this.options.tabDisabled)return;break;case 38:this.moveUp();break;case 40:this.moveDown();break;default:return}a.stopImmediatePropagation();a.preventDefault()}},onKeyUp:function(a){var b=this;if(!b.disabled){switch(a.which){case 38:case 40:return}clearInterval(b.onChangeInterval);if(b.currentValue!==b.el.val())if(b.findBestHint(),0<b.options.deferRequestBy)b.onChangeInterval=setInterval(function(){b.onValueChange()},b.options.deferRequestBy);else b.onValueChange()}},onValueChange:function(){var a=
this.options,b=this.el.val(),c=this.getQuery(b);this.selection&&(this.selection=null,(a.onInvalidateSelection||d.noop).call(this.element));clearInterval(this.onChangeInterval);this.currentValue=b;this.selectedIndex=-1;if(a.triggerSelectOnValidInput&&(b=this.findSuggestionIndex(c),-1!==b)){this.select(b);return}c.length<a.minChars?this.hide():this.getSuggestions(c)},findSuggestionIndex:function(a){var b=-1,c=a.toLowerCase();d.each(this.suggestions,function(a,d){if(d.value.toLowerCase()===c)return b=
a,!1});return b},getQuery:function(a){var b=this.options.delimiter;if(!b)return a;a=a.split(b);return d.trim(a[a.length-1])},getSuggestionsLocal:function(a){var b=this.options,c=a.toLowerCase(),e=b.lookupFilter,f=parseInt(b.lookupLimit,10),b={suggestions:d.grep(b.lookup,function(b){return e(b,a,c)})};f&&b.suggestions.length>f&&(b.suggestions=b.suggestions.slice(0,f));return b},getSuggestions:function(a){var b,c=this,e=c.options,f=e.serviceUrl,l,g;e.params[e.paramName]=a;l=e.ignoreParams?null:e.params;
c.isLocal?b=c.getSuggestionsLocal(a):(d.isFunction(f)&&(f=f.call(c.element,a)),g=f+"?"+d.param(l||{}),b=c.cachedResponse[g]);b&&d.isArray(b.suggestions)?(c.suggestions=b.suggestions,c.suggest()):c.isBadQuery(a)||!1===e.onSearchStart.call(c.element,e.params)||(c.currentRequest&&c.currentRequest.abort(),c.currentRequest=d.ajax({url:f,data:l,type:e.type,dataType:e.dataType}).done(function(b){c.currentRequest=null;c.processResponse(b,a,g);e.onSearchComplete.call(c.element,a)}).fail(function(b,d,f){e.onSearchError.call(c.element,
a,b,d,f)}))},isBadQuery:function(a){for(var b=this.badQueries,c=b.length;c--;)if(0===a.indexOf(b[c]))return!0;return!1},hide:function(){this.visible=!1;this.selectedIndex=-1;d(this.suggestionsContainer).hide();this.signalHint(null)},suggest:function(){if(0===this.suggestions.length)this.hide();else{var a=this.options,b=a.formatResult,c=this.getQuery(this.currentValue),e=this.classes.suggestion,f=this.classes.selected,g=d(this.suggestionsContainer),k=a.beforeRender,m="",h;if(a.triggerSelectOnValidInput&&
(h=this.findSuggestionIndex(c),-1!==h)){this.select(h);return}d.each(this.suggestions,function(a,d){m+='<div class="'+e+'" data-index="'+a+'">'+b(d,c)+"</div>"});"auto"===a.width&&(h=this.el.outerWidth()-2,g.width(0<h?h:300));g.html(m);a.autoSelectFirst&&(this.selectedIndex=0,g.children().first().addClass(f));d.isFunction(k)&&k.call(this.element,g);g.show();this.visible=!0;this.findBestHint()}},findBestHint:function(){var a=this.el.val().toLowerCase(),b=null;a&&(d.each(this.suggestions,function(c,
d){var f=0===d.value.toLowerCase().indexOf(a);f&&(b=d);return!f}),this.signalHint(b))},signalHint:function(a){var b="";a&&(b=this.currentValue+a.value.substr(this.currentValue.length));this.hintValue!==b&&(this.hintValue=b,this.hint=a,(this.options.onHint||d.noop)(b))},verifySuggestionsFormat:function(a){return a.length&&"string"===typeof a[0]?d.map(a,function(a){return{value:a,data:null}}):a},processResponse:function(a,b,c){var d=this.options;a=d.transformResult(a,b);a.suggestions=this.verifySuggestionsFormat(a.suggestions);
d.noCache||(this.cachedResponse[c]=a,0===a.suggestions.length&&this.badQueries.push(c));b===this.getQuery(this.currentValue)&&(this.suggestions=a.suggestions,this.suggest())},activate:function(a){var b=this.classes.selected,c=d(this.suggestionsContainer),e=c.children();c.children("."+b).removeClass(b);this.selectedIndex=a;return-1!==this.selectedIndex&&e.length>this.selectedIndex?(a=e.get(this.selectedIndex),d(a).addClass(b),a):null},selectHint:function(){var a=d.inArray(this.hint,this.suggestions);
this.select(a)},select:function(a){this.hide();this.onSelect(a)},moveUp:function(){-1!==this.selectedIndex&&(0===this.selectedIndex?(d(this.suggestionsContainer).children().first().removeClass(this.classes.selected),this.selectedIndex=-1,this.el.val(this.currentValue),this.findBestHint()):this.adjustScroll(this.selectedIndex-1))},moveDown:function(){this.selectedIndex!==this.suggestions.length-1&&this.adjustScroll(this.selectedIndex+1)},adjustScroll:function(a){var b=this.activate(a),c,e;b&&(b=b.offsetTop,
c=d(this.suggestionsContainer).scrollTop(),e=c+this.options.maxHeight-25,b<c?d(this.suggestionsContainer).scrollTop(b):b>e&&d(this.suggestionsContainer).scrollTop(b-this.options.maxHeight+25),this.el.val(this.getValue(this.suggestions[a].value)),this.signalHint(null))},onSelect:function(a){var b=this.options.onSelect;a=this.suggestions[a];this.currentValue=this.getValue(a.value);this.el.val(this.currentValue);this.signalHint(null);this.suggestions=[];this.selection=a;d.isFunction(b)&&b.call(this.element,
a)},getValue:function(a){var b=this.options.delimiter,c;if(!b)return a;c=this.currentValue;b=c.split(b);return 1===b.length?a:c.substr(0,c.length-b[b.length-1].length)+a},dispose:function(){this.el.off(".autocomplete").removeData("autocomplete");this.disableKillerFn();d(window).off("resize.autocomplete",this.fixPositionCapture);d(this.suggestionsContainer).remove()}};d.fn.autocomplete=function(a,b){return 0===arguments.length?this.first().data("autocomplete"):this.each(function(){var c=d(this),e=
c.data("autocomplete");if("string"===typeof a){if(e&&"function"===typeof e[a])e[a](b)}else e&&e.dispose&&e.dispose(),e=new g(this,a),c.data("autocomplete",e)})}});

426
api/static/map/network.js

@ -0,0 +1,426 @@ @@ -0,0 +1,426 @@
"use strict";
var nodes = [];
var edges = [];
var canvas = null;
var ctx = null;
var mapOffset = {x: 0, y: 0};
var zoom = 1.0;
function changeHash(hash) {
window.location.replace(('' + window.location).split('#')[0] + '#' + hash);
}
function updateCanvasSize() {
$(canvas).attr({height: $(canvas).height(), width: $(canvas).width()});
ctx.translate(mapOffset.x, mapOffset.y);
}
function drawCircle(ctx, x, y, radius, color) {
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI*2, true);
ctx.fill();
}
function drawLine(ctx, x1, y1, x2, y2, color) {
ctx.strokeStyle = color;
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.closePath();
ctx.stroke();
}
function drawText(ctx, x, y, text, color, font) {
// ctx.save();
// ctx.translate(x, y);
// ctx.rotate(Math.PI/4);
ctx.fillStyle = color;
ctx.font = font;
ctx.textAlign = 'center';
ctx.fillText(text, x, y);
// ctx.restore();
}
function drawNetwork() {
ctx.save();
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.restore();
// Draw edges
for (var i = 0; i < edges.length; ++i) {
var edge = edges[i];
var highlight = edge.sourceNode.hover || edge.targetNode.hover;
var color = highlight ? 'rgba(0, 0, 0, 0.5)' : 'rgba(0, 0, 0, 0.15)';
drawLine(ctx,
edge.sourceNode.x, edge.sourceNode.y,
edge.targetNode.x, edge.targetNode.y,
color);
}
// Draw nodes
for (var i = 0; i < nodes.length; ++i) {
var node = nodes[i];
drawCircle(ctx, node.x, node.y, node.radius, node.color);
}
// Draw labels
for (var i = 0; i < nodes.length; ++i) {
var node = nodes[i];
if (node.radius > 4 || node.selected || node.hover) {
var fontSize = 4 + node.radius * 0.4;
drawText(ctx, node.x, node.y - node.radius - 1,
node.label, node.textColor, fontSize + 'pt "ubuntu mono"');
}
}
}
function getNodeAt(x, y) {
x -= mapOffset.x;
y -= mapOffset.y;
for (var i = nodes.length - 1; i >= 0; --i) {
var node = nodes[i];
var distPow2 = (node.x - x) * (node.x - x) + (node.y - y) * (node.y - y);
if (distPow2 <= node.radius * node.radius) {
return node;
}
}
return null;
}
function searchNode(id) {
for (var i = 0; i < nodes.length; ++i) {
if (nodes[i].id == id)
return nodes[i];
}
return null;
}
function clearNodes() {
changeHash('');
$('#node-info').html('');
for (var i = 0; i < nodes.length; ++i) {
var node = nodes[i];
node.depth = 0xFFFF;
node.color = node.originalColor;
node.textColor = node.color;
node.selected = false;
}
}
function selectNode(node, redraw) {
clearNodes();
changeHash(node.id);
node.selected = true;
showNodeInfo(node);
markPeers(node, 0);
if (redraw)
drawNetwork();
}
function markPeers(node, depth) {
node.depth = depth;
// var colors = ['#000000', '#333333', '#555555', '#777777', '#999999', '#BBBBBB', '#DDDDDD'];
// var colors = ['#000000', '#29BBFF', '#09E844', '#FFBD0F', '#FF5E14', '#FF3C14', '#FF7357', '#FF9782', '#FFC8BD', '#FFE6E0'];
var colors = ['#000000', '#096EE8', '#09E8B8', '#36E809', '#ADE809', '#E8B809', '#E87509', '#E83A09', '#E86946', '#E8AC9B', '#E8C9C1'];
var txtCol = ['#000000', '#032247', '#034537', '#0E3D02', '#354703', '#403203', '#3D1F02', '#3B0E02', '#3B0E02', '#3B0E02', '#3B0E02'];
// var colors = ['#000000', '#064F8F', '#068F81', '#068F38', '#218F06', '#6F8F06', '#8F7806', '#8F5106'];
// var colors = ['#FFFFFF', '#29BBFF', '#17FF54', '#FFBD0F', '#FF3C14', '#590409'];
node.color = (depth >= colors.length) ? '#FFFFFF' : colors[depth];
node.textColor = (depth >= txtCol.length) ? '#FFFFFF' : txtCol[depth];
for (var i = 0; i < node.peers.length; ++i) {
var n = node.peers[i];
if (n.depth > depth + 1)
markPeers(n, depth + 1);
}
}
function showNodeInfo(node) {
var ip_peers = [];
var dns_peers = [];
for (var i = 0; i < node.peers.length; ++i) {
var n = node.peers[i];
if (/^[0-9A-F]{4}$/i.test(n.label))
ip_peers.push(n);
else
dns_peers.push(n);
}
var label_compare = function(a, b) {
return a.label.localeCompare(b.label);
}
dns_peers.sort(label_compare);
ip_peers.sort(label_compare);
var peers = dns_peers.concat(ip_peers);
var html =
'<h2>' + node.label + '</h2>' +
'<span class="tt">' + node.id + '</span><br>' +
'<br>' +
'<strong>Coords:</strong> ' + node.coords + '<br>' +
'<strong>Peers:</strong> ' + node.peers.length + '<br>' +
'<strong>Centrality:</strong> ' + node.centrality + '<br>' +
'<table>' +
// '<tr><td></td><td><strong>Their peers #</strong></td></tr>' +
peers.map(function (n) {
return '<tr>' +
'<td><a href="#' + n.id + '" class="tt">' + n.label + '</a></td>' +
'<td>' + n.peers.length + '</td></tr>';
}).join('') +
'</table>';
$('#node-info').html(html);
}
function mousePos(e) {
var rect = canvas.getBoundingClientRect();
return {x: e.clientX - rect.left, y: e.clientY - rect.top};
}
$(document).ready(function() {
canvas = document.getElementById('map');
ctx = canvas.getContext('2d');
updateCanvasSize();
jQuery.getJSON('/static/graph.json', function(data) {
nodes = data.nodes;
edges = data.edges;
// Calculate node radiuses
for (var i = 0; i < nodes.length; ++i) {
var node = nodes[i];
node.x = node.x * 1.2;
node.y = node.y * 1.2;
node.radius = node.size;
node.hover = false;
node.selected = false;
node.edges = [];
node.peers = [];
node.depth = 0xFFFF;
// node.color = '#000';
node.originalColor = node.color;
node.textColor = node.color;
}
var newEdges = []
// Find node references for edges
for (var i = 0; i < edges.length; ++i) {
var edge = edges[i];
for (var n = 0; n < nodes.length; ++n) {
if (nodes[n].id == edge.sourceID) {
edge.sourceNode = nodes[n];
// edge.sourceNode.edges.append(edge);
}
else if (nodes[n].id == edge.targetID)
edge.targetNode = nodes[n];
}
if (!edge.sourceNode || !edge.targetNode)
continue;
edge.sourceNode.edges.push(edge);
edge.targetNode.edges.push(edge);
edge.sourceNode.peers.push(edge.targetNode);
edge.targetNode.peers.push(edge.sourceNode);
newEdges.push(edge);
}
edges = newEdges;
// Set update time
var delta = Math.round(new Date().getTime() / 1000) - data.created;
var min = Math.floor(delta / 60);
var sec = delta % 60;
$('#update-time').text(min + ' min, ' + sec + ' s ago');
// Set stats
$('#number-of-nodes').text(nodes.length);
$('#number-of-connections').text(edges.length);
if (window.location.hash) {
var id = window.location.hash.substring(1);
var node = searchNode(id);
if (node) selectNode(node, false);
}
drawNetwork();
$(window).resize(function() {
updateCanvasSize();
drawNetwork();
});
// Initialize search
var searchArray = [];
for (var i = 0; i < nodes.length; ++i) {
var node = nodes[i];
searchArray.push({
value: node.label,
data: node
});
searchArray.push({
value: node.id,
data: node
});
}
$('#search-box').autocomplete({
lookup: searchArray,
autoSelectFirst: true,
lookupLimit: 7,
onSelect: function(suggestion) {
selectNode(suggestion.data, true);
}
});
$('#search-box').keypress(function(e) {
if (e.which == 13) {
selectNode(searchNode($('#search-box').val()), true);
}
});
$(document).on('click', '#node-info a', function(e) {
var id = e.target.hash.substring(1);
selectNode(searchNode(id), true);
});
});
var mouseDownPos = null;
var mouseLastPos = null;
var mouseDownNode = null;
var mouseHoverNode = null;
$(canvas).mousemove(function(e) {
var mouse = mousePos(e);
// Dragging
if (mouseDownPos != null) {
$('body').css('cursor', 'move');
var dx = mouse.x - mouseLastPos.x;
var dy = mouse.y - mouseLastPos.y;
mapOffset.x += dx;
mapOffset.y += dy;
ctx.translate(dx, dy);
mouseLastPos = {x: mouse.x, y: mouse.y};
drawNetwork();
}
// Hovering
else {
var node = getNodeAt(mouse.x, mouse.y);
if (node == mouseHoverNode)
return;
if (node == null) {
nodeMouseOut(mouseHoverNode);
}
else {
if (mouseHoverNode != null)
nodeMouseOut(mouseHoverNode);
nodeMouseIn(node);
}
mouseHoverNode = node;
drawNetwork();
}
});
$(canvas).mousedown(function(e) {
var mouse = mousePos(e);
mouseLastPos = mouseDownPos = {x: mouse.x, y: mouse.y};
mouseDownNode = getNodeAt(mouse.x, mouse.y);
return false;
});
$(canvas).mouseup(function(e) {
var mouse = mousePos(e);
var mouseMoved =
Math.abs(mouse.x - mouseDownPos.x) +
Math.abs(mouse.y - mouseDownPos.y) > 3
if (!mouseMoved) {
if (mouseDownNode)
selectNode(mouseDownNode, true);
else {
clearNodes();
drawNetwork();
}
}
else {
$('body').css('cursor', 'auto');
}
mouseDownPos = null;
mouseDownNode = null;
return false;
});
function handleScroll(e) {
var mouse = mousePos(e);
var delta = Math.max(-1, Math.min(1, (e.wheelDelta || -e.detail)));
var ratio = (delta < 0) ? (3 / 4) : 1 + (1 / 3);
var mx = mouse.x - mapOffset.x;
var my = mouse.y - mapOffset.y;
zoom *= ratio;
for (var i = 0; i < nodes.length; ++i) {
var node = nodes[i];
node.x = (node.x - mx) * ratio + mx;
node.y = (node.y - my) * ratio + my;
// node.x *= ratio;
// node.y *= ratio;
// node.radius *= ratio;
node.radius = (node.size) * zoom;
}
drawNetwork();
}
canvas.addEventListener("mousewheel", handleScroll, false);
canvas.addEventListener("DOMMouseScroll", handleScroll, false);
});
function nodeMouseIn(node) {
node.hover = true;
$('body').css('cursor', 'pointer');
}
function nodeMouseOut(node) {
node.hover = false;
$('body').css('cursor', 'auto');
}

202
api/static/map/style.css

@ -0,0 +1,202 @@ @@ -0,0 +1,202 @@
* {
margin: 0;
padding: 0;
}
html, body {
background: #F5F5F5;
font-family: 'sans serif';
}
#header {
background: #FFF;
height: 48px;
line-height: 48px;
position: absolute;
top: 0;
left: 0;
right: 0;
}
h1 {
font-family: 'Inconsolata', 'Consolas', 'Ubuntu Mono', monospace;
font-size: 32px;
float: left;
padding: 0 40px;
font-weight: 100;
color: #333;
}
small {
font-size: 16px;
}
.grey {
color: #999;
}
ul {
list-style-type: none;
height: 100%;
}
li {
float: left;
height: 100%;
}
#header a {
color: #777;
padding: 0 20px;
font-family: 'Open Sans', 'sans-serif';
font-size: 14px;
text-decoration: none;
height: 100%;
display: block;
}
#header a.selected {
background: #DDD;
}
#header a:hover {
background: #EEE;
}
#general-info {
position: absolute;
bottom: 0;
left: 0;
font-size: 10px;
padding: 5px;
line-height: 150%;
z-index: 999;
}
#sidebar {
padding: 20px 20px;
background: rgba(220, 220, 220, 0.8);
position: absolute;
top: 0;
right: 0;
/*bottom: 0;*/
max-height: calc(100vh - 40px);
min-width: 250px;
z-index: 999;
overflow-y: scroll;
font-size: 12px;
}
#search-wrapper {
width: 100%;
margin-bottom: 20px;
/*position: absolute;*/
/*top: 0;*/
/*right: 10px;*/
/*z-index: 5;*/
font-size: 10px;
}
#search-box {
width: 100%;
padding: 5px;
outline: none;
border: none;
/*border: 1px solid #CCC;*/
margin: -5px;
font-size: inherit;
}
#sidebar a {
color: #333;
text-decoration: none;
}
#sidebar a:hover {
color: #AAA;
}
#sidebar h2 {
text-align: center;
margin-bottom: 5px;
color: #29BBFF;
}
#node-info table {
width: 100%;
}
#node-info td + td {
text-align: right;
}
#node-info strong {
color: #29BBFF;
letter-spacing: 1px;
}
.tt {
font-family: monospace;
font-size: 10px;
}
.autocomplete-suggestions {
font-family: monospace;
font-size: 10px;
border: 1px solid #FFF;
background: #FFF;
overflow: auto;
color: #555;
}
.autocomplete-suggestion {
padding: 2px 5px;
white-space: nowrap;
overflow: hidden;
}
.autocomplete-selected { background: #7FD6FF; }
.autocomplete-suggestions strong {
color: #000000;
}
#content-wrapper {
position: absolute;
top: 48px;
left: 0;
right: 0;
bottom: 0;
}
#map {
position: absolute;
width:100%;
height:100%;
}
#content {
width: 500px;
margin: 30px auto;
font-family: sans-serif;
font-size: 16px;
color: #333;
line-height: 28px;
letter-spacing: 0.2px;
}
#content h2 {
text-align: center;
margin-bottom: 20px;
color: #29BBFF;
}
#content h3 {
margin-top: 20px;
text-align: center;
color: #29BBFF;
}
#content a {
color: #29BBFF;
}

16
api/templates/index.html

@ -14,29 +14,31 @@ @@ -14,29 +14,31 @@
<strong>Current Nodes online<br />
<font size="18">{{ nodes }}</font></strong>
<br /><br />
You can see them in <i>Yggdrasil Interactive World map</i> <sup><a href="http://[21f:dd73:7cdb:773b:a924:7ec0:800b:221e]/">[&#8663;]</a> <a href="http://map.ygg/" target="_blank">[DNS]</a></sup>
You can see them in <a href="/map"><i>Yggdrasil Interactive World map</i></a>
<br />
Arceliar's map can be found here: <sup><a href="http://[21e:e795:8e82:a9e2:ff48:952d:55f2:f0bb]/" target="_blank">[&#8663;]</a> <a href="http://map.ygg/" target="_blank">[DNS]</a></sup>
</center>
<br /><br />
<div class="wide"></div>
<strong>Make an API request</strong><br />
<small>note: data updated every 15 minutes, all requests are returned in JSON.</small><br /><br />
<small>note: data is updated hourly (due to network growth and instabillity), all requests are returned in JSON.</small><br /><br />
Get a current list of active and online nodes:<br />
<div class="apireq">
http://[31a:fb8a:c43e:ca59::2]/current <sup><a href="http://[31a:fb8a:c43e:ca59::2]/current" target="_blank">[&#8663;]</a> <a href="http://nodelist.ygg/current" target="_blank">[DNS]</a></sup>
http://[316:c51a:62a3:8b9::2]/current <sup><a href="http://[316:c51a:62a3:8b9::2]/current" target="_blank">[&#8663;]</a> <a href="http://nodelist.ygg/current" target="_blank">[DNS]</a></sup>
</div>
Nodeinfo from all current active nodes:<br />
<div class="apireq">
http://[31a:fb8a:c43e:ca59::2]/nodeinfo <sup><a href="http://[31a:fb8a:c43e:ca59::2]/nodeinfo" target="_blank">[&#8663;]</a> <a href="http://nodelist.ygg/nodeinfo" target="_blank">[DNS]</a></sup>
http://[316:c51a:62a3:8b9::2]/nodeinfo <sup><a href="http://[316:c51a:62a3:8b9::2]/nodeinfo" target="_blank">[&#8663;]</a> <a href="http://nodelist.ygg/nodeinfo" target="_blank">[DNS]</a></sup>
</div>
Active nodes count for last 24 hours:<br />
<div class="apireq">
http://[31a:fb8a:c43e:ca59::2]/nodes24h <sup><a href="http://[31a:fb8a:c43e:ca59::2]/nodes24h" target="_blank">[&#8663;]</a> <a href="http://nodelist.ygg/nodes24h" target="_blank">[DNS]</a></sup>
http://[316:c51a:62a3:8b9::2]/nodes24h <sup><a href="http://[316:c51a:62a3:8b9::2]/nodes24h" target="_blank">[&#8663;]</a> <a href="http://nodelist.ygg/nodes24h" target="_blank">[DNS]</a></sup>
</div>
Active nodes count for last 30 days:<br />
<div class="apireq">
http://[31a:fb8a:c43e:ca59::2]/nodes30d <sup><a href="http://[31a:fb8a:c43e:ca59::2]/nodes30d" target="_blank">[&#8663;]</a> <a href="http://nodelist.ygg/nodes30d" target="_blank">[DNS]</a></sup>
http://[316:c51a:62a3:8b9::2]/nodes30d <sup><a href="http://[316:c51a:62a3:8b9::2]/nodes30d" target="_blank">[&#8663;]</a> <a href="http://nodelist.ygg/nodes30d" target="_blank">[DNS]</a></sup>
</div>
<div class="wide"></div>
@ -47,7 +49,7 @@ @@ -47,7 +49,7 @@
</div>
<div class="wide"></div>
<small>Made with <a href="https://github.com/r4sas/Niflheim-api" target="_blank">fork</a> of <a href="https://github.com/yakamok/Niflheim-api" target="_blank">Niflheim-API</a> by yakamok</small>
<small>Made with <a href="https://github.com/r4sas/yggdrasil-monitor" target="_blank">fork</a> of <a href="https://github.com/yakamok/Niflheim-api" target="_blank">Niflheim-API</a> by yakamok</small>
</div>
</body>
</html>

17
api/templates/map/about.html

@ -0,0 +1,17 @@ @@ -0,0 +1,17 @@
{% extends "map/base.html" %}
{% block content %}
<div id="content-wrapper">
<div id="content">
<h2>About</h2>
<p>This is a project that aims to demystify what the <a href="https://yggdrasil-network.github.io/">Yggdrasil</a> network is like. Currently the only thing we have here is a map of the spanning tree subset of the network. The full source code is at <a href="https://github.com/Arceliar/yggdrasil-map">GitHub</a>.</p>
<h3>Network map</h3>
<p>The network page has a map of Yggdrasil's spanning tree as it is now. The map is not complete since it is hard/impossible to get a full picture of the network, and it only includes the minimum subset of links needed to construct the spanning tree. The known nodes and tree coordinates are taken from <a href="http://y.yakamo.org:3000/">Yakamo's API</a>. Node names can be configured by setting a "name" field in <a href="https://yggdrasil-network.github.io/configuration.html">NodeInfo</a>, or from <a href="https://github.com/yakamok/yggdrasil-nodelist">Yakamo's node list</a> as a fallback.</p>
<h3>Contact</h3>
<p>This project was forked from <em>zielmicha</em>'s fork of <em>Randati</em>'s fc00.
The yggdrasil developers can be contacted over matrix or IRC, for more info see: <a href="https://yggdrasil-network.github.io/">yggdrasil-network.github.io</a>.</p>
</div>
</div>
{% endblock %}

25
api/templates/map/base.html

@ -0,0 +1,25 @@ @@ -0,0 +1,25 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>0200::/7 – Mapping The Yggdrasil Network</title>
<script src="{{ url_for('static', filename='map/jquery-2.0.3.min.js')}}"></script>
<script src="{{ url_for('static', filename='map/jquery.autocomplete.min.js')}}"></script>
<link href="{{ url_for('static', filename='map/style.css')}}" rel="stylesheet" type="text/css">
</head>
<body>
<div id="header">
<h1>0200<span class="grey">::/7</span></h1>
<ul>
<li><a href="/">Status</a></li>
<li><a href="/map" {% if page == 'network' %} class="selected" {% endif %}>Map</a></li>
<li><a href="/map/about"{% if page == 'about' %} class="selected" {% endif %}>About</a></li>
<li><a href="https://github.com/r4sas/yggdrasil-monitor">Source</a></li>
<li><tt>{% if ip is not none %}{{ ip }}{% endif %}</tt></li>
</ul>
</div>
{% block content %}{% endblock %}
</body>
</html>

22
api/templates/map/network.html

@ -0,0 +1,22 @@ @@ -0,0 +1,22 @@
{% extends "map/base.html" %}
{% block content %}
<div id="general-info">
Nodes: <span id="number-of-nodes">-</span><br>
Links: <span id="number-of-connections">-</span><br>
Updated <span id="update-time">-</span><br>
</div>
<div id="sidebar">
<div id="search-wrapper">
<input id="search-box" class="tt" type="text" placeholder="Search nodes...">
</div>
<div id="node-info"></div>
</div>
<div id="content-wrapper">
<canvas id="map"></canvas>
</div>
<script type="text/javascript" src="static/map/network.js"></script>
{% endblock %}

78
api/updateGraph.py

@ -0,0 +1,78 @@ @@ -0,0 +1,78 @@
#!/usr/bin/env python
import graphPlotter
import html
import urllib.request, json
url = "http://[316:c51a:62a3:8b9::2]/result.json"
# nodes indexed by coords
class NodeInfo:
def __init__(self, ip, coords):
self.ip = str(ip)
self.label = str(ip).split(":")[-1]
self.coords = str(coords)
self.version = "unknown"
def getCoordList(self):
return self.coords.strip("[]").split(" ")
def getParent(self):
p = self.getCoordList()
if len(p) > 0: p = p[:-1]
return "[" + " ".join(p).strip() + "]"
def getLink(self):
c = self.getCoordList()
return int(self.getCoordList()[-1].strip() or "0")
class LinkInfo:
def __init__(self, a, b):
self.a = a # NodeInfo
self.b = b # NodeInfo
def generate_graph(time_limit=60*60*3):
response = urllib.request.urlopen(url)
data = json.loads(response.read())["yggnodes"]
toAdd = []
for key in data:
if 'address' not in data[key] or 'coords' not in data[key]: continue
ip = data[key]['address']
coords = data[key]['coords']
info = NodeInfo(ip, coords)
try:
if 'nodeinfo' in data[key]:
if 'name' in data[key]['nodeinfo']:
label = str(data[key]['nodeinfo']['name'])
if len(label) <= 64:
info.label = label
except: pass
info.label = html.escape(info.label)
toAdd.append(info)
nodes = dict()
def addAncestors(info):
coords = info.getParent()
parent = NodeInfo("{} {}".format("?", coords), coords)
parent.label = parent.ip
nodes[parent.coords] = parent
if parent.coords != parent.getParent(): addAncestors(parent)
for info in toAdd: addAncestors(info)
for info in toAdd: nodes[info.coords] = info
sortedNodes = sorted(nodes.values(), key=(lambda x: x.getLink()))
#for node in sortedNodes: print node.ip, node.coords, node.getParent(), node.getLink()
edges = []
for node in sortedNodes:
if node.coords == node.getParent: continue
edges.append(LinkInfo(node, nodes[node.getParent()]))
print('%d nodes, %d edges' % (len(nodes), len(edges)))
graph = graphPlotter.position_nodes(nodes, edges)
js = graphPlotter.get_graph_json(graph)
with open('api/static/graph.json', 'w') as f:
f.write(js)
if __name__ == '__main__':
generate_graph()

7
api/web.fcgi

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
#!/usr/bin/env python
from flup.server.fcgi import WSGIServer
from web import app
if __name__ == '__main__':
WSGIServer(app).run()

89
api/niflheim-api.py → api/web.py

@ -1,47 +1,62 @@ @@ -1,47 +1,62 @@
#!/usr/bin/env python
import signal, sys, time
from flask import Flask, render_template
from functools import wraps
from flask import Flask, Response, render_template
from flask_restful import Resource, Api
import requests
import psycopg2
import json
from config import DB_PASS, DB_USER, DB_NAME, DB_HOST, DB_RETRIES, DB_RECONNIDLE, ALIVE_SECONDS
######
DB_PASS = "password"
DB_USER = "yggindex"
DB_NAME = "yggindex"
DB_HOST = "localhost"
app = Flask(__name__)
api = Api(app)
# count peer alive if it was available not more that amount of seconds ago
# I'm using 1 hour beause of running crawler every 15 minutes
ALIVE_SECONDS = 3600 # 1 hour
def pg_connect():
return psycopg2.connect(host=DB_HOST,\
database=DB_NAME,\
user=DB_USER,\
password=DB_PASS)
######
app = Flask(__name__)
api = Api(app)
# dbconn = pg_connect() # initialize connection
def retry(fn):
@wraps(fn)
def wrapper(*args, **kw):
for x in range(DB_RETRIES):
try:
return fn(*args, **kw)
except (psycopg2.InterfaceError, psycopg2.OperationalError) as e:
print ("\nDatabase Connection [InterfaceError or OperationalError]")
print ("Idle for %s seconds" % (cls._reconnectIdle))
time.sleep(DB_RECONNIDLE)
dbconn = pg_connect()
return wrapper
dbconn = psycopg2.connect(host=DB_HOST,\
database=DB_NAME,\
user=DB_USER,\
password=DB_PASS)
def signal_handler(sig, frame):
dbconn.close()
sys.exit(0)
def age_calc(ustamp):
if (time.time() - ustamp) <= ALIVE_SECONDS:
return True
else:
return False
# active nodes
class nodesCurrent(Resource):
@retry
def get(self):
dbconn = pg_connect()
cur = dbconn.cursor()
nodes = {}
cur.execute("select * from yggindex")
for i in cur.fetchall():
@ -51,15 +66,18 @@ class nodesCurrent(Resource): @@ -51,15 +66,18 @@ class nodesCurrent(Resource):
dbconn.commit()
cur.close()
nodelist = {}
nodelist['yggnodes'] = nodes
nodeinfo = {}
nodeinfo['yggnodes'] = nodes
return nodelist
dbconn.close()
return nodeinfo
# nodes info
class nodesInfo(Resource):
@retry
def get(self):
dbconn = pg_connect()
cur = dbconn.cursor()
nodes = {}
cur.execute("select * from yggnodeinfo")
@ -73,12 +91,15 @@ class nodesInfo(Resource): @@ -73,12 +91,15 @@ class nodesInfo(Resource):
nodeinfo = {}
nodeinfo['yggnodeinfo'] = nodes
dbconn.close()
return nodeinfo
# alive nodes count for latest 24 hours
class nodes24h(Resource):
@retry
def get(self):
dbconn = pg_connect()
cur = dbconn.cursor()
nodes = {}
cur.execute("SELECT * FROM timeseries ORDER BY unixtstamp DESC LIMIT 24")
@ -91,12 +112,15 @@ class nodes24h(Resource): @@ -91,12 +112,15 @@ class nodes24h(Resource):
nodeinfo = {}
nodeinfo['nodes24h'] = nodes
dbconn.close()
return nodeinfo
# alive nodes count for latest 30 days
class nodes30d(Resource):
@retry
def get(self):
dbconn = pg_connect()
cur = dbconn.cursor()
nodes = {}
cur.execute("SELECT * FROM timeseries ORDER BY unixtstamp DESC LIMIT 24 * 30")
@ -109,20 +133,14 @@ class nodes30d(Resource): @@ -109,20 +133,14 @@ class nodes30d(Resource):
nodeinfo = {}
nodeinfo['nodes30d'] = nodes
dbconn.close()
return nodeinfo
# alive nodes count for latest 24 hours
class crawlResult(Resource):
def get(self):
with open('api/results.json', 'r') as f:
data = json.load(f)
return data
@app.route("/")
@retry
def fpage():
dbconn = pg_connect()
cur = dbconn.cursor()
nodes = 0
cur.execute("select * from yggindex")
@ -134,19 +152,32 @@ def fpage(): @@ -134,19 +152,32 @@ def fpage():
dbconn.commit()
cur.close()
dbconn.close()
return render_template('index.html', nodes=nodes)
@app.route('/map')
@app.route('/map/network')
def page_network():
return render_template('map/network.html', page='network')
@app.route('/map/about')
def page_about():
return render_template('map/about.html', page='about')
@app.after_request
def add_header(response):
response.cache_control.max_age = 300
return response
# sort out the api request here for the url
api.add_resource(nodesCurrent, '/current')
api.add_resource(nodesInfo, '/nodeinfo')
api.add_resource(nodes24h, '/nodes24h')
api.add_resource(nodes30d, '/nodes30d')
api.add_resource(crawlResult, '/result.json')
# regirster signal handler
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
if __name__ == '__main__':
app.run(host='::', port=3000)
app.run(host='127.0.0.1', port=3001)

36
ygg-crawl.sh

@ -1,12 +1,32 @@ @@ -1,12 +1,32 @@
#!/bin/sh
#!/bin/bash
YGGCRAWL="/opt/yggcrawl/yggcrawl" # path to yggcrawl binary
YGGAPIPATH="/opt/yggdrasil-api" # path to Niflheim-API directory
ulimit -n 4096
CRAWLPEER="tcp://127.0.0.1:12345" # Yggdrasil peer address
CRAWLFILE="api/results.json"
CRAWLRETR=3
YGGCRAWL="/opt/yggdrasil-crawler/crawler"
YGGAPIPATH="/opt/yggdrasil-api"
TMPFILE="api/current.json"
CRAWLFILE="api/result.json"
# Crawler timeout in minutes. It must be lesser then crontab job period
# Increased to 50 minutes and crontab runs hourly due to network instabillity
#CRAWLTIMEOUT=50
##############################################################################
cd $YGGAPIPATH
$YGGCRAWL -peer $CRAWLPEER -retry $CRAWLRETR -file $CRAWLFILE > api/yggcrawl.log 2>&1
venv/bin/python api/importer.py >> api/yggcrawl.log 2>&1
#let "TIMEOUT = $CRAWLTIMEOUT * 60"
#timeout $TIMEOUT $YGGCRAWL > $TMPFILE 2>logs/crawler.log
$YGGCRAWL > $TMPFILE 2>logs/crawler.log
if [[ $? == 0 ]] # Crawler not triggered error or was killed
then
# add a little delay...
sleep 3
mv -f $TMPFILE $CRAWLFILE
venv/bin/python api/importer.py > logs/importer.log 2>&1
venv/bin/python api/addresses.py > logs/addresses.log 2>&1
venv/bin/python api/updateGraph.py > logs/graph.log 2>&1
fi

2
yggapi.service

@ -4,7 +4,7 @@ After=network.target @@ -4,7 +4,7 @@ After=network.target
[Service]
WorkingDirectory=/opt/yggdrasil-api
ExecStart=/opt/yggdrasil-api/venv/bin/python /opt/yggdrasil-api/api/niflheim-api.py
ExecStart=/opt/yggdrasil-api/venv/bin/python /opt/yggdrasil-api/api/web.py
Restart=always
Type=simple

Loading…
Cancel
Save