Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
33 changes: 27 additions & 6 deletions src/services/hal/backends/radio/providers/openwrt/init.lua
Original file line number Diff line number Diff line change
@@ -1,15 +1,36 @@
local impl = require "services.hal.backends.radio.providers.openwrt.impl"
local file = require "fibers.io.file"

---Check whether OpenWrt UCI is available on this device.
local function file_exists(path)
local f, _ = file.open(path, 'r')
if not f then return false end
f:close()
return true
end

local function path_has_command(name)
local path = os.getenv('PATH') or ''
for dir in path:gmatch('[^:]+') do
if file_exists(dir .. '/' .. name) then
return true
end
end
return false
end

local backend = {}

function backend.new(...)
local impl = require "services.hal.backends.radio.providers.openwrt.impl"
return impl.new(...)
end

---Check whether OpenWrt UCI and iw are available on this device.
---@return boolean
local function is_supported()
local f, _ = file.open('/etc/openwrt_release', 'r')
if f then f:close() return true end
return false
return file_exists('/etc/openwrt_release') and path_has_command('iw')
end

return {
is_supported = is_supported,
backend = impl,
backend = backend,
}
18 changes: 15 additions & 3 deletions src/services/hal/drivers/radio.lua
Original file line number Diff line number Diff line change
Expand Up @@ -461,7 +461,12 @@ function RadioDriver:stats_loop()
local connected = {}
local interfaces_set = {}

backend:start_client_monitor()
local ok, err = backend:start_client_monitor()

if not ok then
self.log:error({ what = 'radio_stats_loop_failed', id = id, err = err })
return
end

