lager-cli 0.32.3__tar.gz → 0.32.5__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.32.3/lager_cli.egg-info → lager_cli-0.32.5}/PKG-INFO +1 -1
- {lager_cli-0.32.3 → lager_cli-0.32.5}/__init__.py +1 -1
- {lager_cli-0.32.3 → lager_cli-0.32.5}/battery/websocket_client.py +30 -13
- {lager_cli-0.32.3 → lager_cli-0.32.5}/box_storage.py +67 -21
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/box/boxes.py +26 -1
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/box/config.py +7 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/box/diagnose.py +7 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/box/lock.py +7 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/communication/uart.py +4 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/communication/websocket_client.py +31 -17
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/development/debug/service_client.py +11 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/utility/uninstall.py +8 -3
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/utility/update.py +7 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/context/session.py +10 -1
- {lager_cli-0.32.3 → lager_cli-0.32.5}/core/version_skew.py +9 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/core/ws_diagnose.py +18 -1
- {lager_cli-0.32.3 → lager_cli-0.32.5}/deployment/scripts/setup_and_deploy_box.sh +19 -15
- {lager_cli-0.32.3 → lager_cli-0.32.5}/gateway_auth.py +135 -12
- {lager_cli-0.32.3 → lager_cli-0.32.5/lager_cli.egg-info}/PKG-INFO +1 -1
- {lager_cli-0.32.3 → lager_cli-0.32.5}/lager_cli.egg-info/SOURCES.txt +1 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/status.py +8 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/supply/websocket_client.py +30 -13
- {lager_cli-0.32.3 → lager_cli-0.32.5}/tests/test_box_lager_imports.py +48 -48
- {lager_cli-0.32.3 → lager_cli-0.32.5}/tests/test_gateway_auth.py +160 -0
- lager_cli-0.32.5/tests/test_gateway_callsites.py +177 -0
- lager_cli-0.32.5/tests/test_io_imports.py +266 -0
- lager_cli-0.32.3/tests/test_io_imports.py +0 -351
- {lager_cli-0.32.3 → lager_cli-0.32.5}/LICENSE +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/MANIFEST.in +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/README.md +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/__main__.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/address_utils.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/battery/__init__.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/battery/battery_tui.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/__init__.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/box/__init__.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/box/_host_ops.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/box/_mount_prep.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/box/_pip_validation.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/box/_shim_verbs.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/box/_ssh.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/box/box_group.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/box/dut.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/box/hello.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/box/instruments.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/box/labjack_pins.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/box/net_tui.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/box/nets.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/box/ssh.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/box/ssh_setup.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/communication/__init__.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/communication/ble.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/communication/blufi.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/communication/i2c.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/communication/router.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/communication/spi.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/communication/usb.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/communication/wifi.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/development/__init__.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/development/arm.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/development/debug/__init__.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/development/debug/commands.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/development/debug/gdb.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/development/debug/net_cache.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/development/debug/service_helper.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/development/debug/tunnel.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/development/devenv.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/development/python.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/measurement/__init__.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/measurement/adc.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/measurement/dac.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/measurement/energy.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/measurement/gpi.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/measurement/gpo.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/measurement/logic.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/measurement/scope.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/measurement/thermocouple.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/measurement/watt.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/power/__init__.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/power/battery.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/power/battery_model_csv.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/power/eload.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/power/solar.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/power/supply.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/utility/__init__.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/utility/binaries.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/utility/defaults.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/utility/exec_.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/utility/install.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/utility/install_wheel.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/utility/login.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/utility/logs.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/commands/utility/webcam.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/config.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/context/__init__.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/context/ci_detection.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/context/constants.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/context/core.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/context/error_handlers.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/core/__init__.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/core/group_usage.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/core/matchers.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/core/net_group.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/core/net_helpers.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/core/net_storage.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/core/param_types.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/core/ssh_utils.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/core/utils.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/deployment/__init__.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/deployment/scripts/__init__.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/deployment/scripts/configure_docker_dns.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/deployment/scripts/configure_docker_dns.sh +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/deployment/scripts/convert_to_sparse_checkout.sh +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/deployment/scripts/setup_ssh_key.sh +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/deployment/security/__init__.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/deployment/security/secure_box_firewall.sh +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/elftools/__init__.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/elftools/common/__init__.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/elftools/common/construct_utils.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/elftools/common/exceptions.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/elftools/common/py3compat.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/elftools/common/utils.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/elftools/construct/__init__.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/elftools/construct/adapters.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/elftools/construct/core.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/elftools/construct/debug.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/elftools/construct/macros.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/elftools/dwarf/__init__.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/elftools/dwarf/abbrevtable.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/elftools/dwarf/aranges.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/elftools/dwarf/callframe.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/elftools/dwarf/compileunit.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/elftools/dwarf/constants.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/elftools/dwarf/descriptions.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/elftools/dwarf/die.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/elftools/dwarf/dwarf_expr.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/elftools/dwarf/dwarfinfo.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/elftools/dwarf/enums.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/elftools/dwarf/lineprogram.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/elftools/dwarf/locationlists.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/elftools/dwarf/namelut.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/elftools/dwarf/ranges.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/elftools/dwarf/structs.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/elftools/ehabi/__init__.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/elftools/ehabi/constants.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/elftools/ehabi/decoder.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/elftools/ehabi/ehabiinfo.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/elftools/ehabi/structs.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/elftools/elf/__init__.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/elftools/elf/constants.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/elftools/elf/descriptions.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/elftools/elf/dynamic.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/elftools/elf/elffile.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/elftools/elf/enums.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/elftools/elf/gnuversions.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/elftools/elf/hash.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/elftools/elf/notes.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/elftools/elf/relocation.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/elftools/elf/sections.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/elftools/elf/segments.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/elftools/elf/structs.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/errors.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/exceptions.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/impl/__init__.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/impl/box_config.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/impl/measurement/__init__.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/impl/measurement/scope.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/impl/measurement/scope_stream.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/impl/power/__init__.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/impl/power/enable_disable.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/lager_cli.egg-info/dependency_links.txt +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/lager_cli.egg-info/entry_points.txt +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/lager_cli.egg-info/requires.txt +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/lager_cli.egg-info/top_level.txt +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/main.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/pyproject.toml +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/safe_unpickle.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/setup.cfg +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/setup.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/simple_hdlc.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/sort_utils.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/supply/__init__.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/supply/supply_tui.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/terminal/__init__.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/terminal/core/__init__.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/terminal/core/executor.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/terminal/main.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/terminal/ui/__init__.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/terminal/ui/completer.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/terminal/ui/display.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/terminal/ui/logo.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/terminal/ui/repl.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/terminal/ui/themes.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/tests/test_box_storage.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/tests/test_update_gate.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/update_check.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/vendor/PyCRC/CRC16.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/vendor/PyCRC/CRC16DNP.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/vendor/PyCRC/CRC16Kermit.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/vendor/PyCRC/CRC16SICK.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/vendor/PyCRC/CRC32.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/vendor/PyCRC/CRCCCITT.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/vendor/PyCRC/__init__.py +0 -0
- {lager_cli-0.32.3 → lager_cli-0.32.5}/vendor/__init__.py +0 -0
|
@@ -132,19 +132,36 @@ class BatteryWebSocketClient:
|
|
|
132
132
|
Returns:
|
|
133
133
|
True if connected successfully, False otherwise
|
|
134
134
|
"""
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
135
|
+
from ..gateway_auth import (
|
|
136
|
+
auth_headers_for_url, plain_error_text, ws_handshake_recovery,
|
|
137
|
+
)
|
|
138
|
+
headers = auth_headers_for_url(self.box_url)
|
|
139
|
+
for attempt in (0, 1):
|
|
140
|
+
try:
|
|
141
|
+
self.sio.connect(
|
|
142
|
+
self.box_url,
|
|
143
|
+
namespaces=['/battery'],
|
|
144
|
+
wait_timeout=timeout,
|
|
145
|
+
headers=headers
|
|
146
|
+
)
|
|
147
|
+
return True
|
|
148
|
+
except Exception as e:
|
|
149
|
+
# The handshake exception hides the HTTP response, so probe
|
|
150
|
+
# the box over HTTP: a gated box's first contact records the
|
|
151
|
+
# mapping and hands back the token for one retry; a genuine
|
|
152
|
+
# denial becomes the actionable message.
|
|
153
|
+
retry_headers, denial = ws_handshake_recovery(self.box_url, headers)
|
|
154
|
+
if denial is not None:
|
|
155
|
+
if self.on_error:
|
|
156
|
+
self.on_error(plain_error_text(denial))
|
|
157
|
+
return False
|
|
158
|
+
if attempt == 0 and retry_headers:
|
|
159
|
+
headers = retry_headers
|
|
160
|
+
continue
|
|
161
|
+
if self.on_error:
|
|
162
|
+
self.on_error(f"Could not connect to box at {self.box_url}: {str(e)}")
|
|
163
|
+
return False
|
|
164
|
+
return False
|
|
148
165
|
|
|
149
166
|
def start_monitoring(self) -> bool:
|
|
150
167
|
"""
|
|
@@ -583,43 +583,49 @@ def _gateway_kwargs(ip):
|
|
|
583
583
|
|
|
584
584
|
def _resend_with_auth(prepared, headers):
|
|
585
585
|
"""Re-send an already-prepared request with extra headers merged in.
|
|
586
|
-
Returns the new response, or None if the resend itself failed.
|
|
586
|
+
Returns the new response, or None if the resend itself failed.
|
|
587
|
+
|
|
588
|
+
Sent with ``stream=True`` so callers that stream the original request
|
|
589
|
+
(pip/python output) keep streaming on the retried response; non-streaming
|
|
590
|
+
callers read ``.json()``/``.text`` which consumes it identically."""
|
|
587
591
|
import requests
|
|
588
592
|
req = prepared.copy()
|
|
589
593
|
for key, value in headers.items():
|
|
590
594
|
req.headers[key] = value
|
|
591
595
|
try:
|
|
592
|
-
return requests.Session().send(req, timeout=30, stream=
|
|
596
|
+
return requests.Session().send(req, timeout=30, stream=True)
|
|
593
597
|
except requests.RequestException:
|
|
594
598
|
return None
|
|
595
599
|
|
|
596
600
|
|
|
597
|
-
def
|
|
598
|
-
"""
|
|
599
|
-
|
|
600
|
-
|
|
601
|
+
def _resolve_gateway(resp, ip):
|
|
602
|
+
"""Record-and-retry core shared by :func:`_check_gateway` and
|
|
603
|
+
:func:`check_gateway_status` — the single implementation of gateway
|
|
604
|
+
discovery. Returns ``(resp, denied)``:
|
|
601
605
|
|
|
606
|
+
- Non-gateway responses (no discovery header) pass through untouched
|
|
607
|
+
with ``denied=False`` — plain boxes are never affected.
|
|
602
608
|
- First contact — we sent no token (the box→auth-server link is only
|
|
603
609
|
learned from this very 401's discovery header) but we already hold a
|
|
604
610
|
session for that server: record the mapping, attach the token, and
|
|
605
|
-
retry the request once.
|
|
606
|
-
|
|
607
|
-
because only a gateway sends the discovery header.
|
|
608
|
-
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
Callers should adopt the return value: ``resp = _check_gateway(resp, ip)``.
|
|
611
|
+
retry the request once. On success the retried, authenticated response
|
|
612
|
+
is returned with ``denied=False``. A plain box never receives the
|
|
613
|
+
token, because only a gateway sends the discovery header.
|
|
614
|
+
- Anything else is a genuine denial: the mapping is still recorded (so
|
|
615
|
+
the next attempt authenticates) and ``denied=True`` tells the caller
|
|
616
|
+
to handle ``resp`` as a gateway denial.
|
|
612
617
|
"""
|
|
613
618
|
from .gateway_auth import (
|
|
614
|
-
DISCOVERY_HEADER,
|
|
615
|
-
record_box_auth_server, auth_headers_for_box,
|
|
619
|
+
DISCOVERY_HEADER, record_box_auth_server, auth_headers_for_box,
|
|
616
620
|
)
|
|
617
|
-
|
|
618
|
-
|
|
621
|
+
# getattr: tolerate response-shaped fakes without a headers attribute.
|
|
622
|
+
resp_headers = getattr(resp, 'headers', None) or {}
|
|
623
|
+
if not (resp.status_code in (401, 403, 503) and DISCOVERY_HEADER in resp_headers):
|
|
624
|
+
return resp, False
|
|
619
625
|
|
|
626
|
+
record_box_auth_server(ip, resp_headers[DISCOVERY_HEADER])
|
|
620
627
|
sent_auth = 'Authorization' in getattr(resp.request, 'headers', {})
|
|
621
628
|
if resp.status_code == 401 and not sent_auth:
|
|
622
|
-
record_box_auth_server(ip, resp.headers[DISCOVERY_HEADER])
|
|
623
629
|
headers = auth_headers_for_box(ip)
|
|
624
630
|
if headers:
|
|
625
631
|
retried = _resend_with_auth(resp.request, headers)
|
|
@@ -627,13 +633,47 @@ def _check_gateway(resp, ip):
|
|
|
627
633
|
gated = (retried.status_code in (401, 403, 503)
|
|
628
634
|
and DISCOVERY_HEADER in retried.headers)
|
|
629
635
|
if not gated:
|
|
630
|
-
return retried
|
|
631
|
-
resp = retried
|
|
636
|
+
return retried, False # transparently authenticated
|
|
637
|
+
resp = retried # still refused — report on the retry
|
|
638
|
+
|
|
639
|
+
return resp, True
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
def _check_gateway(resp, ip):
|
|
643
|
+
"""Resolve a gateway response, returning the response the caller should use.
|
|
644
|
+
|
|
645
|
+
On a plain (un-gated) box this is a passthrough. On a gated box the
|
|
646
|
+
first contact is retried transparently (see :func:`_resolve_gateway`);
|
|
647
|
+
genuine denials (revoked session, no access grant, auth server down)
|
|
648
|
+
raise the actionable `lager login` / "ask your admin" error as before.
|
|
632
649
|
|
|
633
|
-
|
|
650
|
+
Callers should adopt the return value: ``resp = _check_gateway(resp, ip)``.
|
|
651
|
+
"""
|
|
652
|
+
from .gateway_auth import handle_gateway_denial
|
|
653
|
+
resp, denied = _resolve_gateway(resp, ip)
|
|
654
|
+
if denied:
|
|
655
|
+
handle_gateway_denial(resp, ip) # raises the actionable error
|
|
634
656
|
return resp
|
|
635
657
|
|
|
636
658
|
|
|
659
|
+
def check_gateway_status(resp, ip):
|
|
660
|
+
"""Non-raising variant of :func:`_check_gateway` for fan-out and
|
|
661
|
+
fail-open callers (`lager boxes`, health polls) that must not abort on a
|
|
662
|
+
single box's denial.
|
|
663
|
+
|
|
664
|
+
Performs the same record-and-retry as ``_check_gateway``. Returns
|
|
665
|
+
``(resp, label)``: ``label`` is None when the caller should simply use
|
|
666
|
+
``resp`` (plain box, or the retry authenticated transparently), else a
|
|
667
|
+
short user-facing verdict — 'sign-in required', 'session rejected',
|
|
668
|
+
'no access', or 'auth server down'.
|
|
669
|
+
"""
|
|
670
|
+
from .gateway_auth import denial_label
|
|
671
|
+
resp, denied = _resolve_gateway(resp, ip)
|
|
672
|
+
if not denied:
|
|
673
|
+
return resp, None
|
|
674
|
+
return resp, denial_label(resp)
|
|
675
|
+
|
|
676
|
+
|
|
637
677
|
def _lock_url(ip, suffix=''):
|
|
638
678
|
# Lock state is shared box-wide; both the :5000 and :9000 servers expose
|
|
639
679
|
# it via lager.lock_state. The CLI talks to :9000 (the primary HTTP API).
|
|
@@ -807,6 +847,9 @@ def release_box_lock(ip, holder, *, quiet=True):
|
|
|
807
847
|
)
|
|
808
848
|
return False
|
|
809
849
|
|
|
850
|
+
# Non-raising gateway check (this function promises never to raise);
|
|
851
|
+
# a denial falls through to the failure warning below.
|
|
852
|
+
resp, _gate_verdict = check_gateway_status(resp, ip)
|
|
810
853
|
if resp.status_code == 200:
|
|
811
854
|
return True
|
|
812
855
|
if not quiet:
|
|
@@ -848,6 +891,9 @@ def heartbeat_box_lock(ip, holder, *, quiet=True):
|
|
|
848
891
|
fg='yellow', err=True,
|
|
849
892
|
)
|
|
850
893
|
return False
|
|
894
|
+
# Non-raising gateway check; a denial reads as a failed heartbeat, which
|
|
895
|
+
# callers already treat as "carry on" (server TTL is authoritative).
|
|
896
|
+
resp, _gate_verdict = check_gateway_status(resp, ip)
|
|
851
897
|
return resp.status_code == 200
|
|
852
898
|
|
|
853
899
|
|
|
@@ -47,6 +47,7 @@ def _list_boxes_live(port=9000, timeout=5):
|
|
|
47
47
|
|
|
48
48
|
results = []
|
|
49
49
|
failed_count = 0
|
|
50
|
+
auth_failed_count = 0
|
|
50
51
|
needs_update_count = 0
|
|
51
52
|
newer_count = 0
|
|
52
53
|
|
|
@@ -67,8 +68,14 @@ def _list_boxes_live(port=9000, timeout=5):
|
|
|
67
68
|
busy_info = ''
|
|
68
69
|
try:
|
|
69
70
|
from ...gateway_auth import auth_headers_for_box
|
|
71
|
+
from ...box_storage import check_gateway_status
|
|
70
72
|
lock_resp = requests.get(f'http://{ip}:{port}/lock', timeout=3,
|
|
71
73
|
headers={'Cache-Control': 'no-cache', **auth_headers_for_box(ip)})
|
|
74
|
+
# Non-raising gateway check: one gated box must not abort the
|
|
75
|
+
# whole table. On first contact this records the box→auth-server
|
|
76
|
+
# mapping and retries with the stored token, so the /status call
|
|
77
|
+
# below authenticates normally (no second round trip).
|
|
78
|
+
lock_resp, _ = check_gateway_status(lock_resp, ip)
|
|
72
79
|
if lock_resp.status_code == 200:
|
|
73
80
|
lock_data = lock_resp.json()
|
|
74
81
|
if lock_data.get('locked'):
|
|
@@ -81,12 +88,18 @@ def _list_boxes_live(port=9000, timeout=5):
|
|
|
81
88
|
# It predates the newer capability fields, so even older box images
|
|
82
89
|
# answer it — unlike a brand-new endpoint would.
|
|
83
90
|
from ...gateway_auth import auth_headers_for_box
|
|
91
|
+
from ...box_storage import check_gateway_status
|
|
84
92
|
url = f'http://{ip}:{port}/status'
|
|
85
93
|
headers = {'Cache-Control': 'no-cache', 'Pragma': 'no-cache',
|
|
86
94
|
**auth_headers_for_box(ip)}
|
|
87
95
|
response = requests.get(url, timeout=timeout, headers=headers)
|
|
96
|
+
response, gate_verdict = check_gateway_status(response, ip)
|
|
88
97
|
|
|
89
|
-
if
|
|
98
|
+
if gate_verdict:
|
|
99
|
+
results.append((name, ip, user, '-', gate_verdict, locked_by, busy_info))
|
|
100
|
+
auth_failed_count += 1
|
|
101
|
+
|
|
102
|
+
elif response.status_code == 200:
|
|
90
103
|
try:
|
|
91
104
|
data = response.json()
|
|
92
105
|
box_version = data.get('version') or data.get('box_version')
|
|
@@ -205,6 +218,18 @@ def _list_boxes_live(port=9000, timeout=5):
|
|
|
205
218
|
if failed_count > 0:
|
|
206
219
|
box_word = 'box' if failed_count == 1 else 'boxes'
|
|
207
220
|
click.secho(f'{failed_count} {box_word} could not be reached', fg='red')
|
|
221
|
+
if auth_failed_count > 0:
|
|
222
|
+
from ...gateway_auth import auth_server_for_box
|
|
223
|
+
box_word = 'box needs' if auth_failed_count == 1 else 'boxes need'
|
|
224
|
+
click.secho(f'{auth_failed_count} {box_word} sign-in or an access grant', fg='red')
|
|
225
|
+
# The denial recorded each box's auth server, so we can say exactly
|
|
226
|
+
# where to sign in. Usually one server covers the whole fleet.
|
|
227
|
+
signin_urls = sorted({
|
|
228
|
+
url for r in results if r[4] in ('sign-in required', 'session rejected')
|
|
229
|
+
for url in [auth_server_for_box(r[1])] if url
|
|
230
|
+
})
|
|
231
|
+
for url in signin_urls:
|
|
232
|
+
click.secho(f' Sign in with: lager login {url}', fg='red')
|
|
208
233
|
|
|
209
234
|
root_locked = [r[0] for r in results if r[5] == 'root']
|
|
210
235
|
if root_locked:
|
|
@@ -1627,8 +1627,15 @@ def _box_api_responding(box_ip: str, *, timeout: float = 2.0) -> bool:
|
|
|
1627
1627
|
poll cheaply."""
|
|
1628
1628
|
try:
|
|
1629
1629
|
from ...gateway_auth import auth_headers_for_box
|
|
1630
|
+
from ...box_storage import check_gateway_status
|
|
1630
1631
|
r = requests.get(f"http://{box_ip}:{_BOX_API_PORT}/hello", timeout=timeout,
|
|
1631
1632
|
headers=auth_headers_for_box(box_ip))
|
|
1633
|
+
# Non-raising gateway check: first contact with a gated box records
|
|
1634
|
+
# the mapping and retries with the stored token so the probe sees the
|
|
1635
|
+
# real /hello. A genuine denial reads as not-responding (False) —
|
|
1636
|
+
# this probe sits inside the apply/restart poll loop, which must not
|
|
1637
|
+
# abort mid-restart; the flow's own requests surface auth errors.
|
|
1638
|
+
r, _gate_verdict = check_gateway_status(r, box_ip)
|
|
1632
1639
|
return r.status_code == 200
|
|
1633
1640
|
except Exception:
|
|
1634
1641
|
return False
|
|
@@ -30,6 +30,7 @@ import click
|
|
|
30
30
|
import requests
|
|
31
31
|
|
|
32
32
|
from ...box_storage import resolve_and_validate_box_with_name
|
|
33
|
+
from ...errors import LagerError
|
|
33
34
|
|
|
34
35
|
|
|
35
36
|
# Net-role choices for --type. Mirrors the role strings produced by
|
|
@@ -51,8 +52,10 @@ def _fetch_net_info(
|
|
|
51
52
|
that — otherwise the returned role wins."""
|
|
52
53
|
try:
|
|
53
54
|
from ...gateway_auth import auth_headers_for_box
|
|
55
|
+
from ...box_storage import _check_gateway
|
|
54
56
|
r = requests.get(f'http://{box_ip}:9000/nets/list', timeout=5,
|
|
55
57
|
headers=auth_headers_for_box(box_ip))
|
|
58
|
+
r = _check_gateway(r, box_ip)
|
|
56
59
|
r.raise_for_status()
|
|
57
60
|
nets = r.json()
|
|
58
61
|
# Older box images wrap the list: {"nets": [...]}. Unwrap so the
|
|
@@ -72,6 +75,10 @@ def _fetch_net_info(
|
|
|
72
75
|
f'Check network/Tailscale connectivity, then `lager hello --box {display_name}`.',
|
|
73
76
|
fg='red'), err=True)
|
|
74
77
|
return None, None
|
|
78
|
+
except LagerError:
|
|
79
|
+
# A gateway denial from _check_gateway is already actionable — let it
|
|
80
|
+
# render as-is rather than collapsing into the generic message below.
|
|
81
|
+
raise
|
|
75
82
|
except Exception as e:
|
|
76
83
|
click.echo(click.style(f'Could not fetch net list from box: {e}', fg='red'), err=True)
|
|
77
84
|
return None, None
|
|
@@ -35,6 +35,7 @@ def lock(ctx, box, lock_user):
|
|
|
35
35
|
# lock immune to the new heartbeat-driven auto-reap that ephemeral
|
|
36
36
|
# `lager python` locks participate in.
|
|
37
37
|
from ...gateway_auth import auth_headers_for_box
|
|
38
|
+
from ...box_storage import _check_gateway
|
|
38
39
|
resp = requests.post(
|
|
39
40
|
f'http://{ip}:9000/lock',
|
|
40
41
|
headers=auth_headers_for_box(ip),
|
|
@@ -45,6 +46,7 @@ def lock(ctx, box, lock_user):
|
|
|
45
46
|
},
|
|
46
47
|
timeout=5,
|
|
47
48
|
)
|
|
49
|
+
resp = _check_gateway(resp, ip)
|
|
48
50
|
except requests.exceptions.RequestException as e:
|
|
49
51
|
click.secho(f"Error: Could not reach box '{display_name}': {e}", fg='red', err=True)
|
|
50
52
|
ctx.exit(1)
|
|
@@ -80,12 +82,17 @@ def unlock(ctx, box, force):
|
|
|
80
82
|
|
|
81
83
|
try:
|
|
82
84
|
from ...gateway_auth import auth_headers_for_box
|
|
85
|
+
from ...box_storage import _check_gateway
|
|
83
86
|
resp = requests.post(
|
|
84
87
|
f'http://{ip}:9000/unlock',
|
|
85
88
|
headers=auth_headers_for_box(ip),
|
|
86
89
|
json={'user': user, 'force': force},
|
|
87
90
|
timeout=5,
|
|
88
91
|
)
|
|
92
|
+
# Gateway denials raise here with the actionable sign-in error; the
|
|
93
|
+
# 403 branch below keeps meaning "locked by another user" only (a
|
|
94
|
+
# plain box's application 403 has no discovery header).
|
|
95
|
+
resp = _check_gateway(resp, ip)
|
|
89
96
|
except requests.exceptions.RequestException as e:
|
|
90
97
|
click.secho(f"Error: Could not reach box '{display_name}': {e}", fg='red', err=True)
|
|
91
98
|
ctx.exit(1)
|
|
@@ -100,8 +100,12 @@ def _run_query_instruments(ctx: click.Context, box_ip: str) -> list[dict]:
|
|
|
100
100
|
"""
|
|
101
101
|
try:
|
|
102
102
|
from ...gateway_auth import auth_headers_for_box
|
|
103
|
+
from ...box_storage import _check_gateway
|
|
103
104
|
resp = requests.get(f'http://{box_ip}:9000/instruments/list', timeout=15,
|
|
104
105
|
headers=auth_headers_for_box(box_ip))
|
|
106
|
+
# A gateway denial raises the actionable LagerError (not caught
|
|
107
|
+
# below): an auth problem must not be reported as "no instruments".
|
|
108
|
+
resp = _check_gateway(resp, box_ip)
|
|
105
109
|
if resp.status_code == 200:
|
|
106
110
|
data = resp.json()
|
|
107
111
|
if isinstance(data, list):
|
|
@@ -260,23 +260,37 @@ class UARTWebSocketClient:
|
|
|
260
260
|
self._setup_terminal()
|
|
261
261
|
|
|
262
262
|
# Connect to WebSocket
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
263
|
+
from ...gateway_auth import auth_headers_for_url, ws_handshake_recovery
|
|
264
|
+
headers = auth_headers_for_url(self.box_url)
|
|
265
|
+
for attempt in (0, 1):
|
|
266
|
+
try:
|
|
267
|
+
self.sio.connect(
|
|
268
|
+
self.box_url,
|
|
269
|
+
namespaces=['/uart'],
|
|
270
|
+
wait_timeout=10,
|
|
271
|
+
headers=headers
|
|
272
|
+
)
|
|
273
|
+
break
|
|
274
|
+
except Exception as e:
|
|
275
|
+
# The handshake exception hides the HTTP response, so
|
|
276
|
+
# probe the box over HTTP: a gated box's first contact
|
|
277
|
+
# records the mapping and hands back the token for one
|
|
278
|
+
# retry; a genuine denial shows the actionable error.
|
|
279
|
+
# Preserve the return-code contract (don't sys.exit /
|
|
280
|
+
# .die here): the caller relies on the return value and
|
|
281
|
+
# the finally-block terminal restore below.
|
|
282
|
+
retry_headers, denial = ws_handshake_recovery(self.box_url, headers)
|
|
283
|
+
if denial is not None:
|
|
284
|
+
denial.show()
|
|
285
|
+
return 1
|
|
286
|
+
if attempt == 0 and retry_headers:
|
|
287
|
+
headers = retry_headers
|
|
288
|
+
continue
|
|
289
|
+
from ...errors import connection_error
|
|
290
|
+
from urllib.parse import urlparse
|
|
291
|
+
host = urlparse(self.box_url).hostname or self.box_url
|
|
292
|
+
connection_error(e, host=host).show()
|
|
293
|
+
return 1
|
|
280
294
|
|
|
281
295
|
# Start UART session
|
|
282
296
|
self.sio.emit('start_uart', {
|
|
@@ -44,6 +44,17 @@ class DebugServiceClient:
|
|
|
44
44
|
'User-Agent': 'lager-cli/1.0',
|
|
45
45
|
})
|
|
46
46
|
|
|
47
|
+
if not ssh_tunnel:
|
|
48
|
+
# Direct connection may cross an authenticating gateway: attach
|
|
49
|
+
# the bearer token when this box is known to be gated, and turn
|
|
50
|
+
# gateway denials into actionable errors instead of raw 401s.
|
|
51
|
+
# No-op for plain boxes (the hook only fires on the discovery
|
|
52
|
+
# header, and no token is sent to an unknown box). Tunneled
|
|
53
|
+
# connections terminate at 127.0.0.1 and never see the gateway.
|
|
54
|
+
from ....gateway_auth import auth_headers_for_box, gateway_response_hook
|
|
55
|
+
self.session.headers.update(auth_headers_for_box(box_host))
|
|
56
|
+
self.session.hooks['response'] = [gateway_response_hook(box_host)]
|
|
57
|
+
|
|
47
58
|
def health_check(self) -> Dict[str, Any]:
|
|
48
59
|
"""Check if service is healthy."""
|
|
49
60
|
response = self.session.get(f'{self.base_url}/health', timeout=5)
|
|
@@ -603,7 +603,7 @@ def uninstall(ctx, box, ip, user, keep_config, keep_docker_images, remove_all, y
|
|
|
603
603
|
click.echo(" - Docker containers (lager, pigpio)")
|
|
604
604
|
click.echo(" - Docker network (lagernet)")
|
|
605
605
|
if not keep_docker_images:
|
|
606
|
-
click.echo(" - Docker images (
|
|
606
|
+
click.echo(" - Docker images (the lager image and dangling layers only)")
|
|
607
607
|
click.echo(" - ~/box directory")
|
|
608
608
|
if not keep_config:
|
|
609
609
|
click.echo(" - /etc/lager directory (saved nets)")
|
|
@@ -707,10 +707,15 @@ def uninstall(ctx, box, ip, user, keep_config, keep_docker_images, remove_all, y
|
|
|
707
707
|
run_ssh("docker network rm lagernet 2>/dev/null", "Removing lagernet network", allow_fail=True)
|
|
708
708
|
click.echo()
|
|
709
709
|
|
|
710
|
-
# Remove Docker images (unless --keep-docker-images)
|
|
710
|
+
# Remove Docker images (unless --keep-docker-images). Scoped to the
|
|
711
|
+
# lager image plus dangling layers: 'prune -af' would also delete
|
|
712
|
+
# images belonging to any OTHER stopped container on the box (a
|
|
713
|
+
# management agent, a user's own services) — infrastructure this
|
|
714
|
+
# command cannot restore.
|
|
711
715
|
click.secho("[Step 2/5] Cleaning Docker...", fg='cyan')
|
|
712
716
|
if not keep_docker_images:
|
|
713
|
-
run_ssh("docker
|
|
717
|
+
run_ssh("docker rmi -f lager 2>/dev/null", "Removing lager image", allow_fail=True)
|
|
718
|
+
run_ssh("docker image prune -f 2>/dev/null", "Removing dangling images", allow_fail=True)
|
|
714
719
|
run_ssh("docker builder prune -af 2>/dev/null", "Clearing Docker build cache", allow_fail=True)
|
|
715
720
|
else:
|
|
716
721
|
click.echo(" Skipping Docker image removal (--keep-docker-images)")
|
|
@@ -74,8 +74,15 @@ def wait_for_box_ready(box_ip, *, timeout_s=60, initial_delay_s=2):
|
|
|
74
74
|
for port in sorted(pending):
|
|
75
75
|
try:
|
|
76
76
|
from ...gateway_auth import auth_headers_for_box
|
|
77
|
+
from ...box_storage import check_gateway_status
|
|
77
78
|
r = requests.get(f'http://{box_ip}:{port}/health', timeout=3,
|
|
78
79
|
headers=auth_headers_for_box(box_ip))
|
|
80
|
+
# Non-raising gateway check: first contact with a gated box
|
|
81
|
+
# records the mapping and retries with the stored token so a
|
|
82
|
+
# healthy gated box reads as ready. A denial reads as
|
|
83
|
+
# not-ready and falls to the timeout path rather than
|
|
84
|
+
# aborting the update's readiness poll.
|
|
85
|
+
r, _gate_verdict = check_gateway_status(r, box_ip)
|
|
79
86
|
if r.status_code == 200:
|
|
80
87
|
pending.discard(port)
|
|
81
88
|
except requests.exceptions.RequestException:
|
|
@@ -358,8 +358,10 @@ class DirectIPSession:
|
|
|
358
358
|
|
|
359
359
|
try:
|
|
360
360
|
from ..gateway_auth import auth_headers_for_box
|
|
361
|
+
from ..box_storage import _check_gateway
|
|
361
362
|
response = requests.post(url, json=payload, timeout=5,
|
|
362
363
|
headers=auth_headers_for_box(self.ip_address))
|
|
364
|
+
response = _check_gateway(response, self.ip_address)
|
|
363
365
|
return response
|
|
364
366
|
except requests.exceptions.RequestException as e:
|
|
365
367
|
# Return a mock response with error
|
|
@@ -479,12 +481,19 @@ class DirectIPSession:
|
|
|
479
481
|
url = f'http://{self.ip_address}:5001/run/pip'
|
|
480
482
|
|
|
481
483
|
try:
|
|
484
|
+
from ..gateway_auth import auth_headers_for_box
|
|
485
|
+
from ..box_storage import _check_gateway
|
|
482
486
|
response = requests.post(
|
|
483
487
|
url,
|
|
484
488
|
json={'args': args},
|
|
485
489
|
stream=True,
|
|
486
|
-
timeout=600 # 10 minute timeout for pip operations
|
|
490
|
+
timeout=600, # 10 minute timeout for pip operations
|
|
491
|
+
headers=auth_headers_for_box(self.ip_address),
|
|
487
492
|
)
|
|
493
|
+
# Gateway discovery/denial handling; on a gated box's first
|
|
494
|
+
# contact the request is retried with the stored token so the
|
|
495
|
+
# caller streams the real pip output.
|
|
496
|
+
response = _check_gateway(response, self.ip_address)
|
|
488
497
|
|
|
489
498
|
# Check for HTTP errors
|
|
490
499
|
if response.status_code != 200:
|
|
@@ -61,8 +61,17 @@ def check_and_warn(box_ip: str, box_name: str | None = None) -> None:
|
|
|
61
61
|
# it's the same endpoint `lager box hello` uses. 1.5s timeout
|
|
62
62
|
# keeps the latency penalty bounded if the box is briefly slow.
|
|
63
63
|
from ..gateway_auth import auth_headers_for_box
|
|
64
|
+
from ..box_storage import check_gateway_status
|
|
64
65
|
r = requests.get(f'http://{box_ip}:9000/status', timeout=1.5,
|
|
65
66
|
headers=auth_headers_for_box(box_ip))
|
|
67
|
+
# Non-raising gateway check: on first contact with a gated box this
|
|
68
|
+
# records the mapping and retries with the stored token; a genuine
|
|
69
|
+
# denial just skips the warning (this module is fail-open — the
|
|
70
|
+
# command's own request will surface the actionable auth error).
|
|
71
|
+
r, gate_verdict = check_gateway_status(r, box_ip)
|
|
72
|
+
if gate_verdict:
|
|
73
|
+
logger.debug('version-skew check: gateway denied (%s)', gate_verdict)
|
|
74
|
+
return
|
|
66
75
|
if r.status_code == 404:
|
|
67
76
|
# The :9000 server answered but has no /status route — the box
|
|
68
77
|
# image predates the :9000 API surface this CLI requires. That is
|
|
@@ -37,9 +37,26 @@ def make_ws_failure_message(box_ip: str, original_error: str | Exception = '') -
|
|
|
37
37
|
# /health is a cheap, always-present endpoint on box_http_server
|
|
38
38
|
# (port 9000). 2s timeout matches the worst case of a healthy box
|
|
39
39
|
# under temporary load.
|
|
40
|
-
from ..gateway_auth import auth_headers_for_box
|
|
40
|
+
from ..gateway_auth import auth_headers_for_box, auth_server_for_box
|
|
41
|
+
from ..box_storage import check_gateway_status
|
|
41
42
|
r = requests.get(f'http://{box_ip}:9000/health', timeout=2.0,
|
|
42
43
|
headers=auth_headers_for_box(box_ip))
|
|
44
|
+
# Non-raising gateway check (this helper promises never to raise):
|
|
45
|
+
# first contact with a gated box records the mapping and retries with
|
|
46
|
+
# the stored token; a genuine denial becomes the actionable message.
|
|
47
|
+
r, gate_verdict = check_gateway_status(r, box_ip)
|
|
48
|
+
if gate_verdict:
|
|
49
|
+
url = auth_server_for_box(box_ip) or 'your auth server'
|
|
50
|
+
if gate_verdict in ('sign-in required', 'session rejected'):
|
|
51
|
+
action = f'sign in with `lager login {url}` and retry.'
|
|
52
|
+
elif gate_verdict == 'no access':
|
|
53
|
+
action = 'ask an org admin to grant you access to this box.'
|
|
54
|
+
else: # auth server down
|
|
55
|
+
action = 'its auth server is unreachable — try again shortly.'
|
|
56
|
+
return (
|
|
57
|
+
f'{base}{detail}.\n'
|
|
58
|
+
f'Action: the box refused the connection ({gate_verdict}) — {action}'
|
|
59
|
+
)
|
|
43
60
|
if r.status_code == 200:
|
|
44
61
|
return (
|
|
45
62
|
f'{base}{detail}.\n'
|
|
@@ -1339,33 +1339,37 @@ print_info "Ensuring Docker service is enabled (for auto-start on boot)..."
|
|
|
1339
1339
|
ssh $SSH_OPTS "${BOX_USER}@${BOX_IP}" "sudo systemctl enable docker >/dev/null 2>&1 || true"
|
|
1340
1340
|
print_success "Docker service enabled"
|
|
1341
1341
|
|
|
1342
|
-
print_info "Stopping and removing
|
|
1343
|
-
#
|
|
1342
|
+
print_info "Stopping and removing lager containers..."
|
|
1343
|
+
# Scoped to the containers this deployment owns (lager, pigpio, and the
|
|
1344
|
+
# legacy controller). A box may run third-party containers alongside lager
|
|
1345
|
+
# — a management agent, a user's own services — and removing containers we
|
|
1346
|
+
# did not create takes down infrastructure this script cannot restore.
|
|
1347
|
+
# Update restart policy first to prevent auto-restart, then force remove.
|
|
1344
1348
|
ssh $SSH_OPTS "${BOX_USER}@${BOX_IP}" "
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
docker rm -f \$(docker ps -aq) 2>/dev/null || true
|
|
1351
|
-
# Prune any leftover containers
|
|
1352
|
-
docker container prune -f 2>/dev/null || true
|
|
1349
|
+
for c in lager pigpio controller; do
|
|
1350
|
+
docker update --restart=no \$c 2>/dev/null || true
|
|
1351
|
+
docker stop \$c 2>/dev/null || true
|
|
1352
|
+
docker rm -f \$c 2>/dev/null || true
|
|
1353
|
+
done
|
|
1353
1354
|
# Wait a moment for Docker to clean up
|
|
1354
1355
|
sleep 2
|
|
1355
1356
|
" 2>/dev/null || true
|
|
1356
|
-
print_success "
|
|
1357
|
+
print_success "Lager containers cleaned up"
|
|
1357
1358
|
|
|
1358
|
-
print_info "Cleaning up Docker
|
|
1359
|
+
print_info "Cleaning up Docker build cache and dangling images..."
|
|
1359
1360
|
echo ""
|
|
1361
|
+
# Dangling-only (no -a): 'prune -af' would also delete images belonging to
|
|
1362
|
+
# third-party containers stopped at this moment, which cannot be re-pulled
|
|
1363
|
+
# by this script.
|
|
1360
1364
|
ssh $SSH_OPTS "${BOX_USER}@${BOX_IP}" "
|
|
1361
|
-
echo 'Removing
|
|
1362
|
-
docker image prune -
|
|
1365
|
+
echo 'Removing dangling Docker images...'
|
|
1366
|
+
docker image prune -f 2>/dev/null || true
|
|
1363
1367
|
echo 'Removing Docker build cache...'
|
|
1364
1368
|
docker builder prune -af 2>/dev/null || true
|
|
1365
1369
|
echo 'Cleanup complete'
|
|
1366
1370
|
" 2>/dev/null || true
|
|
1367
1371
|
echo ""
|
|
1368
|
-
print_success "Docker
|
|
1372
|
+
print_success "Docker build cache and dangling images cleaned up"
|
|
1369
1373
|
|
|
1370
1374
|
print_info "Checking available disk space..."
|
|
1371
1375
|
ssh $SSH_OPTS "${BOX_USER}@${BOX_IP}" "df -h / | tail -n 1 | awk '{print \"Available: \" \$4 \" (\" \$5 \" used)\"}'" 2>/dev/null || true
|