ctucx.git: fritzbox-exporter

[nimlang] prometheus exporter for lte fritzboxes

commit 028155f33c38eda55a2239868bd75121136af5af
parent bd3a22c9eaec3d27df7f9790b2a851726050c086
Author: ctucx <c@ctu.cx>
Date: Tue, 18 Aug 2020 20:29:53 +0200

foo
6 files changed, 407 insertions(+), 90 deletions(-)
A
default.nix
|
46
++++++++++++++++++++++++++++++++++++++++++++++
A
nim.cfg
|
2
++
A
result
|
2
++
M
src/fb_exporter.nim
|
110
+++++++++++++++----------------------------------------------------------------
A
src/fb_exporter_client.nim
|
266
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
src/utils.nim
|
71
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
diff --git a/default.nix b/default.nix
@@ -0,0 +1,46 @@
+let
+  crossPkgs = import <nixpkgs> {
+    crossSystem = {
+      config = "mips-unknown-linux-musl";
+      platform = {
+        name = "fritzbox";
+        kernelMajor = "4.4";
+        kernelArch = "mips";
+        gcc = {
+          arch = "mips32";
+          float = "soft";
+        };
+      };
+    };
+
+    crossOverlays = [
+      (import <nixpkgs/pkgs/top-level/static.nix>)
+    ];
+
+    overlays = [
+      (self: super: rec {
+      })
+    ];
+
+    config.allowUnsupportedSystem = true;
+  };
+
+  pkgs = import <nixpkgs> {};
+
+in crossPkgs.stdenv.mkDerivation {
+  name = "fritzbox-exporter";
+
+  nativeBuildInputs = [ pkgs.nim ];
+  src = ./.;
+
+  buildPhase = ''
+    runHook preBuild
+    nim c --cpu:mips -d:release --nimcache:$PWD src/fb_exporter_client.nim
+    runHook postBuild
+  '';
+  installPhase = ''
+    runHook preInstall
+    install -Dm755 src/fb_exporter_client $out/bin/fb_exporter_client
+    runHook postInstall
+  '';
+}
diff --git a/nim.cfg b/nim.cfg
@@ -0,0 +1,2 @@
+mips.linux.gcc.exe = "mips-unknown-linux-musl-gcc"
+mips.linux.gcc.linkerexe = "mips-unknown-linux-musl-gcc"
diff --git a/result b/result
@@ -0,0 +1 @@
+/nix/store/jvgwrz1hhvd622x0wkgmdy15r072fffz-fritzbox-exporter-mips-unknown-linux-musl+
\ No newline at end of file
diff --git a/src/fb_exporter.nim b/src/fb_exporter.nim
@@ -1,75 +1,6 @@
-import times, posix
+import times, posix, utils
 import asyncHttpServer
 
-proc CtrlCHook() {.noconv.} =
-  echo "Ctrl+C fired! \nStopping Server now!"
-  quit()
-
-proc isInt* (s: string): bool =
-  try:
-    discard s.parseInt()
-    result = true
-  except:
-    discard
-
-proc isFloat* (s: string): bool =
-  try:
-    discard s.parseFloat()
-    result = true
-  except:
-    discard
-
-proc parseCtlMgr* (n: string): JsonNode =
-  var input = n.split("\n")
-
-  input.del(0)
-
-  var last_group = ""
-
-  var data = newJObject()
-
-  for i in items(input):
-    let line = strip(i)
-    if line == "": continue
-
-    if endsWith(line, "/"):
-      last_group = replace(line, "/", "")
-      continue
-
-    let entry = line.split("=")
-
-    var value = ""
-
-    if entry.len > 1:
-        value = entry[1]
-
-    if last_group == "":
-      if isInt(value):
-        data.add(entry[0], newJInt(parseInt(value)))
-      elif isFloat(value):
-        data.add(entry[0], newJFloat(parseFloat(value)))
-      else:
-        data.add(entry[0], %(value))
-    else:
-      if not data.hasKey(last_group):
-        data.add(last_group, newJObject())
-
-      if isInt(value):
-        data[last_group].add(entry[0], newJInt(parseInt(value)))
-      elif isFloat(value):
-        data[last_group].add(entry[0], newJFloat(parseFloat(value)))
-      else:
-        data[last_group].add(entry[0], %(value))
-  return data
-
-proc getTxTraffic (data: JsonNode, mode: string): int =
-  if data[mode].hasKey("BytesSentHigh") and data[mode].hasKey("BytesSentLow"):
-    return data[mode]["BytesSentHigh"].getInt shl 32 + data[mode]["BytesSentLow"].getInt
-
-proc getRxTraffic (data: JsonNode, mode: string): int =
-  if data[mode].hasKey("BytesReceivedHigh") and data[mode].hasKey("BytesReceivedLow"):
-    return data[mode]["BytesReceivedHigh"].getInt shl 32 + data[mode]["BytesReceivedLow"].getInt
-
 proc prometheusResponse* (request: Request, state: JsonNode) {.async.} = 
   var res = ""
 

@@ -120,31 +51,30 @@ proc prometheusResponse* (request: Request, state: JsonNode) {.async.} =
           if cells[num] != "" and data.hasKey("cell"&cells[num]):
             let cell = "cell"&cells[num]
 
-            if cell != "":
-              if data[cell].hasKey("cell_type"):
-                let cell_technology = data[cell]["cell_type"].getStr
-                let tech_id = %* {
-                  "umts": 3,
-                  "lte":  4
-                }
+            if data[cell].hasKey("cell_type"):
+              let cell_technology = data[cell]["cell_type"].getStr
+              let tech_id = %* {
+                "umts": 3,
+                "lte":  4
+              }
 
-                res &= "fritzbox_cell_techology{cell=\"" & $num & "\"} " & $(tech_id[cell_technology].getInt) & " " & $(lastUpdated * 1000) & "\n"
+              res &= "fritzbox_cell_techology{cell=\"" & $num & "\"} " & $(tech_id[cell_technology].getInt) & " " & $(lastUpdated * 1000) & "\n"
 
-              if data[cell].hasKey("quality"):
-                let cell_quality = data[cell]["quality"].getInt
-                res &= "fritzbox_network_quality{cell=\"" & $num & "\"} " & $(cell_quality) & " " & $(lastUpdated * 1000) & "\n"
+            if data[cell].hasKey("quality"):
+              let cell_quality = data[cell]["quality"].getInt
+              res &= "fritzbox_network_quality{cell=\"" & $num & "\"} " & $(cell_quality) & " " & $(lastUpdated * 1000) & "\n"
 
-              if data[cell].hasKey("band"):
-                let cell_band = data[cell]["band"].getInt
-                res &= "fritzbox_network_band{cell=\"" & $num & "\"} " & $(cell_band) & " " & $(lastUpdated * 1000) & "\n"
+            if data[cell].hasKey("band"):
+              let cell_band = data[cell]["band"].getInt
+              res &= "fritzbox_network_band{cell=\"" & $num & "\"} " & $(cell_band) & " " & $(lastUpdated * 1000) & "\n"
 
-              if data[cell].hasKey("distance"):
-                let cell_distance = data[cell]["distance"].getInt
-                res &= "fritzbox_network_distance{cell=\"" & $num & "\"} " & $(cell_distance/1000) & " " & $(lastUpdated * 1000) & "\n"
+            if data[cell].hasKey("distance"):
+              let cell_distance = data[cell]["distance"].getInt
+              res &= "fritzbox_network_distance{cell=\"" & $num & "\"} " & $(cell_distance/1000) & " " & $(lastUpdated * 1000) & "\n"
 
-              if data[cell].hasKey("usage"):
-                let cell_usage = data[cell]["usage"].getInt
-                res &= "fritzbox_network_usage{cell=\"" & $num & "\"} " & $(cell_usage) & " " & $(lastUpdated * 1000) & "\n"
+            if data[cell].hasKey("usage"):
+              let cell_usage = data[cell]["usage"].getInt
+              res &= "fritzbox_network_usage{cell=\"" & $num & "\"} " & $(cell_usage) & " " & $(lastUpdated * 1000) & "\n"
 
         data.parseCell(0) 
         data.parseCell(1)
