lager-cli 0.4.2__tar.gz → 0.7.0__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.4.2/lager_cli.egg-info → lager_cli-0.7.0}/PKG-INFO +1 -1
- {lager_cli-0.4.2 → lager_cli-0.7.0}/__init__.py +1 -1
- {lager_cli-0.4.2 → lager_cli-0.7.0}/commands/box/boxes.py +97 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/commands/development/devenv.py +13 -1
- {lager_cli-0.4.2 → lager_cli-0.7.0}/commands/development/python.py +128 -7
- {lager_cli-0.4.2 → lager_cli-0.7.0}/commands/utility/__init__.py +0 -2
- {lager_cli-0.4.2 → lager_cli-0.7.0}/commands/utility/update.py +2 -81
- {lager_cli-0.4.2 → lager_cli-0.7.0}/context/session.py +20 -1
- {lager_cli-0.4.2 → lager_cli-0.7.0}/deployment/scripts/setup_and_deploy_box.sh +12 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0/lager_cli.egg-info}/PKG-INFO +1 -1
- {lager_cli-0.4.2 → lager_cli-0.7.0}/lager_cli.egg-info/SOURCES.txt +0 -2
- {lager_cli-0.4.2 → lager_cli-0.7.0}/main.py +32 -2
- {lager_cli-0.4.2 → lager_cli-0.7.0}/pyproject.toml +1 -1
- lager_cli-0.4.2/commands/utility/factory.py +0 -73
- {lager_cli-0.4.2 → lager_cli-0.7.0}/LICENSE +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/MANIFEST.in +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/README.md +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/__main__.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/battery/__init__.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/battery/battery_tui.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/battery/websocket_client.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/box_storage.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/commands/__init__.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/commands/box/__init__.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/commands/box/hello.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/commands/box/instruments.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/commands/box/net_tui.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/commands/box/nets.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/commands/box/ssh.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/commands/communication/__init__.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/commands/communication/ble.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/commands/communication/blufi.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/commands/communication/i2c.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/commands/communication/spi.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/commands/communication/uart.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/commands/communication/usb.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/commands/communication/websocket_client.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/commands/communication/wifi.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/commands/development/__init__.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/commands/development/arm.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/commands/development/debug/__init__.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/commands/development/debug/commands.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/commands/development/debug/gdb.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/commands/development/debug/net_cache.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/commands/development/debug/service_client.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/commands/development/debug/service_helper.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/commands/development/debug/tunnel.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/commands/measurement/__init__.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/commands/measurement/adc.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/commands/measurement/dac.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/commands/measurement/energy.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/commands/measurement/gpi.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/commands/measurement/gpo.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/commands/measurement/logic.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/commands/measurement/scope.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/commands/measurement/thermocouple.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/commands/measurement/watt.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/commands/power/__init__.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/commands/power/battery.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/commands/power/eload.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/commands/power/solar.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/commands/power/supply.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/commands/utility/binaries.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/commands/utility/defaults.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/commands/utility/exec_.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/commands/utility/install.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/commands/utility/logs.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/commands/utility/pip.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/commands/utility/uninstall.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/commands/utility/webcam.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/config.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/context/__init__.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/context/ci_detection.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/context/constants.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/context/core.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/context/error_handlers.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/core/__init__.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/core/matchers.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/core/net_helpers.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/core/net_storage.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/core/param_types.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/core/ssh_utils.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/core/utils.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/deployment/__init__.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/deployment/scripts/__init__.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/deployment/scripts/convert_to_sparse_checkout.sh +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/deployment/scripts/setup_ssh_key.sh +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/deployment/security/__init__.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/deployment/security/secure_box_firewall.sh +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/elftools/__init__.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/elftools/common/__init__.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/elftools/common/construct_utils.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/elftools/common/exceptions.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/elftools/common/py3compat.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/elftools/common/utils.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/elftools/construct/__init__.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/elftools/construct/adapters.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/elftools/construct/core.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/elftools/construct/debug.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/elftools/construct/macros.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/elftools/dwarf/__init__.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/elftools/dwarf/abbrevtable.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/elftools/dwarf/aranges.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/elftools/dwarf/callframe.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/elftools/dwarf/compileunit.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/elftools/dwarf/constants.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/elftools/dwarf/descriptions.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/elftools/dwarf/die.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/elftools/dwarf/dwarf_expr.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/elftools/dwarf/dwarfinfo.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/elftools/dwarf/enums.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/elftools/dwarf/lineprogram.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/elftools/dwarf/locationlists.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/elftools/dwarf/namelut.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/elftools/dwarf/ranges.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/elftools/dwarf/structs.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/elftools/ehabi/__init__.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/elftools/ehabi/constants.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/elftools/ehabi/decoder.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/elftools/ehabi/ehabiinfo.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/elftools/ehabi/structs.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/elftools/elf/__init__.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/elftools/elf/constants.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/elftools/elf/descriptions.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/elftools/elf/dynamic.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/elftools/elf/elffile.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/elftools/elf/enums.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/elftools/elf/gnuversions.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/elftools/elf/hash.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/elftools/elf/notes.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/elftools/elf/relocation.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/elftools/elf/sections.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/elftools/elf/segments.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/elftools/elf/structs.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/exceptions.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/impl/__init__.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/impl/communication/__init__.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/impl/communication/ble.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/impl/communication/blufi.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/impl/communication/i2c.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/impl/communication/spi.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/impl/communication/uart.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/impl/communication/wifi.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/impl/device/__init__.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/impl/device/arm.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/impl/device/hello.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/impl/device/usb.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/impl/device/webcam.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/impl/measurement/__init__.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/impl/measurement/adc.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/impl/measurement/dac.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/impl/measurement/energy.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/impl/measurement/gpio.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/impl/measurement/scope.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/impl/measurement/scope_stream.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/impl/measurement/thermocouple.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/impl/measurement/watt.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/impl/net.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/impl/power/__init__.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/impl/power/battery.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/impl/power/eload.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/impl/power/enable_disable.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/impl/power/solar.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/impl/power/supply.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/impl/query_instruments.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/lager_cli.egg-info/dependency_links.txt +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/lager_cli.egg-info/entry_points.txt +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/lager_cli.egg-info/requires.txt +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/lager_cli.egg-info/top_level.txt +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/mcp/__init__.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/mcp/__main__.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/mcp/server.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/mcp/tools/__init__.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/mcp/tools/arm.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/mcp/tools/battery.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/mcp/tools/binaries.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/mcp/tools/ble.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/mcp/tools/blufi.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/mcp/tools/box.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/mcp/tools/debug.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/mcp/tools/defaults.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/mcp/tools/eload.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/mcp/tools/i2c.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/mcp/tools/logic.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/mcp/tools/logs.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/mcp/tools/measurement.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/mcp/tools/pip_tools.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/mcp/tools/power.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/mcp/tools/python_run.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/mcp/tools/scope.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/mcp/tools/solar.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/mcp/tools/spi.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/mcp/tools/uart.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/mcp/tools/usb.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/mcp/tools/webcam.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/mcp/tools/wifi.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/safe_unpickle.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/setup.cfg +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/setup.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/simple_hdlc.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/sort_utils.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/status.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/supply/__init__.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/supply/supply_tui.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/supply/websocket_client.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/terminal/__init__.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/terminal/core/__init__.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/terminal/core/executor.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/terminal/main.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/terminal/ui/__init__.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/terminal/ui/completer.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/terminal/ui/display.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/terminal/ui/logo.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/terminal/ui/repl.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/terminal/ui/themes.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/tests/test_box_lager_imports.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/tests/test_io_imports.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/update_check.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/vendor/PyCRC/CRC16.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/vendor/PyCRC/CRC16DNP.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/vendor/PyCRC/CRC16Kermit.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/vendor/PyCRC/CRC16SICK.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/vendor/PyCRC/CRC32.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/vendor/PyCRC/CRCCCITT.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/vendor/PyCRC/__init__.py +0 -0
- {lager_cli-0.4.2 → lager_cli-0.7.0}/vendor/__init__.py +0 -0
|
@@ -679,6 +679,103 @@ def import_boxes(file, merge, yes):
|
|
|
679
679
|
click.echo(click.style(f"[OK] Successfully imported {len(import_boxes_data)} box(es) from {file}", fg='green'))
|
|
680
680
|
|
|
681
681
|
|
|
682
|
+
@boxes.command('connect')
|
|
683
|
+
@click.option('--box', required=True, help='Name of the box to connect')
|
|
684
|
+
@click.option('--url', default='https://api.stoutdata.ai', help='Control plane URL')
|
|
685
|
+
@click.option('--api-key', required=True, help='API key for control plane authentication')
|
|
686
|
+
@click.option('--heartbeat-interval', default=30, type=int, help='Heartbeat interval in seconds (default: 30)')
|
|
687
|
+
@click.option('--yes', is_flag=True, help='Confirm the action without prompting.')
|
|
688
|
+
def connect(box, url, api_key, heartbeat_interval, yes):
|
|
689
|
+
"""Connect a box to a control plane for heartbeat reporting."""
|
|
690
|
+
import subprocess
|
|
691
|
+
import time
|
|
692
|
+
import requests
|
|
693
|
+
import shlex
|
|
694
|
+
from ...box_storage import get_box_ip, get_box_user
|
|
695
|
+
from ...core.ssh_utils import get_reusable_ssh_command
|
|
696
|
+
|
|
697
|
+
# Validate URL
|
|
698
|
+
from urllib.parse import urlparse
|
|
699
|
+
parsed = urlparse(url)
|
|
700
|
+
if parsed.scheme not in ('http', 'https'):
|
|
701
|
+
click.secho(f"Error: URL scheme must be http or https, got '{parsed.scheme}'", fg='red', err=True)
|
|
702
|
+
raise click.Abort()
|
|
703
|
+
if not parsed.hostname:
|
|
704
|
+
click.secho("Error: URL must include a hostname", fg='red', err=True)
|
|
705
|
+
raise click.Abort()
|
|
706
|
+
|
|
707
|
+
# Warn if control plane is unreachable from this machine (may still work from the box)
|
|
708
|
+
import requests as _req
|
|
709
|
+
try:
|
|
710
|
+
_req.get(f'{url.rstrip("/")}/healthz', timeout=5)
|
|
711
|
+
except _req.exceptions.RequestException:
|
|
712
|
+
click.secho(
|
|
713
|
+
f"Warning: Could not reach {url} from this machine. "
|
|
714
|
+
"It may still be reachable from the box.",
|
|
715
|
+
fg='yellow',
|
|
716
|
+
)
|
|
717
|
+
|
|
718
|
+
ip = get_box_ip(box)
|
|
719
|
+
if not ip:
|
|
720
|
+
click.secho(f"Error: Box '{box}' not found in configuration", fg='red', err=True)
|
|
721
|
+
raise click.Abort()
|
|
722
|
+
|
|
723
|
+
user = get_box_user(box) or 'lagerdata'
|
|
724
|
+
|
|
725
|
+
if not yes:
|
|
726
|
+
click.echo(f"\nConnect box '{box}' ({ip}) to control plane:")
|
|
727
|
+
click.echo(f" URL: {url}")
|
|
728
|
+
click.echo(f" API Key: {api_key[:8]}...")
|
|
729
|
+
click.echo(f" Interval: {heartbeat_interval}s")
|
|
730
|
+
click.echo()
|
|
731
|
+
if not click.confirm("Proceed?", default=False):
|
|
732
|
+
click.echo("Cancelled.")
|
|
733
|
+
return
|
|
734
|
+
|
|
735
|
+
config = json.dumps({
|
|
736
|
+
'url': url,
|
|
737
|
+
'api_key': api_key,
|
|
738
|
+
'heartbeat_interval_seconds': heartbeat_interval,
|
|
739
|
+
'enabled': True,
|
|
740
|
+
})
|
|
741
|
+
|
|
742
|
+
# Write config into the lager Docker container (avoids needing sudo on host)
|
|
743
|
+
click.echo(f"Writing control plane config to {box}...")
|
|
744
|
+
write_cmd = f'echo {shlex.quote(config)} | docker exec -u root -i lager tee /etc/lager/control_plane.json > /dev/null'
|
|
745
|
+
ssh_cmd = get_reusable_ssh_command(ip, user=user, command=write_cmd)
|
|
746
|
+
result = subprocess.run(ssh_cmd, capture_output=True, text=True)
|
|
747
|
+
if result.returncode != 0:
|
|
748
|
+
click.secho(f"Error writing config: {result.stderr.strip()}", fg='red', err=True)
|
|
749
|
+
raise click.Abort()
|
|
750
|
+
|
|
751
|
+
# Restart the box container
|
|
752
|
+
click.echo(f"Restarting lager container on {box}...")
|
|
753
|
+
ssh_cmd = get_reusable_ssh_command(ip, user=user, command='docker restart lager')
|
|
754
|
+
result = subprocess.run(ssh_cmd, capture_output=True, text=True)
|
|
755
|
+
if result.returncode != 0:
|
|
756
|
+
click.secho(f"Error restarting container: {result.stderr.strip()}", fg='red', err=True)
|
|
757
|
+
raise click.Abort()
|
|
758
|
+
|
|
759
|
+
# Verify the box is healthy
|
|
760
|
+
click.echo("Waiting for box to come back up...")
|
|
761
|
+
time.sleep(5)
|
|
762
|
+
try:
|
|
763
|
+
resp = requests.get(f'http://{ip}:5000/status', timeout=10)
|
|
764
|
+
if resp.status_code == 200:
|
|
765
|
+
data = resp.json()
|
|
766
|
+
click.secho(f"Box '{box}' connected successfully!", fg='green')
|
|
767
|
+
click.echo(f" Healthy: {data.get('healthy')}")
|
|
768
|
+
click.echo(f" Version: {data.get('version')}")
|
|
769
|
+
nets = data.get('nets', [])
|
|
770
|
+
if nets:
|
|
771
|
+
click.echo(f" Nets: {len(nets)}")
|
|
772
|
+
else:
|
|
773
|
+
click.secho(f"Warning: Box returned HTTP {resp.status_code}", fg='yellow')
|
|
774
|
+
except requests.exceptions.RequestException as e:
|
|
775
|
+
click.secho(f"Warning: Could not verify box status: {e}", fg='yellow')
|
|
776
|
+
click.echo("The config was written. The box may still be starting up.")
|
|
777
|
+
|
|
778
|
+
|
|
682
779
|
def compare_versions(v1, v2):
|
|
683
780
|
"""
|
|
684
781
|
Compare two version strings.
|
|
@@ -140,7 +140,9 @@ def create(ctx, image, mount_dir, shell):
|
|
|
140
140
|
@click.option('--entrypoint', help='Container entrypoint', required=False)
|
|
141
141
|
@click.option('--network', help='Network mode', required=False)
|
|
142
142
|
@click.option('--platform', help='Platform', required=False)
|
|
143
|
-
|
|
143
|
+
@click.option('--attach', '-a', 'attach_container', help='Attach to a running container by name', required=False, default=None)
|
|
144
|
+
@click.option('--shell', '-s', help='Shell to use when attaching (default: config shell or /bin/bash)', required=False, default=None)
|
|
145
|
+
def terminal(ctx, mount, user, group, name, detach, port, entrypoint, network, platform, attach_container, shell):
|
|
144
146
|
"""
|
|
145
147
|
Start interactive terminal
|
|
146
148
|
"""
|
|
@@ -151,6 +153,16 @@ def terminal(ctx, mount, user, group, name, detach, port, entrypoint, network, p
|
|
|
151
153
|
ctx.exit(1)
|
|
152
154
|
devenv_config = data['DEVENV']
|
|
153
155
|
|
|
156
|
+
if attach_container:
|
|
157
|
+
shell_cmd = shell or devenv_config.get('shell', '/bin/bash')
|
|
158
|
+
attach_args = ['docker', 'exec', '-it', attach_container, shell_cmd]
|
|
159
|
+
try:
|
|
160
|
+
proc = subprocess.run(attach_args, check=False)
|
|
161
|
+
except FileNotFoundError:
|
|
162
|
+
click.secho("Error: Docker is not installed or not in PATH", fg='red', err=True)
|
|
163
|
+
ctx.exit(1)
|
|
164
|
+
ctx.exit(proc.returncode)
|
|
165
|
+
|
|
154
166
|
image = devenv_config.get('image')
|
|
155
167
|
source_dir = os.path.dirname(path)
|
|
156
168
|
mount_dir = devenv_config.get('mount_dir')
|
|
@@ -66,7 +66,7 @@ def sigint_handler(kill_python, box_ip, _sig, _frame):
|
|
|
66
66
|
|
|
67
67
|
# Now restore original handler and kill remote process
|
|
68
68
|
signal.signal(signal.SIGINT, _ORIGINAL_SIGINT_HANDLER)
|
|
69
|
-
kill_python(signal.
|
|
69
|
+
kill_python(signal.SIGTERM)
|
|
70
70
|
|
|
71
71
|
|
|
72
72
|
def _do_exit(exit_code, box, session, downloads):
|
|
@@ -377,6 +377,19 @@ def run_python_internal(ctx, runnable, box, env, passenv, kill, download, allow_
|
|
|
377
377
|
click.echo(resp.text, err=True)
|
|
378
378
|
ctx.exit(1)
|
|
379
379
|
|
|
380
|
+
# Handle detached mode: parse JSON response and return immediately
|
|
381
|
+
if detach:
|
|
382
|
+
try:
|
|
383
|
+
data = resp.json()
|
|
384
|
+
process_id = data.get('lager_process_id', lager_process_id)
|
|
385
|
+
box_label = dut_name or box_ip
|
|
386
|
+
click.echo(f'Process detached (Process ID: {process_id})')
|
|
387
|
+
click.echo(f'To reattach: lager python --reattach {process_id} --box {box_label}')
|
|
388
|
+
click.echo(f'To kill: lager python --kill {process_id} --box {box_label}')
|
|
389
|
+
except Exception:
|
|
390
|
+
click.echo('Process detached.')
|
|
391
|
+
return
|
|
392
|
+
|
|
380
393
|
kill_python = functools.partial(session.kill_python, box_ip, lager_process_id)
|
|
381
394
|
handler = functools.partial(sigint_handler, kill_python, box_ip)
|
|
382
395
|
signal.signal(signal.SIGINT, handler)
|
|
@@ -414,6 +427,84 @@ def run_python_internal(ctx, runnable, box, env, passenv, kill, download, allow_
|
|
|
414
427
|
sys.exit(1)
|
|
415
428
|
|
|
416
429
|
|
|
430
|
+
def _handle_reattach(ctx, box_ip, process_id, session, dut_name):
|
|
431
|
+
"""
|
|
432
|
+
Reattach to a detached process and stream its output.
|
|
433
|
+
|
|
434
|
+
Ctrl+C kills the process (same as normal lager python).
|
|
435
|
+
Ctrl+D detaches from the stream without killing the process.
|
|
436
|
+
"""
|
|
437
|
+
try:
|
|
438
|
+
resp = session.attach_python(box_ip, process_id)
|
|
439
|
+
except requests.exceptions.ConnectionError:
|
|
440
|
+
click.secho(f'Could not connect to box at {box_ip}', fg='red', err=True)
|
|
441
|
+
ctx.exit(1)
|
|
442
|
+
except requests.exceptions.Timeout:
|
|
443
|
+
click.secho(f'Connection to box at {box_ip} timed out', fg='red', err=True)
|
|
444
|
+
ctx.exit(1)
|
|
445
|
+
|
|
446
|
+
if resp.status_code == 404 or resp.status_code == 422:
|
|
447
|
+
try:
|
|
448
|
+
error_data = resp.json()
|
|
449
|
+
click.secho(error_data.get('error', 'Process not found'), fg='red', err=True)
|
|
450
|
+
except Exception:
|
|
451
|
+
click.secho(f'Process not found: {process_id}', fg='red', err=True)
|
|
452
|
+
ctx.exit(1)
|
|
453
|
+
elif resp.status_code >= 400:
|
|
454
|
+
click.secho(f'Error: Box returned HTTP {resp.status_code}', fg='red', err=True)
|
|
455
|
+
ctx.exit(1)
|
|
456
|
+
|
|
457
|
+
# Ctrl+C = kill the process (same as normal lager python)
|
|
458
|
+
kill_python = functools.partial(session.kill_python, box_ip, process_id)
|
|
459
|
+
handler = functools.partial(sigint_handler, kill_python, box_ip)
|
|
460
|
+
signal.signal(signal.SIGINT, handler)
|
|
461
|
+
|
|
462
|
+
# Ctrl+D = detach (stdin EOF watcher thread)
|
|
463
|
+
detached_by_user = False
|
|
464
|
+
|
|
465
|
+
def watch_stdin_for_detach():
|
|
466
|
+
nonlocal detached_by_user
|
|
467
|
+
try:
|
|
468
|
+
while True:
|
|
469
|
+
ch = sys.stdin.read(1)
|
|
470
|
+
if not ch: # EOF (Ctrl+D)
|
|
471
|
+
detached_by_user = True
|
|
472
|
+
click.echo('\nDetaching...')
|
|
473
|
+
resp.close()
|
|
474
|
+
return
|
|
475
|
+
except (ValueError, OSError):
|
|
476
|
+
return
|
|
477
|
+
|
|
478
|
+
if sys.stdin.isatty():
|
|
479
|
+
stdin_thread = threading.Thread(target=watch_stdin_for_detach, daemon=True)
|
|
480
|
+
stdin_thread.start()
|
|
481
|
+
|
|
482
|
+
try:
|
|
483
|
+
for (datatype, content) in stream_python_output(resp):
|
|
484
|
+
if datatype == StreamDatatypes.EXIT:
|
|
485
|
+
signal.signal(signal.SIGINT, _ORIGINAL_SIGINT_HANDLER)
|
|
486
|
+
click.echo(f'Process exited with code {content}')
|
|
487
|
+
sys.exit(content)
|
|
488
|
+
elif datatype == StreamDatatypes.STDOUT:
|
|
489
|
+
click.echo(content.decode("utf-8", errors="ignore"), nl=False)
|
|
490
|
+
elif datatype == StreamDatatypes.STDERR:
|
|
491
|
+
click.echo(content.decode("utf-8", errors="ignore"), nl=False, err=True)
|
|
492
|
+
elif datatype == StreamDatatypes.OUTPUT:
|
|
493
|
+
click.echo(content)
|
|
494
|
+
except (BrokenPipeError, requests.exceptions.ChunkedEncodingError):
|
|
495
|
+
pass
|
|
496
|
+
except OutputFormatNotSupported:
|
|
497
|
+
click.secho('Response format not supported. Please upgrade lager-cli', fg='red', err=True)
|
|
498
|
+
sys.exit(1)
|
|
499
|
+
finally:
|
|
500
|
+
signal.signal(signal.SIGINT, _ORIGINAL_SIGINT_HANDLER)
|
|
501
|
+
|
|
502
|
+
if detached_by_user:
|
|
503
|
+
box_label = dut_name or box_ip
|
|
504
|
+
click.echo(f'To reattach: lager python --reattach {process_id} --box {box_label}')
|
|
505
|
+
click.echo(f'To kill: lager python --kill {process_id} --box {box_label}')
|
|
506
|
+
|
|
507
|
+
|
|
417
508
|
@click.command()
|
|
418
509
|
@click.pass_context
|
|
419
510
|
@click.argument('runnable', required=False, type=click.Path(exists=True))
|
|
@@ -424,17 +515,19 @@ def run_python_internal(ctx, runnable, box, env, passenv, kill, download, allow_
|
|
|
424
515
|
@click.option(
|
|
425
516
|
'--passenv',
|
|
426
517
|
multiple=True, help='Environment variable to inherit')
|
|
427
|
-
@click.option('--kill',
|
|
518
|
+
@click.option('--kill', default=None, help='Kill a specific process by process ID')
|
|
519
|
+
@click.option('--kill-all', is_flag=True, default=False, help='Kill all running scripts')
|
|
428
520
|
@click.option('--download', type=click.Path(exists=False, dir_okay=False), multiple=True, help='File to download after completion')
|
|
429
521
|
@click.option('--allow-overwrite', is_flag=True, default=False, help='Overwrite existing files when downloading')
|
|
430
|
-
@click.option('--signal', 'signum', default='SIGTERM', type=_SIGNAL_CHOICES, help='Signal to use with --kill', show_default=True)
|
|
522
|
+
@click.option('--signal', 'signum', default='SIGTERM', type=_SIGNAL_CHOICES, help='Signal to use with --kill/--kill-all', show_default=True)
|
|
431
523
|
@click.option('--timeout', type=click.IntRange(min=0), default=0, required=False, help='Max runtime in seconds (0=no timeout)')
|
|
432
524
|
@click.option('--detach', '-d', is_flag=True, required=False, default=False, help='Detach')
|
|
433
525
|
@click.option('--port', '-p', multiple=True, help='Port forwarding (SRC_PORT[:DST_PORT][/PROTOCOL])', type=PortForwardType())
|
|
434
526
|
@click.option('--org', default=None, hidden=True)
|
|
435
527
|
@click.option('--add-file', type=click.Path(exists=True, dir_okay=False), multiple=True, help='File to upload with script')
|
|
528
|
+
@click.option('--reattach', default=None, help='Reattach to detached process by process ID')
|
|
436
529
|
@click.argument('args', nargs=-1)
|
|
437
|
-
def python(ctx, runnable, box, env, passenv, kill, download, allow_overwrite, signum, timeout, detach, port, org, add_file, args):
|
|
530
|
+
def python(ctx, runnable, box, env, passenv, kill, kill_all, download, allow_overwrite, signum, timeout, detach, port, org, add_file, reattach, args):
|
|
438
531
|
"""Run Python script on box"""
|
|
439
532
|
from ...box_storage import resolve_and_validate_box
|
|
440
533
|
|
|
@@ -442,8 +535,36 @@ def python(ctx, runnable, box, env, passenv, kill, download, allow_overwrite, si
|
|
|
442
535
|
box_name = box
|
|
443
536
|
box_ip = resolve_and_validate_box(ctx, box_name)
|
|
444
537
|
|
|
445
|
-
if not runnable and not kill:
|
|
446
|
-
raise click.UsageError('Please supply a RUNNABLE
|
|
538
|
+
if not runnable and not kill and not kill_all and not reattach:
|
|
539
|
+
raise click.UsageError('Please supply a RUNNABLE, --kill, --kill-all, or --reattach option')
|
|
540
|
+
|
|
541
|
+
if kill:
|
|
542
|
+
session = ctx.obj.get_session_for_box(box_ip, box_name=box_name)
|
|
543
|
+
signum_val = _get_signal_number(signum)
|
|
544
|
+
resp = session.kill_python(box_ip, kill, signum_val)
|
|
545
|
+
if resp.status_code == 422:
|
|
546
|
+
try:
|
|
547
|
+
error_data = resp.json()
|
|
548
|
+
click.secho(error_data.get('error', 'Invalid request'), fg='red', err=True)
|
|
549
|
+
except Exception:
|
|
550
|
+
click.secho(f'Invalid process ID: {kill}', fg='red', err=True)
|
|
551
|
+
ctx.exit(1)
|
|
552
|
+
resp.raise_for_status()
|
|
553
|
+
click.echo(f'Process {kill} killed')
|
|
554
|
+
return
|
|
555
|
+
|
|
556
|
+
if kill_all:
|
|
557
|
+
session = ctx.obj.get_session_for_box(box_ip, box_name=box_name)
|
|
558
|
+
signum_val = _get_signal_number(signum)
|
|
559
|
+
resp = session.kill_python(box_ip, None, signum_val)
|
|
560
|
+
resp.raise_for_status()
|
|
561
|
+
click.echo('All processes killed')
|
|
562
|
+
return
|
|
563
|
+
|
|
564
|
+
if reattach:
|
|
565
|
+
session = ctx.obj.get_session_for_box(box_ip, box_name=box_name)
|
|
566
|
+
_handle_reattach(ctx, box_ip, reattach, session, box_name)
|
|
567
|
+
return
|
|
447
568
|
|
|
448
569
|
if not allow_overwrite:
|
|
449
570
|
for filename in download:
|
|
@@ -457,4 +578,4 @@ def python(ctx, runnable, box, env, passenv, kill, download, allow_overwrite, si
|
|
|
457
578
|
if box_name:
|
|
458
579
|
env.append(f'LAGER_BOX={box_name}')
|
|
459
580
|
|
|
460
|
-
run_python_internal(ctx, runnable, box_ip, env, passenv,
|
|
581
|
+
run_python_internal(ctx, runnable, box_ip, env, passenv, False, download, allow_overwrite, signum, timeout, detach, port, org, args, add_file, dut_name=box_name)
|
|
@@ -24,7 +24,6 @@ from .logs import logs
|
|
|
24
24
|
from .webcam import webcam
|
|
25
25
|
from .install import install
|
|
26
26
|
from .uninstall import uninstall
|
|
27
|
-
from .factory import factory
|
|
28
27
|
|
|
29
28
|
__all__ = [
|
|
30
29
|
"defaults",
|
|
@@ -36,5 +35,4 @@ __all__ = [
|
|
|
36
35
|
"webcam",
|
|
37
36
|
"install",
|
|
38
37
|
"uninstall",
|
|
39
|
-
"factory",
|
|
40
38
|
]
|
|
@@ -304,8 +304,8 @@ def update(ctx, box, update_all, yes, skip_restart, version, verbose, force):
|
|
|
304
304
|
ctx.exit(0)
|
|
305
305
|
|
|
306
306
|
# Initialize progress bar (only in non-verbose mode)
|
|
307
|
-
# Total steps: connectivity, repo check, git state check, fetch, checkout/pull, flatten, udev, sudoers, docker stop, [force image removal], docker build, cleanup, /etc/lager, docker start, binaries, jlink, verify, version
|
|
308
|
-
total_steps =
|
|
307
|
+
# Total steps: connectivity, repo check, git state check, fetch, checkout/pull, flatten, udev, sudoers, docker stop, [force image removal], docker build, cleanup, /etc/lager, docker start, binaries, jlink, verify, version
|
|
308
|
+
total_steps = 20 if force else 19
|
|
309
309
|
progress = None if verbose else ProgressBar(total_steps=total_steps)
|
|
310
310
|
|
|
311
311
|
if not verbose:
|
|
@@ -1206,85 +1206,6 @@ fi
|
|
|
1206
1206
|
if box_cli_version and box:
|
|
1207
1207
|
update_box_version(box, box_cli_version)
|
|
1208
1208
|
|
|
1209
|
-
# Step 14: Sync .lager boxes to factory dashboard (if factory is deployed)
|
|
1210
|
-
if progress:
|
|
1211
|
-
progress.update("Syncing factory...")
|
|
1212
|
-
log('Syncing factory dashboard...', nl=False)
|
|
1213
|
-
|
|
1214
|
-
try:
|
|
1215
|
-
# Check if factory directory exists on the host (container was stopped earlier)
|
|
1216
|
-
factory_check = run_ssh_command_with_output(
|
|
1217
|
-
'test -d /home/lagerdata/factory',
|
|
1218
|
-
timeout_secs=10
|
|
1219
|
-
)
|
|
1220
|
-
|
|
1221
|
-
if factory_check.returncode != 0:
|
|
1222
|
-
log_status('Syncing factory dashboard...', 'SKIPPED (no factory)', 'yellow')
|
|
1223
|
-
else:
|
|
1224
|
-
import json as json_mod
|
|
1225
|
-
# Build boxes.json from all saved boxes (dict keyed by box name)
|
|
1226
|
-
saved_boxes = list_boxes()
|
|
1227
|
-
boxes_dict = {}
|
|
1228
|
-
for name, box_info in sorted(saved_boxes.items()):
|
|
1229
|
-
if isinstance(box_info, dict):
|
|
1230
|
-
ip = box_info.get('ip', '')
|
|
1231
|
-
user = box_info.get('user', 'lagerdata')
|
|
1232
|
-
else:
|
|
1233
|
-
ip = box_info
|
|
1234
|
-
user = 'lagerdata'
|
|
1235
|
-
|
|
1236
|
-
if not ip or ip == 'unknown':
|
|
1237
|
-
continue
|
|
1238
|
-
|
|
1239
|
-
# For the box being updated, use localhost
|
|
1240
|
-
# so the factory container can reach its own lager container
|
|
1241
|
-
if name == box_name:
|
|
1242
|
-
entry_ip = '127.0.0.1'
|
|
1243
|
-
else:
|
|
1244
|
-
entry_ip = ip
|
|
1245
|
-
|
|
1246
|
-
boxes_dict[name] = {
|
|
1247
|
-
'name': name,
|
|
1248
|
-
'ip': entry_ip,
|
|
1249
|
-
'ssh_user': user,
|
|
1250
|
-
'container': 'lager',
|
|
1251
|
-
}
|
|
1252
|
-
|
|
1253
|
-
boxes_json = json_mod.dumps(boxes_dict, indent=2)
|
|
1254
|
-
# Escape single quotes for shell
|
|
1255
|
-
escaped_json = boxes_json.replace("'", "'\\''")
|
|
1256
|
-
|
|
1257
|
-
# Write boxes.json to the host filesystem
|
|
1258
|
-
write_result = run_ssh_command_with_output(
|
|
1259
|
-
f"echo '{escaped_json}' > /home/lagerdata/factory/webapp/boxes.json",
|
|
1260
|
-
timeout_secs=10
|
|
1261
|
-
)
|
|
1262
|
-
|
|
1263
|
-
if write_result.returncode != 0:
|
|
1264
|
-
log_status('Syncing factory dashboard...', 'FAILED (write)', 'red')
|
|
1265
|
-
else:
|
|
1266
|
-
# Start factory container (it was stopped by the "stop all" step)
|
|
1267
|
-
start_result = run_ssh_command_with_output(
|
|
1268
|
-
'cd /home/lagerdata/factory && docker compose up -d',
|
|
1269
|
-
timeout_secs=60
|
|
1270
|
-
)
|
|
1271
|
-
if start_result.returncode != 0:
|
|
1272
|
-
log_status('Syncing factory dashboard...', 'FAILED (start)', 'red')
|
|
1273
|
-
else:
|
|
1274
|
-
# Copy boxes.json into the running container and restart
|
|
1275
|
-
# so it picks up the new config (docker cp works even without bind mounts)
|
|
1276
|
-
cp_result = run_ssh_command_with_output(
|
|
1277
|
-
'docker cp /home/lagerdata/factory/webapp/boxes.json factory:/factory/webapp/boxes.json && '
|
|
1278
|
-
'docker restart factory',
|
|
1279
|
-
timeout_secs=30
|
|
1280
|
-
)
|
|
1281
|
-
if cp_result.returncode == 0:
|
|
1282
|
-
log_status('Syncing factory dashboard...', f'OK ({len(boxes_dict)} boxes)', 'green')
|
|
1283
|
-
else:
|
|
1284
|
-
log_status('Syncing factory dashboard...', 'FAILED (copy)', 'red')
|
|
1285
|
-
except Exception as e:
|
|
1286
|
-
log_status('Syncing factory dashboard...', f'FAILED ({e})', 'red')
|
|
1287
|
-
|
|
1288
1209
|
# Finish progress bar
|
|
1289
1210
|
if progress:
|
|
1290
1211
|
progress.finish(success=True)
|
|
@@ -625,7 +625,6 @@ class DirectHTTPSession:
|
|
|
625
625
|
'lager_process_id': lager_process_id,
|
|
626
626
|
'signal': int(sig)
|
|
627
627
|
})
|
|
628
|
-
response.raise_for_status()
|
|
629
628
|
return response
|
|
630
629
|
|
|
631
630
|
def box_hello(self, box):
|
|
@@ -643,6 +642,26 @@ class DirectHTTPSession:
|
|
|
643
642
|
url = f'{self.base_url}/nets'
|
|
644
643
|
return self.session.get(url)
|
|
645
644
|
|
|
645
|
+
def attach_python(self, box, lager_process_id):
|
|
646
|
+
"""
|
|
647
|
+
Reattach to a detached Python process on box.
|
|
648
|
+
|
|
649
|
+
Args:
|
|
650
|
+
box: Box IP (ignored, uses self.box_ip)
|
|
651
|
+
lager_process_id: Process ID to reattach to
|
|
652
|
+
|
|
653
|
+
Returns:
|
|
654
|
+
requests.Response object with streaming content
|
|
655
|
+
"""
|
|
656
|
+
url = f'{self.base_url}/python/attach'
|
|
657
|
+
response = self.session.post(
|
|
658
|
+
url,
|
|
659
|
+
json={'lager_process_id': lager_process_id},
|
|
660
|
+
stream=True,
|
|
661
|
+
timeout=(7, None),
|
|
662
|
+
)
|
|
663
|
+
return response
|
|
664
|
+
|
|
646
665
|
def download_file(self, box, filename):
|
|
647
666
|
"""
|
|
648
667
|
Download a file from box via direct HTTP.
|
|
@@ -302,6 +302,18 @@ else
|
|
|
302
302
|
fi
|
|
303
303
|
fi
|
|
304
304
|
|
|
305
|
+
# Always ensure current user is in docker group (handles multi-user scenario)
|
|
306
|
+
print_info "Ensuring ${BOX_USER} is in the docker group..."
|
|
307
|
+
ssh_t "${BOX_USER}@${BOX_IP}" "sudo usermod -aG docker \$USER" 2>/dev/null || true
|
|
308
|
+
|
|
309
|
+
# Verify docker access works for this user
|
|
310
|
+
if ssh $SSH_OPTS "${BOX_USER}@${BOX_IP}" "docker info >/dev/null 2>&1"; then
|
|
311
|
+
print_success "${BOX_USER} has docker access"
|
|
312
|
+
else
|
|
313
|
+
print_warning "Docker group membership may require re-login"
|
|
314
|
+
print_info "If Docker commands fail, log out and back in to the box, then re-run install"
|
|
315
|
+
fi
|
|
316
|
+
|
|
305
317
|
# =============================================================================
|
|
306
318
|
# STEP 1: SSH Key Setup
|
|
307
319
|
# =============================================================================
|
|
@@ -76,7 +76,6 @@ update_check.py
|
|
|
76
76
|
./commands/utility/binaries.py
|
|
77
77
|
./commands/utility/defaults.py
|
|
78
78
|
./commands/utility/exec_.py
|
|
79
|
-
./commands/utility/factory.py
|
|
80
79
|
./commands/utility/install.py
|
|
81
80
|
./commands/utility/logs.py
|
|
82
81
|
./commands/utility/pip.py
|
|
@@ -276,7 +275,6 @@ commands/utility/__init__.py
|
|
|
276
275
|
commands/utility/binaries.py
|
|
277
276
|
commands/utility/defaults.py
|
|
278
277
|
commands/utility/exec_.py
|
|
279
|
-
commands/utility/factory.py
|
|
280
278
|
commands/utility/install.py
|
|
281
279
|
commands/utility/logs.py
|
|
282
280
|
commands/utility/pip.py
|
|
@@ -72,7 +72,36 @@ from .commands.measurement.energy import energy
|
|
|
72
72
|
from .commands.box import hello, boxes, instruments, nets, ssh
|
|
73
73
|
|
|
74
74
|
# Utility commands (from commands.utility package)
|
|
75
|
-
from .commands.utility import defaults, binaries, update, pip, exec_, logs, webcam, install, uninstall
|
|
75
|
+
from .commands.utility import defaults, binaries, update, pip, exec_, logs, webcam, install, uninstall
|
|
76
|
+
|
|
77
|
+
def _check_venv_shadowing():
|
|
78
|
+
"""Warn if a system-installed lager is shadowing the venv version."""
|
|
79
|
+
virtual_env = os.environ.get('VIRTUAL_ENV')
|
|
80
|
+
if not virtual_env:
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
# If VIRTUAL_ENV is set but sys.prefix doesn't point to it, the
|
|
84
|
+
# system `lager` entry-point script (with a hardcoded shebang) is
|
|
85
|
+
# running instead of the venv's copy. This is the exact shadowing
|
|
86
|
+
# scenario we want to catch.
|
|
87
|
+
if os.path.realpath(sys.prefix) == os.path.realpath(virtual_env):
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
click.secho(
|
|
91
|
+
f"WARNING: Lager CLI v{__version__} is running from a system Python ({sys.prefix}), "
|
|
92
|
+
f"not from your active virtual environment ({virtual_env}).",
|
|
93
|
+
fg='yellow', err=True,
|
|
94
|
+
)
|
|
95
|
+
click.secho(
|
|
96
|
+
"This can cause version mismatches. To fix:\n"
|
|
97
|
+
" hash -r # clear shell cache, then retry\n"
|
|
98
|
+
" pip install --force-reinstall lager-cli # if hash -r alone doesn't help\n"
|
|
99
|
+
"Or uninstall the system version:\n"
|
|
100
|
+
" deactivate && pip uninstall lager-cli && source <venv>/bin/activate",
|
|
101
|
+
fg='yellow', err=True,
|
|
102
|
+
)
|
|
103
|
+
click.echo(err=True)
|
|
104
|
+
|
|
76
105
|
|
|
77
106
|
def _decode_environment():
|
|
78
107
|
for key in os.environ:
|
|
@@ -89,6 +118,8 @@ def cli(ctx=None, see_version=None, debug=False, colorize=False, interpreter=Non
|
|
|
89
118
|
"""
|
|
90
119
|
Lager CLI
|
|
91
120
|
"""
|
|
121
|
+
_check_venv_shadowing()
|
|
122
|
+
|
|
92
123
|
if os.getenv('LAGER_DECODE_ENV'):
|
|
93
124
|
_decode_environment()
|
|
94
125
|
|
|
@@ -141,7 +172,6 @@ cli.add_command(logs)
|
|
|
141
172
|
cli.add_command(binaries)
|
|
142
173
|
cli.add_command(install)
|
|
143
174
|
cli.add_command(uninstall)
|
|
144
|
-
cli.add_command(factory)
|
|
145
175
|
cli.add_command(terminal_cmd)
|
|
146
176
|
|
|
147
177
|
def _schedule_update_check(ctx):
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
# Copyright 2024-2026 Lager Data LLC
|
|
2
|
-
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
-
|
|
4
|
-
"""
|
|
5
|
-
lager.commands.utility.factory
|
|
6
|
-
|
|
7
|
-
Start the Lager Factory webapp.
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
import os
|
|
11
|
-
import socket
|
|
12
|
-
import subprocess
|
|
13
|
-
import sys
|
|
14
|
-
|
|
15
|
-
import click
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def _get_local_ip():
|
|
19
|
-
"""Best-effort detection of the machine's LAN IP address."""
|
|
20
|
-
try:
|
|
21
|
-
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
22
|
-
s.settimeout(0)
|
|
23
|
-
s.connect(('10.254.254.254', 1))
|
|
24
|
-
ip = s.getsockname()[0]
|
|
25
|
-
s.close()
|
|
26
|
-
return ip
|
|
27
|
-
except Exception:
|
|
28
|
-
return '127.0.0.1'
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
@click.command('factory')
|
|
32
|
-
@click.option('--port', default=5001, type=int, help='Port number')
|
|
33
|
-
@click.option('--host', default='0.0.0.0', help='Host to bind to')
|
|
34
|
-
def factory(port, host):
|
|
35
|
-
"""Start the Lager Factory."""
|
|
36
|
-
if port < 0 or port > 65535:
|
|
37
|
-
click.secho(f'Error: Port must be between 0 and 65535, got {port}.', fg='red', err=True)
|
|
38
|
-
raise SystemExit(1)
|
|
39
|
-
|
|
40
|
-
# Locate webapp directory relative to CLI package
|
|
41
|
-
cli_dir = os.path.dirname(os.path.dirname(os.path.dirname(
|
|
42
|
-
os.path.abspath(__file__)
|
|
43
|
-
)))
|
|
44
|
-
webapp_dir = os.path.join(os.path.dirname(cli_dir), 'factory', 'webapp')
|
|
45
|
-
|
|
46
|
-
if not os.path.isdir(webapp_dir):
|
|
47
|
-
click.secho(
|
|
48
|
-
f'Factory webapp not found at {webapp_dir}', fg='red', err=True
|
|
49
|
-
)
|
|
50
|
-
raise SystemExit(1)
|
|
51
|
-
|
|
52
|
-
run_py = os.path.join(webapp_dir, 'run.py')
|
|
53
|
-
if not os.path.isfile(run_py):
|
|
54
|
-
click.secho(
|
|
55
|
-
f'run.py not found at {run_py}', fg='red', err=True
|
|
56
|
-
)
|
|
57
|
-
raise SystemExit(1)
|
|
58
|
-
|
|
59
|
-
local_ip = _get_local_ip()
|
|
60
|
-
click.secho('Lager Factory starting...', fg='green')
|
|
61
|
-
click.secho(f' Local: http://127.0.0.1:{port}')
|
|
62
|
-
click.secho(f' Network: http://{local_ip}:{port}')
|
|
63
|
-
|
|
64
|
-
env = dict(os.environ, PORT=str(port), HOST=host)
|
|
65
|
-
|
|
66
|
-
try:
|
|
67
|
-
subprocess.run(
|
|
68
|
-
[sys.executable, 'run.py'],
|
|
69
|
-
cwd=webapp_dir,
|
|
70
|
-
env=env,
|
|
71
|
-
)
|
|
72
|
-
except KeyboardInterrupt:
|
|
73
|
-
pass
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|