sima-cli 2.1.19__tar.gz → 2.1.21__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 (253) hide show
  1. {sima_cli-2.1.19/sima_cli.egg-info → sima_cli-2.1.21}/PKG-INFO +1 -1
  2. {sima_cli-2.1.19 → sima_cli-2.1.21}/pyproject.toml +1 -1
  3. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/__version__.py +1 -1
  4. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/cli.py +23 -9
  5. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/cloudex/client.py +54 -4
  6. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/cloudex/commands.py +234 -15
  7. sima_cli-2.1.21/sima_cli/cloudex/neatops.py +254 -0
  8. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/discover/discover.py +73 -63
  9. sima_cli-2.1.21/sima_cli/discover/identity.py +70 -0
  10. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/discover/linuxll.py +38 -3
  11. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/download/downloader.py +37 -2
  12. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/install/metadata_installer.py +11 -5
  13. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/models/client.py +40 -15
  14. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/models/commands.py +24 -11
  15. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/models/rendering.py +78 -205
  16. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/peripherals/model.py +17 -3
  17. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/sdk/commands.py +38 -2
  18. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/sdk/install.py +91 -27
  19. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/sdk/neat.py +63 -10
  20. sima_cli-2.1.21/sima_cli/sdk/releases.json +63 -0
  21. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/sdk/utils.py +630 -44
  22. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/sdk/vscode_extensions.py +16 -1
  23. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/update/host.py +3 -4
  24. sima_cli-2.1.21/sima_cli/update/mirror_catalog.py +11 -0
  25. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/update/netboot.py +270 -87
  26. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/update/netboot_device.py +16 -4
  27. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/update/overlay.py +19 -7
  28. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/update/remote.py +14 -3
  29. sima_cli-2.1.21/sima_cli/update/reset_overlay.py +92 -0
  30. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/update/swu.py +22 -11
  31. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/update/swu_artifacts.py +17 -6
  32. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/update/swu_certificate.py +1 -2
  33. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/update/swu_target.py +35 -0
  34. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/utils/net.py +4 -3
  35. {sima_cli-2.1.19 → sima_cli-2.1.21/sima_cli.egg-info}/PKG-INFO +1 -1
  36. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli.egg-info/SOURCES.txt +16 -0
  37. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/peripherals_fixtures.py +6 -1
  38. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_cloudex_client.py +140 -11
  39. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_cloudex_commands.py +25 -1
  40. sima_cli-2.1.21/tests/unit/test_cloudex_neatops.py +584 -0
  41. sima_cli-2.1.21/tests/unit/test_discovery_host_tools.py +103 -0
  42. sima_cli-2.1.21/tests/unit/test_download_retries.py +115 -0
  43. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_generate_cli_markdown_docs.py +16 -0
  44. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_host_update.py +11 -1
  45. sima_cli-2.1.21/tests/unit/test_insight_url_routing.py +55 -0
  46. sima_cli-2.1.21/tests/unit/test_install_script_terminal.py +153 -0
  47. sima_cli-2.1.21/tests/unit/test_mirror_catalog.py +61 -0
  48. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_models.py +261 -201
  49. sima_cli-2.1.21/tests/unit/test_netboot_autoflash.py +197 -0
  50. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_netboot_device.py +39 -0
  51. sima_cli-2.1.21/tests/unit/test_netboot_identity.py +331 -0
  52. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_netboot_ip_recovery.py +4 -3
  53. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_overlay.py +41 -2
  54. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_peripherals_commands.py +21 -1
  55. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_peripherals_contract.py +11 -2
  56. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_peripherals_model.py +22 -3
  57. sima_cli-2.1.21/tests/unit/test_reset_overlay.py +159 -0
  58. sima_cli-2.1.21/tests/unit/test_sdk_agent_sessions.py +884 -0
  59. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_sdk_image_detection.py +438 -46
  60. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_sdk_model_compiler.py +47 -0
  61. sima_cli-2.1.21/tests/unit/test_sdk_release_mapping.py +97 -0
  62. sima_cli-2.1.21/tests/unit/test_selectable_resources.py +98 -0
  63. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_swu.py +55 -7
  64. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_swu_certificate.py +4 -3
  65. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_swu_mirror.py +13 -2
  66. sima_cli-2.1.21/tests/unit/test_swu_target.py +158 -0
  67. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_vscode_extension_pins.py +5 -3
  68. sima_cli-2.1.19/tests/unit/test_netboot_autoflash.py +0 -85
  69. sima_cli-2.1.19/tests/unit/test_swu_target.py +0 -58
  70. {sima_cli-2.1.19 → sima_cli-2.1.21}/LICENSE +0 -0
  71. {sima_cli-2.1.19 → sima_cli-2.1.21}/MANIFEST.in +0 -0
  72. {sima_cli-2.1.19 → sima_cli-2.1.21}/README.md +0 -0
  73. {sima_cli-2.1.19 → sima_cli-2.1.21}/requirements.txt +0 -0
  74. {sima_cli-2.1.19 → sima_cli-2.1.21}/setup.cfg +0 -0
  75. {sima_cli-2.1.19 → sima_cli-2.1.21}/setup.py +0 -0
  76. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/__init__.py +0 -0
  77. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/__main__.py +0 -0
  78. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/app_zoo/__init__.py +0 -0
  79. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/app_zoo/app.py +0 -0
  80. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/app_zoo/commands.py +0 -0
  81. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/auth/__init__.py +0 -0
  82. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/auth/auth0.py +0 -0
  83. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/auth/devportal.py +0 -0
  84. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/auth/login.py +0 -0
  85. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/cloudex/__init__.py +0 -0
  86. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/cloudex/installer.py +0 -0
  87. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/data/__init__.py +0 -0
  88. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/data/resources_internal.yaml +0 -0
  89. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/data/resources_public.yaml +0 -0
  90. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/deploy_only/__init__.py +0 -0
  91. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/deploy_only/device/__init__.py +0 -0
  92. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/deploy_only/device/commands.py +0 -0
  93. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/deploy_only/mpk/__init__.py +0 -0
  94. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/deploy_only/mpk/commands.py +0 -0
  95. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/discover/__init__.py +0 -0
  96. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/download/__init__.py +0 -0
  97. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/download/parallel.py +0 -0
  98. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/install/__init__.py +0 -0
  99. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/install/compatibility.py +0 -0
  100. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/install/github_assets.py +0 -0
  101. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/install/hostdriver.py +0 -0
  102. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/install/metadata_info.py +0 -0
  103. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/install/metadata_validator.py +0 -0
  104. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/install/optiview.py +0 -0
  105. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/install/package_builder.py +0 -0
  106. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/install/palette.py +0 -0
  107. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/install/registry.py +0 -0
  108. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/mla/__init__.py +0 -0
  109. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/mla/meminfo.py +0 -0
  110. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/model_zoo/__init__.py +0 -0
  111. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/model_zoo/model.py +0 -0
  112. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/models/__init__.py +0 -0
  113. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/network/__init__.py +0 -0
  114. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/network/network.py +0 -0
  115. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/peripherals/__init__.py +0 -0
  116. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/peripherals/commands.py +0 -0
  117. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/peripherals/sentinel.py +0 -0
  118. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/playbooks/__init__.py +0 -0
  119. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/playbooks/commands.py +0 -0
  120. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/playbooks/manager.py +0 -0
  121. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/sdk/__init__.py +0 -0
  122. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/sdk/browser_ca.py +0 -0
  123. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/sdk/cmdexec.py +0 -0
  124. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/sdk/config.py +0 -0
  125. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/sdk/container_registry.py +0 -0
  126. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/sdk/docker_staging.py +0 -0
  127. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/sdk/linux_devkit_network.py +0 -0
  128. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/sdk/linux_shared_network.py +0 -0
  129. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/sdk/model_compiler.py +0 -0
  130. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/sdk/network_doctor.py +0 -0
  131. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/sdk/preinstall.py +0 -0
  132. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/sdk/requirements.json +0 -0
  133. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/sdk/script.py +0 -0
  134. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/sdk/stop.py +0 -0
  135. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/sdk/uninstall.py +0 -0
  136. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/serial/__init__.py +0 -0
  137. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/serial/serial.py +0 -0
  138. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/storage/__init__.py +0 -0
  139. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/storage/nvme.py +0 -0
  140. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/storage/sdcard.py +0 -0
  141. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/update/__init__.py +0 -0
  142. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/update/ab_state.py +0 -0
  143. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/update/bmaptool.py +0 -0
  144. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/update/bootimg.py +0 -0
  145. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/update/cleanlog.py +0 -0
  146. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/update/elxr.py +0 -0
  147. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/update/emmc.py +0 -0
  148. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/update/fast_upload.py +0 -0
  149. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/update/local.py +0 -0
  150. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/update/netboot_artifacts.py +0 -0
  151. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/update/query.py +0 -0
  152. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/update/rootfs.py +0 -0
  153. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/update/staging.py +0 -0
  154. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/update/swap.py +0 -0
  155. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/update/uboot_environment.py +0 -0
  156. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/update/updater.py +0 -0
  157. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/upgrade/__init__.py +0 -0
  158. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/upgrade/selfupdate.py +0 -0
  159. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/utils/__init__.py +0 -0
  160. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/utils/api_common.py +0 -0
  161. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/utils/artifactory.py +0 -0
  162. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/utils/common.py +0 -0
  163. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/utils/config.py +0 -0
  164. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/utils/config_loader.py +0 -0
  165. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/utils/container_registries.py +0 -0
  166. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/utils/deprecation.py +0 -0
  167. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/utils/device_api.py +0 -0
  168. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/utils/disk.py +0 -0
  169. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/utils/docker.py +0 -0
  170. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/utils/env.py +0 -0
  171. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/utils/errors.py +0 -0
  172. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/utils/mpk_api.py +0 -0
  173. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/utils/network.py +0 -0
  174. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/utils/pcie.py +0 -0
  175. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/utils/pkg_update_check.py +0 -0
  176. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/utils/serializers.py +0 -0
  177. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/utils/services.py +0 -0
  178. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/utils/streaming.py +0 -0
  179. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/utils/tag.py +0 -0
  180. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/vulcan/__init__.py +0 -0
  181. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/vulcan/artifacts.py +0 -0
  182. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli/vulcan/commands.py +0 -0
  183. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli.egg-info/dependency_links.txt +0 -0
  184. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli.egg-info/entry_points.txt +0 -0
  185. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli.egg-info/requires.txt +0 -0
  186. {sima_cli-2.1.19 → sima_cli-2.1.21}/sima_cli.egg-info/top_level.txt +0 -0
  187. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/__init__.py +0 -0
  188. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/e2e/__init__.py +0 -0
  189. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/__init__.py +0 -0
  190. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_ab_overlay.py +0 -0
  191. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_app_zoo.py +0 -0
  192. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_appzoo_commands.py +0 -0
  193. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_artifact_parallel_integration.py +0 -0
  194. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_auth.py +0 -0
  195. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_auth0.py +0 -0
  196. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_bootimg_defaults.py +0 -0
  197. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_bootimg_recovery.py +0 -0
  198. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_cli.py +0 -0
  199. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_cli_stdio.py +0 -0
  200. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_cli_update.py +0 -0
  201. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_cli_update_rerun.py +0 -0
  202. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_cloudex_installer.py +0 -0
  203. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_code_ui_readiness.py +0 -0
  204. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_container_registries.py +0 -0
  205. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_deprecation.py +0 -0
  206. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_device_discovery_cloudex.py +0 -0
  207. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_docker_utils.py +0 -0
  208. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_download.py +0 -0
  209. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_elxr_update.py +0 -0
  210. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_emmc.py +0 -0
  211. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_fast_upload.py +0 -0
  212. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_firmware.py +0 -0
  213. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_firmware_creation_order.py +0 -0
  214. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_firmware_layout.py +0 -0
  215. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_firmware_selector.py +0 -0
  216. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_install_force.py +0 -0
  217. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_install_stub.py +0 -0
  218. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_localized_docs_links.py +0 -0
  219. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_metadata_installer.py +0 -0
  220. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_metadata_validator.py +0 -0
  221. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_mla_meminfo.py +0 -0
  222. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_model_deployments.py +0 -0
  223. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_model_zoo.py +0 -0
  224. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_modelzoo_commands.py +0 -0
  225. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_netboot.py +0 -0
  226. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_netboot_images.py +0 -0
  227. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_netboot_mirror.py +0 -0
  228. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_netboot_trootctl.py +0 -0
  229. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_network_custom_static.py +0 -0
  230. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_network_safety.py +0 -0
  231. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_package_builder.py +0 -0
  232. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_palette_image_discovery.py +0 -0
  233. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_parallel_downloads.py +0 -0
  234. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_pcie.py +0 -0
  235. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_peripherals_sentinel.py +0 -0
  236. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_pkg_update_check.py +0 -0
  237. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_sdk_browser_ca.py +0 -0
  238. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_sdk_container_registry.py +0 -0
  239. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_sdk_host_key_trust.py +0 -0
  240. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_sdk_linux_shared_network.py +0 -0
  241. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_sdk_network_doctor.py +0 -0
  242. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_sdk_preinstall.py +0 -0
  243. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_sdk_uninstall.py +0 -0
  244. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_selfupdate.py +0 -0
  245. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_skills_commands.py +0 -0
  246. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_skills_manager.py +0 -0
  247. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_slot_swap.py +0 -0
  248. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_streaming_downloads.py +0 -0
  249. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_swu_errors.py +0 -0
  250. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_uboot_environment.py +0 -0
  251. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_update_staging.py +0 -0
  252. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_utils.py +0 -0
  253. {sima_cli-2.1.19 → sima_cli-2.1.21}/tests/unit/test_vulcan.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sima-cli
