lager-cli 0.31.2__tar.gz → 0.31.3__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.
Files changed (221) hide show
  1. {lager_cli-0.31.2/lager_cli.egg-info → lager_cli-0.31.3}/PKG-INFO +1 -1
  2. {lager_cli-0.31.2 → lager_cli-0.31.3}/__init__.py +1 -1
  3. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/box/_host_ops.py +130 -46
  4. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/box/_mount_prep.py +6 -0
  5. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/box/config.py +3 -3
  6. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/box/dut.py +24 -14
  7. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/utility/install.py +72 -47
  8. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/utility/update.py +25 -19
  9. {lager_cli-0.31.2 → lager_cli-0.31.3}/deployment/scripts/setup_and_deploy_box.sh +192 -92
  10. {lager_cli-0.31.2 → lager_cli-0.31.3/lager_cli.egg-info}/PKG-INFO +1 -1
  11. {lager_cli-0.31.2 → lager_cli-0.31.3}/LICENSE +0 -0
  12. {lager_cli-0.31.2 → lager_cli-0.31.3}/MANIFEST.in +0 -0
  13. {lager_cli-0.31.2 → lager_cli-0.31.3}/README.md +0 -0
  14. {lager_cli-0.31.2 → lager_cli-0.31.3}/__main__.py +0 -0
  15. {lager_cli-0.31.2 → lager_cli-0.31.3}/address_utils.py +0 -0
  16. {lager_cli-0.31.2 → lager_cli-0.31.3}/battery/__init__.py +0 -0
  17. {lager_cli-0.31.2 → lager_cli-0.31.3}/battery/battery_tui.py +0 -0
  18. {lager_cli-0.31.2 → lager_cli-0.31.3}/battery/websocket_client.py +0 -0
  19. {lager_cli-0.31.2 → lager_cli-0.31.3}/box_storage.py +0 -0
  20. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/__init__.py +0 -0
  21. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/box/__init__.py +0 -0
  22. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/box/_pip_validation.py +0 -0
  23. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/box/_shim_verbs.py +0 -0
  24. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/box/_ssh.py +0 -0
  25. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/box/authorize.py +0 -0
  26. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/box/box_group.py +0 -0
  27. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/box/boxes.py +0 -0
  28. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/box/diagnose.py +0 -0
  29. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/box/hello.py +0 -0
  30. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/box/instruments.py +0 -0
  31. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/box/labjack_pins.py +0 -0
  32. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/box/lock.py +0 -0
  33. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/box/net_tui.py +0 -0
  34. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/box/nets.py +0 -0
  35. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/box/ssh.py +0 -0
  36. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/communication/__init__.py +0 -0
  37. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/communication/ble.py +0 -0
  38. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/communication/blufi.py +0 -0
  39. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/communication/i2c.py +0 -0
  40. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/communication/router.py +0 -0
  41. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/communication/spi.py +0 -0
  42. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/communication/uart.py +0 -0
  43. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/communication/usb.py +0 -0
  44. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/communication/websocket_client.py +0 -0
  45. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/communication/wifi.py +0 -0
  46. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/development/__init__.py +0 -0
  47. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/development/arm.py +0 -0
  48. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/development/debug/__init__.py +0 -0
  49. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/development/debug/commands.py +0 -0
  50. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/development/debug/gdb.py +0 -0
  51. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/development/debug/net_cache.py +0 -0
  52. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/development/debug/service_client.py +0 -0
  53. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/development/debug/service_helper.py +0 -0
  54. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/development/debug/tunnel.py +0 -0
  55. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/development/devenv.py +0 -0
  56. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/development/python.py +0 -0
  57. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/measurement/__init__.py +0 -0
  58. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/measurement/adc.py +0 -0
  59. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/measurement/dac.py +0 -0
  60. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/measurement/energy.py +0 -0
  61. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/measurement/gpi.py +0 -0
  62. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/measurement/gpo.py +0 -0
  63. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/measurement/logic.py +0 -0
  64. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/measurement/scope.py +0 -0
  65. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/measurement/thermocouple.py +0 -0
  66. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/measurement/watt.py +0 -0
  67. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/power/__init__.py +0 -0
  68. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/power/battery.py +0 -0
  69. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/power/eload.py +0 -0
  70. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/power/solar.py +0 -0
  71. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/power/supply.py +0 -0
  72. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/utility/__init__.py +0 -0
  73. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/utility/binaries.py +0 -0
  74. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/utility/defaults.py +0 -0
  75. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/utility/exec_.py +0 -0
  76. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/utility/install_wheel.py +0 -0
  77. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/utility/logs.py +0 -0
  78. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/utility/uninstall.py +0 -0
  79. {lager_cli-0.31.2 → lager_cli-0.31.3}/commands/utility/webcam.py +0 -0
  80. {lager_cli-0.31.2 → lager_cli-0.31.3}/config.py +0 -0
  81. {lager_cli-0.31.2 → lager_cli-0.31.3}/context/__init__.py +0 -0
  82. {lager_cli-0.31.2 → lager_cli-0.31.3}/context/ci_detection.py +0 -0
  83. {lager_cli-0.31.2 → lager_cli-0.31.3}/context/constants.py +0 -0
  84. {lager_cli-0.31.2 → lager_cli-0.31.3}/context/core.py +0 -0
  85. {lager_cli-0.31.2 → lager_cli-0.31.3}/context/error_handlers.py +0 -0
  86. {lager_cli-0.31.2 → lager_cli-0.31.3}/context/session.py +0 -0
  87. {lager_cli-0.31.2 → lager_cli-0.31.3}/core/__init__.py +0 -0
  88. {lager_cli-0.31.2 → lager_cli-0.31.3}/core/group_usage.py +0 -0
  89. {lager_cli-0.31.2 → lager_cli-0.31.3}/core/matchers.py +0 -0
  90. {lager_cli-0.31.2 → lager_cli-0.31.3}/core/net_group.py +0 -0
  91. {lager_cli-0.31.2 → lager_cli-0.31.3}/core/net_helpers.py +0 -0
  92. {lager_cli-0.31.2 → lager_cli-0.31.3}/core/net_storage.py +0 -0
  93. {lager_cli-0.31.2 → lager_cli-0.31.3}/core/param_types.py +0 -0
  94. {lager_cli-0.31.2 → lager_cli-0.31.3}/core/ssh_utils.py +0 -0
  95. {lager_cli-0.31.2 → lager_cli-0.31.3}/core/utils.py +0 -0
  96. {lager_cli-0.31.2 → lager_cli-0.31.3}/core/version_skew.py +0 -0
  97. {lager_cli-0.31.2 → lager_cli-0.31.3}/core/ws_diagnose.py +0 -0
  98. {lager_cli-0.31.2 → lager_cli-0.31.3}/deployment/__init__.py +0 -0
  99. {lager_cli-0.31.2 → lager_cli-0.31.3}/deployment/scripts/__init__.py +0 -0
  100. {lager_cli-0.31.2 → lager_cli-0.31.3}/deployment/scripts/convert_to_sparse_checkout.sh +0 -0
  101. {lager_cli-0.31.2 → lager_cli-0.31.3}/deployment/scripts/setup_ssh_key.sh +0 -0
  102. {lager_cli-0.31.2 → lager_cli-0.31.3}/deployment/security/__init__.py +0 -0
  103. {lager_cli-0.31.2 → lager_cli-0.31.3}/deployment/security/secure_box_firewall.sh +0 -0
  104. {lager_cli-0.31.2 → lager_cli-0.31.3}/elftools/__init__.py +0 -0
  105. {lager_cli-0.31.2 → lager_cli-0.31.3}/elftools/common/__init__.py +0 -0
  106. {lager_cli-0.31.2 → lager_cli-0.31.3}/elftools/common/construct_utils.py +0 -0
  107. {lager_cli-0.31.2 → lager_cli-0.31.3}/elftools/common/exceptions.py +0 -0
  108. {lager_cli-0.31.2 → lager_cli-0.31.3}/elftools/common/py3compat.py +0 -0
  109. {lager_cli-0.31.2 → lager_cli-0.31.3}/elftools/common/utils.py +0 -0
  110. {lager_cli-0.31.2 → lager_cli-0.31.3}/elftools/construct/__init__.py +0 -0
  111. {lager_cli-0.31.2 → lager_cli-0.31.3}/elftools/construct/adapters.py +0 -0
  112. {lager_cli-0.31.2 → lager_cli-0.31.3}/elftools/construct/core.py +0 -0
  113. {lager_cli-0.31.2 → lager_cli-0.31.3}/elftools/construct/debug.py +0 -0
  114. {lager_cli-0.31.2 → lager_cli-0.31.3}/elftools/construct/macros.py +0 -0
  115. {lager_cli-0.31.2 → lager_cli-0.31.3}/elftools/dwarf/__init__.py +0 -0
  116. {lager_cli-0.31.2 → lager_cli-0.31.3}/elftools/dwarf/abbrevtable.py +0 -0
  117. {lager_cli-0.31.2 → lager_cli-0.31.3}/elftools/dwarf/aranges.py +0 -0
  118. {lager_cli-0.31.2 → lager_cli-0.31.3}/elftools/dwarf/callframe.py +0 -0
  119. {lager_cli-0.31.2 → lager_cli-0.31.3}/elftools/dwarf/compileunit.py +0 -0
  120. {lager_cli-0.31.2 → lager_cli-0.31.3}/elftools/dwarf/constants.py +0 -0
  121. {lager_cli-0.31.2 → lager_cli-0.31.3}/elftools/dwarf/descriptions.py +0 -0
  122. {lager_cli-0.31.2 → lager_cli-0.31.3}/elftools/dwarf/die.py +0 -0
  123. {lager_cli-0.31.2 → lager_cli-0.31.3}/elftools/dwarf/dwarf_expr.py +0 -0
  124. {lager_cli-0.31.2 → lager_cli-0.31.3}/elftools/dwarf/dwarfinfo.py +0 -0
  125. {lager_cli-0.31.2 → lager_cli-0.31.3}/elftools/dwarf/enums.py +0 -0
  126. {lager_cli-0.31.2 → lager_cli-0.31.3}/elftools/dwarf/lineprogram.py +0 -0
  127. {lager_cli-0.31.2 → lager_cli-0.31.3}/elftools/dwarf/locationlists.py +0 -0
  128. {lager_cli-0.31.2 → lager_cli-0.31.3}/elftools/dwarf/namelut.py +0 -0
  129. {lager_cli-0.31.2 → lager_cli-0.31.3}/elftools/dwarf/ranges.py +0 -0
  130. {lager_cli-0.31.2 → lager_cli-0.31.3}/elftools/dwarf/structs.py +0 -0
  131. {lager_cli-0.31.2 → lager_cli-0.31.3}/elftools/ehabi/__init__.py +0 -0
  132. {lager_cli-0.31.2 → lager_cli-0.31.3}/elftools/ehabi/constants.py +0 -0
  133. {lager_cli-0.31.2 → lager_cli-0.31.3}/elftools/ehabi/decoder.py +0 -0
  134. {lager_cli-0.31.2 → lager_cli-0.31.3}/elftools/ehabi/ehabiinfo.py +0 -0
  135. {lager_cli-0.31.2 → lager_cli-0.31.3}/elftools/ehabi/structs.py +0 -0
  136. {lager_cli-0.31.2 → lager_cli-0.31.3}/elftools/elf/__init__.py +0 -0
  137. {lager_cli-0.31.2 → lager_cli-0.31.3}/elftools/elf/constants.py +0 -0
  138. {lager_cli-0.31.2 → lager_cli-0.31.3}/elftools/elf/descriptions.py +0 -0
  139. {lager_cli-0.31.2 → lager_cli-0.31.3}/elftools/elf/dynamic.py +0 -0
  140. {lager_cli-0.31.2 → lager_cli-0.31.3}/elftools/elf/elffile.py +0 -0
  141. {lager_cli-0.31.2 → lager_cli-0.31.3}/elftools/elf/enums.py +0 -0
  142. {lager_cli-0.31.2 → lager_cli-0.31.3}/elftools/elf/gnuversions.py +0 -0
  143. {lager_cli-0.31.2 → lager_cli-0.31.3}/elftools/elf/hash.py +0 -0
  144. {lager_cli-0.31.2 → lager_cli-0.31.3}/elftools/elf/notes.py +0 -0
  145. {lager_cli-0.31.2 → lager_cli-0.31.3}/elftools/elf/relocation.py +0 -0
  146. {lager_cli-0.31.2 → lager_cli-0.31.3}/elftools/elf/sections.py +0 -0
  147. {lager_cli-0.31.2 → lager_cli-0.31.3}/elftools/elf/segments.py +0 -0
  148. {lager_cli-0.31.2 → lager_cli-0.31.3}/elftools/elf/structs.py +0 -0
  149. {lager_cli-0.31.2 → lager_cli-0.31.3}/errors.py +0 -0
  150. {lager_cli-0.31.2 → lager_cli-0.31.3}/exceptions.py +0 -0
  151. {lager_cli-0.31.2 → lager_cli-0.31.3}/impl/__init__.py +0 -0
  152. {lager_cli-0.31.2 → lager_cli-0.31.3}/impl/box_config.py +0 -0
  153. {lager_cli-0.31.2 → lager_cli-0.31.3}/impl/communication/__init__.py +0 -0
  154. {lager_cli-0.31.2 → lager_cli-0.31.3}/impl/communication/ble.py +0 -0
  155. {lager_cli-0.31.2 → lager_cli-0.31.3}/impl/communication/blufi.py +0 -0
  156. {lager_cli-0.31.2 → lager_cli-0.31.3}/impl/communication/i2c.py +0 -0
  157. {lager_cli-0.31.2 → lager_cli-0.31.3}/impl/communication/router.py +0 -0
  158. {lager_cli-0.31.2 → lager_cli-0.31.3}/impl/communication/spi.py +0 -0
  159. {lager_cli-0.31.2 → lager_cli-0.31.3}/impl/communication/uart.py +0 -0
  160. {lager_cli-0.31.2 → lager_cli-0.31.3}/impl/communication/wifi.py +0 -0
  161. {lager_cli-0.31.2 → lager_cli-0.31.3}/impl/custom_devices.py +0 -0
  162. {lager_cli-0.31.2 → lager_cli-0.31.3}/impl/device/__init__.py +0 -0
  163. {lager_cli-0.31.2 → lager_cli-0.31.3}/impl/device/arm.py +0 -0
  164. {lager_cli-0.31.2 → lager_cli-0.31.3}/impl/device/hello.py +0 -0
  165. {lager_cli-0.31.2 → lager_cli-0.31.3}/impl/device/usb.py +0 -0
  166. {lager_cli-0.31.2 → lager_cli-0.31.3}/impl/device/webcam.py +0 -0
  167. {lager_cli-0.31.2 → lager_cli-0.31.3}/impl/measurement/__init__.py +0 -0
  168. {lager_cli-0.31.2 → lager_cli-0.31.3}/impl/measurement/adc.py +0 -0
  169. {lager_cli-0.31.2 → lager_cli-0.31.3}/impl/measurement/dac.py +0 -0
  170. {lager_cli-0.31.2 → lager_cli-0.31.3}/impl/measurement/energy.py +0 -0
  171. {lager_cli-0.31.2 → lager_cli-0.31.3}/impl/measurement/gpio.py +0 -0
  172. {lager_cli-0.31.2 → lager_cli-0.31.3}/impl/measurement/scope.py +0 -0
  173. {lager_cli-0.31.2 → lager_cli-0.31.3}/impl/measurement/scope_stream.py +0 -0
  174. {lager_cli-0.31.2 → lager_cli-0.31.3}/impl/measurement/thermocouple.py +0 -0
  175. {lager_cli-0.31.2 → lager_cli-0.31.3}/impl/measurement/watt.py +0 -0
  176. {lager_cli-0.31.2 → lager_cli-0.31.3}/impl/net.py +0 -0
  177. {lager_cli-0.31.2 → lager_cli-0.31.3}/impl/power/__init__.py +0 -0
  178. {lager_cli-0.31.2 → lager_cli-0.31.3}/impl/power/battery.py +0 -0
  179. {lager_cli-0.31.2 → lager_cli-0.31.3}/impl/power/eload.py +0 -0
  180. {lager_cli-0.31.2 → lager_cli-0.31.3}/impl/power/enable_disable.py +0 -0
  181. {lager_cli-0.31.2 → lager_cli-0.31.3}/impl/power/solar.py +0 -0
  182. {lager_cli-0.31.2 → lager_cli-0.31.3}/impl/power/supply.py +0 -0
  183. {lager_cli-0.31.2 → lager_cli-0.31.3}/impl/query_instruments.py +0 -0
  184. {lager_cli-0.31.2 → lager_cli-0.31.3}/lager_cli.egg-info/SOURCES.txt +0 -0
  185. {lager_cli-0.31.2 → lager_cli-0.31.3}/lager_cli.egg-info/dependency_links.txt +0 -0
  186. {lager_cli-0.31.2 → lager_cli-0.31.3}/lager_cli.egg-info/entry_points.txt +0 -0
  187. {lager_cli-0.31.2 → lager_cli-0.31.3}/lager_cli.egg-info/requires.txt +0 -0
  188. {lager_cli-0.31.2 → lager_cli-0.31.3}/lager_cli.egg-info/top_level.txt +0 -0
  189. {lager_cli-0.31.2 → lager_cli-0.31.3}/main.py +0 -0
  190. {lager_cli-0.31.2 → lager_cli-0.31.3}/pyproject.toml +0 -0
  191. {lager_cli-0.31.2 → lager_cli-0.31.3}/safe_unpickle.py +0 -0
  192. {lager_cli-0.31.2 → lager_cli-0.31.3}/setup.cfg +0 -0
  193. {lager_cli-0.31.2 → lager_cli-0.31.3}/setup.py +0 -0
  194. {lager_cli-0.31.2 → lager_cli-0.31.3}/simple_hdlc.py +0 -0
  195. {lager_cli-0.31.2 → lager_cli-0.31.3}/sort_utils.py +0 -0
  196. {lager_cli-0.31.2 → lager_cli-0.31.3}/status.py +0 -0
  197. {lager_cli-0.31.2 → lager_cli-0.31.3}/supply/__init__.py +0 -0
  198. {lager_cli-0.31.2 → lager_cli-0.31.3}/supply/supply_tui.py +0 -0
  199. {lager_cli-0.31.2 → lager_cli-0.31.3}/supply/websocket_client.py +0 -0
  200. {lager_cli-0.31.2 → lager_cli-0.31.3}/terminal/__init__.py +0 -0
  201. {lager_cli-0.31.2 → lager_cli-0.31.3}/terminal/core/__init__.py +0 -0
  202. {lager_cli-0.31.2 → lager_cli-0.31.3}/terminal/core/executor.py +0 -0
  203. {lager_cli-0.31.2 → lager_cli-0.31.3}/terminal/main.py +0 -0
  204. {lager_cli-0.31.2 → lager_cli-0.31.3}/terminal/ui/__init__.py +0 -0
  205. {lager_cli-0.31.2 → lager_cli-0.31.3}/terminal/ui/completer.py +0 -0
  206. {lager_cli-0.31.2 → lager_cli-0.31.3}/terminal/ui/display.py +0 -0
  207. {lager_cli-0.31.2 → lager_cli-0.31.3}/terminal/ui/logo.py +0 -0
  208. {lager_cli-0.31.2 → lager_cli-0.31.3}/terminal/ui/repl.py +0 -0
  209. {lager_cli-0.31.2 → lager_cli-0.31.3}/terminal/ui/themes.py +0 -0
  210. {lager_cli-0.31.2 → lager_cli-0.31.3}/tests/test_box_lager_imports.py +0 -0
  211. {lager_cli-0.31.2 → lager_cli-0.31.3}/tests/test_box_storage.py +0 -0
  212. {lager_cli-0.31.2 → lager_cli-0.31.3}/tests/test_io_imports.py +0 -0
  213. {lager_cli-0.31.2 → lager_cli-0.31.3}/update_check.py +0 -0
  214. {lager_cli-0.31.2 → lager_cli-0.31.3}/vendor/PyCRC/CRC16.py +0 -0
  215. {lager_cli-0.31.2 → lager_cli-0.31.3}/vendor/PyCRC/CRC16DNP.py +0 -0
  216. {lager_cli-0.31.2 → lager_cli-0.31.3}/vendor/PyCRC/CRC16Kermit.py +0 -0
  217. {lager_cli-0.31.2 → lager_cli-0.31.3}/vendor/PyCRC/CRC16SICK.py +0 -0
  218. {lager_cli-0.31.2 → lager_cli-0.31.3}/vendor/PyCRC/CRC32.py +0 -0
  219. {lager_cli-0.31.2 → lager_cli-0.31.3}/vendor/PyCRC/CRCCCITT.py +0 -0
  220. {lager_cli-0.31.2 → lager_cli-0.31.3}/vendor/PyCRC/__init__.py +0 -0
  221. {lager_cli-0.31.2 → lager_cli-0.31.3}/vendor/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: lager-cli
