sima-cli 2.1.19__tar.gz → 2.1.20__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.
- {sima_cli-2.1.19/sima_cli.egg-info → sima_cli-2.1.20}/PKG-INFO +1 -1
- {sima_cli-2.1.19 → sima_cli-2.1.20}/pyproject.toml +1 -1
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/__version__.py +1 -1
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/cli.py +17 -6
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/cloudex/client.py +55 -4
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/discover/discover.py +4 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/discover/linuxll.py +38 -3
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/download/downloader.py +37 -2
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/install/metadata_installer.py +11 -5
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/models/client.py +40 -15
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/models/commands.py +24 -11
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/models/rendering.py +78 -205
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/peripherals/model.py +17 -3
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/sdk/commands.py +33 -2
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/sdk/install.py +48 -3
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/sdk/neat.py +57 -10
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/sdk/utils.py +548 -15
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/update/host.py +3 -4
- sima_cli-2.1.20/sima_cli/update/mirror_catalog.py +11 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/update/netboot.py +77 -14
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/update/overlay.py +19 -7
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/update/remote.py +14 -3
- sima_cli-2.1.20/sima_cli/update/reset_overlay.py +92 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/update/swu.py +22 -11
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/update/swu_artifacts.py +17 -6
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/update/swu_certificate.py +1 -2
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/update/swu_target.py +35 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20/sima_cli.egg-info}/PKG-INFO +1 -1
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli.egg-info/SOURCES.txt +9 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/peripherals_fixtures.py +6 -1
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_cloudex_client.py +132 -10
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_cloudex_commands.py +24 -0
- sima_cli-2.1.20/tests/unit/test_discovery_host_tools.py +103 -0
- sima_cli-2.1.20/tests/unit/test_download_retries.py +115 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_generate_cli_markdown_docs.py +16 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_host_update.py +11 -1
- sima_cli-2.1.20/tests/unit/test_install_script_terminal.py +153 -0
- sima_cli-2.1.20/tests/unit/test_mirror_catalog.py +61 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_models.py +261 -201
- sima_cli-2.1.20/tests/unit/test_netboot_autoflash.py +196 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_overlay.py +41 -2
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_peripherals_commands.py +21 -1
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_peripherals_contract.py +11 -2
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_peripherals_model.py +22 -3
- sima_cli-2.1.20/tests/unit/test_reset_overlay.py +159 -0
- sima_cli-2.1.20/tests/unit/test_sdk_agent_sessions.py +884 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_sdk_image_detection.py +367 -37
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_sdk_model_compiler.py +24 -0
- sima_cli-2.1.20/tests/unit/test_selectable_resources.py +98 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_swu.py +55 -7
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_swu_certificate.py +4 -3
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_swu_mirror.py +13 -2
- sima_cli-2.1.20/tests/unit/test_swu_target.py +158 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_vscode_extension_pins.py +1 -0
- sima_cli-2.1.19/tests/unit/test_netboot_autoflash.py +0 -85
- sima_cli-2.1.19/tests/unit/test_swu_target.py +0 -58
- {sima_cli-2.1.19 → sima_cli-2.1.20}/LICENSE +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/MANIFEST.in +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/README.md +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/requirements.txt +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/setup.cfg +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/setup.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/__init__.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/__main__.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/app_zoo/__init__.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/app_zoo/app.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/app_zoo/commands.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/auth/__init__.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/auth/auth0.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/auth/devportal.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/auth/login.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/cloudex/__init__.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/cloudex/commands.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/cloudex/installer.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/data/__init__.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/data/resources_internal.yaml +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/data/resources_public.yaml +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/deploy_only/__init__.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/deploy_only/device/__init__.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/deploy_only/device/commands.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/deploy_only/mpk/__init__.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/deploy_only/mpk/commands.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/discover/__init__.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/download/__init__.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/download/parallel.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/install/__init__.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/install/compatibility.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/install/github_assets.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/install/hostdriver.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/install/metadata_info.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/install/metadata_validator.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/install/optiview.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/install/package_builder.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/install/palette.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/install/registry.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/mla/__init__.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/mla/meminfo.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/model_zoo/__init__.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/model_zoo/model.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/models/__init__.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/network/__init__.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/network/network.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/peripherals/__init__.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/peripherals/commands.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/peripherals/sentinel.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/playbooks/__init__.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/playbooks/commands.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/playbooks/manager.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/sdk/__init__.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/sdk/browser_ca.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/sdk/cmdexec.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/sdk/config.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/sdk/container_registry.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/sdk/docker_staging.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/sdk/linux_devkit_network.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/sdk/linux_shared_network.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/sdk/model_compiler.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/sdk/network_doctor.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/sdk/preinstall.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/sdk/requirements.json +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/sdk/script.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/sdk/stop.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/sdk/uninstall.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/sdk/vscode_extensions.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/serial/__init__.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/serial/serial.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/storage/__init__.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/storage/nvme.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/storage/sdcard.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/update/__init__.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/update/ab_state.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/update/bmaptool.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/update/bootimg.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/update/cleanlog.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/update/elxr.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/update/emmc.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/update/fast_upload.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/update/local.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/update/netboot_artifacts.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/update/netboot_device.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/update/query.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/update/rootfs.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/update/staging.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/update/swap.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/update/uboot_environment.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/update/updater.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/upgrade/__init__.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/upgrade/selfupdate.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/utils/__init__.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/utils/api_common.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/utils/artifactory.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/utils/common.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/utils/config.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/utils/config_loader.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/utils/container_registries.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/utils/deprecation.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/utils/device_api.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/utils/disk.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/utils/docker.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/utils/env.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/utils/errors.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/utils/mpk_api.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/utils/net.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/utils/network.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/utils/pcie.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/utils/pkg_update_check.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/utils/serializers.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/utils/services.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/utils/streaming.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/utils/tag.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/vulcan/__init__.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/vulcan/artifacts.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli/vulcan/commands.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli.egg-info/dependency_links.txt +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli.egg-info/entry_points.txt +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli.egg-info/requires.txt +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/sima_cli.egg-info/top_level.txt +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/__init__.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/e2e/__init__.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/__init__.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_ab_overlay.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_app_zoo.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_appzoo_commands.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_artifact_parallel_integration.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_auth.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_auth0.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_bootimg_defaults.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_bootimg_recovery.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_cli.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_cli_stdio.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_cli_update.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_cli_update_rerun.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_cloudex_installer.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_code_ui_readiness.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_container_registries.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_deprecation.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_device_discovery_cloudex.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_docker_utils.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_download.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_elxr_update.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_emmc.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_fast_upload.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_firmware.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_firmware_creation_order.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_firmware_layout.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_firmware_selector.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_install_force.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_install_stub.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_localized_docs_links.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_metadata_installer.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_metadata_validator.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_mla_meminfo.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_model_deployments.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_model_zoo.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_modelzoo_commands.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_netboot.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_netboot_device.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_netboot_images.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_netboot_ip_recovery.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_netboot_mirror.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_netboot_trootctl.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_network_custom_static.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_network_safety.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_package_builder.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_palette_image_discovery.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_parallel_downloads.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_pcie.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_peripherals_sentinel.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_pkg_update_check.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_sdk_browser_ca.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_sdk_container_registry.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_sdk_host_key_trust.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_sdk_linux_shared_network.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_sdk_network_doctor.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_sdk_preinstall.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_sdk_uninstall.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_selfupdate.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_skills_commands.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_skills_manager.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_slot_swap.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_streaming_downloads.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_swu_errors.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_uboot_environment.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_update_staging.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_utils.py +0 -0
- {sima_cli-2.1.19 → sima_cli-2.1.20}/tests/unit/test_vulcan.py +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
# sima_cli/__version__.py
|
|
2
|
-
__version__ = "2.1.
|
|
2
|
+
__version__ = "2.1.20"
|
|
@@ -180,7 +180,7 @@ 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
|
|
|
@@ -363,15 +363,16 @@ def download(ctx, url, dest):
|
|
|
363
363
|
"--dryrun",
|
|
364
364
|
is_flag=True,
|
|
365
365
|
default=False,
|
|
366
|
-
help="For eLxr, preview an update
|
|
366
|
+
help="For eLxr, preview an update, slot swap, or overlay reset without making changes."
|
|
367
367
|
)
|
|
368
368
|
@click.option("--inspect", "inspect_state", is_flag=True, help="Show eLxr 3.0+ A/B slots and overlay customizations without updating (local or --ip).")
|
|
369
|
+
@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
370
|
@click.option("--swap", is_flag=True, help="Select the available inactive eLxr 3.0+ slot for next boot; use --reboot to restart now.")
|
|
370
371
|
@click.option("--verbose", is_flag=True, help="Show detailed overlay file categories during eLxr 3.0+ inspection or update.")
|
|
371
372
|
@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
|
|
373
|
+
@click.option("--reboot", is_flag=True, help="Reboot after successful eLxr 3.0+ installation, slot selection, or overlay reset.")
|
|
373
374
|
@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):
|
|
375
|
+
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
376
|
"""
|
|
376
377
|
Update a SiMa DevKit, remote device, or Linux PCIe host.
|
|
377
378
|
|
|
@@ -383,6 +384,10 @@ def update(ctx, version_or_url, version_option, ip, yes, passwd, flavor, force,
|
|
|
383
384
|
eLxr 3.0+ uses signed full-system SWU bundles. Use --inspect for A/B
|
|
384
385
|
state, --ip for remote updates, and --reboot to reboot after installation.
|
|
385
386
|
|
|
387
|
+
Use --reset-overlay to discard local software and settings without updating
|
|
388
|
+
firmware or switching slots. Save needed files first; the saved inventory
|
|
389
|
+
contains metadata only. Reboot promptly, or add --reboot. Use --dryrun to preview.
|
|
390
|
+
|
|
386
391
|
Use --swap to select the valid inactive slot without downloading an update.
|
|
387
392
|
The current and target versions are shown before confirmation; --yes skips
|
|
388
393
|
confirmation. Reboot separately to apply, or add --reboot to restart now.
|
|
@@ -393,7 +398,8 @@ def update(ctx, version_or_url, version_option, ip, yes, passwd, flavor, force,
|
|
|
393
398
|
|
|
394
399
|
The verification certificate is downloaded for the selected channel when configured; use
|
|
395
400
|
--signing-cert URL_OR_FILE for an image signed with your own certificate.
|
|
396
|
-
|
|
401
|
+
For official eLxr 3.0+ releases, specify -v 3.0.0 (or -v 3.0) to
|
|
402
|
+
download the SWU from docs.sima.ai using your developer-portal login.
|
|
397
403
|
|
|
398
404
|
Internal mode warns that pre-release software may be unstable and is used
|
|
399
405
|
at your own risk. On eLxr 3.0+, unavailable Artifactory access falls back
|
|
@@ -501,6 +507,7 @@ def update(ctx, version_or_url, version_option, ip, yes, passwd, flavor, force,
|
|
|
501
507
|
or troot_only
|
|
502
508
|
or inspect_state
|
|
503
509
|
or swap
|
|
510
|
+
or reset_overlay
|
|
504
511
|
or verbose
|
|
505
512
|
or reboot
|
|
506
513
|
or force
|
|
@@ -518,6 +525,8 @@ def update(ctx, version_or_url, version_option, ip, yes, passwd, flavor, force,
|
|
|
518
525
|
return
|
|
519
526
|
|
|
520
527
|
is_elxr = is_devkit_running_elxr()
|
|
528
|
+
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"):
|
|
529
|
+
raise click.UsageError("--reset-overlay cannot be combined with --inspect, --swap, or installation options.")
|
|
521
530
|
if swap and (inspect_state or version_or_url or force or troot_only or signing_cert is not None or flavor != "auto"):
|
|
522
531
|
raise click.UsageError("--swap cannot be combined with --inspect or installation options.")
|
|
523
532
|
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 +536,7 @@ def update(ctx, version_or_url, version_option, ip, yes, passwd, flavor, force,
|
|
|
527
536
|
internal=ctx.obj.get("internal", False),
|
|
528
537
|
auto_confirm=yes or ctx.obj.get("yes", False), dryrun=dryrun,
|
|
529
538
|
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):
|
|
539
|
+
troot_only=troot_only, flavor=flavor, local_elxr=is_elxr, verbose=verbose, swap=swap, reset_overlay=reset_overlay):
|
|
531
540
|
return
|
|
532
541
|
except (click.ClickException, click.Abort):
|
|
533
542
|
raise
|
|
@@ -815,6 +824,8 @@ def bootimg_cmd(ctx, version, boardtype, netboot, images, delete_cache, devkit_i
|
|
|
815
824
|
else:
|
|
816
825
|
write_image(version, boardtype, fwtype, internal, flavor='headless', recovery=recovery)
|
|
817
826
|
click.echo("✅ Boot image successfully written.")
|
|
827
|
+
except click.ClickException:
|
|
828
|
+
raise
|
|
818
829
|
except Exception as e:
|
|
819
830
|
click.echo(f"❌ Failed to write boot image: {e}", err=True)
|
|
820
831
|
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,8 @@ 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"]
|
|
72
|
-
raise CloudExError("CloudEx
|
|
74
|
+
if profile["environment"] not in {"vulcan-staging", "vulcan-production"}:
|
|
75
|
+
raise CloudExError("CloudEx supports only vulcan-staging or vulcan-production allocation keys")
|
|
73
76
|
return profile
|
|
74
77
|
|
|
75
78
|
|
|
@@ -393,6 +396,9 @@ def _forwarder_failure(log_path):
|
|
|
393
396
|
"start wireguard-go": "the WireGuard interface could not start",
|
|
394
397
|
"invalid session config": "the native forwarder rejected the session configuration",
|
|
395
398
|
}
|
|
399
|
+
if sys.platform == "darwin":
|
|
400
|
+
for needle in ("wireguard is unavailable", "start wireguard-go"):
|
|
401
|
+
mappings[needle] += ". " + MACOS_WIREGUARD_INSTALL
|
|
396
402
|
for needle, message in mappings.items():
|
|
397
403
|
if needle in detail:
|
|
398
404
|
return message + suffix
|
|
@@ -552,6 +558,50 @@ def _reconcile_existing_connection(profile, store, progress):
|
|
|
552
558
|
return None
|
|
553
559
|
|
|
554
560
|
|
|
561
|
+
def _forwarder_sudo_prefix(progress=None):
|
|
562
|
+
"""Install missing macOS tools as the user, then expose them through sudo."""
|
|
563
|
+
prefix = ["sudo", "-n", "--"]
|
|
564
|
+
if sys.platform != "darwin":
|
|
565
|
+
return prefix
|
|
566
|
+
missing = [name for name in ("wg", "wireguard-go")
|
|
567
|
+
if shutil.which(name, path=MACOS_TUNNEL_PATH) is None]
|
|
568
|
+
if missing:
|
|
569
|
+
brew = shutil.which("brew", path=MACOS_TUNNEL_PATH)
|
|
570
|
+
if brew is None:
|
|
571
|
+
raise CloudExError(
|
|
572
|
+
"CloudEx requires {} on macOS. Install Homebrew, then retry CloudEx; "
|
|
573
|
+
"missing WireGuard dependencies will be installed automatically.".format(
|
|
574
|
+
", ".join(missing)
|
|
575
|
+
)
|
|
576
|
+
)
|
|
577
|
+
packages = ["wireguard-tools" if name == "wg" else name for name in missing]
|
|
578
|
+
if progress:
|
|
579
|
+
progress.update("Installing missing WireGuard dependencies with Homebrew")
|
|
580
|
+
environment = dict(os.environ, PATH=MACOS_TUNNEL_PATH)
|
|
581
|
+
try:
|
|
582
|
+
result = subprocess.run(
|
|
583
|
+
[brew, "install", *packages],
|
|
584
|
+
env=environment, stdin=subprocess.DEVNULL,
|
|
585
|
+
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=600,
|
|
586
|
+
)
|
|
587
|
+
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
588
|
+
raise CloudExError(
|
|
589
|
+
"Could not install WireGuard dependencies with Homebrew. " + MACOS_WIREGUARD_INSTALL
|
|
590
|
+
) from exc
|
|
591
|
+
if result.returncode:
|
|
592
|
+
detail = (result.stderr or result.stdout or "Homebrew failed").strip()
|
|
593
|
+
raise CloudExError("WireGuard dependency installation failed: " + detail[-1024:])
|
|
594
|
+
missing = [name for name in ("wg", "wireguard-go")
|
|
595
|
+
if shutil.which(name, path=MACOS_TUNNEL_PATH) is None]
|
|
596
|
+
if missing:
|
|
597
|
+
raise CloudExError(
|
|
598
|
+
"Homebrew completed but CloudEx still cannot find: {}. {}".format(
|
|
599
|
+
", ".join(missing), MACOS_WIREGUARD_INSTALL
|
|
600
|
+
)
|
|
601
|
+
)
|
|
602
|
+
return prefix + ["/usr/bin/env", "PATH=" + MACOS_TUNNEL_PATH]
|
|
603
|
+
|
|
604
|
+
|
|
555
605
|
def connect(profile, store, progress, attempts=3, transport="auto"):
|
|
556
606
|
"""Create a session and return its durable local record."""
|
|
557
607
|
from .installer import ensure_forwarder
|
|
@@ -561,6 +611,7 @@ def connect(profile, store, progress, attempts=3, transport="auto"):
|
|
|
561
611
|
existing = _reconcile_existing_connection(profile, store, progress)
|
|
562
612
|
if existing:
|
|
563
613
|
return existing
|
|
614
|
+
sudo_prefix = _forwarder_sudo_prefix(progress=progress)
|
|
564
615
|
forwarder = ensure_forwarder(progress=progress)
|
|
565
616
|
choices = (["p2p"] * attempts + ["routed"] if transport == "auto"
|
|
566
617
|
else ["routed"] if transport == "turn" else ["p2p"] * attempts)
|
|
@@ -610,8 +661,8 @@ def connect(profile, store, progress, attempts=3, transport="auto"):
|
|
|
610
661
|
config_path.chmod(0o600)
|
|
611
662
|
with log_path.open("x", encoding="utf-8") as log:
|
|
612
663
|
process = subprocess.Popen(
|
|
613
|
-
[
|
|
614
|
-
|
|
664
|
+
sudo_prefix + [
|
|
665
|
+
str(forwarder), "session", "--role", "user",
|
|
615
666
|
"--config", str(config_path), "--report", str(report_path),
|
|
616
667
|
],
|
|
617
668
|
stdin=subprocess.DEVNULL,
|
|
@@ -5,6 +5,7 @@ import subprocess
|
|
|
5
5
|
import json
|
|
6
6
|
import time
|
|
7
7
|
import re
|
|
8
|
+
import shutil
|
|
8
9
|
import psutil
|
|
9
10
|
from typing import List, Dict
|
|
10
11
|
from rich.console import Console
|
|
@@ -51,6 +52,9 @@ def get_sima_devices_from_arp():
|
|
|
51
52
|
try:
|
|
52
53
|
output = subprocess.check_output(["ip", "neigh", "show"], text=True)
|
|
53
54
|
except FileNotFoundError:
|
|
55
|
+
if not shutil.which("arp"):
|
|
56
|
+
console.print("[yellow]ARP lookup unavailable: install iproute2 (ip) or net-tools (arp); continuing with multicast discovery.[/yellow]")
|
|
57
|
+
return []
|
|
54
58
|
output = subprocess.check_output(["arp", "-n"], text=True)
|
|
55
59
|
pattern = re.compile(
|
|
56
60
|
r"(?P<ip>\d+\.\d+\.\d+\.\d+).*?(?P<mac>(?:[0-9a-f]{1,2}:){5}[0-9a-f]{1,2})",
|
|
@@ -11,9 +11,41 @@ import re
|
|
|
11
11
|
import time
|
|
12
12
|
import shlex
|
|
13
13
|
import subprocess
|
|
14
|
+
import shutil
|
|
15
|
+
import sys
|
|
16
|
+
|
|
17
|
+
import click
|
|
14
18
|
from sima_cli.utils.env import get_environment_type
|
|
15
19
|
from typing import Optional, Dict
|
|
16
20
|
|
|
21
|
+
def ensure_discovery_tools():
|
|
22
|
+
"""Provision required Linux discovery tools only after an interactive opt-in."""
|
|
23
|
+
if shutil.which('ip'):
|
|
24
|
+
return
|
|
25
|
+
guidance = (
|
|
26
|
+
"Device discovery needs 'ip' (package iproute2). "
|
|
27
|
+
"On Ubuntu/Debian run: sudo apt-get update && sudo apt-get install -y iproute2 ethtool. "
|
|
28
|
+
"For netboot, pass --devkit-ip <IPv4> to bypass auto-discovery."
|
|
29
|
+
)
|
|
30
|
+
if not shutil.which('apt-get') or not sys.stdin.isatty():
|
|
31
|
+
raise click.ClickException(guidance)
|
|
32
|
+
packages = ['iproute2']
|
|
33
|
+
if not shutil.which('ethtool'):
|
|
34
|
+
packages.append('ethtool')
|
|
35
|
+
if not click.confirm('Device discovery needs host packages. Install ' + ' '.join(packages) + '?', default=False):
|
|
36
|
+
raise click.ClickException(guidance)
|
|
37
|
+
prefix = [] if os.geteuid() == 0 else ['sudo']
|
|
38
|
+
if prefix and not shutil.which('sudo'):
|
|
39
|
+
raise click.ClickException(guidance + ' sudo is unavailable; ask an administrator to install them.')
|
|
40
|
+
try:
|
|
41
|
+
subprocess.run(prefix + ['apt-get', 'update'], check=True)
|
|
42
|
+
subprocess.run(prefix + ['apt-get', 'install', '-y', '--no-install-recommends'] + packages, check=True)
|
|
43
|
+
except (OSError, subprocess.CalledProcessError) as error:
|
|
44
|
+
raise click.ClickException(f'Device discovery package installation failed: {error}. {guidance}') from error
|
|
45
|
+
if not shutil.which('ip'):
|
|
46
|
+
raise click.ClickException(guidance)
|
|
47
|
+
|
|
48
|
+
|
|
17
49
|
def _run(cmd: str, check=False, capture=True) -> subprocess.CompletedProcess:
|
|
18
50
|
return subprocess.run(
|
|
19
51
|
shlex.split(cmd),
|
|
@@ -24,7 +56,7 @@ def _run(cmd: str, check=False, capture=True) -> subprocess.CompletedProcess:
|
|
|
24
56
|
)
|
|
25
57
|
|
|
26
58
|
def _has_nmcli() -> bool:
|
|
27
|
-
return
|
|
59
|
+
return shutil.which("nmcli") is not None
|
|
28
60
|
|
|
29
61
|
|
|
30
62
|
def _iface_has_ipv4(iface: str) -> bool:
|
|
@@ -41,7 +73,7 @@ def _get_mac(iface: str) -> str:
|
|
|
41
73
|
def _is_usb_ethernet(iface: str) -> bool:
|
|
42
74
|
"""Return True if this interface is a USB Ethernet device."""
|
|
43
75
|
dev_path = f"/sys/class/net/{iface}/device"
|
|
44
|
-
if not os.path.exists(dev_path):
|
|
76
|
+
if not os.path.exists(dev_path) or not shutil.which("udevadm"):
|
|
45
77
|
return False
|
|
46
78
|
|
|
47
79
|
cp = _run(f"udevadm info -q property -p {dev_path}", capture=True)
|
|
@@ -63,7 +95,7 @@ def _usb_attrs(iface: str) -> Dict[str, str]:
|
|
|
63
95
|
Returns possibly empty dict.
|
|
64
96
|
"""
|
|
65
97
|
dev_path = f"/sys/class/net/{iface}/device"
|
|
66
|
-
if not os.path.exists(dev_path):
|
|
98
|
+
if not os.path.exists(dev_path) or not shutil.which("udevadm"):
|
|
67
99
|
return {}
|
|
68
100
|
|
|
69
101
|
cp = _run(f"udevadm info -q property -p {dev_path}", capture=True)
|
|
@@ -100,6 +132,8 @@ def _usb_attrs(iface: str) -> Dict[str, str]:
|
|
|
100
132
|
|
|
101
133
|
|
|
102
134
|
def _driver_info(iface: str) -> Dict[str, str]:
|
|
135
|
+
if not shutil.which("ethtool"):
|
|
136
|
+
return {"driver": "", "bus-info": "", "firmware": ""}
|
|
103
137
|
cp = _run(f"ethtool -i {iface}", capture=True)
|
|
104
138
|
info = {}
|
|
105
139
|
for line in cp.stdout.splitlines():
|
|
@@ -227,6 +261,7 @@ def suggest_and_switch_to_linklocal():
|
|
|
227
261
|
envtype, env_subtype = get_environment_type()
|
|
228
262
|
|
|
229
263
|
if envtype == 'host' and env_subtype == 'linux':
|
|
264
|
+
ensure_discovery_tools()
|
|
230
265
|
cp = _run("ip -o link", capture=True)
|
|
231
266
|
ifaces = []
|
|
232
267
|
for line in cp.stdout.splitlines():
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import os
|
|
2
|
+
import time
|
|
2
3
|
import json
|
|
3
4
|
import hashlib
|
|
4
5
|
import re
|
|
@@ -13,6 +14,40 @@ from sima_cli.utils.config import get_auth_token
|
|
|
13
14
|
from sima_cli.auth.login import login
|
|
14
15
|
from sima_cli.install.github_assets import download_github_asset
|
|
15
16
|
|
|
17
|
+
# Separate connect and read limits; a cold CDN request can exceed ten seconds.
|
|
18
|
+
_DOWNLOAD_TIMEOUT = (10, 60)
|
|
19
|
+
_DOWNLOAD_ATTEMPTS = 3
|
|
20
|
+
_RETRYABLE_STATUS_CODES = {429, 500, 502, 503, 504}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _request_download(request_fn, url, **kwargs):
|
|
24
|
+
"""Retry transient failures before response headers, never replay a body."""
|
|
25
|
+
for attempt in range(_DOWNLOAD_ATTEMPTS):
|
|
26
|
+
response = None
|
|
27
|
+
try:
|
|
28
|
+
response = request_fn(url, timeout=_DOWNLOAD_TIMEOUT, **kwargs)
|
|
29
|
+
response.raise_for_status()
|
|
30
|
+
return response
|
|
31
|
+
except requests.exceptions.SSLError:
|
|
32
|
+
raise
|
|
33
|
+
except requests.HTTPError:
|
|
34
|
+
if response is None or response.status_code not in _RETRYABLE_STATUS_CODES:
|
|
35
|
+
if response is not None:
|
|
36
|
+
response.close()
|
|
37
|
+
raise
|
|
38
|
+
response.close()
|
|
39
|
+
if attempt == _DOWNLOAD_ATTEMPTS - 1:
|
|
40
|
+
raise
|
|
41
|
+
except (requests.Timeout, requests.ConnectionError):
|
|
42
|
+
if attempt == _DOWNLOAD_ATTEMPTS - 1:
|
|
43
|
+
raise
|
|
44
|
+
delay = 2 ** attempt
|
|
45
|
+
# Do not print URLs: authenticated downloads can include credentials.
|
|
46
|
+
print(f"⚠️ Transient download request failure; retrying in {delay}s "
|
|
47
|
+
f"(attempt {attempt + 2}/{_DOWNLOAD_ATTEMPTS}).")
|
|
48
|
+
time.sleep(delay)
|
|
49
|
+
|
|
50
|
+
|
|
16
51
|
def _list_directory_files(url: str, internal: bool = False) -> List[str]:
|
|
17
52
|
"""
|
|
18
53
|
Attempt to list files in a server-hosted directory with index browsing enabled.
|
|
@@ -110,7 +145,7 @@ def download_file_from_url(url: str, dest_folder: str = ".", internal: bool = Fa
|
|
|
110
145
|
head_fn = session.head
|
|
111
146
|
|
|
112
147
|
# Only resume a partial when a strong validator identifies its bytes.
|
|
113
|
-
head = head_fn
|
|
148
|
+
head = _request_download(head_fn, url, headers=headers, allow_redirects=True)
|
|
114
149
|
try:
|
|
115
150
|
head.raise_for_status()
|
|
116
151
|
raw_size = head.headers.get('content-length')
|
|
@@ -148,7 +183,7 @@ def download_file_from_url(url: str, dest_folder: str = ".", internal: bool = Fa
|
|
|
148
183
|
headers['Range'] = f'bytes={existing_size}-'
|
|
149
184
|
headers['If-Range'] = etag
|
|
150
185
|
|
|
151
|
-
with request_fn
|
|
186
|
+
with _request_download(request_fn, url, stream=True, headers=headers) as r:
|
|
152
187
|
r.raise_for_status()
|
|
153
188
|
if r.headers.get('Content-Encoding', 'identity').lower() != 'identity':
|
|
154
189
|
raise ValueError("Server ignored identity content encoding")
|
|
@@ -693,11 +693,17 @@ def selectable_resource_handler(metadata):
|
|
|
693
693
|
- 'Skip' option appended at the end
|
|
694
694
|
- Honors `selected: true` and existing `resources` for preselection
|
|
695
695
|
- Removes unselected items from `selectable-resources` field
|
|
696
|
+
- Skips opt-in resources when stdin is not a terminal
|
|
696
697
|
"""
|
|
697
698
|
selectable = metadata.get("selectable-resources")
|
|
698
699
|
if not selectable:
|
|
699
700
|
return metadata
|
|
700
701
|
|
|
702
|
+
if sys.stdin is None or not sys.stdin.isatty():
|
|
703
|
+
click.echo("Skipping optional resources because stdin is not a terminal.")
|
|
704
|
+
metadata["selectable-resources"] = []
|
|
705
|
+
return metadata
|
|
706
|
+
|
|
701
707
|
# ──────────────────────────────────────────────
|
|
702
708
|
# Build choice list with preselection
|
|
703
709
|
# ──────────────────────────────────────────────
|
|
@@ -1430,8 +1436,8 @@ def _run_installation_script(metadata: Dict, extract_path: str = ".", force: boo
|
|
|
1430
1436
|
Behavior:
|
|
1431
1437
|
• Force installs set FORCE_INSTALL=1 in the script environment.
|
|
1432
1438
|
• macOS / Linux / eLxr:
|
|
1433
|
-
- Run via interactive login shell unless nested inside sima-cli
|
|
1434
|
-
-
|
|
1439
|
+
- Run via non-interactive login shell unless nested inside sima-cli
|
|
1440
|
+
- Nested installs source the first available RC file among:
|
|
1435
1441
|
~/.zshrc → ~/.bashrc → ~/.bash_profile → ~/.profile
|
|
1436
1442
|
• Windows:
|
|
1437
1443
|
- Run via cmd.exe or PowerShell
|
|
@@ -1487,9 +1493,9 @@ def _run_installation_script(metadata: Dict, extract_path: str = ".", force: boo
|
|
|
1487
1493
|
cmd = ["bash", "-c", f"{rc_source}{script}"]
|
|
1488
1494
|
|
|
1489
1495
|
else:
|
|
1490
|
-
#
|
|
1491
|
-
|
|
1492
|
-
cmd = [shell, "-
|
|
1496
|
+
# Load login setup without enabling interactive job control on
|
|
1497
|
+
# the caller's terminal (including when invoked over ssh -t).
|
|
1498
|
+
cmd = [shell, "-l", "-c", script]
|
|
1493
1499
|
|
|
1494
1500
|
result = subprocess.run(cmd, cwd=extract_path, env=env)
|
|
1495
1501
|
exit_code = result.returncode
|
|
@@ -23,9 +23,9 @@ from sima_cli.download.parallel import PARALLEL_MIN_SIZE, download_parallel, con
|
|
|
23
23
|
|
|
24
24
|
ORGANIZATION = "sima-neat"
|
|
25
25
|
REPOSITORY = "models"
|
|
26
|
+
PRODUCTION_BASE_URL = "https://models-registry.neat.sima.ai"
|
|
26
27
|
STAGING_BASE_URL = "https://models-registry.stg.neat.sima.ai"
|
|
27
28
|
TRANSIENT_STATUS_CODES = (429, 502, 503, 504)
|
|
28
|
-
SUPPORTED_BENCHMARK_SCHEMAS = (1, 2, 3, 4, 5)
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
class ModelRegistryError(RuntimeError):
|
|
@@ -51,10 +51,7 @@ def resolve_base_url(staging: bool = False, override: Optional[str] = None) -> s
|
|
|
51
51
|
production = os.environ.get("SIMA_MODELS_REGISTRY_PRODUCTION_URL")
|
|
52
52
|
if production:
|
|
53
53
|
return normalize_base_url(production)
|
|
54
|
-
|
|
55
|
-
"The production Model Registry endpoint is not configured. "
|
|
56
|
-
"Use --stg or set SIMA_MODELS_REGISTRY_PRODUCTION_URL."
|
|
57
|
-
)
|
|
54
|
+
return PRODUCTION_BASE_URL
|
|
58
55
|
|
|
59
56
|
|
|
60
57
|
def _new_session() -> requests.Session:
|
|
@@ -134,6 +131,17 @@ def latest_runs_by_model_variant(runs: Sequence[Dict[str, Any]]) -> List[Dict[st
|
|
|
134
131
|
)
|
|
135
132
|
|
|
136
133
|
|
|
134
|
+
def _benchmark_fingerprints(model: Dict[str, Any], profile: str, run_ids: Sequence[str]) -> Dict[str, str]:
|
|
135
|
+
"""A fingerprint names the builds it measured, so only a row showing exactly the catalog builds keeps it."""
|
|
136
|
+
builds = {
|
|
137
|
+
build.get("run_id") for build in model.get("builds") or []
|
|
138
|
+
if isinstance(build, dict) and build.get("build_profile_id") == profile
|
|
139
|
+
}
|
|
140
|
+
if not run_ids or set(run_ids) != builds:
|
|
141
|
+
return {}
|
|
142
|
+
return (model.get("benchmark_fingerprints") or {}).get(profile) or {}
|
|
143
|
+
|
|
144
|
+
|
|
137
145
|
def find_latest_run(
|
|
138
146
|
runs: Sequence[Dict[str, Any]], model_id: str, variant_id: str
|
|
139
147
|
) -> Dict[str, Any]:
|
|
@@ -458,7 +466,13 @@ class RegistryClient:
|
|
|
458
466
|
payload = self._get_json(
|
|
459
467
|
("v1", "repositories", ORGANIZATION, REPOSITORY, "catalogs", "current"),
|
|
460
468
|
params={"git_ref": f"refs/heads/{branch}"},
|
|
469
|
+
allow_not_found=True,
|
|
461
470
|
)
|
|
471
|
+
if payload is None:
|
|
472
|
+
raise ModelRegistryError(
|
|
473
|
+
f"No published model catalog for branch {branch!r} at {self.base_url} (HTTP 404). "
|
|
474
|
+
"Use models branches to find available branches; no fallback was attempted."
|
|
475
|
+
)
|
|
462
476
|
catalog = payload.get("catalog") if payload else None
|
|
463
477
|
document = catalog.get("document") if isinstance(catalog, dict) else None
|
|
464
478
|
models = document.get("models") if isinstance(document, dict) else None
|
|
@@ -513,6 +527,10 @@ class RegistryClient:
|
|
|
513
527
|
)
|
|
514
528
|
artifact_metadata = artifact.get("metadata") or {}
|
|
515
529
|
model = definitions[model_id]
|
|
530
|
+
shown_run_ids = (
|
|
531
|
+
list((metadata.get("component_run_ids") or {}).values())
|
|
532
|
+
if root.get("package_id") else [run["id"]]
|
|
533
|
+
)
|
|
516
534
|
searchable = " ".join(str(value) for value in (
|
|
517
535
|
model_id, profile, model.get("display_name"), model.get("description"),
|
|
518
536
|
model.get("categories"), branch,
|
|
@@ -529,6 +547,7 @@ class RegistryClient:
|
|
|
529
547
|
"model_categories": model.get("categories") or {},
|
|
530
548
|
"size_bytes": artifact_metadata.get("deployment_size_bytes", artifact.get("size_bytes")),
|
|
531
549
|
"sha256": artifact.get("sha256"),
|
|
550
|
+
"benchmark_fingerprints": _benchmark_fingerprints(model, profile, shown_run_ids),
|
|
532
551
|
})
|
|
533
552
|
|
|
534
553
|
return sorted(
|
|
@@ -607,15 +626,23 @@ class RegistryClient:
|
|
|
607
626
|
payload = self._get_json(("v1", "runs", run_id, "metrics"))
|
|
608
627
|
return payload or {}
|
|
609
628
|
|
|
610
|
-
def
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
629
|
+
def whole_model_result(self, stage: str, fingerprint: Optional[str]) -> Optional[Dict[str, Any]]:
|
|
630
|
+
"""Return the model test result of the newest completed run for a benchmark fingerprint."""
|
|
631
|
+
if not fingerprint:
|
|
632
|
+
return None
|
|
633
|
+
resolved = self._get_json(
|
|
634
|
+
("v1", "repositories", ORGANIZATION, REPOSITORY, "resolve"),
|
|
635
|
+
params={"run_type": f"model_{stage}", "fingerprint": fingerprint},
|
|
636
|
+
allow_not_found=True,
|
|
618
637
|
)
|
|
638
|
+
run_id = ((resolved or {}).get("run") or {}).get("id")
|
|
639
|
+
if not run_id:
|
|
640
|
+
return None
|
|
641
|
+
results = [
|
|
642
|
+
result for result in self.test_results(str(run_id)).get("test_results") or []
|
|
643
|
+
if isinstance(result, dict) and result.get("suite") == stage and result.get("name") == "model"
|
|
644
|
+
]
|
|
645
|
+
return results[-1] if results else None
|
|
619
646
|
|
|
620
647
|
def taxonomy(self) -> Optional[Dict[str, Any]]:
|
|
621
648
|
return self._get_json(
|
|
@@ -629,8 +656,6 @@ class RegistryClient:
|
|
|
629
656
|
"provenance": self.provenance(run_id),
|
|
630
657
|
"tests": self.test_results(run_id),
|
|
631
658
|
"metrics": self.metrics(run_id),
|
|
632
|
-
"benchmark": self.benchmark(run_id),
|
|
633
|
-
"accuracy": self.accuracy(run_id),
|
|
634
659
|
}
|
|
635
660
|
|
|
636
661
|
def artifact_download(self, run_id: str, name: str) -> Dict[str, Any]:
|
|
@@ -32,7 +32,7 @@ def _staging_option(function):
|
|
|
32
32
|
"staging",
|
|
33
33
|
is_flag=True,
|
|
34
34
|
default=False,
|
|
35
|
-
help="Use the staging Model Registry.",
|
|
35
|
+
help="Use the staging Model Registry unless SIMA_MODELS_REGISTRY_BASE_URL is set.",
|
|
36
36
|
)(function)
|
|
37
37
|
|
|
38
38
|
|
|
@@ -107,6 +107,7 @@ def _selected_model_detail(
|
|
|
107
107
|
client: RegistryClient, selected: Dict[str, Any]
|
|
108
108
|
) -> Dict[str, Any]:
|
|
109
109
|
selected_metadata = selected.get("metadata") or {}
|
|
110
|
+
fingerprints = selected.get("benchmark_fingerprints") or {}
|
|
110
111
|
component_run_ids = selected_metadata.get("component_run_ids") or {}
|
|
111
112
|
if isinstance(component_run_ids, dict) and component_run_ids:
|
|
112
113
|
components = sorted(
|
|
@@ -169,18 +170,30 @@ def _selected_model_detail(
|
|
|
169
170
|
for metric in (detail.get("metrics") or {}).get("measurements") or []
|
|
170
171
|
]
|
|
171
172
|
},
|
|
172
|
-
"benchmark":
|
|
173
|
-
|
|
174
|
-
None,
|
|
175
|
-
),
|
|
176
|
-
"accuracy": next(
|
|
177
|
-
(detail.get("accuracy") for _, detail in details if detail.get("accuracy")),
|
|
178
|
-
None,
|
|
179
|
-
),
|
|
173
|
+
"benchmark": client.whole_model_result("performance", fingerprints.get("performance")),
|
|
174
|
+
"accuracy": client.whole_model_result("accuracy", fingerprints.get("accuracy")),
|
|
180
175
|
}
|
|
181
176
|
|
|
182
177
|
|
|
183
|
-
@click.group(name="models",
|
|
178
|
+
@click.group(name="models", hidden=True, help=(
|
|
179
|
+
"Browse and download Model Registry artifacts from production by default.\n\n"
|
|
180
|
+
"Uses https://models-registry.neat.sima.ai without AWS credentials. "
|
|
181
|
+
"This preview command remains hidden from top-level help.\n\n"
|
|
182
|
+
"SIMA_MODELS_REGISTRY_BASE_URL overrides all CLI endpoint selections, including --stg. "
|
|
183
|
+
"Otherwise --stg/--staging selects https://models-registry.stg.neat.sima.ai; "
|
|
184
|
+
"SIMA_MODELS_REGISTRY_PRODUCTION_URL overrides only the production default. "
|
|
185
|
+
"The Python resolve_base_url helper's explicit override takes precedence over environment variables.\n\n"
|
|
186
|
+
"Branch listings describe registered runs; listed branches may lack a published catalog. "
|
|
187
|
+
"Unpublished catalogs report an error; published catalogs with no matching builds return "
|
|
188
|
+
"an empty models array with --json or a message interactively. "
|
|
189
|
+
"No fallback to another branch or environment is attempted.\n\n"
|
|
190
|
+
"Examples (choose a currently published branch and model):\n\n\b\n"
|
|
191
|
+
" sima-cli models branches --json\n"
|
|
192
|
+
" sima-cli models list --branch BRANCH --json\n"
|
|
193
|
+
" sima-cli models download --branch BRANCH --id MODEL --variant VARIANT --output ./models --json\n"
|
|
194
|
+
" sima-cli models --stg branches --json\n"
|
|
195
|
+
" sima-cli models list --staging --branch develop --json"
|
|
196
|
+
))
|
|
184
197
|
@_staging_option
|
|
185
198
|
@click.pass_context
|
|
186
199
|
def models_group(ctx: click.Context, staging: bool) -> None:
|
|
@@ -260,7 +273,7 @@ def list_command(
|
|
|
260
273
|
echo_json({"branch": branch, "query": query, "models": runs})
|
|
261
274
|
return
|
|
262
275
|
if not runs:
|
|
263
|
-
click.echo("No completed model builds
|
|
276
|
+
click.echo(f"No completed model builds match branch {branch!r} and the requested query.")
|
|
264
277
|
return
|
|
265
278
|
if not _is_interactive():
|
|
266
279
|
raise ModelRegistryError(
|