Skip to content

Instantly share code, notes, and snippets.

@nemesifier
Last active March 24, 2021 18:14
Show Gist options
  • Save nemesifier/cb005dc80a75536cf8537105c3038a71 to your computer and use it in GitHub Desktop.
Save nemesifier/cb005dc80a75536cf8537105c3038a71 to your computer and use it in GitHub Desktop.
OpenWISP Monitoring Template
{
"files": [
{
"path": "/usr/sbin/openwisp-monitoring",
"mode": "0744",
"contents": "uuid=$(uci get openwisp.http.uuid)\nkey=$(uci get openwisp.http.key)\nbase_url=$(uci get openwisp.http.url)\nverify_ssl=$(uci get openwisp.http.verify_ssl)\nincluded_interfaces=$(uci get openwisp.monitoring.included_interfaces)\nurl=\"$base_url/api/v1/monitoring/device/$uuid/?key=$key\"\ndata=$(/usr/sbin/netjson-monitoring \"$included_interfaces\")\nif [ \"$verify_ssl\" = 0 ]; then\n curl_command='curl -k'\nelse\n curl_command='curl'\nfi\n# send data via POST\n$curl_command -H \"Content-Type: application/json\" \\\n -X POST \\\n -d \"$data\" \\\n -v $url\n"
},
{
"path": "/usr/sbin/netjson-monitoring",
"mode": "0744",
"contents": "#!/usr/bin/env lua\n-- retrieve monitoring information\n-- and return it as NetJSON Output\nio = require('io')\nubus_lib = require('ubus')\ncjson = require('cjson')\nnixio = require('nixio')\nuci = require('uci')\nuci_cursor = uci.cursor()\n\n-- split function\nfunction split(str, pat)\n local t = {}\n local fpat = \"(.-)\" .. pat\n local last_end = 1\n local s, e, cap = str:find(fpat, 1)\n while s do\n if s ~= 1 or cap ~= \"\" then\n table.insert(t, cap)\n end\n last_end = e + 1\n s, e, cap = str:find(fpat, last_end)\n end\n if last_end <= #str then\n cap = str:sub(last_end)\n table.insert(t, cap)\n end\n return t\nend\n\nlocal function has_value(tab, val)\n for index, value in ipairs(tab) do\n if value == val then\n return true\n end\n end\n return false\nend\n\nlocal function starts_with(str, start)\n return str:sub(1, #start) == start\nend\n\n-- parse /proc/net/arp\nfunction parse_arp()\n arp_info = {}\n for line in io.lines('/proc/net/arp 2> /dev/null') do\n if line:sub(1, 10) ~= 'IP address' then\n ip, hw, flags, mac, mask, dev = line:match(\"(%S+)%s+(%S+)%s+(%S+)%s+(%S+)%s+(%S+)%s+(%S+)\")\n table.insert(arp_info, {\n ip = ip,\n mac = mac,\n interface = dev,\n state = ''\n })\n end\n end\n return arp_info\nend\n\nfunction get_ip_neigh_json()\n arp_info = {}\n output = io.popen('ip -json neigh 2> /dev/null'):read()\n if output ~= nil and pcall(function () json_output = cjson.decode(output) end) then\n json_output = cjson.decode(output)\n for _, arp_entry in pairs(json_output) do\n table.insert(arp_info, {\n ip = arp_entry[\"dst\"],\n mac = arp_entry[\"lladdr\"],\n interface = arp_entry[\"dev\"],\n state = arp_entry[\"state\"][1]\n })\n end\n end\n return arp_info\nend\n\nfunction get_ip_neigh()\n arp_info = {}\n output = io.popen('ip neigh 2> /dev/null')\n for line in output:lines() do\n ip, dev, mac, state = line:match(\"(%S+)%s+dev%s+(%S+)%s+lladdr%s+(%S+).*%s(%S+)\")\n if mac ~= nil then\n table.insert(arp_info, {\n ip = ip,\n mac = mac,\n interface = dev,\n state = state\n })\n end\n end\n return arp_info\nend\n\nfunction get_neighbors()\n arp_table = get_ip_neigh_json()\n if next(arp_table) == nil then\n arp_table = get_ip_neigh()\n end\n if next(arp_table) == nil then\n arp_table = parse_arp()\n end\n return arp_table\nend\n\nfunction parse_dhcp_lease_file(path, leases)\n local f = io.open(path, 'r')\n if not f then\n return leases\n end\n\n for line in f:lines() do\n local expiry, mac, ip, name, id = line:match('(%S+)%s+(%S+)%s+(%S+)%s+(%S+)%s+(%S+)')\n table.insert(leases, {\n expiry = tonumber(expiry),\n mac = mac,\n ip = ip,\n client_name = name,\n client_id = id\n })\n end\n\n return leases\nend\n\nfunction get_dhcp_leases()\n local dhcp_configs = uci_cursor:get_all('dhcp')\n local leases = {}\n\n if not dhcp_configs or not next(dhcp_configs) then\n return nil\n end\n\n for name, config in pairs(dhcp_configs) do\n if config and config['.type'] == 'dnsmasq' and config.leasefile then\n leases = parse_dhcp_lease_file(config.leasefile, leases)\n end\n end\n return leases\nend\n\nfunction is_table_empty(table_)\n return not table_ or next(table_) == nil\nend\n\nfunction parse_hostapd_clients(clients)\n local data = {}\n for mac, properties in pairs(clients) do\n properties.mac = mac\n table.insert(data, properties)\n end\n return data\nend\n\nfunction parse_iwinfo_clients(clients)\n local data = {}\n for i, p in pairs(clients) do\n client = {}\n client.ht = p.rx.ht\n client.mac = p.mac\n client.authorized = p.authorized\n client.vht = p.rx.vht\n client.wmm = p.wme\n client.mfp = p.mfp\n client.auth = p.authenticated\n client.signal = p.signal\n client.noise = p.noise\n table.insert(data, client)\n end\n return data\nend\n\n-- takes ubus wireless.status clients output and converts it to NetJSON\nfunction netjson_clients(clients, is_mesh)\n return (is_mesh and parse_iwinfo_clients(clients) or parse_hostapd_clients(clients))\nend\n\nubus = ubus_lib.connect()\nif not ubus then\n error('Failed to connect to ubusd')\nend\n\n-- helpers\niwinfo_modes = {\n ['Master'] = 'access_point',\n ['Client'] = 'station',\n ['Mesh Point'] = '802.11s',\n ['Ad-Hoc'] = 'adhoc'\n}\n\n-- collect system info\nsystem_info = ubus:call('system', 'info', {})\nboard = ubus:call('system', 'board', {})\nloadavg_output = io.popen('cat /proc/loadavg'):read()\nloadavg_output = split(loadavg_output, ' ')\nload_average = {tonumber(loadavg_output[1]), tonumber(loadavg_output[2]), tonumber(loadavg_output[3])}\n\nfunction parse_disk_usage()\n file = io.popen('df')\n disk_usage_info = {}\n for line in file:lines() do\n if line:sub(1, 10) ~= 'Filesystem' then\n filesystem, size, used, available, percent, location =\n line:match('(%S+)%s+(%S+)%s+(%S+)%s+(%S+)%s+(%S+)%s+(%S+)')\n if filesystem ~= 'tmpfs' and not string.match(filesystem, 'overlayfs') then\n percent = percent:gsub('%W', '')\n -- available, size and used are in KiB\n table.insert(disk_usage_info, {\n filesystem = filesystem,\n available_bytes = tonumber(available) * 1024,\n size_bytes = tonumber(size) * 1024,\n used_bytes = tonumber(used) * 1024,\n used_percent = tonumber(percent),\n mount_point = location\n })\n end\n end\n end\n file:close()\n return disk_usage_info\nend\n\nfunction get_cpus()\n processors = io.popen('cat /proc/cpuinfo | grep -c processor')\n cpus = tonumber(processors:read('*a'))\n processors:close()\n return cpus\nend\n\nfunction get_vpn_interfaces()\n -- only openvpn supported for now\n local items = uci_cursor:get_all('openvpn')\n local vpn_interfaces = {}\n\n if is_table_empty(items) then\n return {}\n end\n\n for name, config in pairs(items) do\n if config and config.dev then\n vpn_interfaces[config.dev] = true\n end\n end\n return vpn_interfaces\nend\n\n-- init netjson data structure\nnetjson = {\n type = 'DeviceMonitoring',\n general = {\n hostname = board.hostname,\n local_time = system_info.localtime,\n uptime = system_info.uptime\n },\n resources = {\n load = load_average,\n memory = system_info.memory,\n swap = system_info.swap,\n cpus = get_cpus(),\n disk = parse_disk_usage()\n }\n}\n\ndhcp_leases = get_dhcp_leases()\nif not is_table_empty(dhcp_leases) then\n netjson.dhcp_leases = dhcp_leases\nend\n\nneighbors = get_neighbors()\nif not is_table_empty(neighbors) then\n netjson.neighbors = neighbors\nend\n\n-- determine the interfaces to monitor\ntraffic_monitored = arg[1]\ninclude_stats = {}\nif traffic_monitored then\n traffic_monitored = split(traffic_monitored, ' ')\n for i, name in pairs(traffic_monitored) do\n include_stats[name] = true\n end\nend\n\nfunction is_excluded(name)\n return name == 'lo'\nend\n\nfunction find_default_gateway(routes)\n for i = 1, #routes do\n if routes[i].target == '0.0.0.0' then\n return routes[i].nexthop\n end\n end\n return nil\nend\n\n-- collect device data\nnetwork_status = ubus:call('network.device', 'status', {})\nwireless_status = ubus:call('network.wireless', 'status', {})\ninterface_data = ubus:call('network.interface', 'dump', {})\nnixio_data = nixio.getifaddrs()\nvpn_interfaces = get_vpn_interfaces()\nwireless_interfaces = {}\ninterfaces = {}\ndns_servers = {}\ndns_search = {}\n\nfunction new_address_array(address, interface, family)\n proto = interface['proto']\n if proto == 'dhcpv6' then\n proto = 'dhcp'\n end\n new_address = {\n address = address['address'],\n mask = address['mask'],\n proto = proto,\n family = family,\n gateway = find_default_gateway(interface.route)\n }\n return new_address\nend\n\nfunction get_interface_info(name, netjson_interface)\n info = {\n dns_search = nil,\n dns_servers = nil\n }\n for _, interface in pairs(interface_data['interface']) do\n if interface['l3_device'] == name then\n if next(interface['dns-search']) then\n info.dns_search = interface['dns-search']\n end\n if next(interface['dns-server']) then\n info.dns_servers = interface['dns-server']\n end\n if netjson_interface.type == 'bridge' then\n info.stp = uci_cursor.get('network', interface['interface'], 'stp') == '1'\n end\n end\n end\n return info\nend\n\nfunction array_concat(source, destination)\n table.foreach(source, function(key, value)\n table.insert(destination, value)\n end)\nend\n\nfunction dict_merge(source, destination)\n table.foreach(source, function(key, value)\n destination[key] = value\n end)\nend\n\n-- collect interface addresses\nfunction get_addresses(name)\n addresses = {}\n interface_list = interface_data['interface']\n addresses_list = {}\n for _, interface in pairs(interface_list) do\n if interface['l3_device'] == name then\n proto = interface['proto']\n if proto == 'dhcpv6' then\n proto = 'dhcp'\n end\n for _, address in pairs(interface['ipv4-address']) do\n table.insert(addresses_list, address['address'])\n new_address = new_address_array(address, interface, 'ipv4')\n table.insert(addresses, new_address)\n end\n for _, address in pairs(interface['ipv6-address']) do\n table.insert(addresses_list, address['address'])\n new_address = new_address_array(address, interface, 'ipv6')\n table.insert(addresses, new_address)\n end\n end\n end\n for i = 1, #nixio_data do\n if nixio_data[i].name == name then\n if not is_excluded(name) then\n family = nixio_data[i].family\n addr = nixio_data[i].addr\n if family == 'inet' then\n family = 'ipv4'\n -- Since we don't already know this from the dump, we can\n -- consider this dynamically assigned, this is the case for\n -- example for OpenVPN interfaces, which get their address\n -- from the DHCP server embedded in OpenVPN\n proto = 'dhcp'\n elseif family == 'inet6' then\n family = 'ipv6'\n if starts_with(addr, 'fe80') then\n proto = 'static'\n else\n ula = uci_cursor.get('network', 'globals', 'ula_prefix')\n ula_prefix = split(ula, '::')[1]\n if starts_with(addr, ula_prefix) then\n proto = 'static'\n else\n proto = 'dhcp'\n end\n end\n end\n if family == 'ipv4' or family == 'ipv6' then\n if not has_value(addresses_list, addr) then\n table.insert(addresses, {\n address = addr,\n mask = nixio_data[i].prefix,\n proto = proto,\n family = family\n })\n end\n end\n end\n end\n end\n return addresses\nend\n\n-- collect relevant wireless interface stats\n-- (traffic and connected clients)\nfor radio_name, radio in pairs(wireless_status) do\n for i, interface in ipairs(radio.interfaces) do\n name = interface.ifname\n local is_mesh = false\n if name and not is_excluded(name) then\n iwinfo = ubus:call('iwinfo', 'info', {\n device = name\n })\n netjson_interface = {\n name = name,\n type = 'wireless',\n wireless = {\n ssid = iwinfo.ssid,\n mode = iwinfo_modes[iwinfo.mode] or iwinfo.mode,\n channel = iwinfo.channel,\n frequency = iwinfo.frequency,\n tx_power = iwinfo.txpower,\n signal = iwinfo.signal,\n noise = iwinfo.noise,\n country = iwinfo.country\n }\n }\n if iwinfo.mode == 'Ad-Hoc' or iwinfo.mode == 'Mesh Point' then\n clients = ubus:call('iwinfo', 'assoclist', {\n device = name\n }).results\n is_mesh = true\n else\n clients = ubus:call('hostapd.' .. name, 'get_clients', {}).clients\n end\n if clients and next(clients) ~= nil then\n netjson_interface.wireless.clients = netjson_clients(clients, is_mesh)\n end\n wireless_interfaces[name] = netjson_interface\n end\n end\nend\n\nfunction needs_inversion(interface)\n return interface.type == 'wireless' and interface.wireless.mode == 'access_point'\nend\n\nfunction invert_rx_tx(interface)\n for k, v in pairs(interface) do\n if string.sub(k, 0, 3) == \"rx_\" then\n local tx_key = \"tx_\" .. string.sub(k, 4)\n local tx_val = interface[tx_key]\n interface[tx_key] = v\n interface[k] = tx_val\n end\n end\n return interface\nend\n\n-- collect interface stats\nfor name, interface in pairs(network_status) do\n -- only collect data from iterfaces which have not been excluded\n if not is_excluded(name) then\n netjson_interface = {\n name = name,\n type = string.lower(interface.type),\n up = interface.up,\n mac = interface.macaddr,\n txqueuelen = interface.txqueuelen,\n mtu = interface.mtu,\n speed = interface.speed,\n bridge_members = interface['bridge-members'],\n multicast = interface.multicast\n }\n if wireless_interfaces[name] then\n dict_merge(wireless_interfaces[name], netjson_interface)\n interface.type = netjson_interface.type\n end\n if interface.type == 'Network device' then\n link_supported = interface['link-supported']\n if link_supported and next(link_supported) then\n netjson_interface.type = 'ethernet'\n netjson_interface.link_supported = link_supported\n elseif vpn_interfaces[name] then\n netjson_interface.type = 'virtual'\n else\n netjson_interface.type = 'other'\n end\n end\n if include_stats[name] then\n if needs_inversion(netjson_interface) then\n --- ensure wifi access point interfaces\n --- show download and upload values from\n --- the user's perspective and not from the router perspective\n interface.statistics = invert_rx_tx(interface.statistics)\n end\n netjson_interface.statistics = interface.statistics\n end\n addresses = get_addresses(name)\n if next(addresses) then\n netjson_interface.addresses = addresses\n end\n info = get_interface_info(name, netjson_interface)\n if info.stp ~= nil then\n netjson_interface.stp = info.stp\n end\n table.insert(interfaces, netjson_interface)\n -- DNS info is independent from interface\n if info.dns_servers then\n array_concat(info.dns_servers, dns_servers)\n end\n if info.dns_search then\n array_concat(info.dns_search, dns_search)\n end\n end\nend\n\nif next(interfaces) ~= nil then\n netjson.interfaces = interfaces\nend\nif next(dns_servers) ~= nil then\n netjson.dns_servers = dns_servers\nend\nif next(dns_search) ~= nil then\n netjson.dns_search = dns_search\nend\n\nprint(cjson.encode(netjson))\n"
},
{
"path": "/etc/crontabs/root",
"mode": "0644",
"contents": "*/5 * * * * /usr/sbin/openwisp-monitoring\n"
},
{
"path": "/etc/rc.local",
"mode": "0644",
"contents": "# Put your custom commands here that should be executed once\n# the system init finished. By default this file does nothing.\n\n/usr/sbin/openwisp-monitoring\ntouch /etc/crontabs/root\n/etc/init.d/cron start\n\nexit 0\n"
},
{
"path": "/usr/sbin/update-openwisp-packages",
"mode": "0744",
"contents": "#!/bin/sh\nreboot=false\n\n# updates opkg lists only if necessary\nopkg_update(){\n (\n test -d /tmp/opkg-lists/ && \\\n test -f /tmp/opkg-lists/openwrt_base && \\\n test -f /tmp/opkg-lists/openwrt_packages && \\\n test -f /tmp/opkg-lists/openwrt_core\n ) || opkg update;\n}\n\n# installs libubus-lua if necessary\nlibubus_lua_installed=$(opkg list-installed | grep libubus-lua -c)\nif [ \"$libubus_lua_installed\" == \"0\" ]; then\n opkg_update\n opkg install libubus-lua\nfi\n\n# installs lua-cjson if necessary\nluacjson_installed=$(opkg list-installed | grep lua-cjson -c)\nif [ \"$luacjson_installed\" == \"0\" ]; then\n opkg_update\n opkg install lua-cjson\nfi\n\n# installs rpcd-mod-iwinfo if necessary\nrpcd_mod_iwinfo_installed=$(opkg list-installed | grep rpcd-mod-iwinfo -c)\nif [ \"$rpcd_mod_iwinfo_installed\" == \"0\" ]; then\n opkg_update\n opkg install rpcd-mod-iwinfo\n reboot=true \nfi\n\n# upgrades openwisp-config if necessary\nopenwisp_config_version=$(openwisp_config --version)\nif [ \"$openwisp_config_version\" != \"openwisp-config 0.5.0\" ]; then\n # backup config just in case...\n cp /etc/config/openwisp /etc/config/openwisp-backup\n opkg_update\n opkg install http://downloads.openwisp.io/openwisp-config/2021-01-07-162007/openwisp-config-mbedtls_0.5.0-1_all.ipk\n # restore backup\n mv /etc/config/openwisp-backup /etc/config/openwisp\n # remove default conf\n rm /etc/config/openwisp-opkg\n /etc/init.d/openwisp_config restart\nfi\n\n# reboots if rpcd-mod-iwinfo has been installed\nif [ \"$reboot\" == \"true\" ]; then\n sleep 5\n reboot && exit\nfi\n"
},
{
"path": "/etc/openwisp/post-reload-hook",
"mode": "0744",
"contents": "#!/bin/sh\ntouch /etc/crontabs/root\n/etc/init.d/cron start\n/usr/sbin/update-openwisp-packages\n/usr/sbin/openwisp-monitoring\n"
}
],
"openwisp": [
{
"config_name": "monitoring",
"config_value": "monitoring",
"included_interfaces": "tun0 wlan0 wlan1 br-lan"
}
]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment