lager-cli 0.34.2__tar.gz → 0.34.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {lager_cli-0.34.2/lager_cli.egg-info → lager_cli-0.34.4}/PKG-INFO +1 -1
- {lager_cli-0.34.2 → lager_cli-0.34.4}/__init__.py +1 -1
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/box/diagnose.py +136 -1
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/box/nets.py +60 -5
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/communication/usb.py +22 -8
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/power/battery.py +4 -4
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/power/supply.py +4 -4
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/utility/update.py +58 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/core/net_helpers.py +22 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4/lager_cli.egg-info}/PKG-INFO +1 -1
- {lager_cli-0.34.2 → lager_cli-0.34.4}/LICENSE +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/MANIFEST.in +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/README.md +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/__main__.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/address_utils.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/battery/__init__.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/battery/battery_tui.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/battery/websocket_client.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/box_storage.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/__init__.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/box/__init__.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/box/_host_ops.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/box/_mount_prep.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/box/_pip_validation.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/box/_shim_verbs.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/box/_ssh.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/box/box_group.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/box/boxes.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/box/config.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/box/dut.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/box/hello.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/box/instruments.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/box/labjack_pins.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/box/lock.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/box/net_tui.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/box/ssh.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/box/ssh_setup.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/communication/__init__.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/communication/ble.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/communication/blufi.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/communication/i2c.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/communication/router.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/communication/spi.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/communication/uart.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/communication/websocket_client.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/communication/wifi.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/development/__init__.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/development/arm.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/development/debug/__init__.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/development/debug/commands.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/development/debug/gdb.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/development/debug/net_cache.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/development/debug/service_client.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/development/debug/service_helper.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/development/debug/tunnel.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/development/devenv.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/development/python.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/measurement/__init__.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/measurement/adc.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/measurement/dac.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/measurement/energy.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/measurement/gpi.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/measurement/gpo.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/measurement/logic.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/measurement/scope.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/measurement/thermocouple.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/measurement/watt.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/power/__init__.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/power/battery_model_csv.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/power/eload.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/power/solar.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/utility/__init__.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/utility/_host_cli.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/utility/binaries.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/utility/defaults.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/utility/exec_.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/utility/install.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/utility/install_wheel.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/utility/login.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/utility/logs.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/utility/uninstall.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/commands/utility/webcam.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/config.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/context/__init__.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/context/ci_detection.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/context/constants.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/context/core.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/context/error_handlers.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/context/session.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/core/__init__.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/core/group_usage.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/core/matchers.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/core/net_group.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/core/net_storage.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/core/param_types.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/core/ssh_utils.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/core/utils.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/core/version_skew.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/core/ws_diagnose.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/deployment/__init__.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/deployment/scripts/__init__.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/deployment/scripts/configure_docker_dns.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/deployment/scripts/configure_docker_dns.sh +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/deployment/scripts/convert_to_sparse_checkout.sh +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/deployment/scripts/setup_and_deploy_box.sh +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/deployment/scripts/setup_ssh_key.sh +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/deployment/security/__init__.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/deployment/security/secure_box_firewall.sh +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/elftools/__init__.py +0 -0
- {lager_cli-0.34.2/impl/power → lager_cli-0.34.4/elftools/common}/__init__.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/elftools/common/construct_utils.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/elftools/common/exceptions.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/elftools/common/py3compat.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/elftools/common/utils.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/elftools/construct/__init__.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/elftools/construct/adapters.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/elftools/construct/core.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/elftools/construct/debug.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/elftools/construct/macros.py +0 -0
- {lager_cli-0.34.2/impl → lager_cli-0.34.4/elftools/dwarf}/__init__.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/elftools/dwarf/abbrevtable.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/elftools/dwarf/aranges.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/elftools/dwarf/callframe.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/elftools/dwarf/compileunit.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/elftools/dwarf/constants.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/elftools/dwarf/descriptions.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/elftools/dwarf/die.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/elftools/dwarf/dwarf_expr.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/elftools/dwarf/dwarfinfo.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/elftools/dwarf/enums.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/elftools/dwarf/lineprogram.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/elftools/dwarf/locationlists.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/elftools/dwarf/namelut.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/elftools/dwarf/ranges.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/elftools/dwarf/structs.py +0 -0
- {lager_cli-0.34.2/elftools/elf → lager_cli-0.34.4/elftools/ehabi}/__init__.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/elftools/ehabi/constants.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/elftools/ehabi/decoder.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/elftools/ehabi/ehabiinfo.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/elftools/ehabi/structs.py +0 -0
- {lager_cli-0.34.2/elftools/ehabi → lager_cli-0.34.4/elftools/elf}/__init__.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/elftools/elf/constants.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/elftools/elf/descriptions.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/elftools/elf/dynamic.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/elftools/elf/elffile.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/elftools/elf/enums.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/elftools/elf/gnuversions.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/elftools/elf/hash.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/elftools/elf/notes.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/elftools/elf/relocation.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/elftools/elf/sections.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/elftools/elf/segments.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/elftools/elf/structs.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/errors.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/exceptions.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/gateway_auth.py +0 -0
- {lager_cli-0.34.2/elftools/dwarf → lager_cli-0.34.4/impl}/__init__.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/impl/box_config.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/impl/measurement/__init__.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/impl/measurement/scope.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/impl/measurement/scope_stream.py +0 -0
- {lager_cli-0.34.2/elftools/common → lager_cli-0.34.4/impl/power}/__init__.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/impl/power/enable_disable.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/lager_cli.egg-info/SOURCES.txt +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/lager_cli.egg-info/dependency_links.txt +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/lager_cli.egg-info/entry_points.txt +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/lager_cli.egg-info/requires.txt +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/lager_cli.egg-info/top_level.txt +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/main.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/pyproject.toml +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/safe_unpickle.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/setup.cfg +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/setup.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/simple_hdlc.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/sort_utils.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/status.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/supply/__init__.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/supply/supply_tui.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/supply/websocket_client.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/terminal/__init__.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/terminal/core/__init__.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/terminal/core/executor.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/terminal/main.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/terminal/ui/__init__.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/terminal/ui/completer.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/terminal/ui/display.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/terminal/ui/logo.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/terminal/ui/repl.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/terminal/ui/themes.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/tests/test_box_lager_imports.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/tests/test_box_storage.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/tests/test_gateway_auth.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/tests/test_gateway_callsites.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/tests/test_host_cli.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/tests/test_io_imports.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/tests/test_update_gate.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/update_check.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/vendor/PyCRC/CRC16.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/vendor/PyCRC/CRC16DNP.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/vendor/PyCRC/CRC16Kermit.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/vendor/PyCRC/CRC16SICK.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/vendor/PyCRC/CRC32.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/vendor/PyCRC/CRCCCITT.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/vendor/PyCRC/__init__.py +0 -0
- {lager_cli-0.34.2 → lager_cli-0.34.4}/vendor/__init__.py +0 -0
|
@@ -120,7 +120,12 @@ def _call(url: str, timeout: float = 8.0) -> dict:
|
|
|
120
120
|
r = requests.get(url, timeout=timeout, headers=auth_headers_for_box(box_host))
|
|
121
121
|
r = _check_gateway(r, box_host)
|
|
122
122
|
if r.status_code == 404:
|
|
123
|
-
|
|
123
|
+
# Deliberately does not name a version: this is shared by every
|
|
124
|
+
# diagnose endpoint, and they were not all added in the same
|
|
125
|
+
# release. Naming 0.20 sent a reader looking for the wrong thing
|
|
126
|
+
# when a newer endpoint was the one missing.
|
|
127
|
+
return {'unavailable': 'endpoint not served by this box; '
|
|
128
|
+
'update the box to use this section'}
|
|
124
129
|
if r.status_code >= 400:
|
|
125
130
|
return {'transport_error': f'HTTP {r.status_code}: {r.text[:200]}'}
|
|
126
131
|
return r.json()
|
|
@@ -398,6 +403,128 @@ def _classify_jlink(usb_info: dict, jlink_info: dict) -> tuple[str, str]:
|
|
|
398
403
|
return ('yellow', 'UNCLEAR — review the J-Link section above and rerun if needed.')
|
|
399
404
|
|
|
400
405
|
|
|
406
|
+
def _fmt_usbhub_lines(d: dict):
|
|
407
|
+
"""Render the USB-hub section."""
|
|
408
|
+
devnum = d.get('devnum')
|
|
409
|
+
lo, hi = d.get('devnum_min'), d.get('devnum_max')
|
|
410
|
+
med = d.get('devnum_median')
|
|
411
|
+
churn = f'{devnum}' if devnum is not None else '—'
|
|
412
|
+
if (devnum is not None and lo is not None and hi is not None
|
|
413
|
+
and med is not None):
|
|
414
|
+
# The interpretation, inline, because the raw number means nothing to
|
|
415
|
+
# someone who does not already know how devnums are assigned.
|
|
416
|
+
churn += f' (bench: {lo}–{hi}, median {med})'
|
|
417
|
+
if devnum > med * 1.3 and devnum >= hi:
|
|
418
|
+
churn += ' <- far above its peers: this device has re-enumerated'
|
|
419
|
+
|
|
420
|
+
visible = d.get('serial_visible_to_sdk')
|
|
421
|
+
visible_s = {True: 'yes', False: 'NO', None: 'unknown'}.get(visible, 'unknown')
|
|
422
|
+
|
|
423
|
+
lines = [
|
|
424
|
+
f'enumerated: {d.get("enumerated")}',
|
|
425
|
+
f'sysfs: {d.get("sysfs_path") or "—"}',
|
|
426
|
+
f'devnum: {churn}',
|
|
427
|
+
f'seen by SDK: {visible_s}',
|
|
428
|
+
]
|
|
429
|
+
serials = d.get('sdk_scan_serials')
|
|
430
|
+
if serials is not None:
|
|
431
|
+
lines.append(f' SDK scan: {", ".join(serials) if serials else "(nothing)"}')
|
|
432
|
+
if d.get('sdk_scan_error'):
|
|
433
|
+
lines.append(f' scan error: {d["sdk_scan_error"]}')
|
|
434
|
+
|
|
435
|
+
for dev in (d.get('vendor_devices') or []):
|
|
436
|
+
lines.append(
|
|
437
|
+
f' bus: {dev.get("sysfs_name")} {dev.get("vid")}:{dev.get("pid")}'
|
|
438
|
+
f' serial={dev.get("serial") or "(none)"}'
|
|
439
|
+
f' devnum={dev.get("devnum")} {dev.get("product") or ""}'.rstrip()
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
if d.get('probe_skipped'):
|
|
443
|
+
lines.append(f'hub open: skipped — {d.get("probe_skip_reason")}')
|
|
444
|
+
elif d.get('hub_opens') is True:
|
|
445
|
+
lines.append('hub open: OK')
|
|
446
|
+
elif d.get('hub_opens') is False:
|
|
447
|
+
lines.append(f'hub open: FAILED — {d.get("hub_open_error")}')
|
|
448
|
+
if d.get('hub_open_detail'):
|
|
449
|
+
lines.append(f' detail: {d["hub_open_detail"]}')
|
|
450
|
+
for h in (d.get('holders') or []):
|
|
451
|
+
lines.append(f' held by: pid {h.get("pid")} {h.get("command") or ""}'.rstrip())
|
|
452
|
+
return lines
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def _classify_usb_hub(d: dict):
|
|
456
|
+
"""(colour, headline) for a USB hub net. Most specific first."""
|
|
457
|
+
if d.get('unavailable') or d.get('transport_error'):
|
|
458
|
+
return ('yellow',
|
|
459
|
+
'UNCLEAR — this box does not serve /diagnose/usbhub. '
|
|
460
|
+
'Update the box to diagnose hub nets.')
|
|
461
|
+
|
|
462
|
+
if d.get('enumerated') is False:
|
|
463
|
+
return ('red',
|
|
464
|
+
'NOT ENUMERATED — the kernel does not see this hub at all. '
|
|
465
|
+
'Check its upstream cable and whether its port has power.')
|
|
466
|
+
|
|
467
|
+
if d.get('probe_skipped'):
|
|
468
|
+
return ('yellow',
|
|
469
|
+
f'BUSY — {d.get("probe_skip_reason")}. Something else is '
|
|
470
|
+
f'driving this hub; rerun when it is idle.')
|
|
471
|
+
|
|
472
|
+
# The distinction this endpoint exists for: enumerated, and the vendor SDK
|
|
473
|
+
# still cannot see it. sysfs and lsof both report a healthy device here.
|
|
474
|
+
if d.get('serial_visible_to_sdk') is False:
|
|
475
|
+
extra = ''
|
|
476
|
+
devnum, med = d.get('devnum'), d.get('devnum_median')
|
|
477
|
+
if devnum is not None and med is not None and devnum > med * 1.3:
|
|
478
|
+
extra = (f' Its devnum ({devnum}) is well above the rest of the '
|
|
479
|
+
f'bench (median {med}), so it has been re-enumerating.')
|
|
480
|
+
return ('red',
|
|
481
|
+
'HUB WEDGED (electrical) — the kernel has it enumerated but it '
|
|
482
|
+
'does not answer its vendor SDK. That is the hub or its cabling, '
|
|
483
|
+
'not the box software: restarting box services will not help.'
|
|
484
|
+
+ extra)
|
|
485
|
+
|
|
486
|
+
if d.get('hub_opens') is False:
|
|
487
|
+
holders = d.get('holders') or []
|
|
488
|
+
if holders:
|
|
489
|
+
who = ', '.join(f'pid {h.get("pid")}' for h in holders)
|
|
490
|
+
return ('yellow',
|
|
491
|
+
f'HUB CLAIMED — the SDK can see it but the open failed and '
|
|
492
|
+
f'it is held by {who}.')
|
|
493
|
+
return ('red', f'HUB WILL NOT OPEN — {d.get("hub_open_error")}')
|
|
494
|
+
|
|
495
|
+
if d.get('hub_opens') is True:
|
|
496
|
+
return ('green',
|
|
497
|
+
'REACHABLE — the hub enumerated, the SDK sees it, and it opened '
|
|
498
|
+
'and closed cleanly. This tests the path to the hub, not the '
|
|
499
|
+
'DUT behind any given port.')
|
|
500
|
+
|
|
501
|
+
return ('yellow', 'UNCLEAR — review the section above and rerun if needed.')
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def _diagnose_usb_hub(box_ip: str, address: str) -> None:
|
|
505
|
+
"""USB-hub branch of `lager diagnose`.
|
|
506
|
+
|
|
507
|
+
Fetches the instrument-agnostic host-side USB section plus
|
|
508
|
+
`/diagnose/usbhub` in parallel, renders them, and classifies.
|
|
509
|
+
"""
|
|
510
|
+
urls = {
|
|
511
|
+
'usb': f'http://{box_ip}:9000/diagnose/usb?address={address}',
|
|
512
|
+
'hub': f'http://{box_ip}:9000/diagnose/usbhub?address={address}',
|
|
513
|
+
}
|
|
514
|
+
results: dict[str, dict] = {}
|
|
515
|
+
with ThreadPoolExecutor(max_workers=2) as pool:
|
|
516
|
+
futures = {pool.submit(_call, url): name for name, url in urls.items()}
|
|
517
|
+
for fut in as_completed(futures):
|
|
518
|
+
results[futures[fut]] = fut.result()
|
|
519
|
+
|
|
520
|
+
_print_section('USB (host-side)', results['usb'], _fmt_usb_lines)
|
|
521
|
+
_print_section('USB hub', results['hub'], _fmt_usbhub_lines)
|
|
522
|
+
|
|
523
|
+
color, headline = _classify_usb_hub(results['hub'])
|
|
524
|
+
click.echo()
|
|
525
|
+
click.echo(click.style(f'Classification: {headline}', fg=color, bold=True))
|
|
526
|
+
|
|
527
|
+
|
|
401
528
|
def _diagnose_debug(box_ip: str, net: str, address: str) -> None:
|
|
402
529
|
"""Debug-net (J-Link) branch of `lager diagnose`.
|
|
403
530
|
|
|
@@ -471,6 +598,14 @@ def diagnose(ctx, net, box, net_type):
|
|
|
471
598
|
_diagnose_debug(resolved_box, net, address)
|
|
472
599
|
return
|
|
473
600
|
|
|
601
|
+
# USB hub nets are not USB-TMC either, and the pyvisa probe below has
|
|
602
|
+
# nothing to say about them. They used to fall through to the generic
|
|
603
|
+
# "NOT USB-TMC, check the role command yourself" bucket, which is exactly
|
|
604
|
+
# no help when the question is why a hub will not answer.
|
|
605
|
+
if role == 'usb':
|
|
606
|
+
_diagnose_usb_hub(resolved_box, address)
|
|
607
|
+
return
|
|
608
|
+
|
|
474
609
|
# Fire the three endpoints in parallel.
|
|
475
610
|
# Port mapping inside the box container:
|
|
476
611
|
# 8080 — hardware_service.py (/invoke, /diagnose/dispatcher)
|
|
@@ -739,6 +739,7 @@ def _display_table(
|
|
|
739
739
|
records,
|
|
740
740
|
state_map: dict[str, str | None] | None = None,
|
|
741
741
|
reason_map: dict[str, str] | None = None,
|
|
742
|
+
code_map: dict[str, str] | None = None,
|
|
742
743
|
):
|
|
743
744
|
"""Render the grouped net table.
|
|
744
745
|
|
|
@@ -850,7 +851,7 @@ def _display_table(
|
|
|
850
851
|
click.secho(f"{row[0]:<{col_w[0]}}", bold=True, nl=False)
|
|
851
852
|
click.echo(" " + " ".join(f"{row[i]:<{col_w[i]}}" for i in range(1, len(col_w))))
|
|
852
853
|
|
|
853
|
-
_print_state_reasons(records, reason_map)
|
|
854
|
+
_print_state_reasons(records, reason_map, code_map)
|
|
854
855
|
|
|
855
856
|
|
|
856
857
|
# Roles with no live probe report no state by design (uart, spi, i2c, ...).
|
|
@@ -858,12 +859,48 @@ def _display_table(
|
|
|
858
859
|
# on every bench and stops being read.
|
|
859
860
|
_EXPECTED_NO_STATE_REASON = "no probe for role"
|
|
860
861
|
|
|
862
|
+
# What a classification means for the person reading the footnote, and how
|
|
863
|
+
# loudly to say it. Only "go and look at the hardware" is red; the others are
|
|
864
|
+
# usually a bench in a normal state or a net pointing at the wrong device.
|
|
865
|
+
#
|
|
866
|
+
# Looked up with .get() and ALWAYS optional -- a newer box can send a code this
|
|
867
|
+
# CLI has never heard of, and the box's own `reason` already says everything
|
|
868
|
+
# needed. A KeyError here would crash the exact command someone runs when
|
|
869
|
+
# something is already broken.
|
|
870
|
+
_REASON_CODE_NOTES = {
|
|
871
|
+
"hub-unreachable": (
|
|
872
|
+
"the hub is on the USB bus but will not answer. That is hardware:\n"
|
|
873
|
+
" check its power and upstream cable. Restarting box services\n"
|
|
874
|
+
" will not help.",
|
|
875
|
+
"red",
|
|
876
|
+
),
|
|
877
|
+
"hub-absent": (
|
|
878
|
+
"the hub is not on the USB bus at all -- unplugged, or its\n"
|
|
879
|
+
" upstream port is off.",
|
|
880
|
+
"yellow",
|
|
881
|
+
),
|
|
882
|
+
"hub-serial-mismatch": (
|
|
883
|
+
"hubs are on the bus but none has the serial this net names.\n"
|
|
884
|
+
" Check the net's address.",
|
|
885
|
+
"yellow",
|
|
886
|
+
),
|
|
887
|
+
}
|
|
888
|
+
|
|
861
889
|
|
|
862
|
-
def _print_state_reasons(
|
|
890
|
+
def _print_state_reasons(
|
|
891
|
+
records,
|
|
892
|
+
reason_map: dict[str, str] | None,
|
|
893
|
+
code_map: dict[str, str] | None = None,
|
|
894
|
+
) -> None:
|
|
863
895
|
"""Explain the ``–`` cells, grouped by reason.
|
|
864
896
|
|
|
865
897
|
Only unexpected ones: a role with no probe is normal. Silent when the box
|
|
866
898
|
sent no reasons at all, which is what an older box does.
|
|
899
|
+
|
|
900
|
+
``code_map`` only ever adds a remedy line under a group. The box's
|
|
901
|
+
``reason`` is always printed and always self-sufficient, so an older box
|
|
902
|
+
that sends no codes, or a newer one sending a code this CLI does not know,
|
|
903
|
+
both render exactly as they did before.
|
|
867
904
|
"""
|
|
868
905
|
if not reason_map:
|
|
869
906
|
return
|
|
@@ -879,11 +916,19 @@ def _print_state_reasons(records, reason_map: dict[str, str] | None) -> None:
|
|
|
879
916
|
if not by_reason:
|
|
880
917
|
return
|
|
881
918
|
|
|
919
|
+
code_map = code_map or {}
|
|
882
920
|
click.echo()
|
|
883
921
|
click.secho("Nets reporting no state:", bold=True, err=True)
|
|
884
922
|
for reason in sorted(by_reason):
|
|
885
|
-
names =
|
|
886
|
-
click.echo(f" {names}: {reason}", err=True)
|
|
923
|
+
names = by_reason[reason]
|
|
924
|
+
click.echo(f" {', '.join(names)}: {reason}", err=True)
|
|
925
|
+
# One remedy per group, not per net: four dead nets on one hub are one
|
|
926
|
+
# fault, and printing the same sentence four times buries it.
|
|
927
|
+
codes = {code_map.get(n) for n in names}
|
|
928
|
+
note = _REASON_CODE_NOTES.get(codes.pop()) if len(codes) == 1 else None
|
|
929
|
+
if note:
|
|
930
|
+
text, colour = note
|
|
931
|
+
click.secho(f" -> {text}", fg=colour, err=True)
|
|
887
932
|
if any(r == "deadline" for r in by_reason):
|
|
888
933
|
click.secho(
|
|
889
934
|
" Note: 'deadline' is the box's budget for probing every\n"
|
|
@@ -2251,6 +2296,10 @@ def state_cmd(ctx: click.Context, box: str | None, as_json: bool) -> None:
|
|
|
2251
2296
|
# Why a net has no state. Absent on boxes older than this field; the
|
|
2252
2297
|
# display and the JSON both have to read as they did before in that case.
|
|
2253
2298
|
reason_map: dict[str, str] = {}
|
|
2299
|
+
# The machine-readable half of the same answer, where the box sent one.
|
|
2300
|
+
# Never required: an older box sends no codes and everything below falls
|
|
2301
|
+
# back to printing `reason`, which is what it did before this existed.
|
|
2302
|
+
code_map: dict[str, str] = {}
|
|
2254
2303
|
state_list = _fetch_net_state(resolved_box)
|
|
2255
2304
|
if state_list is None:
|
|
2256
2305
|
click.secho(
|
|
@@ -2265,6 +2314,9 @@ def state_cmd(ctx: click.Context, box: str | None, as_json: bool) -> None:
|
|
|
2265
2314
|
reason = entry.get("reason")
|
|
2266
2315
|
if reason:
|
|
2267
2316
|
reason_map[name] = reason
|
|
2317
|
+
code = entry.get("reason_code")
|
|
2318
|
+
if code:
|
|
2319
|
+
code_map[name] = code
|
|
2268
2320
|
|
|
2269
2321
|
if as_json:
|
|
2270
2322
|
merged = []
|
|
@@ -2273,11 +2325,14 @@ def state_cmd(ctx: click.Context, box: str | None, as_json: bool) -> None:
|
|
|
2273
2325
|
item = {**rec, "live_state": state_map.get(name)}
|
|
2274
2326
|
if name in reason_map:
|
|
2275
2327
|
item["live_state_reason"] = reason_map[name]
|
|
2328
|
+
if name in code_map:
|
|
2329
|
+
item["live_state_reason_code"] = code_map[name]
|
|
2276
2330
|
merged.append(item)
|
|
2277
2331
|
click.echo(json.dumps(merged, indent=2))
|
|
2278
2332
|
return
|
|
2279
2333
|
|
|
2280
|
-
_display_table(records, state_map=state_map, reason_map=reason_map
|
|
2334
|
+
_display_table(records, state_map=state_map, reason_map=reason_map,
|
|
2335
|
+
code_map=code_map)
|
|
2281
2336
|
|
|
2282
2337
|
|
|
2283
2338
|
@nets.command("show", help="Show full details of a saved net, including metadata")
|
|
@@ -25,6 +25,7 @@ from ...core.net_helpers import (
|
|
|
25
25
|
list_nets_by_role,
|
|
26
26
|
display_nets_table,
|
|
27
27
|
validate_net_exists,
|
|
28
|
+
box_command_error,
|
|
28
29
|
echo_box_request_failure,
|
|
29
30
|
NET_HTTP_PORT,
|
|
30
31
|
)
|
|
@@ -49,6 +50,18 @@ def _display_usb_nets(ctx: click.Context, box: str) -> None:
|
|
|
49
50
|
display_nets_table(nets, empty_message="No USB nets found on this box.")
|
|
50
51
|
|
|
51
52
|
|
|
53
|
+
# Must exceed the box's own worst case for /usb/command, or the box's
|
|
54
|
+
# structured errors are undeliverable by construction: the client gives up
|
|
55
|
+
# before the answer it is waiting for exists. The handler's bounds are
|
|
56
|
+
# additive — up to `_USB_LOCK_TIMEOUT_S` (10s) queueing on the process-wide
|
|
57
|
+
# hub lock, then up to `HUB_OP_TIMEOUT_S` (30s) in the driver deadline — so a
|
|
58
|
+
# wedged hub answers 504 at ~40s. A 30s client timeout turned that into a
|
|
59
|
+
# transport error, which reads as "the box is unreachable" and hides both the
|
|
60
|
+
# real diagnosis and the fact that the box already scheduled its own recovery.
|
|
61
|
+
# Raise this if either box-side bound grows.
|
|
62
|
+
_USB_COMMAND_TIMEOUT_S = 45
|
|
63
|
+
|
|
64
|
+
|
|
52
65
|
def _invoke_remote(
|
|
53
66
|
ctx: click.Context,
|
|
54
67
|
net_name: str,
|
|
@@ -68,18 +81,16 @@ def _invoke_remote(
|
|
|
68
81
|
from ...box_storage import _check_gateway
|
|
69
82
|
|
|
70
83
|
url = f"http://{target_box}:{NET_HTTP_PORT}/usb/command"
|
|
71
|
-
# First contact with an Acroname hub runs BrainStem USB discovery on the
|
|
72
|
-
# box, which can take well over 10s cold; budget for it.
|
|
73
84
|
try:
|
|
74
85
|
resp = requests.post(
|
|
75
86
|
url,
|
|
76
87
|
json={"netname": net_name, "action": command},
|
|
77
|
-
timeout=
|
|
88
|
+
timeout=_USB_COMMAND_TIMEOUT_S,
|
|
78
89
|
headers=auth_headers_for_box(target_box),
|
|
79
90
|
)
|
|
80
91
|
resp = _check_gateway(resp, target_box)
|
|
81
92
|
except (requests.ConnectionError, requests.Timeout) as e:
|
|
82
|
-
echo_box_request_failure(target_box, e, timeout=
|
|
93
|
+
echo_box_request_failure(target_box, e, timeout=_USB_COMMAND_TIMEOUT_S)
|
|
83
94
|
ctx.exit(1)
|
|
84
95
|
except requests.RequestException as e:
|
|
85
96
|
click.secho(f"Error: USB request to box failed: {e}", fg='red', err=True)
|
|
@@ -99,11 +110,14 @@ def _invoke_remote(
|
|
|
99
110
|
click.echo(f"[OK] {message}")
|
|
100
111
|
return
|
|
101
112
|
|
|
102
|
-
error =
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
f"update the box.")
|
|
113
|
+
error = box_command_error(
|
|
114
|
+
result, resp.status_code, '/usb/command',
|
|
115
|
+
f'USB command failed (HTTP {resp.status_code})')
|
|
106
116
|
click.secho(f"Error: {error}", fg='red', err=True)
|
|
117
|
+
|
|
118
|
+
detail = result.get('reason_detail')
|
|
119
|
+
if detail:
|
|
120
|
+
click.secho(f" {detail}", fg='yellow', err=True)
|
|
107
121
|
ctx.exit(1)
|
|
108
122
|
|
|
109
123
|
|
|
@@ -21,6 +21,7 @@ import click
|
|
|
21
21
|
from ...core.net_group import NetGroup
|
|
22
22
|
# Import consolidated helpers from cli.core.net_helpers
|
|
23
23
|
from ...core.net_helpers import (
|
|
24
|
+
box_command_error,
|
|
24
25
|
require_netname,
|
|
25
26
|
resolve_box,
|
|
26
27
|
validate_net,
|
|
@@ -81,10 +82,9 @@ def _run_backend(ctx, box, action: str, **params):
|
|
|
81
82
|
click.echo(f"\033[92m{result.get('message', 'Command executed')}\033[0m")
|
|
82
83
|
return
|
|
83
84
|
|
|
84
|
-
error =
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
f"update the box.")
|
|
85
|
+
error = box_command_error(
|
|
86
|
+
result, response.status_code, '/battery/command',
|
|
87
|
+
f"battery command failed (HTTP {response.status_code})")
|
|
88
88
|
click.secho(f"Error: {error}", fg='red', err=True)
|
|
89
89
|
raise SystemExit(1)
|
|
90
90
|
|
|
@@ -24,6 +24,7 @@ import click
|
|
|
24
24
|
from ...core.net_group import NetGroup
|
|
25
25
|
# Import consolidated helpers from cli.core.net_helpers
|
|
26
26
|
from ...core.net_helpers import (
|
|
27
|
+
box_command_error,
|
|
27
28
|
require_netname,
|
|
28
29
|
resolve_box,
|
|
29
30
|
validate_net,
|
|
@@ -127,10 +128,9 @@ def _run_backend(ctx, box, action: str, **params):
|
|
|
127
128
|
click.echo(f"[OK] {result.get('message', 'Command executed')}")
|
|
128
129
|
return
|
|
129
130
|
|
|
130
|
-
error =
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
f"update the box.")
|
|
131
|
+
error = box_command_error(
|
|
132
|
+
result, response.status_code, '/supply/command',
|
|
133
|
+
f"supply command failed (HTTP {response.status_code})")
|
|
134
134
|
click.secho(f"Error: {error}", fg='red', err=True)
|
|
135
135
|
raise SystemExit(1)
|
|
136
136
|
|
|
@@ -103,6 +103,42 @@ def wait_for_box_ready(box_ip, *, timeout_s=60, initial_delay_s=2):
|
|
|
103
103
|
return False
|
|
104
104
|
|
|
105
105
|
|
|
106
|
+
# Files whose contents are secret and whose owner therefore matters as much as
|
|
107
|
+
# their mode. Mode 0600 grants the OWNER alone, so tightening one of these under
|
|
108
|
+
# the wrong owner locks the runtime out of it entirely.
|
|
109
|
+
_SECRET_FILES = ('/etc/lager/org_secrets.json', '/etc/lager/secret_key')
|
|
110
|
+
|
|
111
|
+
# uid 33 is www-data, the user the container runs as. Hardcoded because it is
|
|
112
|
+
# baked into the container image, not discovered at runtime.
|
|
113
|
+
_CONTAINER_UID = 33
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _secret_ownership_shell_cmd(files=_SECRET_FILES, uid=_CONTAINER_UID):
|
|
117
|
+
"""Shell snippet that gives the secret files back to the container user.
|
|
118
|
+
|
|
119
|
+
A secrets file copied onto a box by hand belongs to whoever copied it. The
|
|
120
|
+
boot script then tightens it to 0600 — successfully, because that same user
|
|
121
|
+
owns it — and uid 33 can no longer read its own secrets. Nothing fails
|
|
122
|
+
loudly: secret injection just returns empty and scripts break far from the
|
|
123
|
+
cause.
|
|
124
|
+
|
|
125
|
+
Best-effort by construction. `sudo -n` so a box whose sudoers predates the
|
|
126
|
+
chown grant fails instantly instead of blocking on a password prompt no one
|
|
127
|
+
is there to answer, and every branch swallows its failure — an unreadable
|
|
128
|
+
secrets file must never be the reason an update fails. Boxes that miss the
|
|
129
|
+
repair still get the boot-time warning telling them what to run by hand.
|
|
130
|
+
|
|
131
|
+
No chmod: after the chown this user no longer owns the file, and the
|
|
132
|
+
container tightens the mode itself on the next load.
|
|
133
|
+
"""
|
|
134
|
+
quoted = ' '.join(shlex.quote(f) for f in files)
|
|
135
|
+
return (
|
|
136
|
+
f'for f in {quoted}; do '
|
|
137
|
+
f'if [ -f "$f" ]; then sudo -n /bin/chown {uid}:{uid} "$f" 2>/dev/null || true; fi; '
|
|
138
|
+
'done; true'
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
106
142
|
def _flatten_shell_cmd():
|
|
107
143
|
"""Shell snippet that flattens the sparse-checkout layout (`box/` -> root).
|
|
108
144
|
|
|
@@ -1846,6 +1882,27 @@ def _update_logic(ctx, *, box, yes, version, verbose, check, force=False):
|
|
|
1846
1882
|
# to be running: 'container-down' falls through to the rebuild path below
|
|
1847
1883
|
# and brings the box back up. `--force` still skips the early-exit
|
|
1848
1884
|
# unconditionally.
|
|
1885
|
+
# Secret files must be owned by uid 33 (the container user), not merely
|
|
1886
|
+
# mode 0600. 0600 grants the owner alone, so a secrets file left under a
|
|
1887
|
+
# different owner — e.g. copied onto the box by hand — locks the runtime
|
|
1888
|
+
# out of it the moment anything tightens the mode. The symptom is silent:
|
|
1889
|
+
# secret injection returns empty and scripts fail nowhere near the cause.
|
|
1890
|
+
#
|
|
1891
|
+
# BEFORE the rebuild gate on purpose, like the host-CLI reconcile below.
|
|
1892
|
+
# This is a heal, not part of building an image, and the box that most
|
|
1893
|
+
# needs it is the one already running current code: an operator whose
|
|
1894
|
+
# secrets stopped working reaches for `lager update`, and if the repair
|
|
1895
|
+
# sat after the gate that update would early-exit "already up to date"
|
|
1896
|
+
# and fix nothing. Running it here costs one short SSH round-trip on the
|
|
1897
|
+
# fast path and makes the remedy work whenever it is reached for.
|
|
1898
|
+
#
|
|
1899
|
+
# `lager update` is the only path that runs with real privilege, so it is
|
|
1900
|
+
# where the repair belongs; the box's boot script can only warn when it
|
|
1901
|
+
# lacks the grant. Best-effort throughout — an unreadable secrets file must
|
|
1902
|
+
# never be the reason an update fails — so boxes that miss the repair still
|
|
1903
|
+
# get the boot warning telling them what to run.
|
|
1904
|
+
run_ssh_command_with_output(_secret_ownership_shell_cmd(), timeout_secs=30)
|
|
1905
|
+
|
|
1849
1906
|
_gate = _rebuild_gate_verdict(
|
|
1850
1907
|
facts,
|
|
1851
1908
|
git_sync_confirmed=git_sync_confirmed,
|
|
@@ -2323,6 +2380,7 @@ def _update_logic(ctx, *, box, yes, version, verbose, check, force=False):
|
|
|
2323
2380
|
ctx.exit(1)
|
|
2324
2381
|
log_status('OK', 'green')
|
|
2325
2382
|
|
|
2383
|
+
|
|
2326
2384
|
# Persist the build-inputs hash now that /etc/lager is group-writable by this
|
|
2327
2385
|
# user (Step 10). This is what lets the next run's cache-validity check short-circuit to
|
|
2328
2386
|
# the no-op fast path instead of rebuilding; a stale hash defeats it. Unlike
|
|
@@ -166,6 +166,28 @@ NET_HTTP_PORT = 9000
|
|
|
166
166
|
_NET_HTTP_TIMEOUT = 10
|
|
167
167
|
|
|
168
168
|
|
|
169
|
+
def box_command_error(result, status_code, endpoint, fallback) -> str:
|
|
170
|
+
"""Build the user-facing error for a failed :9000 command response.
|
|
171
|
+
|
|
172
|
+
The "update the box" hint is gated on the SHAPE of the body, not on the
|
|
173
|
+
status code. Every :9000 command handler sets ``success`` in its own JSON,
|
|
174
|
+
including its 404s — and a handler's 404 means the net or its instrument
|
|
175
|
+
was not found, which an update cannot fix. Only Flask's built-in 404, which
|
|
176
|
+
carries no ``success`` key, means this box image predates the endpoint.
|
|
177
|
+
|
|
178
|
+
Keying the hint on the code alone told people to update the box whenever an
|
|
179
|
+
instrument was unplugged: an absent USB hub reported "device-not-found: No
|
|
180
|
+
Acroname hub detected on USB with serial 0x… . This box image does not
|
|
181
|
+
expose /usb/command; update the box." Both halves cannot be true, and the
|
|
182
|
+
second one sends the diagnosis somewhere the fault never was.
|
|
183
|
+
"""
|
|
184
|
+
body = result if isinstance(result, dict) else {}
|
|
185
|
+
error = body.get('error') or fallback
|
|
186
|
+
if status_code == 404 and 'success' not in body:
|
|
187
|
+
error = f"{error}. This box image does not expose {endpoint}; update the box."
|
|
188
|
+
return error
|
|
189
|
+
|
|
190
|
+
|
|
169
191
|
def echo_box_request_failure(box_ip: str, exc, timeout=None) -> None:
|
|
170
192
|
"""Print the right diagnosis for a failed box HTTP call.
|
|
171
193
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|