sima-cli 2.1.20__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 (251) hide show
  1. {sima_cli-2.1.20/sima_cli.egg-info → sima_cli-2.1.21}/PKG-INFO +1 -1
  2. {sima_cli-2.1.20 → sima_cli-2.1.21}/pyproject.toml +1 -1
  3. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/__version__.py +1 -1
  4. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/cli.py +6 -3
  5. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/cloudex/client.py +1 -2
  6. {sima_cli-2.1.20 → 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.20 → sima_cli-2.1.21}/sima_cli/discover/discover.py +69 -63
  9. sima_cli-2.1.21/sima_cli/discover/identity.py +70 -0
  10. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/sdk/commands.py +6 -1
  11. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/sdk/install.py +45 -26
  12. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/sdk/neat.py +6 -0
  13. sima_cli-2.1.21/sima_cli/sdk/releases.json +63 -0
  14. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/sdk/utils.py +93 -40
  15. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/sdk/vscode_extensions.py +16 -1
  16. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/update/netboot.py +193 -73
  17. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/update/netboot_device.py +16 -4
  18. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/utils/net.py +4 -3
  19. {sima_cli-2.1.20 → sima_cli-2.1.21/sima_cli.egg-info}/PKG-INFO +1 -1
  20. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli.egg-info/SOURCES.txt +7 -0
  21. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_cloudex_client.py +13 -6
  22. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_cloudex_commands.py +1 -1
  23. sima_cli-2.1.21/tests/unit/test_cloudex_neatops.py +584 -0
  24. sima_cli-2.1.21/tests/unit/test_insight_url_routing.py +55 -0
  25. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_netboot_autoflash.py +2 -1
  26. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_netboot_device.py +39 -0
  27. sima_cli-2.1.21/tests/unit/test_netboot_identity.py +331 -0
  28. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_netboot_ip_recovery.py +4 -3
  29. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_sdk_image_detection.py +96 -34
  30. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_sdk_model_compiler.py +23 -0
  31. sima_cli-2.1.21/tests/unit/test_sdk_release_mapping.py +97 -0
  32. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_vscode_extension_pins.py +4 -3
  33. {sima_cli-2.1.20 → sima_cli-2.1.21}/LICENSE +0 -0
  34. {sima_cli-2.1.20 → sima_cli-2.1.21}/MANIFEST.in +0 -0
  35. {sima_cli-2.1.20 → sima_cli-2.1.21}/README.md +0 -0
  36. {sima_cli-2.1.20 → sima_cli-2.1.21}/requirements.txt +0 -0
  37. {sima_cli-2.1.20 → sima_cli-2.1.21}/setup.cfg +0 -0
  38. {sima_cli-2.1.20 → sima_cli-2.1.21}/setup.py +0 -0
  39. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/__init__.py +0 -0
  40. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/__main__.py +0 -0
  41. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/app_zoo/__init__.py +0 -0
  42. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/app_zoo/app.py +0 -0
  43. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/app_zoo/commands.py +0 -0
  44. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/auth/__init__.py +0 -0
  45. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/auth/auth0.py +0 -0
  46. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/auth/devportal.py +0 -0
  47. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/auth/login.py +0 -0
  48. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/cloudex/__init__.py +0 -0
  49. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/cloudex/installer.py +0 -0
  50. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/data/__init__.py +0 -0
  51. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/data/resources_internal.yaml +0 -0
  52. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/data/resources_public.yaml +0 -0
  53. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/deploy_only/__init__.py +0 -0
  54. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/deploy_only/device/__init__.py +0 -0
  55. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/deploy_only/device/commands.py +0 -0
  56. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/deploy_only/mpk/__init__.py +0 -0
  57. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/deploy_only/mpk/commands.py +0 -0
  58. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/discover/__init__.py +0 -0
  59. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/discover/linuxll.py +0 -0
  60. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/download/__init__.py +0 -0
  61. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/download/downloader.py +0 -0
  62. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/download/parallel.py +0 -0
  63. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/install/__init__.py +0 -0
  64. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/install/compatibility.py +0 -0
  65. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/install/github_assets.py +0 -0
  66. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/install/hostdriver.py +0 -0
  67. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/install/metadata_info.py +0 -0
  68. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/install/metadata_installer.py +0 -0
  69. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/install/metadata_validator.py +0 -0
  70. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/install/optiview.py +0 -0
  71. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/install/package_builder.py +0 -0
  72. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/install/palette.py +0 -0
  73. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/install/registry.py +0 -0
  74. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/mla/__init__.py +0 -0
  75. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/mla/meminfo.py +0 -0
  76. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/model_zoo/__init__.py +0 -0
  77. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/model_zoo/model.py +0 -0
  78. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/models/__init__.py +0 -0
  79. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/models/client.py +0 -0
  80. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/models/commands.py +0 -0
  81. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/models/rendering.py +0 -0
  82. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/network/__init__.py +0 -0
  83. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/network/network.py +0 -0
  84. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/peripherals/__init__.py +0 -0
  85. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/peripherals/commands.py +0 -0
  86. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/peripherals/model.py +0 -0
  87. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/peripherals/sentinel.py +0 -0
  88. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/playbooks/__init__.py +0 -0
  89. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/playbooks/commands.py +0 -0
  90. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/playbooks/manager.py +0 -0
  91. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/sdk/__init__.py +0 -0
  92. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/sdk/browser_ca.py +0 -0
  93. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/sdk/cmdexec.py +0 -0
  94. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/sdk/config.py +0 -0
  95. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/sdk/container_registry.py +0 -0
  96. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/sdk/docker_staging.py +0 -0
  97. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/sdk/linux_devkit_network.py +0 -0
  98. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/sdk/linux_shared_network.py +0 -0
  99. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/sdk/model_compiler.py +0 -0
  100. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/sdk/network_doctor.py +0 -0
  101. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/sdk/preinstall.py +0 -0
  102. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/sdk/requirements.json +0 -0
  103. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/sdk/script.py +0 -0
  104. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/sdk/stop.py +0 -0
  105. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/sdk/uninstall.py +0 -0
  106. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/serial/__init__.py +0 -0
  107. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/serial/serial.py +0 -0
  108. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/storage/__init__.py +0 -0
  109. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/storage/nvme.py +0 -0
  110. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/storage/sdcard.py +0 -0
  111. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/update/__init__.py +0 -0
  112. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/update/ab_state.py +0 -0
  113. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/update/bmaptool.py +0 -0
  114. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/update/bootimg.py +0 -0
  115. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/update/cleanlog.py +0 -0
  116. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/update/elxr.py +0 -0
  117. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/update/emmc.py +0 -0
  118. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/update/fast_upload.py +0 -0
  119. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/update/host.py +0 -0
  120. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/update/local.py +0 -0
  121. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/update/mirror_catalog.py +0 -0
  122. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/update/netboot_artifacts.py +0 -0
  123. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/update/overlay.py +0 -0
  124. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/update/query.py +0 -0
  125. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/update/remote.py +0 -0
  126. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/update/reset_overlay.py +0 -0
  127. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/update/rootfs.py +0 -0
  128. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/update/staging.py +0 -0
  129. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/update/swap.py +0 -0
  130. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/update/swu.py +0 -0
  131. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/update/swu_artifacts.py +0 -0
  132. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/update/swu_certificate.py +0 -0
  133. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/update/swu_target.py +0 -0
  134. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/update/uboot_environment.py +0 -0
  135. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/update/updater.py +0 -0
  136. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/upgrade/__init__.py +0 -0
  137. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/upgrade/selfupdate.py +0 -0
  138. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/utils/__init__.py +0 -0
  139. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/utils/api_common.py +0 -0
  140. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/utils/artifactory.py +0 -0
  141. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/utils/common.py +0 -0
  142. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/utils/config.py +0 -0
  143. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/utils/config_loader.py +0 -0
  144. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/utils/container_registries.py +0 -0
  145. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/utils/deprecation.py +0 -0
  146. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/utils/device_api.py +0 -0
  147. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/utils/disk.py +0 -0
  148. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/utils/docker.py +0 -0
  149. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/utils/env.py +0 -0
  150. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/utils/errors.py +0 -0
  151. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/utils/mpk_api.py +0 -0
  152. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/utils/network.py +0 -0
  153. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/utils/pcie.py +0 -0
  154. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/utils/pkg_update_check.py +0 -0
  155. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/utils/serializers.py +0 -0
  156. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/utils/services.py +0 -0
  157. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/utils/streaming.py +0 -0
  158. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/utils/tag.py +0 -0
  159. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/vulcan/__init__.py +0 -0
  160. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/vulcan/artifacts.py +0 -0
  161. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli/vulcan/commands.py +0 -0
  162. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli.egg-info/dependency_links.txt +0 -0
  163. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli.egg-info/entry_points.txt +0 -0
  164. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli.egg-info/requires.txt +0 -0
  165. {sima_cli-2.1.20 → sima_cli-2.1.21}/sima_cli.egg-info/top_level.txt +0 -0
  166. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/__init__.py +0 -0
  167. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/e2e/__init__.py +0 -0
  168. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/__init__.py +0 -0
  169. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/peripherals_fixtures.py +0 -0
  170. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_ab_overlay.py +0 -0
  171. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_app_zoo.py +0 -0
  172. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_appzoo_commands.py +0 -0
  173. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_artifact_parallel_integration.py +0 -0
  174. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_auth.py +0 -0
  175. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_auth0.py +0 -0
  176. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_bootimg_defaults.py +0 -0
  177. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_bootimg_recovery.py +0 -0
  178. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_cli.py +0 -0
  179. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_cli_stdio.py +0 -0
  180. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_cli_update.py +0 -0
  181. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_cli_update_rerun.py +0 -0
  182. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_cloudex_installer.py +0 -0
  183. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_code_ui_readiness.py +0 -0
  184. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_container_registries.py +0 -0
  185. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_deprecation.py +0 -0
  186. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_device_discovery_cloudex.py +0 -0
  187. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_discovery_host_tools.py +0 -0
  188. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_docker_utils.py +0 -0
  189. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_download.py +0 -0
  190. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_download_retries.py +0 -0
  191. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_elxr_update.py +0 -0
  192. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_emmc.py +0 -0
  193. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_fast_upload.py +0 -0
  194. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_firmware.py +0 -0
  195. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_firmware_creation_order.py +0 -0
  196. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_firmware_layout.py +0 -0
  197. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_firmware_selector.py +0 -0
  198. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_generate_cli_markdown_docs.py +0 -0
  199. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_host_update.py +0 -0
  200. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_install_force.py +0 -0
  201. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_install_script_terminal.py +0 -0
  202. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_install_stub.py +0 -0
  203. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_localized_docs_links.py +0 -0
  204. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_metadata_installer.py +0 -0
  205. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_metadata_validator.py +0 -0
  206. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_mirror_catalog.py +0 -0
  207. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_mla_meminfo.py +0 -0
  208. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_model_deployments.py +0 -0
  209. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_model_zoo.py +0 -0
  210. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_models.py +0 -0
  211. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_modelzoo_commands.py +0 -0
  212. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_netboot.py +0 -0
  213. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_netboot_images.py +0 -0
  214. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_netboot_mirror.py +0 -0
  215. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_netboot_trootctl.py +0 -0
  216. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_network_custom_static.py +0 -0
  217. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_network_safety.py +0 -0
  218. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_overlay.py +0 -0
  219. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_package_builder.py +0 -0
  220. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_palette_image_discovery.py +0 -0
  221. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_parallel_downloads.py +0 -0
  222. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_pcie.py +0 -0
  223. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_peripherals_commands.py +0 -0
  224. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_peripherals_contract.py +0 -0
  225. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_peripherals_model.py +0 -0
  226. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_peripherals_sentinel.py +0 -0
  227. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_pkg_update_check.py +0 -0
  228. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_reset_overlay.py +0 -0
  229. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_sdk_agent_sessions.py +0 -0
  230. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_sdk_browser_ca.py +0 -0
  231. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_sdk_container_registry.py +0 -0
  232. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_sdk_host_key_trust.py +0 -0
  233. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_sdk_linux_shared_network.py +0 -0
  234. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_sdk_network_doctor.py +0 -0
  235. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_sdk_preinstall.py +0 -0
  236. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_sdk_uninstall.py +0 -0
  237. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_selectable_resources.py +0 -0
  238. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_selfupdate.py +0 -0
  239. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_skills_commands.py +0 -0
  240. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_skills_manager.py +0 -0
  241. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_slot_swap.py +0 -0
  242. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_streaming_downloads.py +0 -0
  243. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_swu.py +0 -0
  244. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_swu_certificate.py +0 -0
  245. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_swu_errors.py +0 -0
  246. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_swu_mirror.py +0 -0
  247. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_swu_target.py +0 -0
  248. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_uboot_environment.py +0 -0
  249. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_update_staging.py +0 -0
  250. {sima_cli-2.1.20 → sima_cli-2.1.21}/tests/unit/test_utils.py +0 -0
  251. {sima_cli-2.1.20 → 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.20
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.20"
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.20"
2
+ __version__ = "2.1.21"
@@ -187,7 +187,7 @@ class InspectionAwareGroup(click.Group):
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
@@ -71,8 +71,7 @@ def decode_key(value):
71
71
  raise CloudExError("--key contains an invalid value")
72
72
  if len(profile["allocation_secret"]) < 32:
73
73
  raise CloudExError("--key contains an invalid allocation secret")
74
- if profile["environment"] not in {"vulcan-staging", "vulcan-production"}:
75
- raise CloudExError("CloudEx supports only vulcan-staging or vulcan-production allocation keys")
74
+ # The environment is a descriptive label; api_url selects the backend.
76
75
  return profile
77
76
 
78
77
 
@@ -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
@@ -0,0 +1,254 @@
1
+ """NeatOps browser authentication and personal device allocation API."""
2
+
3
+ import base64
4
+ import hashlib
5
+ import json
6
+ import os
7
+ import secrets
8
+ import stat
9
+ import tempfile
10
+ import time
11
+ import webbrowser
12
+ from http.server import BaseHTTPRequestHandler, HTTPServer
13
+ from pathlib import Path
14
+ from urllib.parse import parse_qs, urlencode, urlsplit
15
+
16
+ import requests
17
+
18
+ from .client import CloudExError, DEFAULT_ROOT
19
+
20
+ DASHBOARDS = {
21
+ "production": "https://ops.neat.sima.ai",
22
+ "staging": "https://ops.stg.neat.sima.ai",
23
+ }
24
+ REDIRECT_URI = "http://127.0.0.1:47182/callback"
25
+
26
+
27
+ def _origin(value):
28
+ if not isinstance(value, str):
29
+ raise CloudExError("NeatOps configuration requires an HTTPS origin")
30
+ parsed = urlsplit(value)
31
+ if (parsed.scheme != "https" or not parsed.hostname or parsed.username
32
+ or parsed.password or parsed.query or parsed.fragment or parsed.path not in {"", "/"}):
33
+ raise CloudExError("NeatOps configuration requires an HTTPS origin")
34
+ return value.rstrip("/")
35
+
36
+
37
+ def _json_request(method, url, **kwargs):
38
+ try:
39
+ response = requests.request(method, url, timeout=20, allow_redirects=False, **kwargs)
40
+ except requests.RequestException:
41
+ raise CloudExError("NeatOps is unavailable; check your connection and retry") from None
42
+ try:
43
+ payload = response.json()
44
+ except ValueError:
45
+ payload = {}
46
+ if not isinstance(payload, dict):
47
+ payload = {}
48
+ return response.status_code, payload
49
+
50
+
51
+ class LoginServer(HTTPServer):
52
+ def get_request(self):
53
+ connection, address = super().get_request()
54
+ connection.settimeout(2)
55
+ return connection, address
56
+
57
+
58
+ class LoginCallback(BaseHTTPRequestHandler):
59
+ """Only accept the matching loopback OAuth response; never log its code."""
60
+
61
+ def log_message(self, *args):
62
+ pass
63
+
64
+ def do_GET(self):
65
+ parsed = urlsplit(self.path)
66
+ params = parse_qs(parsed.query)
67
+ valid = (self.headers.get("Host") == "127.0.0.1:47182"
68
+ and parsed.path == "/callback"
69
+ and params.get("state") == [self.server.oauth_state])
70
+ code = params.get("code", [])
71
+ error = params.get("error", [])
72
+ valid = valid and ((len(code) == 1 and not error) or (len(error) == 1 and not code))
73
+ if valid:
74
+ self.server.oauth_result = {"code": code[0]} if code else {"error": True}
75
+ self.send_response(200 if valid else 400)
76
+ self.send_header("Content-Type", "text/plain; charset=utf-8")
77
+ self.send_header("Cache-Control", "no-store")
78
+ self.send_header("Referrer-Policy", "no-referrer")
79
+ self.end_headers()
80
+ self.wfile.write(b"Return to sima-cli to finish signing in." if valid else b"Invalid sign-in response.")
81
+
82
+
83
+ class LoginRequired(CloudExError):
84
+ """Saved credentials cannot authorize a request without browser sign-in."""
85
+
86
+
87
+ class NeatOps:
88
+ def __init__(self, environment="production", root=None, on_login_required=None):
89
+ self.environment = environment
90
+ self.dashboard = DASHBOARDS[environment]
91
+ self.path = Path(root or DEFAULT_ROOT) / ("neatops-" + environment + ".json")
92
+ self.config = None
93
+ self.on_login_required = on_login_required
94
+ self._login_attempted = False
95
+
96
+ def configuration(self):
97
+ if self.config is None:
98
+ status, config = _json_request("GET", self.dashboard + "/cli-config.json")
99
+ if status != 200:
100
+ raise CloudExError("NeatOps CLI login is not configured on this dashboard yet")
101
+ auth = config.get("auth", {})
102
+ if (not isinstance(auth, dict) or auth.get("enabled") is not True
103
+ or not isinstance(auth.get("clientId"), str) or not auth["clientId"]
104
+ or auth.get("redirectUri") != REDIRECT_URI
105
+ or config.get("apiBase") != self.dashboard + "/api"):
106
+ raise CloudExError("NeatOps CLI login configuration is unavailable or invalid")
107
+ try:
108
+ _origin(auth["domain"])
109
+ except (KeyError, TypeError, ValueError):
110
+ raise CloudExError("NeatOps sign-in domain is invalid") from None
111
+ self.config = config
112
+ return self.config
113
+
114
+ def _save(self, tokens):
115
+ auth = self.configuration()["auth"]
116
+ if not isinstance(tokens.get("access_token"), str) or not tokens["access_token"]:
117
+ raise CloudExError("NeatOps sign-in did not return an access token")
118
+ lifetime = tokens.get("expires_in")
119
+ if isinstance(lifetime, bool) or not isinstance(lifetime, (int, float)) or lifetime <= 0:
120
+ raise CloudExError("NeatOps sign-in returned an invalid token lifetime")
121
+ session = {"access_token": tokens["access_token"], "refresh_token": tokens.get("refresh_token"),
122
+ "expires_at": time.time() + lifetime, "client_id": auth["clientId"], "domain": auth["domain"]}
123
+ self.path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
124
+ descriptor, temporary = tempfile.mkstemp(prefix=".neatops-", dir=str(self.path.parent))
125
+ try:
126
+ with os.fdopen(descriptor, "w") as stream:
127
+ json.dump(session, stream)
128
+ os.replace(temporary, self.path)
129
+ finally:
130
+ if os.path.exists(temporary):
131
+ os.unlink(temporary)
132
+ return session["access_token"]
133
+
134
+ def _exchange(self, parameters):
135
+ auth = self.configuration()["auth"]
136
+ status, tokens = _json_request("POST", auth["domain"].rstrip("/") + "/oauth2/token",
137
+ data={"client_id": auth["clientId"], **parameters})
138
+ if status == 400 and parameters.get("grant_type") == "refresh_token" and tokens.get("error") == "invalid_grant":
139
+ raise LoginRequired("NeatOps sign-in expired; run sima-cli -i cloudex login again")
140
+ if status != 200:
141
+ raise CloudExError("NeatOps sign-in expired or was rejected; run sima-cli -i cloudex login again")
142
+ return tokens
143
+
144
+ def login(self, announce, timeout=180):
145
+ auth = self.configuration()["auth"]
146
+ verifier = secrets.token_urlsafe(48)
147
+ challenge = base64.urlsafe_b64encode(hashlib.sha256(verifier.encode()).digest()).decode().rstrip("=")
148
+ state = secrets.token_urlsafe(32)
149
+ parameters = {"response_type": "code", "client_id": auth["clientId"], "redirect_uri": REDIRECT_URI,
150
+ "scope": "openid email profile", "state": state, "code_challenge_method": "S256",
151
+ "code_challenge": challenge}
152
+ if auth.get("identityProvider"):
153
+ parameters["identity_provider"] = auth["identityProvider"]
154
+ url = auth["domain"].rstrip("/") + "/oauth2/authorize?" + urlencode(parameters)
155
+ try:
156
+ server = LoginServer(("127.0.0.1", 47182), LoginCallback)
157
+ except OSError:
158
+ raise CloudExError("CloudEx sign-in needs local port 47182; close another login and retry") from None
159
+ with server:
160
+ server.oauth_state = state
161
+ server.oauth_result = None
162
+ server.timeout = 0.5
163
+ announce(url)
164
+ try:
165
+ webbrowser.open(url)
166
+ except webbrowser.Error:
167
+ pass # The terminal also displays the URL for manual opening.
168
+ deadline = time.monotonic() + timeout
169
+ while server.oauth_result is None and time.monotonic() < deadline:
170
+ server.handle_request()
171
+ result = server.oauth_result
172
+ if result is None:
173
+ raise CloudExError("NeatOps sign-in timed out; run sima-cli -i cloudex login again")
174
+ if result.get("error"):
175
+ raise CloudExError("NeatOps sign-in was cancelled or rejected")
176
+ self._save(self._exchange({"grant_type": "authorization_code", "code": result["code"],
177
+ "code_verifier": verifier, "redirect_uri": REDIRECT_URI}))
178
+
179
+ def _read_session(self):
180
+ try:
181
+ # Refuse symlinks and shared credential files, as for CloudEx keys.
182
+ descriptor = os.open(str(self.path), os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0))
183
+ with os.fdopen(descriptor) as stream:
184
+ mode = os.fstat(stream.fileno()).st_mode
185
+ if not stat.S_ISREG(mode):
186
+ raise CloudExError("NeatOps credential file must be a regular file")
187
+ if os.name != "nt" and mode & 0o077:
188
+ raise CloudExError("NeatOps credential file must have mode 0600")
189
+ session = json.load(stream)
190
+ return session
191
+ except (FileNotFoundError, ValueError, KeyError, TypeError):
192
+ raise LoginRequired("Sign in first with sima-cli -i cloudex login --environment " + self.environment) from None
193
+ except OSError:
194
+ raise CloudExError("Sign in first with sima-cli -i cloudex login --environment " + self.environment) from None
195
+
196
+ def access_token(self):
197
+ auth = self.configuration()["auth"]
198
+ session = self._read_session()
199
+ try:
200
+ if session["client_id"] != auth["clientId"] or session["domain"] != auth["domain"]:
201
+ raise ValueError("Sign-in policy changed")
202
+ if session["expires_at"] > time.time() + 60:
203
+ return session["access_token"]
204
+ refresh = session.get("refresh_token")
205
+ except (ValueError, KeyError, TypeError):
206
+ raise LoginRequired("Sign-in policy changed; run sima-cli -i cloudex login again") from None
207
+ if not refresh:
208
+ raise LoginRequired("NeatOps sign-in expired; run sima-cli -i cloudex login again")
209
+ tokens = self._exchange({"grant_type": "refresh_token", "refresh_token": refresh})
210
+ tokens.setdefault("refresh_token", refresh)
211
+ return self._save(tokens)
212
+
213
+ def logout(self):
214
+ if self.path.exists():
215
+ # Revoke the CLI's refresh token without signing the browser out.
216
+ auth = self.configuration()["auth"]
217
+ session = self._read_session()
218
+ if session.get("refresh_token"):
219
+ status, _ = _json_request("POST", auth["domain"].rstrip("/") + "/oauth2/revoke",
220
+ data={"client_id": session["client_id"], "token": session["refresh_token"]})
221
+ if status != 200:
222
+ raise CloudExError("NeatOps token revocation failed; retry logout")
223
+ self.path.unlink()
224
+
225
+ def _authenticate(self):
226
+ if self.on_login_required is None or self._login_attempted:
227
+ raise LoginRequired("Sign in again with sima-cli -i cloudex login --environment " + self.environment)
228
+ self._login_attempted = True
229
+ self.on_login_required(self)
230
+ return self.access_token()
231
+
232
+ def call(self, method, path, data=None):
233
+ config = self.configuration()
234
+ try:
235
+ token = self.access_token()
236
+ except LoginRequired:
237
+ token = self._authenticate()
238
+
239
+ def request(access_token):
240
+ return _json_request(method, config["apiBase"] + path,
241
+ headers={"Authorization": "Bearer " + access_token}, json=data)
242
+
243
+ status, payload = request(token)
244
+ # A 401 from the authorizer has no side effects, so retry the same
245
+ # request once after sign-in. Permission failures and outages do not
246
+ # trigger login or replay allocation requests.
247
+ if status == 401 and self.on_login_required is not None and not self._login_attempted:
248
+ status, payload = request(self._authenticate())
249
+ if status in {401, 403}:
250
+ raise CloudExError("NeatOps access denied; sign in again or check your device permissions")
251
+ if not 200 <= status < 300:
252
+ # Do not print backend response bodies, which could contain credentials.
253
+ raise CloudExError("NeatOps request failed (HTTP {}); check availability and lease limits in My Devices".format(status))
254
+ return payload