3
- Version: 2.1.19
3
+ Version: 2.1.21
4
4
  Summary: CLI tool for SiMa Developer Portal to download models, firmware, and apps.
5
5
  Home-page: https://community.sima.ai/
6
6
  Author: SiMa.ai
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "sima-cli"
7
- version = "2.1.19"
7
+ version = "2.1.21"
8
8
  description = "CLI tool for SiMa Developer Portal to download models, firmware, and apps."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -1,2 +1,2 @@
1
1
  # sima_cli/__version__.py
2
- __version__ = "2.1.19"
2
+ __version__ = "2.1.21"
@@ -180,14 +180,14 @@ class InspectionAwareGroup(click.Group):
180
180
  _command_name_from_argv(['sima-cli'] + options) == 'bootimg'
181
181
  and any(arg in ('--netboot', '-n', '--autoflash', '-a') for arg in options)
182
182
  )
183
- if _command_name_from_argv(['sima-cli'] + options) == 'update' and any(arg in ('--inspect', '--swap') for arg in options):
183
+ if _command_name_from_argv(['sima-cli'] + options) == 'update' and any(arg in ('--inspect', '--swap', '--reset-overlay') for arg in options):
184
184
  ctx.meta['update_inspect'] = True
185
185
  return super().parse_args(ctx, args)
