diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3fb83a2 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,35 @@ +FROM ubuntu:focal AS build + +ENV TZ=Europe/Berlin +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && \ + echo $TZ > /etc/timezone + +RUN apt-get update && apt-get install -qq --no-install-recommends\ + apt-transport-https git nodejs npm && npm install --global yarn + +WORKDIR /tmp +RUN git clone https://github.com/hopglass/hopglass + +WORKDIR /tmp/hopglass +RUN yarn install && node_modules/grunt/bin/grunt + +FROM ubuntu:focal +COPY --from=build /tmp/hopglass/build /hopglass + +RUN apt-get update && apt-get install -qq --no-install-recommends\ + nginx python3-venv + +ADD . /app + +RUN cp /app/config.json /hopglass/config.json + +RUN cd /etc/nginx &&\ + cp /app/nginx.conf sites-available/hopglass;\ + ln -s ../sites-available/hopglass sites-enabled/hopglass;\ + rm sites-enabled/default || true + +WORKDIR /app/netglass +RUN python3 -m venv flask &&\ + flask/bin/pip install -U -r requirements.txt + +CMD ["/app/run.sh"] diff --git a/README.md b/README.md index e69de29..84c1efc 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,21 @@ +# Netbox: NetGlass + +View cable topology in [hopglass](https://github.com/hopglass/hopglass) fashioned graph view. + +## Requirements + +* A docker host +* docker-compose + +## Prepare + +Netglass acts as replacement for [hoplass-server](https://github.com/hopglass/hopglass-server) and uses NetBox's API to gather wiring information. + +First go to your NetBox instance and create a read-only Token. Remember that tokens for users from LDAP backends do not work. Create a file `.env` with the contents + +``` +NETBOX_URL=https://netbox.your.tld +NETBOX_TOKEN=70bdtoken89nhuf +``` + +Run `docker-compose up -d` to build the hopglass instance. diff --git a/config.json b/config.json new file mode 100644 index 0000000..b19f7ac --- /dev/null +++ b/config.json @@ -0,0 +1,45 @@ +{ + "dataPath": "/data/", + "siteName": "NetGlass", + "mapSigmaScale": 0.5, + "showContact": false, + "maxAge": 365, + "domainNames": [ + {"domain": "net", "name": "Testnetz"} + ], + "mapLayers": [ + { + "name": "OpenStreetMap.HOT" + } + ], + "_nodeInfos": [ + { "href": "https://map.eulenfunk.de/stats/dashboard/db/node-byid?var-nodeid={NODE_ID}", + "thumbnail": "https://map.eulenfunk.de/stats/render/dashboard-solo/db/node-byid?panelId=1&theme=light&width=600&height=300&var-nodeid={NODE_ID}" + }, + { "href": "https://map.eulenfunk.de/stats/dashboard/db/node-byid?var-nodeid={NODE_ID}", + "thumbnail": "https://map.eulenfunk.de/stats/render/dashboard-solo/db/node-byid?panelId=2&theme=light&width=600&height=500&var-nodeid={NODE_ID}" + }, + { "href": "https://map.eulenfunk.de/stats/dashboard/db/node-byid?var-nodeid={NODE_ID}", + "thumbnail": "https://map.eulenfunk.de/stats/render/dashboard-solo/db/node-byid?panelId=3&theme=light&width=600&height=200&var-nodeid={NODE_ID}" + } + ], + "_globalInfos": [ + { "href": "https://map.eulenfunk.de/stats/dashboard/db/global?var-job=dus", + "thumbnail": "https://map.eulenfunk.de/stats/render/dashboard-solo/db/global?panelId=1&&theme=light&width=800&height=600&var-job=dus" + }, + { "href": "https://map.eulenfunk.de/stats/dashboard/db/global?var-job=dus", + "thumbnail": "https://map.eulenfunk.de/stats/render/dashboard-solo/db/global?panelId=8&&theme=light&width=800&height=600&var-job=dus" + } + ], + "_linkInfos": [ + { "href": "https://map.eulenfunk.de/stats/dashboard/db/links?var-source={SOURCE}&var-target={TARGET}", + "thumbnail": "https://map.eulenfunk.de/stats/render/dashboard-solo/db/links?panelId=1&&theme=light&width=800&height=600&var-source={SOURCE}&var-target={TARGET}" + } + ], + "siteNames": [ + { "site": "net", "name": "Location" } + ], + "_hwImg": [ + ] +} + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..84b9fee --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,8 @@ +version: '3' +services: + web: + build: . + image: netglass:dev + environment: + - NETBOX_URL + - NETBOX_TOKEN diff --git a/netglass/netglass.py b/netglass/netglass.py new file mode 100755 index 0000000..ea720c1 --- /dev/null +++ b/netglass/netglass.py @@ -0,0 +1,111 @@ +#!flask/bin/python3 +import re +import os +from flask import Flask, jsonify +import pynetbox +import json +from datetime import datetime + +box = pynetbox.api( + os.environ.get('NETBOX_URL'), + token=os.environ['NETBOX_TOKEN'] +) + +netglass = Flask(__name__) + +@netglass.route('/graph.json') +def net_graph(): + links = [] + nodes = [] + dcim_port_types = ['dcim.interface','dcim.frontport','dcim.rearport'] + + def _isdevice(device): + if isinstance(device, pynetbox.models.dcim.Devices): + return device + + for wire in box.dcim.cables.all(): + term_a = wire.termination_a.device if wire.termination_a_type in dcim_port_types else None + term_b = wire.termination_b.device if wire.termination_b_type in dcim_port_types else None + + for node in [term_a,term_b]: + if not _isdevice(node): + continue + if not next((n for n in nodes if n['node_id'] == str(node.id)), None): + nodes.append({ + 'node_id': str(node.id), + 'id': str(node.id) + }) + + link = { + 'bidirect': True, + 'source': next(( + i for i, item in enumerate(nodes) + if term_a and item['node_id'] == str(term_a.id)), None), + 'target': next(( + i for i, item in enumerate(nodes) + if term_b and item['node_id'] == str(term_b.id)), None), + 'tq': 1, + } + if link['source'] != None and link['target'] != None: + links.append(link) + + return jsonify({'batadv':{ + 'directed': True, + 'graph': None, + 'multigraph': True, + 'links': links, + 'nodes': nodes, + }, + 'version': 1}) + +@netglass.route('/nodes.json') +def net_nodes(): + nodes = [] + for node in [ + n for n in box.dcim.devices.all() + if isinstance(n, pynetbox.models.dcim.Devices)]: + lastseen = datetime.now().replace(microsecond=0) + addresses = [] + for primary_ip in [node.primary_ip4,node.primary_ip6]: + if primary_ip: + addresses.append(re.sub(r'/\d+', '', primary_ip.address)) + nodes.append({ + 'flags': {'online': True, 'gateway': False}, + 'firstseen': lastseen.isoformat(), + 'lastseen': lastseen.isoformat(), + 'nodeinfo': { + 'hostname': node.display_name, + 'node_id': str(node.id), + 'network': { 'mac': node.name, 'addresses': addresses }, + 'mesh_interfaces': [node.name], + 'mesh': { + 'bat0': { 'interfaces': { 'other': [node.name] } } + }, + 'system': { + 'site_code': 'net' + }, + 'traffic': { + 'forward': {'packets': 0, 'bytes': 0}, + 'mgmt_rx': {'packets': 0, 'bytes': 0}, + 'mgmt_tx': {'packets': 0, 'bytes': 0}, + 'rx': {'packets': 0, 'bytes': 0}, + 'tx': {'packets': 0, 'bytes': 0, 'dropped': 0} + } + }, + 'statistics': { + 'clients': 0, + 'gateway': '', + 'memory_usage': 0, + 'rootfs_usage': 0, + 'uptime': 0 + } + }) + + return jsonify({ + 'nodes': nodes, + 'timestamp': datetime.now().replace(microsecond=0).isoformat(), + 'version': 2 + }) + +if __name__ == '__main__': + netglass.run(debug=False, port=8080) diff --git a/netglass/requirements.txt b/netglass/requirements.txt new file mode 100644 index 0000000..384c3ee --- /dev/null +++ b/netglass/requirements.txt @@ -0,0 +1,2 @@ +flask +pynetbox diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..2a47232 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,44 @@ +upstream netglass { + server 127.0.0.1:8080; +} + +server { + listen 80 default_server; + listen [::]:80 default_server; + + # SSL configuration + # + # listen 443 ssl default_server; + # listen [::]:443 ssl default_server; + # + # Note: You should disable gzip for SSL traffic. + # See: https://bugs.debian.org/773332 + # + # Read up on ssl_ciphers to ensure a secure configuration. + # See: https://bugs.debian.org/765782 + # + # Self signed certs generated by the ssl-cert package + # Don't use them in a production server! + # + # include snippets/snakeoil.conf; + + root /hopglass; + + location /data/ { + rewrite /data/(.*) /$1 break; + proxy_pass http://netglass; + proxy_redirect off; + proxy_set_header Host $host; + } + + # Add index.php to the list if you are using PHP + index index.html index.htm index.nginx-debian.html; + + server_name _; + + location / { + # First attempt to serve request as file, then + # as directory, then fall back to displaying a 404. + try_files $uri $uri/ =404; + } +} diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..80afe45 --- /dev/null +++ b/run.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +service nginx start + +./netglass.py