diff --git a/src/fb_exporter_client.nim b/src/fb_exporter_client.nim
@@ -0,0 +1,266 @@
+import times, posix, osproc, utils
+import asyncHttpServer
+
+proc prometheusResponse* (request: Request, state: JsonNode) {.async.} = 
+  var res = ""
+
+  if state["inetstat"]["lastUpdated"].getInt == 0 and state["mobiled"]["lastUpdated"].getInt == 0:
+    await request.respond(Http500, "500 No data yet", newHttpHeaders([("Content-Type","text/plain")]))
+
+
+  if state["inetstat"]["lastUpdated"].getInt != 0:
+    let data        = state["inetstat"]["data"]
+    let lastUpdated = state["inetstat"]["lastUpdated"].getInt
+
+    if data.hasKey("Total0"):
+      #total
+      res &= "fritzbox_network_transmit_bytes_total " & $(data.getTxTraffic("Total0")) & " " & $(lastUpdated * 1000) & "\n"
+      res &= "fritzbox_network_receive_bytes_total " & $(data.getRxTraffic("Total0")) & " " & $(lastUpdated * 1000) & "\n"
+
+      #month
+      res &= "fritzbox_network_transmit_bytes_month " & $(data.getTxTraffic("ThisMonth0")) & " " & $(lastUpdated * 1000) & "\n"
+      res &= "fritzbox_network_receive_bytes_month " & $(data.getRxTraffic("ThisMonth0")) & " " & $(lastUpdated * 1000) & "\n"
+
+      #today
+      res &= "fritzbox_network_transmit_bytes_today " & $(data.getTxTraffic("Today0")) & " " & $(lastUpdated * 1000) & "\n"
+      res &= "fritzbox_network_receive_bytes_today " & $(data.getRxTraffic("Today0")) & " " & $(lastUpdated * 1000) & "\n"
+
+
+  if state["mobiled"]["lastUpdated"].getInt != 0:
+    let data        = state["mobiled"]["data"]
+    let lastUpdated = state["mobiled"]["lastUpdated"].getInt()
+
+    if data.hasKey("ue0"):
+      if data["ue0"].hasKey("temperature") and data["ue0"]["temperature"].getInt != 0:
+        res &= "fritzbox_temperature " & $(data["ue0"]["temperature"].getInt/1000) & " " & $(lastUpdated * 1000) & "\n"
+
+      if data["ue0"].hasKey("conn_rate_rx"):
+        let downstream = data["ue0"]["conn_rate_rx"].getInt()
+        res &= "fritzbox_network_downstram " & $(downstream) & " " & $(lastUpdated * 1000) & "\n"
+
+
+      if data["ue0"].hasKey("conn_rate_tx"):
+        let upstream = data["ue0"]["conn_rate_tx"].getInt()
+        res &= "fritzbox_network_upstream " & $(upstream) & " " & $(lastUpdated * 1000) & "\n"
+
+ 
+      if data["ue0"].hasKey("conn_cell"):
+        template parseCell(data: JsonNode, num: int) = 
+          let cells = data["ue0"]["conn_cell"].getStr.split(",")
+
+          if cells[num] != "" and data.hasKey("cell"&cells[num]):
+            let cell = "cell"&cells[num]
+
+            if data[cell].hasKey("cell_type"):
+              let cell_technology = data[cell]["cell_type"].getStr
+              let tech_id = %* {
+                "umts": 3,
+                "lte":  4
+              }
+
+              res &= "fritzbox_cell_techology{cell=\"" & $num & "\"} " & $(tech_id[cell_technology].getInt) & " " & $(lastUpdated * 1000) & "\n"
+
+            if data[cell].hasKey("quality"):
+              let cell_quality = data[cell]["quality"].getInt
+              res &= "fritzbox_network_quality{cell=\"" & $num & "\"} " & $(cell_quality) & " " & $(lastUpdated * 1000) & "\n"
+
+            if data[cell].hasKey("band"):
+              let cell_band = data[cell]["band"].getInt
+              res &= "fritzbox_network_band{cell=\"" & $num & "\"} " & $(cell_band) & " " & $(lastUpdated * 1000) & "\n"
+
+            if data[cell].hasKey("distance"):
+              let cell_distance = data[cell]["distance"].getInt
+              res &= "fritzbox_network_distance{cell=\"" & $num & "\"} " & $(cell_distance/1000) & " " & $(lastUpdated * 1000) & "\n"
+
+            if data[cell].hasKey("usage"):
+              let cell_usage = data[cell]["usage"].getInt
+              res &= "fritzbox_network_usage{cell=\"" & $num & "\"} " & $(cell_usage) & " " & $(lastUpdated * 1000) & "\n"
+
+        data.parseCell(0) 
+        data.parseCell(1)
+
+  await request.respond(Http200, res, newHttpHeaders([("Content-Type","text/plain")]))
+
+
+proc statusResponse* (request: Request, state: JsonNode) {.async.} = 
+  var res = ""
+
+  var responseJson = %* {
+    "traffic": {
+      "lastUpdated": 0,
+      "total": {
+        "received":   0,
+        "transmited": 0,
+      },
+      "month": {
+        "received":   0,
+        "transmited": 0,
+      },
+      "today": {
+        "received":   0,
+        "transmited": 0,
+      }
+    },
+
+    "modem": {
+      "lastUpdated":    0,
+      "connectedCells": []
+    }
+  }
+
+  if state["inetstat"]["lastUpdated"].getInt == 0 and state["mobiled"]["lastUpdated"].getInt == 0:
+    await request.respond(Http500, "500 No data yet", newHttpHeaders([("Content-Type","text/plain")]))
+
+
+  if state["inetstat"]["lastUpdated"].getInt != 0:
+    let data                               = state["inetstat"]["data"]
+    responseJson["traffic"]["lastUpdated"] = state["inetstat"]["lastUpdated"]
+
+    if data.hasKey("Total0"):
+      #total
+      responseJson["traffic"]["total"]["transmited"] = %data.getTxTraffic("Total0")
+      responseJson["traffic"]["total"]["received"]   = %data.getRxTraffic("Total0")
+
+      #month
+      responseJson["traffic"]["month"]["transmited"] = %data.getTxTraffic("ThisMonth0")
+      responseJson["traffic"]["month"]["received"]   = %data.getRxTraffic("ThisMonth0")
+
+      #today
+      responseJson["traffic"]["today"]["transmited"] = %data.getTxTraffic("Today0")
+      responseJson["traffic"]["today"]["received"]   = %data.getRxTraffic("Today0")
+
+
+  if state["mobiled"]["lastUpdated"].getInt != 0:
+    let data                             = state["mobiled"]["data"]
+    responseJson["modem"]["lastUpdated"] = state["mobiled"]["lastUpdated"]
+
+    if data.hasKey("ue0"):
+      if data["ue0"].hasKey("temperature") and data["ue0"]["temperature"].getInt != 0:
+        responseJson["modem"]["temperature"] = %(data["ue0"]["temperature"].getInt/1000)
+
+      if data["ue0"].hasKey("spn"):
+        responseJson["modem"]["vendorText"] = data["ue0"]["spn"]
+
+      if data["ue0"].hasKey("conn_rate_rx"):
+        responseJson["modem"]["linkSpeedRx"] = data["ue0"]["conn_rate_rx"]
+
+      if data["ue0"].hasKey("conn_rate_tx"):
+        responseJson["modem"]["linkSpeedTx"] = data["ue0"]["conn_rate_tx"]
+
+ 
+      if data["ue0"].hasKey("conn_cell"):
+        proc parseCell(data: JsonNode, num: int):JsonNode = 
+          let cells  = data["ue0"]["conn_cell"].getStr.split(",")
+          result = %* {}
+
+          let cell = "cell"&cells[num]
+          if cells[num] != "" and data.hasKey(cell):
+
+            if data[cell].hasKey("cell_type"):
+              result["technology"] = data[cell]["cell_type"]
+
+            if data[cell].hasKey("provider"):
+              result["provider"] = data[cell]["provider"]
+
+            if data[cell].hasKey("plmn"):
+              result["plmn"] = data[cell]["plmn"]
+
+            if data[cell].hasKey("plmn"):
+              result["plmn"] = data[cell]["plmn"]
+
+            if data[cell].hasKey("cell_id"):
+              result["cellId"] = data[cell]["cell_id"]
+
+            if data[cell].hasKey("rssi"):
+              result["rssi"] = %data[cell]["rssi"]
+
+            if data[cell].hasKey("rscp"):
+              result["rscp"] = %data[cell]["rscp"]
+
+            if data[cell].hasKey("rsrp"):
+              result["rsrp"] = %data[cell]["rsrp"].getStr.split(",")[0]
+
+            if data[cell].hasKey("rsrq"):
+              result["rsrq"] = %data[cell]["rsrq"].getStr.split(",")[0]
+
+            if data[cell].hasKey("sinr"):
+              result["sinr"] = %data[cell]["sinr"].getStr.split(",")[0]
+
+            if data[cell].hasKey("quality"):
+              result["quality"] = data[cell]["quality"]
+
+            if data[cell].hasKey("band"):
+              result["band"] = data[cell]["band"]
+
+            if data[cell].hasKey("usage"):
+              result["usage"] = data[cell]["usage"]
+
+            if data[cell].hasKey("distance"):
+              result["distance"] = data[cell]["distance"]
+
+        responseJson["modem"]["connectedCells"].add(data.parseCell(0)) 
+        responseJson["modem"]["connectedCells"].add(data.parseCell(1))
+
+  await request.respondJson(Http200, "success", "", responseJson)
+
+proc main =  # for gcsafe
+  setControlCHook(CtrlCHook)
+
+  onSignal(SIGTERM):
+    echo "Got SIGTERM! \nStopping Server now!"
+    quit()
+
+  let authToken = "penis123"
+  var server    = newServer("0.0.0.0", 1234)  # launch on http://localhost:5000
+  var state     = %* {
+      "inetstat": {
+          "data": {},
+          "lastUpdated": 0,
+        },
+      "mobiled": {
+          "data": {},
+          "lastUpdated": 0,
+        }
+    }
+
+  proc timerProc() =
+    let inetstat_data = execProcess("/usr/bin/ctlmgr_ctl u inetstat")
+    let mobiled_data  = execProcess("/usr/bin/ctlmgr_ctl u mobiled")
+
+    state["inetstat"]["data"]        =  parseCtlMgr(inetstat_data)
+    state["inetstat"]["lastUpdated"] = %toUnix(getTime())
+
+    state["mobiled"]["data"]         =  parseCtlMgr(mobiled_data)
+    state["mobiled"]["lastUpdated"]  = %toUnix(getTime())
+
+  addTimer(10000, false, proc(fd: AsyncFD): bool =
+    timerProc()
+  )
+
+  timerProc()
+
+  server.pages:
+    equals("/"):
+      await request.respond(Http200, "Hello, nothing to see here", newHttpHeaders([("Content-Type","text/plain")]))
+
+    equals("/metrics"):
+      await request.prometheusResponse(state)
+
+    equals("/status.json"):
+      await request.statusResponse(state)
+
+    equals("/inetstat.json"):
+      if state["inetstat"]["lastUpdated"].getInt != 0:
+        await request.respondJson(Http200, "success", "", state["inetstat"]["data"])
+      else:
+        await request.respondJson(Http404, "error", "no data yet", newJObject())
+
+    equals("/mobiled.json"):
+      if state["mobiled"]["lastUpdated"].getInt != 0:
+        await request.respondJson(Http200, "success", "", state["mobiled"]["data"])
+      else:
+        await request.respondJson(Http404, "error", "no data yet", newJObject())
+
+  server.start()
+
+main()
diff --git a/src/utils.nim b/src/utils.nim
@@ -0,0 +1,70 @@
+import times, posix, strutils, json
+
+proc CtrlCHook* () {.noconv.} =
+  echo "Ctrl+C fired! \nStopping Server now!"
+  quit()
+
+proc isInt* (s: string): bool =
+  try:
+    discard s.parseInt()
+    result = true
+  except:
+    discard
+
+proc isFloat* (s: string): bool =
+  try:
+    discard s.parseFloat()
+    result = true
+  except:
+    discard
+
+proc parseCtlMgr* (n: string): JsonNode =
+  var input = n.split("\n")
+
+  input.del(0)
+
+  var last_group = ""
+
+  var data = newJObject()
+
+  for i in items(input):
+    let line = strip(i)
+    if line == "": continue
+
+    if endsWith(line, "/"):
+      last_group = replace(line, "/", "")
+      continue
+
+    let entry = line.split("=")
+
+    var value = ""
+
+    if entry.len > 1:
+        value = entry[1]
+
+    if last_group == "":
+      if isInt(value):
+        data.add(entry[0], newJInt(parseInt(value)))
+      elif isFloat(value):
+        data.add(entry[0], newJFloat(parseFloat(value)))
+      else:
+        data.add(entry[0], %(value))
+    else:
+      if not data.hasKey(last_group):
+        data.add(last_group, newJObject())
+
+      if isInt(value):
+        data[last_group].add(entry[0], newJInt(parseInt(value)))
+      elif isFloat(value):
+        data[last_group].add(entry[0], newJFloat(parseFloat(value)))
+      else:
+        data[last_group].add(entry[0], %(value))
+  return data
+
+proc getTxTraffic* (data: JsonNode, mode: string): int =
+  if data[mode].hasKey("BytesSentHigh") and data[mode].hasKey("BytesSentLow"):
+    return data[mode]["BytesSentHigh"].getInt shl 32 + data[mode]["BytesSentLow"].getInt
+
+proc getRxTraffic* (data: JsonNode, mode: string): int =
+  if data[mode].hasKey("BytesReceivedHigh") and data[mode].hasKey("BytesReceivedLow"):
+    return data[mode]["BytesReceivedHigh"].getInt shl 32 + data[mode]["BytesReceivedLow"].getInt+
\ No newline at end of file