Skip to content
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions packages/shared-state-odhcpd_leases/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
include ../../libremesh.mk

define Package/$(PKG_NAME)
SECTION:=lime
CATEGORY:=LibreMesh
TITLE:=odhcpd leases module for shared-state
DEPENDS:=+lua +luci-lib-jsonc +shared-state-async +odhcpd
PKGARCH:=all
endef

define Package/$(PKG_NAME)/description
Synchronize external DHCP leases between LibreMesh nodes by
watching odhcpd’s lease file, publishing updates over shared-state-async
and injecting remote leases locally.
endef

$(eval $(call BuildPackage,$(PKG_NAME)))
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/bin/sh
CRDT='odhcpd-leases'
LEASEFILE='/tmp/ethers.mesh'
TRIGGERFILE='usr/share/shared-state/publishers/shared-state-publish_odhcpd_leases'
mSc='odhcpd_leases'

uci -q set shared-state.$mSc=dataType
uci -q set shared-state.$mSc.name="$CRDT"
uci -q set shared-state.$mSc.scope='community'
uci -q set shared-state.$mSc.update_interval='120'
uci -q set shared-state.$mSc.ttl='1200'
uci commit shared-state

uci -q set dhcp.odhcpd.leasetrigger="$TRIGGERFILE"
uci -q set dhcp.odhcpd.maindhcp='1'
uci commit dhcp

[ -e /etc/ethers ] || ln -s "$LEASEFILE" /etc/ethers

/etc/init.d/odhcpd reload
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/usr/bin/lua

local JSON = require('luci.jsonc')

local OUTPUT_FILE = '/tmp/ethers.mesh'
local TMP_FILE = OUTPUT_FILE .. '.new'

local leases = JSON.parse(io.stdin:read('*a')) or {}

local hostname_file = io.open('/proc/sys/kernel/hostname')
local node_hostname = hostname_file:read('*l')
hostname_file:close()

local out_handle = io.open(TMP_FILE, 'w')

for ip, data in pairs(leases) do

if data and data.mData and data.mData.mac and data.mAuthor ~= node_hostname then
out_handle:write(string.format('%s %s\n', data.mData.mac, ip)) -- Format: MAC IP
end
end
out_handle:close()
os.execute('mv "' .. TMP_FILE .. '" "' .. OUTPUT_FILE .. '"')

os.execute('/etc/init.d/odhcpd reload')
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#!/usr/bin/lua

local JSON = require("luci.jsonc")
local CRDT = "odhcpd-leases"


local handle = io.popen("ubus call dhcp ipv4leases '{}' 2>/dev/null")
local ubus_output = handle:read("*a")
handle:close()


local ubus_data = JSON.parse(ubus_output or "{}")

local output_table = {}


if ubus_data and ubus_data.device then

for device_name, device_data in pairs(ubus_data.device) do
if device_data and device_data.leases then

for _, lease in ipairs(device_data.leases) do

if lease.address and lease.mac then

output_table[lease.address] = {
hostname = lease.hostname or "",
mac = lease.mac
}
end
end
end
end
end


local final_json_string = JSON.stringify(output_table)


local pipe = io.popen("shared-state-async insert " .. CRDT, "w")
if pipe then
pipe:write(final_json_string)
pipe:close()
end


os.execute("/usr/sbin/odhcpd-update >/dev/null 2>&1")
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
local testUtils = require "tests.utils"
local stub = require "luassert.stub"

local publisher_file =
"packages/shared-state-odhcpd_leases/files/usr/share/shared-state/publishers/" ..
"shared-state-publish_odhcpd_leases"
local run_publisher = testUtils.load_lua_file_as_function(publisher_file)

local captured_json
local ubus_reply

local popen_stub, execute_stub

local function stub_system_calls()
popen_stub = stub(io, "popen", function(cmd, _)
if cmd:match("^ubus call dhcp ipv4leases") then
return { read = function() return "" end, close = function() end }

elseif cmd:match("^shared%-state%-async insert") then
return {
write = function(_, s) captured_json = s end,
close = function() end
}

else
return { read = function() return "" end, close = function() end }
end
end)

execute_stub = stub(os, "execute", function() return true end)
end

local function revert_system_stubs()
if popen_stub then popen_stub:revert() end
if execute_stub then execute_stub:revert() end
end

describe("shared-state-odhcpd_leases publisher #odhcpd-leases", function()

before_each(function()
captured_json = nil
ubus_reply = nil

package.loaded["luci.jsonc"] = nil
package.preload["luci.jsonc"] = function()
return {
parse = function() return ubus_reply end,
stringify = function(tbl)
if next(tbl) == nil then return "[]" end
local parts = {}
for ip, info in pairs(tbl) do
parts[#parts + 1] = string.format(
'"%s":{"mac":"%s","hostname":"%s"}',
ip, info.mac or "", info.hostname or "")
end
return "{" .. table.concat(parts, ",") .. "}"
end
}
end

stub_system_calls()
end)

after_each(function()
revert_system_stubs()
package.preload["luci.jsonc"] = nil
end)

it("#happy_path publishes every lease", function()
ubus_reply = {
device = {
eth0 = {
leases = {
{ address = "10.0.0.5", mac = "aa:bb", hostname = "h1" },
{ address = "10.0.0.6", mac = "cc:dd", hostname = "h2" }
}
}
}
}

run_publisher()

assert.is_string(captured_json, "Expected JSON")
assert.matches('"10%.0%.0%.5"%s*:%s*{[^}]-"mac"%s*:%s*"aa:bb"', captured_json)
assert.matches('"10%.0%.0%.6"%s*:%s*{[^}]-"mac"%s*:%s*"cc:dd"', captured_json)
assert.matches('"hostname"%s*:%s*"h1"', captured_json)
assert.matches('"hostname"%s*:%s*"h2"', captured_json)
end)

it("#empty with zero leases publishes '[]'", function()
ubus_reply = {}
run_publisher()
assert.equals("[]", captured_json)
end)

it("#malformed when parse returns nil publishes '[]'", function()
ubus_reply = nil
run_publisher()
assert.equals("[]", captured_json)
end)
end)