diff --git a/package/confd/confd.mk b/package/confd/confd.mk index ccb50d2b..428e85b9 100644 --- a/package/confd/confd.mk +++ b/package/confd/confd.mk @@ -91,6 +91,12 @@ define CONFD_INSTALL_YANG_MODULES_GPS $(BR2_EXTERNAL_INFIX_PATH)/utils/srload $(@D)/yang/gps.inc endef endif +ifeq ($(BR2_PACKAGE_WEBUI),y) +define CONFD_INSTALL_YANG_MODULES_WEBUI + $(COMMON_SYSREPO_ENV) \ + $(BR2_EXTERNAL_INFIX_PATH)/utils/srload $(@D)/yang/web.inc +endef +endif # PER_PACKAGE_DIR # Since the last package in the dependency chain that runs sysrepoctl is confd, we need to @@ -121,6 +127,7 @@ CONFD_POST_INSTALL_TARGET_HOOKS += CONFD_INSTALL_YANG_MODULES CONFD_POST_INSTALL_TARGET_HOOKS += CONFD_INSTALL_YANG_MODULES_CONTAINERS CONFD_POST_INSTALL_TARGET_HOOKS += CONFD_INSTALL_YANG_MODULES_WIFI CONFD_POST_INSTALL_TARGET_HOOKS += CONFD_INSTALL_YANG_MODULES_GPS +CONFD_POST_INSTALL_TARGET_HOOKS += CONFD_INSTALL_YANG_MODULES_WEBUI CONFD_POST_INSTALL_TARGET_HOOKS += CONFD_INSTALL_IN_ROMFS CONFD_TARGET_FINALIZE_HOOKS += CONFD_CLEANUP diff --git a/src/confd/yang/confd.inc b/src/confd/yang/confd.inc index c4b23343..44c29d4f 100644 --- a/src/confd/yang/confd.inc +++ b/src/confd/yang/confd.inc @@ -43,7 +43,7 @@ MODULES=( "infix-firewall-icmp-types@2025-04-26.yang" "infix-meta@2025-12-10.yang" "infix-system@2026-03-09.yang" - "infix-services@2026-03-20.yang" + "infix-services@2026-06-17.yang" "ieee802-ethernet-interface@2025-09-10.yang" "ieee802-ethernet-phy-type@2025-09-10.yang" "infix-ethernet-interface@2026-05-21.yang" diff --git a/src/confd/yang/confd/infix-services.yang b/src/confd/yang/confd/infix-services.yang index 7d64f848..c23651ec 100644 --- a/src/confd/yang/confd/infix-services.yang +++ b/src/confd/yang/confd/infix-services.yang @@ -31,6 +31,11 @@ module infix-services { contact "kernelkit@googlegroups.com"; description "Infix services, generic."; + revision 2026-06-17 { + description "Add web-ui feature, advertised when the web management + interface (webui) is built into the image."; + reference "internal"; + } revision 2026-03-20 { description "Add hostname leaf to mdns container for avahi host-name override. Add neighbors container to mdns for mDNS-SD neighbor table. @@ -71,6 +76,13 @@ module infix-services { reference "internal"; } + feature web-ui { + description "The web management interface (webui) is an optional build-time + feature in Infix. Advertised when it is built; it gates no + data nodes — the /web service tree (including restconf) is + present regardless, so the feature is a pure capability flag."; + } + /* * Data nodes */ diff --git a/src/confd/yang/confd/infix-services@2026-03-20.yang b/src/confd/yang/confd/infix-services@2026-06-17.yang similarity index 100% rename from src/confd/yang/confd/infix-services@2026-03-20.yang rename to src/confd/yang/confd/infix-services@2026-06-17.yang diff --git a/src/confd/yang/web.inc b/src/confd/yang/web.inc new file mode 100644 index 00000000..f9eb6130 --- /dev/null +++ b/src/confd/yang/web.inc @@ -0,0 +1,3 @@ +MODULES=( + "infix-services -e web-ui" +) diff --git a/test/case/services/webui/login_csrf/test.py b/test/case/services/webui/login_csrf/test.py index 27c843b3..910e6749 100755 --- a/test/case/services/webui/login_csrf/test.py +++ b/test/case/services/webui/login_csrf/test.py @@ -102,6 +102,12 @@ with infamy.Test() as test: host = target.location.host password = target.location.password + # The web UI is an optional build-time feature (BR2_PACKAGE_WEBUI); + # minimal builds omit it. infix-services advertises the "web-ui" + # feature only when it's built — skip rather than fail otherwise. + if not target.has_feature("infix-services", "web-ui"): + test.skip() + with test.step("Wait for the web UI to come up"): def webui_ready(): try: diff --git a/test/case/webui/basic/test.py b/test/case/webui/basic/test.py deleted file mode 100644 index c8f02687..00000000 --- a/test/case/webui/basic/test.py +++ /dev/null @@ -1,86 +0,0 @@ -#!/usr/bin/env python3 -""" -WebUI login and logout - -Smoke test that the WebUI is reachable, accepts admin credentials, -serves the dashboard while authenticated, and clears the session on -logout. Pure HTTP — no browser framework — so we only catch backend -and plumbing regressions (nginx down, Go service crashed, TLS or -RESTCONF auth broken, CSRF or session cookie logic broken, wrong -defaults). JS, htmx, and visual issues are deliberately out of -scope. -""" -import re -import time -import infamy -import requests - - -with infamy.Test() as test: - with test.step("Set up topology and wait for WebUI to come up"): - env = infamy.Env() - target = env.attach("target", "mgmt") - password = env.get_password("target") - - # Bracket the host so IPv6 link-local mgmt addresses - # (e.g. fe80::...%eth0) form a valid URL. - base = f"https://[{target.location.host}]" - - sess = requests.Session() - sess.verify = False # device ships a self-signed cert - - # env.attach() returns once NETCONF/RESTCONF is reachable, but - # nginx + the Go webui process may still be coming up. Poll - # /login briefly to ride out the early-boot window where - # default.conf's error_page would otherwise hand us /50x.html. - for _ in range(15): - try: - r = sess.get(f"{base}/login", timeout=10) - if r.status_code == 200 and 'name="csrf"' in r.text: - break - except requests.RequestException: - pass - time.sleep(1) - else: - test.fail("WebUI did not come up within 15s") - m = re.search(r'name="csrf"\s+value="([^"]+)"', r.text) - if not m: - test.fail("CSRF token field not found on /login") - login_csrf = m.group(1) - - with test.step("Authenticate as admin and load the dashboard"): - r = sess.post( - f"{base}/login", - data={"username": "admin", "password": password, "csrf": login_csrf}, - allow_redirects=False, - timeout=10, - ) - if r.status_code != 303: - test.fail(f"POST /login: expected 303, got {r.status_code}") - if "session" not in sess.cookies: - test.fail("session cookie not set after successful login") - - r = sess.get(f"{base}/", timeout=10) - if r.status_code != 200: - test.fail(f"GET /: expected 200, got {r.status_code}") - # base.html ships an htmx-config meta tag on every authenticated page. - if 'name="htmx-config"' not in r.text: - test.fail("htmx-config meta tag missing from /") - m = re.search(r'name="csrf-token"\s+content="([^"]+)"', r.text) - if not m: - test.fail("csrf-token meta tag not found on dashboard") - meta_csrf = m.group(1) - - with test.step("Logout clears the session cookie"): - r = sess.post( - f"{base}/logout", - data={"csrf": meta_csrf}, - allow_redirects=False, - timeout=10, - ) - if r.status_code != 303: - test.fail(f"POST /logout: expected 303, got {r.status_code}") - if "session" in sess.cookies: - test.fail("session cookie still present after logout") - - test.succeed()