fibers.current_scope():finally(function()
if backend and backend.terminate then
Expand All @@ -486,7 +491,7 @@ function RadioDriver:stats_loop()
end

while true do
local name, val = fibers.perform(fibers.named_choice({
local name, val, choice_err = fibers.perform(fibers.named_choice({
client_event = backend:watch_clients_op(),
iface_update = self.iface_update_ch:get_op(),
tick = sleep.sleep_op(report_period),
Expand All @@ -501,7 +506,14 @@ function RadioDriver:stats_loop()
elseif name == 'iface_update' then
update_interfaces(val.op, val.name)
elseif name == 'client_event' then
if val and interfaces_set[val.interface] then
if not val then
self.log:error({
what = 'radio_stats_loop_failed',
id = id,
err = choice_err or 'client monitor stopped',
})
break
elseif interfaces_set[val.interface] then
on_client_event(emit_ch, id, connected, val)
end
elseif name == 'tick' then
Expand Down
9 changes: 6 additions & 3 deletions tests/integration/openwrt_vm/Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
.PHONY: \
preflight fetch verify reset run wait provision provision-force ensure-mwan3 \
stop ssh smoke logs render-default-configs print-default-configs baseline test-baseline test-tc-veth test-lua-uci test-devicecode-uci-manager test-devicecode-uci-manager-async-activation test-devicecode-openwrt-no-blocking-os-io test-devicecode-exec-containment test-openwrt-uart-stty-coreutils test-openwrt-network-provider-apply test-openwrt-network-provider-async-activation test-openwrt-network-provider-fw4-schema test-openwrt-network-provider-snapshot test-openwrt-network-provider-live-snapshot test-openwrt-network-observer-event-ingress test-openwrt-network-provider-vlan-mwan-shaping test-openwrt-network-provider-mwan-live-weights test-openwrt-network-provider-mwan-live-weights-fast test-openwrt-vm-generated-configs-expected test-openwrt-vm-mwan-connected test-openwrt-network-provider-segment-trunk test-openwrt-jan-client-per-host-shaping test-openwrt-shaping-idempotent-apply test-openwrt-segment-shaping-modes test-openwrt-wan-mark-shaping-contract test-openwrt-wan-mark-upload-exemption test-openwrt-wan-mark-download-capability test-openwrt-shaping-fast test-openwrt-shaping-clean test-devicecode-wired-static-provider test-devicecode-full-stack-mock-hal test-devicecode-bigbox-phase1-composition test-devicecode-bigbox-phase1-broken-trunk test-openwrt-jan-client-dhcp-dns test-openwrt-int-bridge-client-dhcp-dns test-openwrt-dnsmasq-multi-instance-resilience \
stop ssh smoke logs render-default-configs print-default-configs baseline test-baseline test-tc-veth test-lua-uci test-devicecode-uci-manager test-devicecode-uci-manager-async-activation test-devicecode-openwrt-no-blocking-os-io test-devicecode-exec-containment test-devicecode-radio-missing-iw-no-hammer test-openwrt-uart-stty-coreutils test-openwrt-network-provider-apply test-openwrt-network-provider-async-activation test-openwrt-network-provider-fw4-schema test-openwrt-network-provider-snapshot test-openwrt-network-provider-live-snapshot test-openwrt-network-observer-event-ingress test-openwrt-network-provider-vlan-mwan-shaping test-openwrt-network-provider-mwan-live-weights test-openwrt-network-provider-mwan-live-weights-fast test-openwrt-vm-generated-configs-expected test-openwrt-vm-mwan-connected test-openwrt-network-provider-segment-trunk test-openwrt-jan-client-per-host-shaping test-openwrt-shaping-idempotent-apply test-openwrt-segment-shaping-modes test-openwrt-wan-mark-shaping-contract test-openwrt-wan-mark-upload-exemption test-openwrt-wan-mark-download-capability test-openwrt-shaping-fast test-openwrt-shaping-clean test-devicecode-wired-static-provider test-devicecode-full-stack-mock-hal test-devicecode-bigbox-phase1-composition test-devicecode-bigbox-phase1-broken-trunk test-openwrt-jan-client-dhcp-dns test-openwrt-int-bridge-client-dhcp-dns test-openwrt-dnsmasq-multi-instance-resilience \
setup-bridge-client-fabric teardown-bridge-client-fabric \
network-lab-fetch network-lab-start network-lab-wait network-lab-provision network-lab-sync network-lab-ssh network-lab-stop network-lab-test test-network-lab \
test openwrt-vm-test clean
Expand Down Expand Up @@ -78,6 +78,10 @@ test-devicecode-uci-manager-async-activation:
test-devicecode-openwrt-no-blocking-os-io:
./tests/test_devicecode_openwrt_no_blocking_os_io.sh

test-devicecode-radio-missing-iw-no-hammer:
./scripts/wait-ssh
./tests/test_devicecode_radio_missing_iw_no_hammer.sh

test-openwrt-uart-stty-coreutils:
./scripts/wait-ssh
./tests/test_openwrt_uart_stty_coreutils.sh
Expand Down Expand Up @@ -269,7 +273,7 @@ test-scp:
grep -q devicecode-openwrt-vm "$$tmp.out"; \
rm -f "$$tmp" "$$tmp.out"

test: test-baseline test-tc-veth test-lua-uci test-devicecode-uci-manager test-devicecode-uci-manager-async-activation test-devicecode-openwrt-no-blocking-os-io test-devicecode-exec-containment test-openwrt-uart-stty-coreutils test-openwrt-network-provider-apply test-openwrt-network-provider-async-activation test-openwrt-network-provider-fw4-schema test-openwrt-network-provider-snapshot test-openwrt-network-provider-live-snapshot test-openwrt-network-observer-event-ingress test-openwrt-network-provider-vlan-mwan-shaping test-openwrt-network-provider-mwan-live-weights test-openwrt-vm-generated-configs-expected test-openwrt-vm-mwan-connected test-openwrt-network-provider-segment-trunk test-openwrt-jan-client-per-host-shaping test-openwrt-shaping-idempotent-apply test-openwrt-segment-shaping-modes test-openwrt-wan-mark-shaping-contract test-openwrt-wan-mark-upload-exemption test-openwrt-wan-mark-download-capability test-devicecode-wired-static-provider test-devicecode-full-stack-mock-hal test-devicecode-bigbox-phase1-composition test-devicecode-bigbox-phase1-broken-trunk test-scp
test: test-baseline test-tc-veth test-lua-uci test-devicecode-uci-manager test-devicecode-uci-manager-async-activation test-devicecode-openwrt-no-blocking-os-io test-devicecode-exec-containment test-devicecode-radio-missing-iw-no-hammer test-openwrt-uart-stty-coreutils test-openwrt-network-provider-apply test-openwrt-network-provider-async-activation test-openwrt-network-provider-fw4-schema test-openwrt-network-provider-snapshot test-openwrt-network-provider-live-snapshot test-openwrt-network-observer-event-ingress test-openwrt-network-provider-vlan-mwan-shaping test-openwrt-network-provider-mwan-live-weights test-openwrt-vm-generated-configs-expected test-openwrt-vm-mwan-connected test-openwrt-network-provider-segment-trunk test-openwrt-jan-client-per-host-shaping test-openwrt-shaping-idempotent-apply test-openwrt-segment-shaping-modes test-openwrt-wan-mark-shaping-contract test-openwrt-wan-mark-upload-exemption test-openwrt-wan-mark-download-capability test-devicecode-wired-static-provider test-devicecode-full-stack-mock-hal test-devicecode-bigbox-phase1-composition test-devicecode-bigbox-phase1-broken-trunk test-scp

openwrt-vm-test: preflight fetch verify stop reset run wait provision test

Expand All @@ -280,4 +284,3 @@ clean:
test-devicecode-exec-containment:
@echo "==> devicecode exec containment"
@$(RUN) tests/test_devicecode_exec_containment.sh

Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/usr/bin/env sh
set -eu

SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
VM_DIR="$(dirname "$SCRIPT_DIR")"
ROOT_DIR="$(CDPATH= cd -- "$VM_DIR/../../.." && pwd)"
SSH="$VM_DIR/scripts/ssh"
SCP_TO="$VM_DIR/scripts/scp-to"
REMOTE="/tmp/devicecode-radio-missing-iw-test"
WORK="$VM_DIR/work/radio-missing-iw-test"

mkdir -p "$WORK"

cat > "$WORK/run_devicecode_radio_missing_iw.lua" <<'LUA'
package.path = table.concat({
'./src/?.lua', './src/?/init.lua',
'./vendor/lua-fibers/src/?.lua', './vendor/lua-fibers/src/?/init.lua',
'./vendor/lua-bus/src/?.lua', './vendor/lua-bus/src/?/init.lua',
'./vendor/lua-trie/src/?.lua', './vendor/lua-trie/src/?/init.lua',
package.path,
}, ';')

local fibers = require 'fibers'
local provider = require 'services.hal.backends.radio.provider'

local function fail(msg) error(msg, 2) end
local function eq(a, b, msg)
if a ~= b then
fail((msg or 'values differ') .. ': expected ' .. tostring(b) .. ', got ' .. tostring(a))
end
end

fibers.run(function()
local backend, err = provider.new('radio0')

eq(backend, nil, 'radio backend should not be selected without iw in PATH')
assert(tostring(err or ''):match('no supported radio backend'), 'unexpected provider error: ' .. tostring(err))
eq(package.loaded['services.hal.backends.radio.providers.openwrt.impl'], nil,
'unsupported radio provider should not load the OpenWrt implementation')
print('devicecode radio missing iw no hammer: ok')
end)
LUA

"$SSH" "rm -rf '$REMOTE'; mkdir -p '$REMOTE'"
"$SSH" "mkdir -p '$REMOTE/src/services/hal/backends/radio/providers/openwrt' '$REMOTE/vendor/lua-fibers'"
"$SCP_TO" "$ROOT_DIR/src/services/hal/backends/radio/provider.lua" "$REMOTE/src/services/hal/backends/radio/provider.lua"
"$SCP_TO" "$ROOT_DIR/src/services/hal/backends/radio/contract.lua" "$REMOTE/src/services/hal/backends/radio/contract.lua"
"$SCP_TO" "$ROOT_DIR/src/services/hal/backends/radio/providers/openwrt/init.lua" "$REMOTE/src/services/hal/backends/radio/providers/openwrt/init.lua"
"$SCP_TO" "$ROOT_DIR/vendor/lua-fibers/src" "$REMOTE/vendor/lua-fibers/src"
"$SCP_TO" "$WORK/run_devicecode_radio_missing_iw.lua" "$REMOTE/run_devicecode_radio_missing_iw.lua"
"$SSH" "mkdir -p /tmp/devicecode-no-iw-path && lua_bin=\"\$(command -v lua)\" && cd '$REMOTE' && PATH=/tmp/devicecode-no-iw-path \"\$lua_bin\" ./run_devicecode_radio_missing_iw.lua"
1 change: 1 addition & 0 deletions tests/run.lua
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ local files = {
"unit.hal.uart_driver_spec",
"unit.hal.uart_manager_spec",
"unit.hal.network_manager_spec",
"unit.hal.radio_driver_spec",
"unit.hal.modem_linux_mm_spec",
"unit.hal.modem_qmi_spec",
"unit.hal.openwrt_network_observer_spec",
Expand Down
113 changes: 113 additions & 0 deletions tests/unit/hal/radio_driver_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
-- tests/unit/hal/radio_driver_spec.lua

local tests = {}

local function eq(a, b, msg)
if a ~= b then
error((msg or 'assertion failed') .. ': expected ' .. tostring(b) .. ', got ' .. tostring(a), 2)
end
end

function tests.test_stats_loop_stops_when_client_monitor_cannot_start()
local channel = require 'fibers.channel'
local runfibers = require 'tests.support.run_fibers'
local radio = require 'services.hal.drivers.radio'

runfibers.run(function ()
local start_calls = 0
local watch_calls = 0
local errors = {}

local backend = {
start_client_monitor = function ()
start_calls = start_calls + 1
return false, 'failed to start iw event: iw not found'
end,

watch_clients_op = function ()
watch_calls = watch_calls + 1
error('watch_clients_op should not be called after monitor startup failure', 2)
end,
}

local driver = setmetatable({
id = 'radio0',
cap_emit_ch = channel.new(4),
iface_update_ch = channel.new(4),
report_period_ch = channel.new(1),
backend = backend,
log = {
error = function (_, row)
errors[#errors + 1] = row
end,
debug = function () end,
},
}, radio.Driver)

driver:stats_loop()

eq(start_calls, 1, 'client monitor should only be started once')
eq(watch_calls, 0, 'stats loop should not watch clients after monitor startup failure')
eq(#errors, 1, 'startup failure should be logged once')
eq(errors[1].what, 'radio_stats_loop_failed', 'failure log event')
eq(errors[1].id, 'radio0', 'failure log id')
end, { timeout = 0.1 })
end

function tests.test_stats_loop_stops_when_client_monitor_stream_closes()
local channel = require 'fibers.channel'
local op = require 'fibers.op'
local runfibers = require 'tests.support.run_fibers'
local radio = require 'services.hal.drivers.radio'

runfibers.run(function ()
local start_calls = 0
local watch_calls = 0
local stop_calls = 0
local errors = {}

local backend = {
start_client_monitor = function ()
start_calls = start_calls + 1
return true, ''
end,

watch_clients_op = function ()
watch_calls = watch_calls + 1
return op.always(nil, 'iw event stream closed')
end,

stop_client_monitor_op = function ()
stop_calls = stop_calls + 1
return op.always(true, '')
end,

terminate = function () return true, nil end,
}

local driver = setmetatable({
id = 'radio0',
cap_emit_ch = channel.new(4),
iface_update_ch = channel.new(4),
report_period_ch = channel.new(1),
backend = backend,
log = {
error = function (_, row)
errors[#errors + 1] = row
end,
debug = function () end,
},
}, radio.Driver)

driver:stats_loop()

eq(start_calls, 1, 'client monitor should only be started once')
eq(watch_calls, 1, 'closed monitor stream should only be watched once')
eq(stop_calls, 1, 'started monitor should be stopped after stream failure')
eq(#errors, 1, 'stream failure should be logged once')
eq(errors[1].what, 'radio_stats_loop_failed', 'failure log event')
eq(errors[1].err, 'iw event stream closed', 'failure log err')
end, { timeout = 0.1 })
end

return tests