lager-cli 0.31.2__tar.gz → 0.31.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.31.2/lager_cli.egg-info → lager_cli-0.31.4}/PKG-INFO +1 -1
- {lager_cli-0.31.2 → lager_cli-0.31.4}/__init__.py +1 -1
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/box/_host_ops.py +130 -46
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/box/_mount_prep.py +6 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/box/config.py +3 -3
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/box/dut.py +24 -14
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/utility/install.py +72 -47
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/utility/update.py +25 -19
- {lager_cli-0.31.2 → lager_cli-0.31.4}/deployment/scripts/setup_and_deploy_box.sh +192 -92
- {lager_cli-0.31.2 → lager_cli-0.31.4/lager_cli.egg-info}/PKG-INFO +1 -1
- {lager_cli-0.31.2 → lager_cli-0.31.4}/LICENSE +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/MANIFEST.in +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/README.md +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/__main__.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/address_utils.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/battery/__init__.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/battery/battery_tui.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/battery/websocket_client.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/box_storage.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/__init__.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/box/__init__.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/box/_pip_validation.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/box/_shim_verbs.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/box/_ssh.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/box/authorize.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/box/box_group.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/box/boxes.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/box/diagnose.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/box/hello.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/box/instruments.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/box/labjack_pins.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/box/lock.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/box/net_tui.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/box/nets.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/box/ssh.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/communication/__init__.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/communication/ble.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/communication/blufi.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/communication/i2c.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/communication/router.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/communication/spi.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/communication/uart.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/communication/usb.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/communication/websocket_client.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/communication/wifi.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/development/__init__.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/development/arm.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/development/debug/__init__.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/development/debug/commands.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/development/debug/gdb.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/development/debug/net_cache.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/development/debug/service_client.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/development/debug/service_helper.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/development/debug/tunnel.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/development/devenv.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/development/python.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/measurement/__init__.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/measurement/adc.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/measurement/dac.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/measurement/energy.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/measurement/gpi.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/measurement/gpo.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/measurement/logic.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/measurement/scope.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/measurement/thermocouple.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/measurement/watt.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/power/__init__.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/power/battery.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/power/eload.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/power/solar.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/power/supply.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/utility/__init__.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/utility/binaries.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/utility/defaults.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/utility/exec_.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/utility/install_wheel.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/utility/logs.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/utility/uninstall.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/commands/utility/webcam.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/config.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/context/__init__.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/context/ci_detection.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/context/constants.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/context/core.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/context/error_handlers.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/context/session.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/core/__init__.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/core/group_usage.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/core/matchers.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/core/net_group.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/core/net_helpers.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/core/net_storage.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/core/param_types.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/core/ssh_utils.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/core/utils.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/core/version_skew.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/core/ws_diagnose.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/deployment/__init__.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/deployment/scripts/__init__.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/deployment/scripts/convert_to_sparse_checkout.sh +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/deployment/scripts/setup_ssh_key.sh +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/deployment/security/__init__.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/deployment/security/secure_box_firewall.sh +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/elftools/__init__.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/elftools/common/__init__.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/elftools/common/construct_utils.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/elftools/common/exceptions.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/elftools/common/py3compat.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/elftools/common/utils.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/elftools/construct/__init__.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/elftools/construct/adapters.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/elftools/construct/core.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/elftools/construct/debug.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/elftools/construct/macros.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/elftools/dwarf/__init__.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/elftools/dwarf/abbrevtable.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/elftools/dwarf/aranges.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/elftools/dwarf/callframe.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/elftools/dwarf/compileunit.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/elftools/dwarf/constants.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/elftools/dwarf/descriptions.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/elftools/dwarf/die.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/elftools/dwarf/dwarf_expr.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/elftools/dwarf/dwarfinfo.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/elftools/dwarf/enums.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/elftools/dwarf/lineprogram.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/elftools/dwarf/locationlists.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/elftools/dwarf/namelut.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/elftools/dwarf/ranges.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/elftools/dwarf/structs.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/elftools/ehabi/__init__.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/elftools/ehabi/constants.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/elftools/ehabi/decoder.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/elftools/ehabi/ehabiinfo.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/elftools/ehabi/structs.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/elftools/elf/__init__.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/elftools/elf/constants.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/elftools/elf/descriptions.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/elftools/elf/dynamic.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/elftools/elf/elffile.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/elftools/elf/enums.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/elftools/elf/gnuversions.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/elftools/elf/hash.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/elftools/elf/notes.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/elftools/elf/relocation.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/elftools/elf/sections.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/elftools/elf/segments.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/elftools/elf/structs.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/errors.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/exceptions.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/impl/__init__.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/impl/box_config.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/impl/communication/__init__.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/impl/communication/ble.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/impl/communication/blufi.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/impl/communication/i2c.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/impl/communication/router.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/impl/communication/spi.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/impl/communication/uart.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/impl/communication/wifi.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/impl/custom_devices.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/impl/device/__init__.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/impl/device/arm.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/impl/device/hello.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/impl/device/usb.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/impl/device/webcam.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/impl/measurement/__init__.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/impl/measurement/adc.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/impl/measurement/dac.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/impl/measurement/energy.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/impl/measurement/gpio.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/impl/measurement/scope.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/impl/measurement/scope_stream.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/impl/measurement/thermocouple.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/impl/measurement/watt.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/impl/net.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/impl/power/__init__.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/impl/power/battery.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/impl/power/eload.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/impl/power/enable_disable.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/impl/power/solar.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/impl/power/supply.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/impl/query_instruments.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/lager_cli.egg-info/SOURCES.txt +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/lager_cli.egg-info/dependency_links.txt +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/lager_cli.egg-info/entry_points.txt +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/lager_cli.egg-info/requires.txt +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/lager_cli.egg-info/top_level.txt +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/main.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/pyproject.toml +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/safe_unpickle.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/setup.cfg +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/setup.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/simple_hdlc.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/sort_utils.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/status.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/supply/__init__.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/supply/supply_tui.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/supply/websocket_client.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/terminal/__init__.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/terminal/core/__init__.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/terminal/core/executor.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/terminal/main.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/terminal/ui/__init__.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/terminal/ui/completer.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/terminal/ui/display.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/terminal/ui/logo.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/terminal/ui/repl.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/terminal/ui/themes.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/tests/test_box_lager_imports.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/tests/test_box_storage.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/tests/test_io_imports.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/update_check.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/vendor/PyCRC/CRC16.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/vendor/PyCRC/CRC16DNP.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/vendor/PyCRC/CRC16Kermit.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/vendor/PyCRC/CRC16SICK.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/vendor/PyCRC/CRC32.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/vendor/PyCRC/CRCCCITT.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/vendor/PyCRC/__init__.py +0 -0
- {lager_cli-0.31.2 → lager_cli-0.31.4}/vendor/__init__.py +0 -0
|
@@ -16,6 +16,7 @@ branch without a real box.
|
|
|
16
16
|
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
|
|
19
|
+
import re
|
|
19
20
|
import shlex
|
|
20
21
|
from dataclasses import dataclass
|
|
21
22
|
from typing import Dict, List, Optional
|
|
@@ -39,45 +40,123 @@ _UDEV_HEADER = (
|
|
|
39
40
|
"# Managed by `lager box config udev`; manual edits are overwritten on apply.\n"
|
|
40
41
|
)
|
|
41
42
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
" sudo chmod 440 /etc/sudoers.d/lagerdata-udev\n"
|
|
54
|
-
"\n"
|
|
55
|
-
"Then re-run `lager box config apply`."
|
|
56
|
-
)
|
|
43
|
+
# --- Box-config sudoers rule: single source of truth ------------------------
|
|
44
|
+
#
|
|
45
|
+
# `lager install` and `lager update` write this rule to BOXCFG_SUDOERS_PATH
|
|
46
|
+
# so `lager box config apply` can run apt-get/sysctl/mkdir/chown over
|
|
47
|
+
# BatchMode SSH. The rule must name the actual login user: it used to
|
|
48
|
+
# hardcode `lagerdata`, so on boxes with a different user (e.g. the juultest
|
|
49
|
+
# fleet) the grant never matched — install ended with "Sudoers file installed
|
|
50
|
+
# but `sudo -n apt-get` still fails" and every apply needed manual setup.
|
|
51
|
+
#
|
|
52
|
+
# The username lands inside a root-owned sudoers file, so callers must gate
|
|
53
|
+
# interpolation on is_valid_unix_username().
|
|
57
54
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
"
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
"
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
)
|
|
55
|
+
BOXCFG_SUDOERS_PATH = "/etc/sudoers.d/lager-box-config"
|
|
56
|
+
BOXCFG_SUDOERS_MARKER = "/etc/lager/.boxcfg-sudoers-v2"
|
|
57
|
+
|
|
58
|
+
# useradd's default charset plus uppercase and dots (both appear in real
|
|
59
|
+
# deployments and are harmless in sudoers). Every allowed character is inert
|
|
60
|
+
# inside the single-quoted rule strings and sudoers syntax; anything else
|
|
61
|
+
# (spaces, quotes, $(), newlines, ...) is refused.
|
|
62
|
+
_UNIX_USERNAME_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_.-]*\$?")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def is_valid_unix_username(user: Optional[str]) -> bool:
|
|
66
|
+
"""True for plain unix usernames — the gate for interpolating a name
|
|
67
|
+
into sudoers content."""
|
|
68
|
+
return bool(user) and _UNIX_USERNAME_RE.fullmatch(user) is not None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def boxcfg_sudoers_rules(user: str = "lagerdata") -> List[str]:
|
|
72
|
+
"""The NOPASSWD rule lines for `lager box config apply`. tee/rm/sysctl
|
|
73
|
+
are path-scoped so a compromised account cannot escalate via them;
|
|
74
|
+
apt-get and mkdir/chown are unscoped because the package list and host
|
|
75
|
+
paths are user-defined. SETENV on apt-get is required so
|
|
76
|
+
DEBIAN_FRONTEND=noninteractive propagates and package postinst scripts
|
|
77
|
+
(iptables-persistent, etc.) don't prompt.
|
|
78
|
+
|
|
79
|
+
Raises ValueError on a non-plain username: callers validate first, so
|
|
80
|
+
this is a backstop that makes a future caller that forgets fail loudly
|
|
81
|
+
instead of interpolating into root-owned sudoers content."""
|
|
82
|
+
if not is_valid_unix_username(user):
|
|
83
|
+
raise ValueError(f"invalid unix username for sudoers rule: {user!r}")
|
|
84
|
+
return [
|
|
85
|
+
f"{user} ALL=(root) NOPASSWD: SETENV: /usr/bin/apt-get",
|
|
86
|
+
f"{user} ALL=(root) NOPASSWD: /bin/mkdir, /bin/chown, "
|
|
87
|
+
"/usr/sbin/sysctl --system, /sbin/sysctl --system, "
|
|
88
|
+
f"/usr/bin/tee {SYSCTL_CONF_PATH}, "
|
|
89
|
+
f"/bin/rm -f {SYSCTL_CONF_PATH}, "
|
|
90
|
+
"/bin/cp /etc/lager/box_config.applied.json /etc/lager/box_config.json",
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def boxcfg_sudoers_bootstrap_cmd(user: str = "lagerdata") -> str:
|
|
95
|
+
"""One shell command that installs the box-config sudoers rule plus the
|
|
96
|
+
versioned marker file. Used verbatim by `lager install` and
|
|
97
|
+
`lager update`; the manual snippet in sudoers_bootstrap() mirrors it."""
|
|
98
|
+
quoted_rules = " ".join(f"'{r}'" for r in boxcfg_sudoers_rules(user))
|
|
99
|
+
return (
|
|
100
|
+
f"printf '%s\\n' {quoted_rules} "
|
|
101
|
+
f"| sudo tee {BOXCFG_SUDOERS_PATH} >/dev/null "
|
|
102
|
+
f"&& sudo chmod 440 {BOXCFG_SUDOERS_PATH} "
|
|
103
|
+
f"&& sudo touch {BOXCFG_SUDOERS_MARKER} "
|
|
104
|
+
f"&& sudo chmod 644 {BOXCFG_SUDOERS_MARKER}"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def udev_sudoers_bootstrap(user: str = "lagerdata") -> str:
|
|
109
|
+
"""Manual-fix text for a box missing the udev sudoers grant."""
|
|
110
|
+
# Error-path text renderer: must never raise (it runs while composing a
|
|
111
|
+
# failure message). The user comes from local box storage unvalidated
|
|
112
|
+
# (resolve_box_user), so a non-plain name falls back to the historical
|
|
113
|
+
# default rather than being interpolated into a paste-into-root-shell
|
|
114
|
+
# snippet; the operator substitutes their real user.
|
|
115
|
+
if not is_valid_unix_username(user):
|
|
116
|
+
user = "lagerdata"
|
|
117
|
+
return (
|
|
118
|
+
"Applying user udev rules needs the passwordless-sudo udev grant that the "
|
|
119
|
+
"box setup script installs. If it's missing (older box), re-run the box "
|
|
120
|
+
"setup/deploy, or add it ONCE on the box:\n"
|
|
121
|
+
"\n"
|
|
122
|
+
" sudo tee /etc/sudoers.d/lagerdata-udev >/dev/null <<'SUDOERS'\n"
|
|
123
|
+
f" {user} ALL=(ALL) NOPASSWD: /bin/cp /tmp/*.rules /etc/udev/rules.d/\n"
|
|
124
|
+
f" {user} ALL=(ALL) NOPASSWD: /bin/chmod 644 /etc/udev/rules.d/*.rules\n"
|
|
125
|
+
f" {user} ALL=(ALL) NOPASSWD: /usr/bin/udevadm control --reload-rules\n"
|
|
126
|
+
f" {user} ALL=(ALL) NOPASSWD: /usr/bin/udevadm trigger\n"
|
|
127
|
+
" SUDOERS\n"
|
|
128
|
+
" sudo chmod 440 /etc/sudoers.d/lagerdata-udev\n"
|
|
129
|
+
"\n"
|
|
130
|
+
"Then re-run `lager box config apply`."
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def sudoers_bootstrap(user: str = "lagerdata") -> str:
|
|
135
|
+
"""Manual-fix text for a box missing the box-config sudoers grant."""
|
|
136
|
+
# Same never-raise rule as udev_sudoers_bootstrap: fall back before
|
|
137
|
+
# boxcfg_sudoers_rules so its ValueError backstop can't fire mid-error.
|
|
138
|
+
if not is_valid_unix_username(user):
|
|
139
|
+
user = "lagerdata"
|
|
140
|
+
rules = boxcfg_sudoers_rules(user)
|
|
141
|
+
return (
|
|
142
|
+
"Box-config apply needs passwordless sudo for a small set of commands. "
|
|
143
|
+
"Run this ONCE on the box (you'll be prompted for the sudo password "
|
|
144
|
+
"the one time):\n"
|
|
145
|
+
"\n"
|
|
146
|
+
" printf '%s\\n' \\\n"
|
|
147
|
+
f" '{rules[0]}' \\\n"
|
|
148
|
+
f" '{rules[1]}' \\\n"
|
|
149
|
+
f" | sudo tee {BOXCFG_SUDOERS_PATH} >/dev/null\n"
|
|
150
|
+
f" sudo chmod 440 {BOXCFG_SUDOERS_PATH}\n"
|
|
151
|
+
f" sudo touch {BOXCFG_SUDOERS_MARKER} && sudo chmod 644 {BOXCFG_SUDOERS_MARKER}\n"
|
|
152
|
+
"\n"
|
|
153
|
+
"Then re-run `lager box config apply`. tee/rm/sysctl are path-scoped so a "
|
|
154
|
+
f"compromised {user} account cannot escalate to root via them; apt-get "
|
|
155
|
+
"and mkdir/chown are unscoped because the package list and host paths are "
|
|
156
|
+
"user-defined. SETENV on apt-get is required so "
|
|
157
|
+
"`DEBIAN_FRONTEND=noninteractive` propagates and package postinst scripts "
|
|
158
|
+
"(iptables-persistent, etc.) don't prompt."
|
|
159
|
+
)
|
|
81
160
|
|
|
82
161
|
_SUDO_BASE_TEXT = "passwordless sudo is not configured on the box for the apply commands."
|
|
83
162
|
|
|
@@ -95,9 +174,11 @@ def apt_install(
|
|
|
95
174
|
packages: List[str],
|
|
96
175
|
*,
|
|
97
176
|
ssh_runner: Optional[SshRunner] = None,
|
|
177
|
+
user: str = "lagerdata",
|
|
98
178
|
) -> HostOpResult:
|
|
99
179
|
"""Install the given apt packages on the box host. Idempotent — apt
|
|
100
|
-
skips packages that are already at the requested version.
|
|
180
|
+
skips packages that are already at the requested version. `user` only
|
|
181
|
+
names the login user in the manual-fix bootstrap text on failure."""
|
|
101
182
|
if not packages:
|
|
102
183
|
return HostOpResult(ok=True, action="noop", message="No apt packages configured.")
|
|
103
184
|
runner = ssh_runner or default_ssh_runner
|
|
@@ -111,7 +192,7 @@ def apt_install(
|
|
|
111
192
|
return HostOpResult(
|
|
112
193
|
ok=False,
|
|
113
194
|
action="failed",
|
|
114
|
-
message=_sudo_or_apt_error(stderr),
|
|
195
|
+
message=_sudo_or_apt_error(stderr, user),
|
|
115
196
|
manual_fix=f"sudo apt-get install -y {quoted_pkgs}",
|
|
116
197
|
)
|
|
117
198
|
return HostOpResult(
|
|
@@ -127,10 +208,12 @@ def sysctl_apply(
|
|
|
127
208
|
*,
|
|
128
209
|
conf_path: str = SYSCTL_CONF_PATH,
|
|
129
210
|
ssh_runner: Optional[SshRunner] = None,
|
|
211
|
+
user: str = "lagerdata",
|
|
130
212
|
) -> HostOpResult:
|
|
131
213
|
"""Write the sysctl conf and run `sysctl --system`. When `sysctl` is
|
|
132
214
|
empty, removes the conf file so the previously-set keys revert to
|
|
133
215
|
defaults on next reboot (and immediately, where the kernel allows it).
|
|
216
|
+
`user` only names the login user in the bootstrap text on failure.
|
|
134
217
|
"""
|
|
135
218
|
runner = ssh_runner or default_ssh_runner
|
|
136
219
|
quoted_path = shlex.quote(conf_path)
|
|
@@ -142,7 +225,7 @@ def sysctl_apply(
|
|
|
142
225
|
return HostOpResult(
|
|
143
226
|
ok=False,
|
|
144
227
|
action="failed",
|
|
145
|
-
message=sudo_error_message(stderr, base_text=_SUDO_BASE_TEXT, bootstrap_text=
|
|
228
|
+
message=sudo_error_message(stderr, base_text=_SUDO_BASE_TEXT, bootstrap_text=sudoers_bootstrap(user)),
|
|
146
229
|
manual_fix=f"sudo rm -f {quoted_path} && sudo sysctl --system",
|
|
147
230
|
)
|
|
148
231
|
return HostOpResult(ok=True, action="cleared", message=f"Removed {conf_path}.")
|
|
@@ -157,7 +240,7 @@ def sysctl_apply(
|
|
|
157
240
|
return HostOpResult(
|
|
158
241
|
ok=False,
|
|
159
242
|
action="failed",
|
|
160
|
-
message=sudo_error_message(stderr, base_text=_SUDO_BASE_TEXT, bootstrap_text=
|
|
243
|
+
message=sudo_error_message(stderr, base_text=_SUDO_BASE_TEXT, bootstrap_text=sudoers_bootstrap(user)),
|
|
161
244
|
manual_fix=(
|
|
162
245
|
f"printf '%s' {shlex.quote(body)} | sudo tee {quoted_path} "
|
|
163
246
|
"&& sudo sysctl --system"
|
|
@@ -201,6 +284,7 @@ def udev_apply(
|
|
|
201
284
|
rules: List[Dict[str, object]],
|
|
202
285
|
*,
|
|
203
286
|
ssh_runner: Optional[SshRunner] = None,
|
|
287
|
+
user: str = "lagerdata",
|
|
204
288
|
) -> HostOpResult:
|
|
205
289
|
"""Install the user udev rules file on the box host and reload udev.
|
|
206
290
|
|
|
@@ -230,7 +314,7 @@ def udev_apply(
|
|
|
230
314
|
ok=False,
|
|
231
315
|
action="failed",
|
|
232
316
|
message=sudo_error_message(
|
|
233
|
-
stderr, base_text=_SUDO_BASE_TEXT, bootstrap_text=
|
|
317
|
+
stderr, base_text=_SUDO_BASE_TEXT, bootstrap_text=udev_sudoers_bootstrap(user)
|
|
234
318
|
),
|
|
235
319
|
manual_fix=(
|
|
236
320
|
f"printf '%s' {shlex.quote(body)} | sudo tee {quoted_path} >/dev/null "
|
|
@@ -249,13 +333,13 @@ def udev_apply(
|
|
|
249
333
|
)
|
|
250
334
|
|
|
251
335
|
|
|
252
|
-
def _sudo_or_apt_error(stderr: str) -> str:
|
|
336
|
+
def _sudo_or_apt_error(stderr: str, user: str = "lagerdata") -> str:
|
|
253
337
|
err = (stderr or "").strip()
|
|
254
338
|
low = err.lower()
|
|
255
339
|
if "a password is required" in low or low.startswith("sudo:"):
|
|
256
|
-
return sudo_error_message(stderr, base_text=_SUDO_BASE_TEXT, bootstrap_text=
|
|
340
|
+
return sudo_error_message(stderr, base_text=_SUDO_BASE_TEXT, bootstrap_text=sudoers_bootstrap(user))
|
|
257
341
|
if "unable to locate package" in low or "has no installation candidate" in low:
|
|
258
342
|
# apt failed for a real reason — package name typo, missing repo, etc.
|
|
259
343
|
# The bootstrap message would only confuse the user.
|
|
260
344
|
return f"apt-get failed: {err}"
|
|
261
|
-
return sudo_error_message(stderr, base_text=_SUDO_BASE_TEXT, bootstrap_text=
|
|
345
|
+
return sudo_error_message(stderr, base_text=_SUDO_BASE_TEXT, bootstrap_text=sudoers_bootstrap(user))
|
|
@@ -25,6 +25,7 @@ import shlex
|
|
|
25
25
|
from dataclasses import dataclass
|
|
26
26
|
from typing import Optional
|
|
27
27
|
|
|
28
|
+
from ._host_ops import is_valid_unix_username
|
|
28
29
|
from ._ssh import SshRunner, default_ssh_runner, resolve_box_user, sudo_error_message
|
|
29
30
|
|
|
30
31
|
CONTAINER_UID = 33
|
|
@@ -34,6 +35,11 @@ EXPECTED_OWNER = f"{CONTAINER_UID}:{CONTAINER_GID}"
|
|
|
34
35
|
|
|
35
36
|
def sudoers_bootstrap(user: str = "lagerdata") -> str:
|
|
36
37
|
"""Copy-pasteable sudoers setup, scoped to the box's actual SSH user."""
|
|
38
|
+
# Error-path text renderer: never raise, never interpolate a non-plain
|
|
39
|
+
# username (it comes from local box storage unvalidated) into a
|
|
40
|
+
# paste-into-root-shell snippet. Same rule as _host_ops.sudoers_bootstrap.
|
|
41
|
+
if not is_valid_unix_username(user):
|
|
42
|
+
user = "lagerdata"
|
|
37
43
|
return (
|
|
38
44
|
"Auto-prep needs passwordless sudo for two specific commands. Run this "
|
|
39
45
|
"ONCE on the box (you'll be prompted for the sudo password the one "
|
|
@@ -1360,7 +1360,7 @@ def _ensure_apt_packages(
|
|
|
1360
1360
|
)
|
|
1361
1361
|
return True
|
|
1362
1362
|
click.echo(f"Installing apt packages on {resolved_box}: {', '.join(pkgs)}")
|
|
1363
|
-
result = apt_install(resolved_box, list(pkgs))
|
|
1363
|
+
result = apt_install(resolved_box, list(pkgs), user=resolve_box_user(resolved_box))
|
|
1364
1364
|
if not result.ok:
|
|
1365
1365
|
click.secho(f"apt install failed: {result.message}", fg="red", err=True)
|
|
1366
1366
|
if result.manual_fix:
|
|
@@ -1386,7 +1386,7 @@ def _ensure_sysctl(
|
|
|
1386
1386
|
click.echo(f"Applying {len(sysctl)} sysctl key(s) on {resolved_box}...")
|
|
1387
1387
|
else:
|
|
1388
1388
|
click.echo(f"Clearing sysctl conf on {resolved_box}...")
|
|
1389
|
-
result = sysctl_apply(resolved_box, dict(sysctl))
|
|
1389
|
+
result = sysctl_apply(resolved_box, dict(sysctl), user=resolve_box_user(resolved_box))
|
|
1390
1390
|
if not result.ok:
|
|
1391
1391
|
click.secho(f"sysctl apply failed: {result.message}", fg="red", err=True)
|
|
1392
1392
|
if result.manual_fix:
|
|
@@ -1414,7 +1414,7 @@ def _ensure_udev_rules(
|
|
|
1414
1414
|
click.echo(f"Installing {len(rules)} udev rule(s) on {resolved_box}...")
|
|
1415
1415
|
else:
|
|
1416
1416
|
click.echo(f"Clearing user udev rules on {resolved_box}...")
|
|
1417
|
-
result = udev_apply(resolved_box, list(rules))
|
|
1417
|
+
result = udev_apply(resolved_box, list(rules), user=resolve_box_user(resolved_box))
|
|
1418
1418
|
if not result.ok:
|
|
1419
1419
|
click.secho(f"udev apply failed: {result.message}", fg="red", err=True)
|
|
1420
1420
|
if result.manual_fix:
|
|
@@ -32,6 +32,7 @@ from typing import Any, Optional
|
|
|
32
32
|
import click
|
|
33
33
|
|
|
34
34
|
from .config import _resolve_box
|
|
35
|
+
from ._host_ops import is_valid_unix_username
|
|
35
36
|
from ._ssh import (
|
|
36
37
|
default_ssh_runner,
|
|
37
38
|
resolve_box_user,
|
|
@@ -56,19 +57,28 @@ _BENCH_SUDO_BASE = (
|
|
|
56
57
|
"it directly."
|
|
57
58
|
)
|
|
58
59
|
|
|
59
|
-
|
|
60
|
-
"
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
)
|
|
60
|
+
def _bench_sudoers_bootstrap(user: str = "lagerdata") -> str:
|
|
61
|
+
"""Manual-fix text for a box missing the bench.json sudo grant. Names
|
|
62
|
+
the box's actual login user so the pasted rule matches it (a hardcoded
|
|
63
|
+
`lagerdata` never matched on e.g. juultest boxes)."""
|
|
64
|
+
# Error-path text renderer: never raise, never interpolate a non-plain
|
|
65
|
+
# username (it comes from local box storage unvalidated) into a
|
|
66
|
+
# paste-into-root-shell snippet. Same rule as _host_ops.sudoers_bootstrap.
|
|
67
|
+
if not is_valid_unix_username(user):
|
|
68
|
+
user = "lagerdata"
|
|
69
|
+
return (
|
|
70
|
+
"This box is missing the bench.json sudo grant (older box, or not yet "
|
|
71
|
+
"re-provisioned). Re-provision with `lager update --box <BOX>` (or "
|
|
72
|
+
"`lager install`), or add it ONCE on the box:\n"
|
|
73
|
+
"\n"
|
|
74
|
+
" sudo tee /etc/sudoers.d/lager-bench-json >/dev/null <<'SUDOERS'\n"
|
|
75
|
+
f" {user} ALL=(ALL) NOPASSWD: /bin/cp /tmp/lager-bench.json.tmp /etc/lager/bench.json\n"
|
|
76
|
+
f" {user} ALL=(ALL) NOPASSWD: /bin/chmod 644 /etc/lager/bench.json\n"
|
|
77
|
+
" SUDOERS\n"
|
|
78
|
+
" sudo chmod 440 /etc/sudoers.d/lager-bench-json\n"
|
|
79
|
+
"\n"
|
|
80
|
+
"Then re-run the command."
|
|
81
|
+
)
|
|
72
82
|
|
|
73
83
|
|
|
74
84
|
def _read_bench_json(box_ip: str) -> dict:
|
|
@@ -136,7 +146,7 @@ def _write_bench_json(box_ip: str, payload: dict) -> bool:
|
|
|
136
146
|
msg = sudo_error_message(
|
|
137
147
|
stderr,
|
|
138
148
|
base_text=_BENCH_SUDO_BASE,
|
|
139
|
-
bootstrap_text=
|
|
149
|
+
bootstrap_text=_bench_sudoers_bootstrap(resolve_box_user(box_ip)),
|
|
140
150
|
)
|
|
141
151
|
click.secho(f"Failed to write {_BENCH_JSON_PATH}: {msg}", fg="red", err=True)
|
|
142
152
|
return False
|
|
@@ -21,6 +21,7 @@ from ...box_storage import (
|
|
|
21
21
|
)
|
|
22
22
|
from ...core.ssh_utils import host_in_known_hosts
|
|
23
23
|
from ...errors import ssh_error, LagerError
|
|
24
|
+
from ..box._host_ops import boxcfg_sudoers_bootstrap_cmd, is_valid_unix_username
|
|
24
25
|
|
|
25
26
|
|
|
26
27
|
def get_script_path(script_name: str, subdir: str = "scripts") -> Path:
|
|
@@ -461,10 +462,11 @@ def install(ctx, box, ip, user, version, skip_jlink, skip_firewall, skip_verify,
|
|
|
461
462
|
# `lager box config apply` needs root on the host for apt-get install,
|
|
462
463
|
# sysctl writes, and mount-path mkdir/chown. Those run over SSH in a
|
|
463
464
|
# non-interactive context (no TTY for sudo to prompt against), so the
|
|
464
|
-
# rule must grant NOPASSWD up front. The rule
|
|
465
|
-
#
|
|
466
|
-
#
|
|
467
|
-
#
|
|
465
|
+
# rule must grant NOPASSWD up front. The rule content lives in
|
|
466
|
+
# _host_ops.boxcfg_sudoers_rules — it must name the actual login user
|
|
467
|
+
# (it previously hardcoded `lagerdata`, so on boxes with a different
|
|
468
|
+
# user, e.g. the juultest fleet, the grant never matched and the verify
|
|
469
|
+
# below always warned "sudo -n apt-get still fails").
|
|
468
470
|
#
|
|
469
471
|
# Idempotent: re-running install overwrites the file with the same
|
|
470
472
|
# content. Failure here is a warning, not fatal — the box is otherwise
|
|
@@ -474,58 +476,81 @@ def install(ctx, box, ip, user, version, skip_jlink, skip_firewall, skip_verify,
|
|
|
474
476
|
click.echo("(One-time setup. You'll be prompted for the sudo password on the box.)")
|
|
475
477
|
click.echo()
|
|
476
478
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
"&& sudo chmod 440 /etc/sudoers.d/lager-box-config "
|
|
487
|
-
"&& sudo touch /etc/lager/.boxcfg-sudoers-v2 "
|
|
488
|
-
"&& sudo chmod 644 /etc/lager/.boxcfg-sudoers-v2"
|
|
489
|
-
)
|
|
490
|
-
|
|
491
|
-
try:
|
|
492
|
-
bootstrap_result = subprocess.run(
|
|
493
|
-
["ssh", "-t", ssh_host, sudoers_cmd],
|
|
494
|
-
timeout=120,
|
|
479
|
+
if not is_valid_unix_username(user):
|
|
480
|
+
# The username lands inside a root-owned sudoers file; refuse to
|
|
481
|
+
# interpolate anything that isn't a plain unix username.
|
|
482
|
+
click.secho(
|
|
483
|
+
f"Warning: username {user!r} is not a plain unix username; skipping the "
|
|
484
|
+
"passwordless-sudo bootstrap. `lager box config apply` will require "
|
|
485
|
+
"manual sudoers setup on this box. See `lager box config apply --help` "
|
|
486
|
+
"for the snippet to paste.",
|
|
487
|
+
fg='yellow', err=True,
|
|
495
488
|
)
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
# current rule shape was installed) + functional apt-get probe
|
|
506
|
-
# (means the NOPASSWD/SETENV grant is live). Marker name carries
|
|
507
|
-
# a version suffix so older boxes upgrading to a future rule
|
|
508
|
-
# shape re-bootstrap automatically.
|
|
509
|
-
verify_result = subprocess.run(
|
|
489
|
+
else:
|
|
490
|
+
# Skip the bootstrap (and its sudo password prompt) when the grant is
|
|
491
|
+
# already live — marker file present AND `sudo -n apt-get` actually
|
|
492
|
+
# works as this user, the same functional probe `lager update` uses.
|
|
493
|
+
# Re-installs then never prompt here at all. This matters because the
|
|
494
|
+
# prompt lands at the very end of a long install, when the operator
|
|
495
|
+
# may have stepped away.
|
|
496
|
+
try:
|
|
497
|
+
precheck = subprocess.run(
|
|
510
498
|
["ssh", "-o", "BatchMode=yes", ssh_host,
|
|
511
499
|
"test -f /etc/lager/.boxcfg-sudoers-v2 "
|
|
512
500
|
"&& sudo -n DEBIAN_FRONTEND=noninteractive apt-get --version >/dev/null 2>&1"],
|
|
513
501
|
capture_output=True, timeout=15,
|
|
514
502
|
)
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
503
|
+
already_configured = precheck.returncode == 0
|
|
504
|
+
except Exception:
|
|
505
|
+
already_configured = False
|
|
506
|
+
|
|
507
|
+
if already_configured:
|
|
508
|
+
click.secho("Passwordless sudo for `lager box config` already configured", fg='green')
|
|
509
|
+
else:
|
|
510
|
+
sudoers_cmd = boxcfg_sudoers_bootstrap_cmd(user)
|
|
511
|
+
|
|
512
|
+
try:
|
|
513
|
+
# Interactive: waits on a human typing the box's sudo password
|
|
514
|
+
# at the end of a long install. A 120s timeout here killed the
|
|
515
|
+
# bootstrap mid-prompt for a slow (or absent) operator, so give
|
|
516
|
+
# them 10 minutes; the timeout only guards a genuine hang.
|
|
517
|
+
bootstrap_result = subprocess.run(
|
|
518
|
+
["ssh", "-t", ssh_host, sudoers_cmd],
|
|
519
|
+
timeout=600,
|
|
520
|
+
)
|
|
521
|
+
if bootstrap_result.returncode != 0:
|
|
522
|
+
click.secho(
|
|
523
|
+
"Warning: Sudoers rule could not be installed. `lager box config apply` "
|
|
524
|
+
"will require manual sudoers setup on this box. See `lager box config "
|
|
525
|
+
"apply --help` for the snippet to paste.",
|
|
526
|
+
fg='yellow', err=True,
|
|
527
|
+
)
|
|
528
|
+
else:
|
|
529
|
+
# Verify: marker file written by the bootstrap above (means the
|
|
530
|
+
# current rule shape was installed) + functional apt-get probe
|
|
531
|
+
# (means the NOPASSWD/SETENV grant is live). Marker name carries
|
|
532
|
+
# a version suffix so older boxes upgrading to a future rule
|
|
533
|
+
# shape re-bootstrap automatically.
|
|
534
|
+
verify_result = subprocess.run(
|
|
535
|
+
["ssh", "-o", "BatchMode=yes", ssh_host,
|
|
536
|
+
"test -f /etc/lager/.boxcfg-sudoers-v2 "
|
|
537
|
+
"&& sudo -n DEBIAN_FRONTEND=noninteractive apt-get --version >/dev/null 2>&1"],
|
|
538
|
+
capture_output=True, timeout=15,
|
|
539
|
+
)
|
|
540
|
+
if verify_result.returncode == 0:
|
|
541
|
+
click.secho("Passwordless sudo for `lager box config` configured", fg='green')
|
|
542
|
+
else:
|
|
543
|
+
click.secho(
|
|
544
|
+
"Warning: Sudoers file installed but `sudo -n apt-get` still fails. "
|
|
545
|
+
"Check /etc/sudoers.d/lager-box-config on the box for syntax issues.",
|
|
546
|
+
fg='yellow', err=True,
|
|
547
|
+
)
|
|
548
|
+
except (subprocess.TimeoutExpired, Exception) as e:
|
|
518
549
|
click.secho(
|
|
519
|
-
"Warning: Sudoers
|
|
520
|
-
"
|
|
550
|
+
f"Warning: Sudoers bootstrap failed: {e}. `lager box config apply` "
|
|
551
|
+
"will require manual sudoers setup.",
|
|
521
552
|
fg='yellow', err=True,
|
|
522
553
|
)
|
|
523
|
-
except (subprocess.TimeoutExpired, Exception) as e:
|
|
524
|
-
click.secho(
|
|
525
|
-
f"Warning: Sudoers bootstrap failed: {e}. `lager box config apply` "
|
|
526
|
-
"will require manual sudoers setup.",
|
|
527
|
-
fg='yellow', err=True,
|
|
528
|
-
)
|
|
529
554
|
|
|
530
555
|
click.echo()
|
|
531
556
|
|
|
@@ -25,6 +25,7 @@ from ...box_storage import (
|
|
|
25
25
|
)
|
|
26
26
|
from ...context import get_default_box
|
|
27
27
|
from ...core.ssh_utils import get_ssh_connection_pool
|
|
28
|
+
from ..box._host_ops import boxcfg_sudoers_bootstrap_cmd, is_valid_unix_username
|
|
28
29
|
from ..box._ssh import ensure_lager_box_keypair, key_auth_works
|
|
29
30
|
from ...errors import LagerError
|
|
30
31
|
|
|
@@ -1445,33 +1446,35 @@ def _update_logic(ctx, *, box, yes, version, verbose, check, force=False):
|
|
|
1445
1446
|
#
|
|
1446
1447
|
# `lager box config apply` needs root on the host for apt-get install,
|
|
1447
1448
|
# sysctl writes, and mount-path mkdir/chown — all over BatchMode SSH
|
|
1448
|
-
# where sudo can't prompt. The rule
|
|
1449
|
-
#
|
|
1450
|
-
#
|
|
1451
|
-
#
|
|
1452
|
-
#
|
|
1449
|
+
# where sudo can't prompt. The rule (built in
|
|
1450
|
+
# _host_ops.boxcfg_sudoers_bootstrap_cmd) grants narrow NOPASSWD for
|
|
1451
|
+
# exactly those operations, and must name the actual login user — it
|
|
1452
|
+
# previously hardcoded `lagerdata`, so on boxes with a different user
|
|
1453
|
+
# (e.g. the juultest fleet) the grant never matched. The probe ran the
|
|
1454
|
+
# functional check (marker file present + `sudo -n apt-get` actually
|
|
1455
|
+
# works) as that user, so wrong-user boxes come back negative and
|
|
1456
|
+
# re-bootstrap here with the corrected rule. Runs on every update so
|
|
1457
|
+
# existing boxes gradually pick up the rule; idempotent.
|
|
1453
1458
|
if progress:
|
|
1454
1459
|
progress.update("Checking box-config sudoers...")
|
|
1455
1460
|
log('Checking box-config sudoers...', nl=False)
|
|
1456
1461
|
|
|
1457
1462
|
if facts.get('BOXCFG_SUDOERS_OK') == '1':
|
|
1458
1463
|
log_status('OK', 'green')
|
|
1464
|
+
elif not is_valid_unix_username(username):
|
|
1465
|
+
# The username lands inside a root-owned sudoers file; refuse to
|
|
1466
|
+
# interpolate anything that isn't a plain unix username.
|
|
1467
|
+
log_status('SKIPPED (unusual username)', 'yellow')
|
|
1468
|
+
if verbose:
|
|
1469
|
+
click.echo(
|
|
1470
|
+
f' Username {username!r} is not a plain unix username; '
|
|
1471
|
+
'`lager box config apply` will need manual sudoers setup.',
|
|
1472
|
+
err=True,
|
|
1473
|
+
)
|
|
1459
1474
|
else:
|
|
1460
1475
|
log_status('needs bootstrap', 'yellow')
|
|
1461
1476
|
|
|
1462
|
-
boxcfg_sudoers_cmd = (
|
|
1463
|
-
"printf '%s\\n' "
|
|
1464
|
-
"'lagerdata ALL=(root) NOPASSWD: SETENV: /usr/bin/apt-get' "
|
|
1465
|
-
"'lagerdata ALL=(root) NOPASSWD: /bin/mkdir, /bin/chown, "
|
|
1466
|
-
"/usr/sbin/sysctl --system, /sbin/sysctl --system, "
|
|
1467
|
-
"/usr/bin/tee /etc/sysctl.d/99-lager-box-config.conf, "
|
|
1468
|
-
"/bin/rm -f /etc/sysctl.d/99-lager-box-config.conf, "
|
|
1469
|
-
"/bin/cp /etc/lager/box_config.applied.json /etc/lager/box_config.json' "
|
|
1470
|
-
"| sudo tee /etc/sudoers.d/lager-box-config >/dev/null "
|
|
1471
|
-
"&& sudo chmod 440 /etc/sudoers.d/lager-box-config "
|
|
1472
|
-
"&& sudo touch /etc/lager/.boxcfg-sudoers-v2 "
|
|
1473
|
-
"&& sudo chmod 644 /etc/lager/.boxcfg-sudoers-v2"
|
|
1474
|
-
)
|
|
1477
|
+
boxcfg_sudoers_cmd = boxcfg_sudoers_bootstrap_cmd(username)
|
|
1475
1478
|
|
|
1476
1479
|
def _render_boxcfg(ok):
|
|
1477
1480
|
log('Installing box-config sudoers...', nl=False)
|
|
@@ -1530,7 +1533,10 @@ def _update_logic(ctx, *, box, yes, version, verbose, check, force=False):
|
|
|
1530
1533
|
elif verbose:
|
|
1531
1534
|
click.echo()
|
|
1532
1535
|
|
|
1533
|
-
|
|
1536
|
+
# 600s, not the 300s default: this session waits on a human typing the
|
|
1537
|
+
# sudo password late in the run, when the operator may have stepped
|
|
1538
|
+
# away — the timeout should only guard a genuine hang.
|
|
1539
|
+
run_ssh_command_interactive(combined_priv_cmd, timeout_secs=600, allow_sudo_prompt=True)
|
|
1534
1540
|
|
|
1535
1541
|
if not verbose and progress:
|
|
1536
1542
|
progress.resume()
|