186
186
 
187
187
 
188
188
  # Entry point for the CLI tool using Click's command group decorator
189
189
  @click.group(cls=InspectionAwareGroup, context_settings=dict(help_option_names=["-h", "--help", "-?"], max_content_width=120))
190
- @click.option('-i', '--internal', is_flag=True, help="Use internal Artifactory resources, Authorized Sima employees only")
190
+ @click.option('-i', '--internal', is_flag=True, help="Use internal SiMa services, authorized SiMa employees only")
191
191
  @click.option('-y', '--yes', is_flag=True, help="Assume yes for confirmation prompts.")
192
192
  @click.version_option(version=f"{__version__}", message="SiMa CLI version: %(version)s")
193
193
  @click.pass_context
@@ -196,7 +196,7 @@ def main(ctx, internal, yes):
196
196
  sima-cli – SiMa Developer Portal CLI Tool
197
197
 
198
198
  Global Options:
199
- --internal Use internal Artifactory resources (can also be set via env variable SIMA_CLI_INTERNAL=1)
199
+ --internal Use internal SiMa services (can also be set via env variable SIMA_CLI_INTERNAL=1)
200
200
  """
201
201
  internal = internal or os.getenv("SIMA_CLI_INTERNAL", "0") in ("1", "true", "yes")
202
202
  if internal:
@@ -204,7 +204,10 @@ def main(ctx, internal, yes):
204
204
  Text("Pre-release software may be unstable. Use at your own risk.", style="yellow"),
205
205
  title="Pre-release software", border_style="yellow", width=72,
206
206
  ))
207
- if ctx.meta.get('update_inspect'):
207
+ # CloudEx selects Developer Portal or internal NeatOps authentication itself;
208
+ # its internal Vulcan flow does not require Artifactory setup.
209
+ if ctx.meta.get('update_inspect') or ctx.invoked_subcommand == 'cloudex':
210
+ _configure_stdio_errors()
208
211
  ctx.ensure_object(dict)
209
212
  ctx.obj.update(internal=internal, yes=yes)
210
213
  return
@@ -363,15 +366,16 @@ def download(ctx, url, dest):
363
366
  "--dryrun",
364
367
  is_flag=True,
365
368
  default=False,
366
- help="For eLxr, preview an update or slot swap without installing, switching slots, or rebooting."
369
+ help="For eLxr, preview an update, slot swap, or overlay reset without making changes."
367
370
  )
368
371
  @click.option("--inspect", "inspect_state", is_flag=True, help="Show eLxr 3.0+ A/B slots and overlay customizations without updating (local or --ip).")
372
+ @click.option("--reset-overlay", is_flag=True, help="Discard local overlay software and settings without updating firmware; reboot to use the clean system (eLxr 3.0+).")
369
373
  @click.option("--swap", is_flag=True, help="Select the available inactive eLxr 3.0+ slot for next boot; use --reboot to restart now.")
370
374
  @click.option("--verbose", is_flag=True, help="Show detailed overlay file categories during eLxr 3.0+ inspection or update.")
371
375
  @click.option("--signing-cert", metavar="URL_OR_FILE", help="SWUpdate PEM verification certificate: HTTP(S) URL or local file. Defaults to the certificate for the selected channel when configured (eLxr 3.0+).")
372
- @click.option("--reboot", is_flag=True, help="Reboot after successful eLxr 3.0+ installation or slot selection; verify remote boot health.")
376
+ @click.option("--reboot", is_flag=True, help="Reboot after successful eLxr 3.0+ installation, slot selection, or overlay reset.")
373
377
  @click.pass_context
374
- def update(ctx, version_or_url, version_option, ip, yes, passwd, flavor, force, troot_only, dryrun, inspect_state, verbose, reboot, signing_cert, swap):
378
+ def update(ctx, version_or_url, version_option, ip, yes, passwd, flavor, force, troot_only, dryrun, inspect_state, verbose, reboot, signing_cert, swap, reset_overlay):
375
379
  """
