changeset 531:f372e9d358d2

rewrite dhcpleases to use dnsmasq's data files, and all the new build stuff Ignore-this: 2dffc17b676253d4fec234fc096db4d5
author drewp@bigasterisk.com
date Tue, 23 Apr 2019 02:56:07 -0700
parents f09fc1a18b66
children 71aa55cd8433
files service/dhcpleases/Dockerfile service/dhcpleases/dhcpleases.py service/dhcpleases/index.html service/dhcpleases/pydeps service/dhcpleases/requirements.txt service/dhcpleases/tasks.py
diffstat 6 files changed, 147 insertions(+), 106 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/service/dhcpleases/Dockerfile	Tue Apr 23 02:56:07 2019 -0700
@@ -0,0 +1,15 @@
+FROM bang6:5000/base_x86
+
+WORKDIR /opt
+
+COPY requirements.txt ./
+
+RUN pip3 install --index-url https://projects.bigasterisk.com/ --extra-index-url https://pypi.org/simple -r requirements.txt
+# not sure why this doesn't work from inside requirements.txt
+RUN pip3 install -U 'https://github.com/drewp/cyclone/archive/python3.zip?v3'
+
+COPY *.py *.html ./
+
+EXPOSE 9073
+
+CMD [ "python3", "dhcpleases.py" ]
--- a/service/dhcpleases/dhcpleases.py	Tue Apr 23 02:18:07 2019 -0700
+++ b/service/dhcpleases/dhcpleases.py	Tue Apr 23 02:56:07 2019 -0700
@@ -3,71 +3,93 @@
 
 also read 'arp -an' and our dns list 
 """
-import sys
-import datetime
-sys.path.append("/my/site/magma")
-from stategraph import StateGraph
+import datetime, itertools, os
+
+from docopt import docopt
+from dateutil.tz import tzlocal
 from rdflib import URIRef, Namespace, Literal, RDF, RDFS, XSD, ConjunctiveGraph
-from dateutil.tz import tzlocal
+from twisted.internet import reactor, task
 import cyclone.web
-from twisted.internet import reactor, task
-from isc_dhcp_leases.iscdhcpleases import IscDhcpLeases
-sys.path.append("/my/proj/homeauto/lib")
+
+from greplin import scales
+from greplin.scales.cyclonehandler import StatsHandler
 from patchablegraph import PatchableGraph, CycloneGraphEventsHandler, CycloneGraphHandler
-sys.path.append("/my/proj/rdfdb")
-from rdfdb.patch import Patch
+from standardservice.logsetup import log, verboseLogging
 
 DEV = Namespace("http://projects.bigasterisk.com/device/")
 ROOM = Namespace("http://projects.bigasterisk.com/room/")
+ctx = DEV['dhcp']
+
+STATS = scales.collection('/root',
+                          scales.PmfStat('readLeases'),
+                          scales.IntStat('filesDidntChange'),
+                          )
 
 def timeLiteral(dt):
     return Literal(dt.replace(tzinfo=tzlocal()).isoformat(),
                    datatype=XSD.dateTime)
+    
+def macUri(macAddress: str) -> URIRef:
+    return URIRef("http://bigasterisk.com/mac/%s" % macAddress.lower())
 
-def update(masterGraph):
-    g = ConjunctiveGraph()
-    ctx = DEV['dhcp']
+class Poller:
+    def __init__(self, graph):
+        self.graph = graph
+        self.fileTimes = {'/opt/dnsmasq/10.1/leases': 0, '/opt/dnsmasq/10.2/leases': 0}
+        task.LoopingCall(self.poll).start(2)
 
-    now = datetime.datetime.now()
-    for mac, lease in IscDhcpLeases('/var/lib/dhcp/dhcpd.leases'
-                                    ).get_current().items():
-        uri = URIRef("http://bigasterisk.com/dhcpLease/%s" % lease.ethernet)
+    def anythingToRead(self):
+        ret = False
+        for f, t in self.fileTimes.items():
+            mtime = os.path.getmtime(f)
+            if mtime > t:
+                self.fileTimes[f] = mtime
+                ret = True
+        return ret
+        
+    def poll(self):
+        if not self.anythingToRead():
+            STATS.filesDidntChange += 1
+            return
 
-        g.add((uri, RDF.type, ROOM['DhcpLease'], ctx))
-        g.add((uri, ROOM['leaseStartTime'], timeLiteral(lease.start), ctx))
-        g.add((uri, ROOM['leaseEndTime'], timeLiteral(lease.end), ctx))
-        if lease.end < now:
-            g.add((uri, RDF.type, ROOM['ExpiredLease'], ctx))
-        ip = URIRef("http://bigasterisk.com/localNet/%s/" % lease.ip)
-        g.add((uri, ROOM['assignedIp'], ip, ctx))
-        g.add((ip, RDFS.label, Literal(lease.ip), ctx))
-        mac = URIRef("http://bigasterisk.com/mac/%s" % lease.ethernet)
-        g.add((uri, ROOM['ethernetAddress'], mac, ctx))
-        g.add((mac, ROOM['macAddress'], Literal(lease.ethernet), ctx))
-        if lease.hostname:
-            g.add((mac, ROOM['dhcpHostname'], Literal(lease.hostname), ctx))
-    masterGraph.setToGraph(g)
-        
+        with STATS.readLeases.time():
+            g = ConjunctiveGraph()
+            for line in itertools.chain(*[open(f) for f in self.fileTimes]):
+                # http://lists.thekelleys.org.uk/pipermail/dnsmasq-discuss/2016q2/010595.html
+                expiration_secs, addr, ip, hostname, clientid = line.strip().split(' ')
+
+                uri = macUri(addr)
+                g.add((uri, RDF.type, ROOM['HasDhcpLease'], ctx))
+                g.add((uri, ROOM['macAddress'], Literal(addr), ctx))
+                g.add((uri, ROOM['assignedIp'], Literal(ip), ctx))
+
+                if hostname != '*':
+                    g.add((uri, ROOM['dhcpHostname'], Literal(hostname), ctx))
+
+            self.graph.setToGraph(g)
+            
 if __name__ == '__main__':
-    config = {
-        'servePort' : 9073,
-        }
-    from twisted.python import log as twlog
-    twlog.startLogging(sys.stdout)
-    #log.setLevel(10)
-    #log.setLevel(logging.DEBUG)
+    arg = docopt("""
+    Usage: store.py [options]
+
+    -v           Verbose
+    --port PORT  Serve on port [default: 9073].
+    """)
+
+    verboseLogging(arg['-v'])
+
     masterGraph = PatchableGraph()
-    task.LoopingCall(update, masterGraph).start(1)
+    poller = Poller(masterGraph)
 
     reactor.listenTCP(
-        config['servePort'],
+        int(arg['--port']),
         cyclone.web.Application(
             [
                 (r"/()", cyclone.web.StaticFileHandler,
                  {"path": ".", "default_filename": "index.html"}),
-                
                 (r'/graph', CycloneGraphHandler, {'masterGraph': masterGraph}),
                 (r'/graph/events', CycloneGraphEventsHandler, {'masterGraph': masterGraph}),
+                (r'/stats/(.*)', StatsHandler, {'serverName': 'dhcpleases'}),
             ], masterGraph=masterGraph
         ))
     reactor.run()
--- a/service/dhcpleases/index.html	Tue Apr 23 02:18:07 2019 -0700
+++ b/service/dhcpleases/index.html	Tue Apr 23 02:56:07 2019 -0700
@@ -4,71 +4,43 @@
     <title>dhcp leases</title>
     <meta charset="utf-8" />
     <script src="/lib/polymer/1.0.9/webcomponentsjs/webcomponents-lite.min.js"></script>
-    <script src="/lib/underscore-1.5.2.min.js"></script>
-    <link rel="import" href="/rdf/n3+polymer/trig-store.html">
-    <link rel="import" href="/lib/polymer/1.0.9/iron-ajax/iron-ajax.html">
+
+    <script src="/lib/require/require-2.3.3.js"></script>
+    <script src="/rdf/common_paths_and_ns.js"></script>
+
+    <link rel="import" href="/rdf/streamed-graph.html">
+    <link rel="import" href="/lib/polymer/1.0.9/polymer/polymer.html">
+
+    <meta name="mobile-web-app-capable" content="yes">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+
   </head>
   <body>
     <h1>Active dhcp leases</h1>
-    <p><a href="../wifi">go to wifi</a></p>
+    <p><a href="../wifi">go to wifi</a>; <a href="/rdf/browse/?graph=https%3A%2F%2Fbigasterisk.com%2Fsse_collector%2Fgraph%2Fnetwork">merge with wifi</a></p>
+
+    <template id="t" is="dom-bind">
 
-    <dom-module id="dhcp-table">
-      <template>
-        <iron-ajax auto url="graph"
-                   params='{"pruneExpired": "true"}'
-                   handle-as="text"
-                   last-response="{{ajaxResponse}}"></iron-ajax>
-        <trig-store id="ts" trig-input="{{ajaxResponse}}"></trig-store>
+      <streamed-graph url="graph/events" graph="{{graph}}"></streamed-graph>
+      <div id="out"></div>
+      <script type="module" src="/rdf/streamed_graph_view.js"></script>
 
-        <table>
-          <template is="dom-repeat" items="{{devices}}">
-            <tr>
-              <td>{{item.dhcpHostname}}</td>
-              <td>{{item.ip}}</td>
-              <td>{{item.mac}}</td>
-            </tr>
-          </template>
-        </table>
-      </template>
-    </dom-module>
-    
-    <script>
-     HTMLImports.whenReady(function () {
-       Polymer({
-         is: "dhcp-table",
-         ready: function() {
-           this.$.ts.addEventListener('store-changed', this.storeChanged.bind(this));
-           this.devices = [];
-         },
-         storeChanged: function(ev) {
-           var store = ev.detail.value;
-           var find = function(s, p, o) { return store.findAllGraphs(s, p, o); };
-           var findOne = function(s, p, o) {
-             var rows = find(s, p, o);
-             return rows[0];
-           };
+    </template>
+    <style>
+     .served-resources {
+         margin-top: 4em;
+         border-top: 1px solid gray;
+         padding-top: 1em;
+     }
+     .served-resources a {
+         padding-right: 2em;
+     }
+    </style>
 
-           this.devices = [];
-           
-           find(null, "room:dhcpHostname", null).forEach(function(row) {
-             var out = {dhcpHostname: N3.Util.getLiteralValue(row.object)}
-             var lease = findOne(null, "room:ethernetAddress",
-                                 row.subject).subject;
-             out.ip = N3.Util.getLiteralValue(
-               findOne(findOne(lease, "room:assignedIp", null).object,
-                       "rdfs:label", null).object);
-             out.mac = N3.Util.getLiteralValue(
-               findOne(findOne(lease, "room:ethernetAddress", null).object,
-                       "room:macAddress", null).object);
-             this.devices.push(out);
-           }.bind(this));
-           this.devices = _.sortBy(this.devices, 'dhcpHostname');
-         }
-       });
-     });
-    </script>
-    <dhcp-table></dhcp-table>
-
-    <iframe src="https://bigasterisk.com/wifi" height="500" style="width: 100%"></iframe>
+    <div class="served-resources">
+      <a href="stats/">/stats/</a>
+      <a href="graph">/graph</a>
+      <a href="graph/events">/graph/events</a>
+    </div>
   </body>
 </html>
--- a/service/dhcpleases/pydeps	Tue Apr 23 02:18:07 2019 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,4 +0,0 @@
-cyclone==1.1
-python-dateutil==2.4.1
-rdflib==4.2.0
-https://github.com/drewp/python-isc-dhcp-leases/archive/master.zip
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/service/dhcpleases/requirements.txt	Tue Apr 23 02:56:07 2019 -0700
@@ -0,0 +1,9 @@
+docopt
+git+http://github.com/drewp/scales.git@448d59fb491b7631877528e7695a93553bfaaa93#egg=scales
+https://github.com/drewp/cyclone/archive/python3.zip
+rdflib==4.2.2
+
+cycloneerr
+patchablegraph==0.5.0
+rdfdb==0.8.0
+standardservice==0.3.0
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/service/dhcpleases/tasks.py	Tue Apr 23 02:56:07 2019 -0700
@@ -0,0 +1,27 @@
+from invoke import task
+
+JOB = 'dhcpleases'
+PORT = 9073
+TAG = f'bang6:5000/{JOB}_x86:latest'
+
+
+@task
+def build_image(ctx):
+    ctx.run(f'docker build --network=host -t {TAG} .')
+
+@task(pre=[build_image])
+def push_image(ctx):
+    ctx.run(f'docker push {TAG}')
+
+@task(pre=[build_image])
+def shell(ctx):
+    ctx.run(f'docker run --rm -it --cap-add SYS_PTRACE --net=host {TAG} /bin/bash', pty=True)
+
+@task(pre=[build_image])
+def local_run(ctx):
+    ctx.run(f'docker run --rm -it -p {PORT}:{PORT} --net=host -v /opt/dnsmasq:/opt/dnsmasq {TAG} python3 dhcpleases.py -v', pty=True)
+
+@task(pre=[push_image])
+def redeploy(ctx):
+    ctx.run(f'sudo /my/proj/ansible/playbook -l bang -t {JOB}')
+    ctx.run(f'supervisorctl -s http://bang:9001/ restart {JOB}_{PORT}')