3
- Version: 0.31.2
3
+ Version: 0.31.3
4
4
  Summary: Lager CLI - Box and Docker connectivity
5
5
  Home-page: https://github.com/lagerdata/lager
6
6
  Author: Lager Data
@@ -7,4 +7,4 @@ Lager CLI
7
7
  A Command Line Interface for Lager Data
8
8
  """
9
9
 
10
- __version__ = '0.31.2'
10
+ __version__ = '0.31.3'
@@ -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
- UDEV_SUDOERS_BOOTSTRAP = (
43
- "Applying user udev rules needs the passwordless-sudo udev grant that the "
44
- "box setup script installs. If it's missing (older box), re-run the box "
45
- "setup/deploy, or add it ONCE on the box:\n"
46
- "\n"
47
- " sudo tee /etc/sudoers.d/lagerdata-udev >/dev/null <<'SUDOERS'\n"
48
- " lagerdata ALL=(ALL) NOPASSWD: /bin/cp /tmp/*.rules /etc/udev/rules.d/\n"
49
- " lagerdata ALL=(ALL) NOPASSWD: /bin/chmod 644 /etc/udev/rules.d/*.rules\n"
50
- " lagerdata ALL=(ALL) NOPASSWD: /usr/bin/udevadm control --reload-rules\n"
51
- " lagerdata ALL=(ALL) NOPASSWD: /usr/bin/udevadm trigger\n"
52
- " SUDOERS\n"
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
- SUDOERS_BOOTSTRAP = (
59
- "Box-config apply needs passwordless sudo for a small set of commands. "
60
- "Run this ONCE on the box (you'll be prompted for the sudo password "
61
- "the one time):\n"
62
- "\n"
63
- " printf '%s\\n' \\\n"
64
- " 'lagerdata ALL=(root) NOPASSWD: SETENV: /usr/bin/apt-get' \\\n"
65
- " 'lagerdata ALL=(root) NOPASSWD: /bin/mkdir, /bin/chown, "
66
- "/usr/sbin/sysctl --system, /sbin/sysctl --system, "
67
- "/usr/bin/tee /etc/sysctl.d/99-lager-box-config.conf, "
68
- "/bin/rm -f /etc/sysctl.d/99-lager-box-config.conf, "
69
- "/bin/cp /etc/lager/box_config.applied.json /etc/lager/box_config.json' \\\n"
70
- " | sudo tee /etc/sudoers.d/lager-box-config >/dev/null\n"
71
- " sudo chmod 440 /etc/sudoers.d/lager-box-config\n"
72
- " sudo touch /etc/lager/.boxcfg-sudoers-v2 && sudo chmod 644 /etc/lager/.boxcfg-sudoers-v2\n"
73
- "\n"
74
- "Then re-run `lager box config apply`. tee/rm/sysctl are path-scoped so a "
75
- "compromised lagerdata account cannot escalate to root via them; apt-get "
76
- "and mkdir/chown are unscoped because the package list and host paths are "
77
- "user-defined. SETENV on apt-get is required so "
78
- "`DEBIAN_FRONTEND=noninteractive` propagates and package postinst scripts "
79
- "(iptables-persistent, etc.) don't prompt."
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=SUDOERS_BOOTSTRAP),
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=SUDOERS_BOOTSTRAP),
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=UDEV_SUDOERS_BOOTSTRAP
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=SUDOERS_BOOTSTRAP)
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=SUDOERS_BOOTSTRAP)
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
- _BENCH_SUDOERS_BOOTSTRAP = (
60
- "This box is missing the bench.json sudo grant (older box, or not yet "
61
- "re-provisioned). Re-provision with `lager update --box <BOX>` (or "
62
- "`lager install`), or add it ONCE on the box:\n"
63
- "\n"
64
- " sudo tee /etc/sudoers.d/lager-bench-json >/dev/null <<'SUDOERS'\n"
65
- " lagerdata ALL=(ALL) NOPASSWD: /bin/cp /tmp/lager-bench.json.tmp /etc/lager/bench.json\n"
66
- " lagerdata ALL=(ALL) NOPASSWD: /bin/chmod 644 /etc/lager/bench.json\n"
67
- " SUDOERS\n"
68
- " sudo chmod 440 /etc/sudoers.d/lager-bench-json\n"
69
- "\n"
70
- "Then re-run the command."
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=_BENCH_SUDOERS_BOOTSTRAP,
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 is narrowly scoped:
465
- # tee/rm/sysctl are path-locked to the exact files used so the
466
- # lagerdata account can't escalate via them; apt-get and mkdir/chown
467
- # are unscoped because the package list and host paths are user-defined.
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
- sudoers_cmd = (
478
- "printf '%s\\n' "
479
- "'lagerdata ALL=(root) NOPASSWD: SETENV: /usr/bin/apt-get' "
480
- "'lagerdata ALL=(root) NOPASSWD: /bin/mkdir, /bin/chown, "
481
- "/usr/sbin/sysctl --system, /sbin/sysctl --system, "
482
- "/usr/bin/tee /etc/sysctl.d/99-lager-box-config.conf, "
483
- "/bin/rm -f /etc/sysctl.d/99-lager-box-config.conf, "
484
- "/bin/cp /etc/lager/box_config.applied.json /etc/lager/box_config.json' "
485
- "| sudo tee /etc/sudoers.d/lager-box-config >/dev/null "
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
- if bootstrap_result.returncode != 0:
497
- click.secho(
498
- "Warning: Sudoers rule could not be installed. `lager box config apply` "
499
- "will require manual sudoers setup on this box. See `lager box config "
500
- "apply --help` for the snippet to paste.",
501
- fg='yellow', err=True,
502
- )
503
- else:
504
- # Verify: marker file written by the bootstrap above (means the
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
- if verify_result.returncode == 0:
516
- click.secho("Passwordless sudo for `lager box config` configured", fg='green')
517
- else:
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 file installed but `sudo -n apt-get` still fails. "
520
- "Check /etc/sudoers.d/lager-box-config on the box for syntax issues.",
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 grants narrow NOPASSWD for exactly
1449
- # those operations. The probe ran the functional check (marker file
1450
- # present + `sudo -n apt-get` actually works), so we only bootstrap when
1451
- # it came back negative. Runs on every update so existing boxes
1452
- # gradually pick up the rule; idempotent.
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
- run_ssh_command_interactive(combined_priv_cmd, allow_sudo_prompt=True)
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()