376
380
  Update a SiMa DevKit, remote device, or Linux PCIe host.
377
381
 
@@ -383,6 +387,10 @@ def update(ctx, version_or_url, version_option, ip, yes, passwd, flavor, force,
383
387
  eLxr 3.0+ uses signed full-system SWU bundles. Use --inspect for A/B
384
388
  state, --ip for remote updates, and --reboot to reboot after installation.
385
389
 
390
+ Use --reset-overlay to discard local software and settings without updating
391
+ firmware or switching slots. Save needed files first; the saved inventory
392
+ contains metadata only. Reboot promptly, or add --reboot. Use --dryrun to preview.
393
+
386
394
  Use --swap to select the valid inactive slot without downloading an update.
387
395
  The current and target versions are shown before confirmation; --yes skips
388
396
  confirmation. Reboot separately to apply, or add --reboot to restart now.
@@ -393,7 +401,8 @@ def update(ctx, version_or_url, version_option, ip, yes, passwd, flavor, force,
393
401
 
394
402
  The verification certificate is downloaded for the selected channel when configured; use
395
403
  --signing-cert URL_OR_FILE for an image signed with your own certificate.
396
- Developer-portal version lookup for 3.0 is not available yet.
404
+ For official eLxr 3.0+ releases, specify -v 3.0.0 (or -v 3.0) to
405
+ download the SWU from docs.sima.ai using your developer-portal login.
397
406
 
398
407
  Internal mode warns that pre-release software may be unstable and is used
399
408
  at your own risk. On eLxr 3.0+, unavailable Artifactory access falls back
@@ -501,6 +510,7 @@ def update(ctx, version_or_url, version_option, ip, yes, passwd, flavor, force,
501
510
  or troot_only
502
511
  or inspect_state
503
512
  or swap
513
+ or reset_overlay
504
514
  or verbose
505
515
  or reboot
506
516
  or force
@@ -518,6 +528,8 @@ def update(ctx, version_or_url, version_option, ip, yes, passwd, flavor, force,
518
528
  return
519
529
 
520
530
  is_elxr = is_devkit_running_elxr()
531
+ if reset_overlay and (inspect_state or swap or version_or_url or force or troot_only or signing_cert is not None or flavor != "auto"):
532
+ raise click.UsageError("--reset-overlay cannot be combined with --inspect, --swap, or installation options.")
521
533
  if swap and (inspect_state or version_or_url or force or troot_only or signing_cert is not None or flavor != "auto"):
522
534
  raise click.UsageError("--swap cannot be combined with --inspect or installation options.")
523
535
  if inspect_state and (version_or_url or dryrun or force or troot_only or reboot or signing_cert is not None or flavor != 'auto'):
@@ -527,7 +539,7 @@ def update(ctx, version_or_url, version_option, ip, yes, passwd, flavor, force,
527
539
  internal=ctx.obj.get("internal", False),
528
540
  auto_confirm=yes or ctx.obj.get("yes", False), dryrun=dryrun,
529
541
  signing_cert=signing_cert, reboot=reboot, inspect=inspect_state, force=force,
530
- troot_only=troot_only, flavor=flavor, local_elxr=is_elxr, verbose=verbose, swap=swap):
542
+ troot_only=troot_only, flavor=flavor, local_elxr=is_elxr, verbose=verbose, swap=swap, reset_overlay=reset_overlay):
531
543
  return
532
544
  except (click.ClickException, click.Abort):
533
545
  raise
@@ -815,6 +827,8 @@ def bootimg_cmd(ctx, version, boardtype, netboot, images, delete_cache, devkit_i
815
827
  else:
816
828
  write_image(version, boardtype, fwtype, internal, flavor='headless', recovery=recovery)
817
829
  click.echo("✅ Boot image successfully written.")
830
+ except click.ClickException:
831
+ raise
818
832
  except Exception as e:
819
833
  click.echo(f"❌ Failed to write boot image: {e}", err=True)
820
834
  ctx.exit(1)
@@ -7,6 +7,7 @@ import json
7
7
  import os
8
8
  import shutil
9
9
  import subprocess
10
+ import sys
10
11
  import time
11
12
  import uuid
12
13
  from pathlib import Path
@@ -17,6 +18,8 @@ import requests
17
18
 
18
19
  DEFAULT_ROOT = Path.home() / ".sima-cli" / "cloudex"
19
20
  FORWARDER_NAME = "kerrigan-p2p-forwarder"
21
+ MACOS_TUNNEL_PATH = "/usr/bin:/bin:/usr/sbin:/sbin:/opt/homebrew/bin:/usr/local/bin"
22
+ MACOS_WIREGUARD_INSTALL = "Install WireGuard CLI support with: brew install wireguard-tools"
20
23
  CONNECTION_SETUP_TIMEOUT_SECONDS = 60
21
24
  DIAGNOSTIC_LOG_LINES = 50
22
25
  SAFE_FORWARDER_STAGES = {
@@ -68,8 +71,7 @@ def decode_key(value):
68
71
  raise CloudExError("--key contains an invalid value")
69
72
  if len(profile["allocation_secret"]) < 32:
70
73
  raise CloudExError("--key contains an invalid allocation secret")
71
- if profile["environment"] != "vulcan-staging":
72
- raise CloudExError("CloudEx currently supports only vulcan-staging allocation keys")
74
+ # The environment is a descriptive label; api_url selects the backend.
73
75
  return profile
74
76
 
75
77
 
@@ -393,6 +395,9 @@ def _forwarder_failure(log_path):
393
395
  "start wireguard-go": "the WireGuard interface could not start",
394
396
  "invalid session config": "the native forwarder rejected the session configuration",
395
397
  }
398
+ if sys.platform == "darwin":
399
+ for needle in ("wireguard is unavailable", "start wireguard-go"):
400
+ mappings[needle] += ". " + MACOS_WIREGUARD_INSTALL
396
401
  for needle, message in mappings.items():
397
402
  if needle in detail:
398
403
  return message + suffix
@@ -552,6 +557,50 @@ def _reconcile_existing_connection(profile, store, progress):
552
557
  return None
553
558
 
554
559
 
560
+ def _forwarder_sudo_prefix(progress=None):
561
+ """Install missing macOS tools as the user, then expose them through sudo."""
562
+ prefix = ["sudo", "-n", "--"]
563
+ if sys.platform != "darwin":
564
+ return prefix
565
+ missing = [name for name in ("wg", "wireguard-go")
566
+ if shutil.which(name, path=MACOS_TUNNEL_PATH) is None]
567
+ if missing:
568
+ brew = shutil.which("brew", path=MACOS_TUNNEL_PATH)
569
+ if brew is None:
570
+ raise CloudExError(
571
+ "CloudEx requires {} on macOS. Install Homebrew, then retry CloudEx; "
572
+ "missing WireGuard dependencies will be installed automatically.".format(
573
+ ", ".join(missing)
574
+ )
575
+ )
576
+ packages = ["wireguard-tools" if name == "wg" else name for name in missing]
577
+ if progress:
578
+ progress.update("Installing missing WireGuard dependencies with Homebrew")
579
+ environment = dict(os.environ, PATH=MACOS_TUNNEL_PATH)
580
+ try:
581
+ result = subprocess.run(
582
+ [brew, "install", *packages],
583
+ env=environment, stdin=subprocess.DEVNULL,
584
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=600,
585
+ )
586
+ except (OSError, subprocess.TimeoutExpired) as exc:
587
+ raise CloudExError(
588
+ "Could not install WireGuard dependencies with Homebrew. " + MACOS_WIREGUARD_INSTALL
589
+ ) from exc
590
+ if result.returncode:
591
+ detail = (result.stderr or result.stdout or "Homebrew failed").strip()
592
+ raise CloudExError("WireGuard dependency installation failed: " + detail[-1024:])
593
+ missing = [name for name in ("wg", "wireguard-go")
594
+ if shutil.which(name, path=MACOS_TUNNEL_PATH) is None]
595
+ if missing:
596
+ raise CloudExError(
597
+ "Homebrew completed but CloudEx still cannot find: {}. {}".format(
598
+ ", ".join(missing), MACOS_WIREGUARD_INSTALL
599
+ )
600
+ )
601
+ return prefix + ["/usr/bin/env", "PATH=" + MACOS_TUNNEL_PATH]
602
+
603
+
555
604
  def connect(profile, store, progress, attempts=3, transport="auto"):
556
605
  """Create a session and return its durable local record."""
557
606
  from .installer import ensure_forwarder
@@ -561,6 +610,7 @@ def connect(profile, store, progress, attempts=3, transport="auto"):
561
610
  existing = _reconcile_existing_connection(profile, store, progress)
562
611
  if existing:
563
612
  return existing
613
+ sudo_prefix = _forwarder_sudo_prefix(progress=progress)
564
614
  forwarder = ensure_forwarder(progress=progress)
565
615
  choices = (["p2p"] * attempts + ["routed"] if transport == "auto"
566
616
  else ["routed"] if transport == "turn" else ["p2p"] * attempts)
@@ -610,8 +660,8 @@ def connect(profile, store, progress, attempts=3, transport="auto"):
610
660
  config_path.chmod(0o600)
611
661
  with log_path.open("x", encoding="utf-8") as log:
612
662
  process = subprocess.Popen(
613
- [
614
- "sudo", "-n", "--", str(forwarder), "session", "--role", "user",
663
+ sudo_prefix + [
664
+ str(forwarder), "session", "--role", "user",
615
665
  "--config", str(config_path), "--report", str(report_path),
616
666
  ],
617
667
  stdin=subprocess.DEVNULL,
@@ -1,10 +1,14 @@
1
1
  """Hidden, experimental CloudEx command group."""
2
2
 
3
+ import base64
4
+ import json
3
5
  import os
6
+ import uuid
4
7
  import stat
5
8
  import sys
6
9
  import time
7
10
  from datetime import datetime, timezone
11
+ from functools import wraps
8
12
  from pathlib import Path
9
13
 
10
14
  import click
@@ -25,6 +29,7 @@ from .client import (
25
29
  run_benchmark,
26
30
  )
27
31
  from .installer import DEFAULT_BRANCH, install_forwarder
32
+ from .neatops import NeatOps
28
33
 
29
34
 
30
35
  console = Console()
@@ -206,9 +211,33 @@ def _session_table(rows, show_benchmark=False):
206
211
  return table
207
212
 
208
213
 
214
+ def _internal_cloudex():
215
+ ctx = click.get_current_context(silent=True)
216
+ return bool(ctx and isinstance(ctx.obj, dict) and ctx.obj.get("internal"))
217
+
218
+
219
+ def _require_vulcan():
220
+ if not _internal_cloudex():
221
+ raise click.ClickException(
222
+ "CloudEx is not available on Arthas yet. Use sima-cli -i cloudex for Vulcan."
223
+ )
224
+
225
+
226
+ def _vulcan_only(function):
227
+ @wraps(function)
228
+ def wrapped(*args, **kwargs):
229
+ _require_vulcan()
230
+ return function(*args, **kwargs)
231
+ return wrapped
232
+
233
+
209
234
  @click.group(name="cloudex", hidden=True)
210
- def cloudex_group():
211
- """Connect to allocated DevKits through CloudEx."""
235
+ @click.option("-i", "--internal", is_flag=True, help="Use internal Vulcan CloudEx and NeatOps authentication.")
236
+ @click.pass_context
237
+ def cloudex_group(ctx, internal):
238
+ """Manage CloudEx devices; use -i for internal Vulcan access."""
239
+ ctx.ensure_object(dict)
240
+ ctx.obj["internal"] = internal or ctx.obj.get("internal", False) or os.getenv("SIMA_CLI_INTERNAL", "0") in ("1", "true", "yes")
212
241
 
213
242
 
214
243
  @cloudex_group.command("connect")
@@ -222,6 +251,8 @@ def cloudex_group():
222
251
  is_flag=True,
223
252
  help="Read the allocation key from standard input for automation.",
224
253
  )
254
+ @click.option("--allocation", help="Connect to your NeatOps allocation ID without copying a key.")
255
+ @click.option("--environment", type=click.Choice(["production", "staging"]), default="production", show_default=True, help="Vulcan environment (requires -i).")
225
256
  @click.option("--transport", type=click.Choice(["auto", "p2p", "turn"]), default="auto", hidden=True)
226
257
  @click.option("--attempts", type=click.IntRange(1, 5), default=3, hidden=True)
227
258
  @click.option(
@@ -229,11 +260,16 @@ def cloudex_group():
229
260
  is_flag=True,
230
261
  help="Print each sanitized connection stage for troubleshooting.",
231
262
  )
232
- def connect_command(key_file, key_stdin, transport, attempts, verbose):
263
+ @_vulcan_only
264
+ def connect_command(key_file, key_stdin, allocation, environment, transport, attempts, verbose):
233
265
  """Connect to an allocated DevKit using a securely supplied key."""
234
- if key_file and key_stdin:
235
- raise click.UsageError("Use either --key-file or --key-stdin, not both.")
266
+ if sum(bool(value) for value in (key_file, key_stdin, allocation)) > 1:
267
+ raise click.UsageError("Use either --key-file or --key-stdin, not both; --allocation must be used alone.")
236
268
  try:
269
+ if allocation:
270
+ profile = _allocation_profile(_neatops_api(environment), _allocation_id(allocation))
271
+ _connect_profile(profile, verbose=verbose, attempts=attempts, transport=transport)
272
+ return
237
273
  if key_file:
238
274
  key = _read_key_file(key_file)
239
275
  elif key_stdin:
@@ -243,16 +279,7 @@ def connect_command(key_file, key_stdin, transport, attempts, verbose):
243
279
  else:
244
280
  key = click.prompt("CloudEx allocation key", hide_input=True).strip()
245
281
  profile = decode_key(key)
246
- console.print("[cyan]→[/cyan] Authorizing encrypted tunnel access")
247
- _authorize_supported_host()
248
- with StageProgress("Preparing CloudEx connection", verbose=verbose) as progress:
249
- session = connect(profile, _store(), progress, attempts=attempts, transport=transport)
250
- progress.success("Connected to {}".format(session["device"]))
251
- path = session.get("ice_path", "unknown")
252
- path_text = "direct" if path == "direct" else "relayed" if path == "relay" else path
253
- console.print(" [dim]Session[/dim] {}".format(_short(session["session_id"])))
254
- console.print(" [dim]Path[/dim] {}".format(path_text))
255
- console.print(" [dim]SSH[/dim] [bold]ssh sima@{}[/bold]".format(session["target"]))
282
+ _connect_profile(profile, verbose=verbose, attempts=attempts, transport=transport)
256
283
  except CloudExError as exc:
257
284
  raise click.ClickException(str(exc)) from exc
258
285
 
@@ -264,6 +291,7 @@ def connect_command(key_file, key_stdin, transport, attempts, verbose):
264
291
  show_default=True,
265
292
  help="Kerrigan branch whose latest forwarder should be installed.",
266
293
  )
294
+ @_vulcan_only
267
295
  def update_command(branch):
268
296
  """Install or update the native CloudEx forwarder."""
269
297
  try:
@@ -326,6 +354,7 @@ def _choose_sessions(sessions):
326
354
  @cloudex_group.command("disconnect")
327
355
  @click.option("--session", "session_id", help="Session ID or unique prefix to disconnect.")
328
356
  @click.option("--all", "disconnect_all", is_flag=True, help="Disconnect every recorded CloudEx session.")
357
+ @_vulcan_only
329
358
  def disconnect_command(session_id, disconnect_all):
330
359
  """Disconnect one or all CloudEx sessions."""
331
360
  if session_id and disconnect_all:
@@ -373,6 +402,7 @@ def disconnect_command(session_id, disconnect_all):
373
402
  @click.option("--benchmark", is_flag=True, help="Stress-test RTT and maximum bidirectional throughput for each connection.")
374
403
  @click.option("--duration", type=click.IntRange(1, 60), default=10, show_default=True,
375
404
  help="Throughput measurement window in seconds.")
405
+ @_vulcan_only
376
406
  def list_command(benchmark, duration):
377
407
  """List locally recorded CloudEx connections."""
378
408
  store = _store()
@@ -420,3 +450,192 @@ def list_command(benchmark, duration):
420
450
 
421
451
  def register_cloudex_commands(main):
422
452
  main.add_command(cloudex_group)
453
+
454
+
455
+ def _connect_profile(profile, verbose=False, attempts=3, transport="auto"):
456
+ console.print("[cyan]→[/cyan] Authorizing encrypted tunnel access")
457
+ _authorize_supported_host()
458
+ with StageProgress("Preparing CloudEx connection", verbose=verbose) as progress:
459
+ session = connect(profile, _store(), progress, attempts=attempts, transport=transport)
460
+ progress.success("Connected to {}".format(session["device"]))
461
+ path = session.get("ice_path", "unknown")
462
+ path_text = "direct" if path == "direct" else "relayed" if path == "relay" else path
463
+ console.print(" [dim]Session[/dim] {}".format(_short(session["session_id"])))
464
+ console.print(" [dim]Path[/dim] {}".format(path_text))
465
+ console.print(" [dim]SSH[/dim] [bold]ssh sima@{}[/bold]".format(session["target"]))
466
+
467
+
468
+ DURATIONS = {"1h": 3600, "4h": 14400, "8h": 28800, "1d": 86400, "7d": 604800, "4w": 2419200}
469
+
470
+
471
+ def _allocation_id(value):
472
+ if len(value) != 32 or any(c not in "0123456789abcdef" for c in value):
473
+ raise CloudExError("Use the full 32-character allocation ID from sima-cli -i cloudex devices")
474
+ return value
475
+
476
+
477
+ def _allocation_profile(api, allocation_id, timeout=180):
478
+ deadline = time.monotonic() + timeout
479
+ with StageProgress("Waiting for device preparation") as progress:
480
+ while True:
481
+ inventory = api.call("GET", "/devkits")
482
+ allocation = next((a for a in inventory.get("allocations", []) if a["session_id"] == allocation_id), None)
483
+ if not allocation or allocation.get("status") in {"released", "releasing", "expired", "failed"}:
484
+ raise CloudExError("Allocation is unavailable; check sima-cli -i cloudex devices")
485
+ if allocation.get("status") in {"ready", "active"}:
486
+ profile = api.call("POST", "/devkits/allocations/" + allocation_id + "/profile", {})
487
+ # Reuse the same strict profile validation as manually supplied keys.
488
+ profile = decode_key(base64.b64encode(json.dumps(profile).encode()).decode())
489
+ progress.success("Device is ready")
490
+ return profile
491
+ if time.monotonic() >= deadline:
492
+ raise CloudExError("Device preparation timed out. Allocation {} is retained; retry with sima-cli -i cloudex connect --allocation {} or release it".format(allocation_id, allocation_id))
493
+ time.sleep(2)
494
+
495
+
496
+ def _neatops_login(api):
497
+ api.login(lambda url: console.print("Open this URL in a browser on this computer:\n" + url, markup=False, soft_wrap=True))
498
+ console.print("[green]Signed in to NeatOps ({})[/green]".format(api.environment))
499
+
500
+
501
+ def _neatops_api(environment):
502
+ _require_vulcan()
503
+ return NeatOps(environment, on_login_required=_neatops_login)
504
+
505
+
506
+ @cloudex_group.command("login")
507
+ @click.option("--environment", type=click.Choice(["production", "staging"]), default="production", show_default=True, help="Vulcan environment (requires -i).")
508
+ def login_command(environment):
509
+ """Sign in to the Developer Portal, or NeatOps with -i."""
510
+ if not _internal_cloudex():
511
+ if environment != "production":
512
+ raise click.UsageError("--environment selects a Vulcan environment and requires -i.")
513
+ from sima_cli.auth.devportal import login_external
514
+ if not login_external(loginDocker=False):
515
+ raise click.ClickException("Developer Portal sign-in was not completed.")
516
+ console.print("Signed in to the Developer Portal. CloudEx is not available on Arthas yet.")
517
+ return
518
+ try:
519
+ _neatops_login(NeatOps(environment))
520
+ except CloudExError as exc:
521
+ raise click.ClickException(str(exc)) from exc
522
+
523
+
524
+ @cloudex_group.command("logout")
525
+ @click.option("--environment", type=click.Choice(["production", "staging"]), default="production", show_default=True, help="Vulcan environment (requires -i).")
526
+ def logout_command(environment):
527
+ """Sign out of the Developer Portal, or NeatOps with -i; keep leases."""
528
+ if not _internal_cloudex():
529
+ if environment != "production":
530
+ raise click.UsageError("--environment selects a Vulcan environment and requires -i.")
531
+ from sima_cli.auth.auth0 import clear_external_login_state
532
+ clear_external_login_state()
533
+ console.print("Signed out of the Developer Portal.")
534
+ return
535
+ try:
536
+ NeatOps(environment).logout()
537
+ console.print("Signed out of NeatOps. Existing device leases are unchanged.")
538
+ except CloudExError as exc:
539
+ raise click.ClickException(str(exc)) from exc
540
+
541
+
542
+ @cloudex_group.command("devices")
543
+ @click.option("--environment", type=click.Choice(["production", "staging"]), default="production", show_default=True, help="Vulcan environment (requires -i).")
544
+ @_vulcan_only
545
+ def devices_command(environment):
546
+ """Show NeatOps device availability and your allocation IDs."""
547
+ try:
548
+ inventory = _neatops_api(environment).call("GET", "/devkits")
549
+ table = Table("Device type", "Firmware version", "ID", "Allocation ID", "State")
550
+ allocations = {(a["cluster_id"], a["target_id"]): a for a in inventory.get("allocations", [])}
551
+ visible_devices = set()
552
+ for d in inventory.get("devices", []):
553
+ device_key = (d["cluster_id"], d["id"])
554
+ visible_devices.add(device_key)
555
+ a = d.get("allocation") or allocations.get(device_key, {})
556
+ table.add_row(d.get("type") or "Unknown", d.get("firmware") or "Unknown", d["id"],
557
+ a.get("session_id") or "—", "Available" if d["available"] else "Not available")
558
+ # Allocations can survive an appliance disappearing from inventory.
559
+ for a in inventory.get("allocations", []):
560
+ if (a["cluster_id"], a["target_id"]) not in visible_devices:
561
+ table.add_row("Unknown", "Unknown", a["target_id"], a["session_id"], "Not available")
562
+ console.print(table)
563
+ except CloudExError as exc:
564
+ raise click.ClickException(str(exc)) from exc
565
+
566
+
567
+ @cloudex_group.command("request")
568
+ @click.option("--device", help="Device ID or exact name; otherwise choose interactively.")
569
+ @click.option("--cluster", help="Appliance ID, to disambiguate devices.")
570
+ @click.option("--duration", type=click.Choice(list(DURATIONS)), default="1h", show_default=True)
571
+ @click.option("--environment", type=click.Choice(["production", "staging"]), default="production", show_default=True, help="Vulcan environment (requires -i).")
572
+ @click.option("--verbose", is_flag=True, help="Print sanitized connection stages.")
573
+ @_vulcan_only
574
+ def request_command(device, cluster, duration, environment, verbose):
575
+ """Request a NeatOps device, wait for preparation, and connect automatically."""
576
+ allocation_id = None
577
+ try:
578
+ api = _neatops_api(environment)
579
+ entitlement = api.call("GET", "/users/me/entitlement")["entitlement"]
580
+ if DURATIONS[duration] > entitlement["max_lease_duration_seconds"]:
581
+ raise CloudExError("Requested duration exceeds your NeatOps lease limit; choose a shorter duration")
582
+ inventory = api.call("GET", "/devkits")
583
+ if len(inventory.get("allocations", [])) >= entitlement["max_active_leases"]:
584
+ raise CloudExError("Your device lease limit is reached; connect to an existing allocation or release one")
585
+ candidates = [d for d in inventory.get("devices", []) if d.get("available")
586
+ and (not cluster or d["cluster_id"] == cluster)
587
+ and (not device or device in {d["id"], d["name"]})]
588
+ if not candidates:
589
+ raise CloudExError("No matching devices are available; check sima-cli -i cloudex devices")
590
+ if device and len(candidates) > 1:
591
+ raise CloudExError("Device name or ID is ambiguous; specify --cluster")
592
+ if not device:
593
+ table = Table("#", "Device type", "Software version", "ID")
594
+ for index, d in enumerate(candidates, 1):
595
+ table.add_row(str(index), d.get("type") or "Unknown", d.get("firmware") or "Unknown", d["id"])
596
+ console.print(table)
597
+ choice = click.prompt("Choose a device", type=click.IntRange(1, len(candidates)))
598
+ selected = candidates[choice - 1]
599
+ else:
600
+ selected = candidates[0]
601
+ # Get local administrator authorization before reserving scarce hardware.
602
+ _authorize_supported_host()
603
+ allocation_id = uuid.uuid4().hex
604
+ console.print("Allocation {} · {} · {}".format(allocation_id, selected["name"], duration), markup=False)
605
+ api.call("POST", "/devkits/allocations", {"request_id": allocation_id,
606
+ "cluster_id": selected["cluster_id"], "target_id": selected["id"], "duration_seconds": DURATIONS[duration]})
607
+ profile = _allocation_profile(api, allocation_id)
608
+ _connect_profile(profile, verbose=verbose)
609
+ except CloudExError as exc:
610
+ if allocation_id:
611
+ console.print("Inspect: sima-cli -i cloudex devices --environment {}\nResume: sima-cli -i cloudex connect --allocation {} --environment {}\nRelease: sima-cli -i cloudex release {} --environment {}".format(environment, allocation_id, environment, allocation_id, environment), markup=False, soft_wrap=True)
612
+ raise click.ClickException(str(exc)) from exc
613
+
614
+
615
+ @cloudex_group.command("release")
616
+ @click.argument("allocation_id")
617
+ @click.option("--environment", type=click.Choice(["production", "staging"]), default="production", show_default=True, help="Vulcan environment (requires -i).")
618
+ @_vulcan_only
619
+ def release_command(allocation_id, environment):
620
+ """Release your NeatOps lease and disconnect its local CloudEx sessions."""
621
+ try:
622
+ allocation_id = _allocation_id(allocation_id)
623
+ store = _store()
624
+ failures = []
625
+ for session in store.list():
626
+ if session.get("allocation_id") == allocation_id:
627
+ try:
628
+ with StageProgress("Disconnecting CloudEx") as progress:
629
+ disconnect_session(session, store, progress)
630
+ except CloudExError as exc:
631
+ failures.append("Tunnel cleanup failed: {}. Local retry state retained; retry sima-cli -i cloudex disconnect.".format(exc))
632
+ try:
633
+ _neatops_api(environment).call("DELETE", "/devkits/allocations/" + allocation_id)
634
+ except CloudExError as exc:
635
+ failures.append("Lease release failed: {}".format(exc))
636
+ else:
637
+ console.print("Lease released; device availability returns after cleanup is confirmed.")
638
+ if failures:
639
+ raise CloudExError("; ".join(failures))
640
+ except CloudExError as exc:
641
+ raise click.ClickException(str(exc)) from exc