redeploy 0.2.46__tar.gz → 0.2.47__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.
- {redeploy-0.2.46 → redeploy-0.2.47}/PKG-INFO +7 -7
- {redeploy-0.2.46 → redeploy-0.2.47}/README.md +5 -5
- {redeploy-0.2.46 → redeploy-0.2.47}/pyproject.toml +2 -2
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/__init__.py +1 -1
- redeploy-0.2.47/redeploy/blueprint/extractor.py +117 -0
- redeploy-0.2.47/redeploy/blueprint/sources/__init__.py +8 -0
- redeploy-0.2.47/redeploy/blueprint/sources/compose.py +146 -0
- redeploy-0.2.47/redeploy/blueprint/sources/hardware.py +45 -0
- redeploy-0.2.47/redeploy/blueprint/sources/infra.py +43 -0
- redeploy-0.2.47/redeploy/blueprint/sources/migration.py +34 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/cli/commands/hardware.py +20 -46
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/cli/commands/plan_apply.py +82 -13
- redeploy-0.2.47/redeploy/cli/query.py +42 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/detect/templates.py +117 -72
- redeploy-0.2.47/redeploy/heal.py +414 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/integrations/op3_bridge.py +91 -11
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy.egg-info/PKG-INFO +7 -7
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy.egg-info/SOURCES.txt +7 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy.egg-info/requires.txt +1 -1
- redeploy-0.2.46/redeploy/blueprint/extractor.py +0 -328
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/apply/__init__.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/apply/exceptions.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/apply/executor.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/apply/handlers.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/apply/progress.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/apply/rollback.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/apply/state.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/apply/state_apply.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/apply/utils/__init__.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/apply/utils/run_container_build.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/audit.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/blueprint/__init__.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/blueprint/generators/__init__.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/blueprint/generators/docker_compose.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/blueprint/generators/migration.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/cli/__init__.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/cli/commands/__init__.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/cli/commands/audit.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/cli/commands/blueprint.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/cli/commands/detect.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/cli/commands/device_map.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/cli/commands/device_map_renderers.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/cli/commands/devices.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/cli/commands/diagnose.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/cli/commands/diff.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/cli/commands/exec_.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/cli/commands/export.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/cli/commands/import_.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/cli/commands/init.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/cli/commands/inspect.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/cli/commands/patterns.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/cli/commands/plugin.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/cli/commands/probe.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/cli/commands/push.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/cli/commands/state.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/cli/commands/status.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/cli/commands/target.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/cli/commands/version/__init__.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/cli/commands/version/commands.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/cli/commands/version/helpers.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/cli/commands/version/monorepo.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/cli/commands/version/release.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/cli/commands/version/scanner.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/cli/commands/workflow.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/cli/core.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/cli/display.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/cli.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/config_apply/__init__.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/config_apply/applier.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/config_apply/loader.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/data_sync.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/detect/__init__.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/detect/builtin/__init__.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/detect/detector.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/detect/hardware.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/detect/hardware_rules.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/detect/probes.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/detect/remote.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/detect/workflow.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/discovery.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/dsl/__init__.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/dsl/loader.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/dsl/parser.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/dsl_python/__init__.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/dsl_python/context.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/dsl_python/decorators.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/dsl_python/docker_steps.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/dsl_python/exceptions.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/dsl_python/runner.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/dsl_python/steps.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/fleet.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/hardware/__init__.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/hardware/config_txt.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/hardware/data/__init__.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/hardware/data/hyperpixel.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/hardware/data/official.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/hardware/data/waveshare.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/hardware/fixes.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/hardware/kiosk/__init__.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/hardware/kiosk/autostart.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/hardware/kiosk/browsers.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/hardware/kiosk/compositors.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/hardware/kiosk/output_profiles.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/hardware/panels.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/hardware/raspi_config.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/iac/__init__.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/iac/base.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/iac/docker_compose.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/iac/parsers/__init__.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/iac/parsers/compose.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/iac/registry.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/integrations/__init__.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/markpact/__init__.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/markpact/compiler.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/markpact/models.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/markpact/parser.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/models.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/observe.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/parse.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/patterns.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/plan/__init__.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/plan/planner.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/plugins/__init__.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/plugins/builtin/__init__.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/plugins/builtin/browser_reload.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/plugins/builtin/hardware_diagnostic.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/plugins/builtin/notify.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/plugins/builtin/process_control.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/plugins/builtin/systemd_reload.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/spec_loader.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/ssh.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/steps.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/tests/__init__.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/tests/test_audit.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/tests/test_cli.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/tests/test_data_sync.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/tests/test_discovery.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/tests/test_dsl.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/tests/test_examples.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/tests/test_executor.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/tests/test_fleet.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/tests/test_git_integration.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/tests/test_iac.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/tests/test_manifest.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/tests/test_markpact_compiler.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/tests/test_markpact_parser.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/tests/test_models.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/tests/test_observe.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/tests/test_parse.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/tests/test_patterns.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/tests/test_planner_edge.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/tests/test_plugins.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/tests/test_probes.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/tests/test_public_api.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/tests/test_resume.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/tests/test_spec_loader.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/tests/test_ssh.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/tests/test_templates.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/tests/test_verify.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/tests/test_version.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/tests/test_version_changelog.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/tests/test_version_cli.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/tests/test_version_manifest.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/tests/test_version_sources.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/tests/test_workflow.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/verify.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/version/__init__.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/version/bump.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/version/changelog.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/version/commits.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/version/diff.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/version/git_integration.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/version/git_transaction.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/version/manifest.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/version/sources/__init__.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/version/sources/base.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/version/sources/json_.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/version/sources/plain.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/version/sources/regex.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/version/sources/toml_.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/version/sources/yaml_.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/version/transaction.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy/version.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy.egg-info/dependency_links.txt +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy.egg-info/entry_points.txt +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/redeploy.egg-info/top_level.txt +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/setup.cfg +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/tests/test_hardware_config.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/tests/test_hardware_kiosk.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/tests/test_hardware_rules.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/tests/test_iac.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/tests/test_parse.py +0 -0
- {redeploy-0.2.46 → redeploy-0.2.47}/tests/test_redeploy.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: redeploy
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.47
|
|
4
4
|
Summary: Infrastructure migration toolkit: detect → plan → apply
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -23,24 +23,24 @@ Requires-Dist: goal>=2.1.0; extra == "dev"
|
|
|
23
23
|
Requires-Dist: costs>=0.1.20; extra == "dev"
|
|
24
24
|
Requires-Dist: pfix>=0.1.60; extra == "dev"
|
|
25
25
|
Provides-Extra: op3
|
|
26
|
-
Requires-Dist: op3>=0.1.
|
|
26
|
+
Requires-Dist: op3>=0.1.8; extra == "op3"
|
|
27
27
|
|
|
28
28
|
# redeploy
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
## AI Cost Tracking
|
|
32
32
|
|
|
33
|
-
     
|
|
34
|
+
  
|
|
35
35
|
|
|
36
|
-
- 🤖 **LLM usage:** $7.5000 (
|
|
37
|
-
- 👤 **Human dev:** ~$
|
|
36
|
+
- 🤖 **LLM usage:** $7.5000 (68 commits)
|
|
37
|
+
- 👤 **Human dev:** ~$2398 (24.0h @ $100/h, 30min dedup)
|
|
38
38
|
|
|
39
39
|
Generated on 2026-04-21 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
40
40
|
|
|
41
41
|
---
|
|
42
42
|
|
|
43
|
-
    
|
|
44
44
|
|
|
45
45
|
Infrastructure migration and device deploy toolkit — VPS, Raspberry Pi kiosk, Podman Quadlet, k3s.
|
|
46
46
|
|
|
@@ -3,17 +3,17 @@
|
|
|
3
3
|
|
|
4
4
|
## AI Cost Tracking
|
|
5
5
|
|
|
6
|
-
     
|
|
7
|
+
  
|
|
8
8
|
|
|
9
|
-
- 🤖 **LLM usage:** $7.5000 (
|
|
10
|
-
- 👤 **Human dev:** ~$
|
|
9
|
+
- 🤖 **LLM usage:** $7.5000 (68 commits)
|
|
10
|
+
- 👤 **Human dev:** ~$2398 (24.0h @ $100/h, 30min dedup)
|
|
11
11
|
|
|
12
12
|
Generated on 2026-04-21 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
13
13
|
|
|
14
14
|
---
|
|
15
15
|
|
|
16
|
-
    
|
|
17
17
|
|
|
18
18
|
Infrastructure migration and device deploy toolkit — VPS, Raspberry Pi kiosk, Podman Quadlet, k3s.
|
|
19
19
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "redeploy"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.47"
|
|
8
8
|
description = "Infrastructure migration toolkit: detect → plan → apply"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -32,7 +32,7 @@ dev = ["pytest>=8.0", "ruff>=0.4",
|
|
|
32
32
|
"costs>=0.1.20",
|
|
33
33
|
"pfix>=0.1.60",
|
|
34
34
|
]
|
|
35
|
-
op3 = ["op3>=0.1.
|
|
35
|
+
op3 = ["op3>=0.1.8"]
|
|
36
36
|
|
|
37
37
|
[tool.ruff]
|
|
38
38
|
line-length = 100
|
|
@@ -7,7 +7,7 @@ Public API (stable from 0.2.0, semver guaranteed)::
|
|
|
7
7
|
|
|
8
8
|
Everything not listed in ``__all__`` is internal and may change without notice.
|
|
9
9
|
"""
|
|
10
|
-
__version__ = "0.2.
|
|
10
|
+
__version__ = "0.2.47"
|
|
11
11
|
|
|
12
12
|
# ── Core models ───────────────────────────────────────────────────────────────
|
|
13
13
|
from .models import ( # noqa: F401
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Blueprint extractor — derive a DeviceBlueprint from multiple sources.
|
|
2
|
+
|
|
3
|
+
Sources (in order of priority):
|
|
4
|
+
1. Running InfraState (podman ps / docker inspect on live device)
|
|
5
|
+
2. DeviceMap hardware snapshot
|
|
6
|
+
3. docker-compose YAML files
|
|
7
|
+
4. markpact / migration YAML definitions
|
|
8
|
+
|
|
9
|
+
The extractor reconciles all available information into a single
|
|
10
|
+
DeviceBlueprint that is self-contained and portable.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Optional
|
|
16
|
+
|
|
17
|
+
from ..models import (
|
|
18
|
+
BlueprintSource,
|
|
19
|
+
DeviceBlueprint,
|
|
20
|
+
DeviceMap,
|
|
21
|
+
InfraState,
|
|
22
|
+
ServiceSpec,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def extract_blueprint(
|
|
27
|
+
*,
|
|
28
|
+
name: str,
|
|
29
|
+
device_map: Optional[DeviceMap] = None,
|
|
30
|
+
infra: Optional[InfraState] = None,
|
|
31
|
+
compose_files: Optional[list[Path]] = None,
|
|
32
|
+
migration_file: Optional[Path] = None,
|
|
33
|
+
markpact_files: Optional[list[Path]] = None,
|
|
34
|
+
version: str = "0.0.0",
|
|
35
|
+
description: str = "",
|
|
36
|
+
tags: Optional[list[str]] = None,
|
|
37
|
+
detect_live: bool = False,
|
|
38
|
+
host: Optional[str] = None,
|
|
39
|
+
) -> DeviceBlueprint:
|
|
40
|
+
"""Build a DeviceBlueprint by reconciling all available sources.
|
|
41
|
+
|
|
42
|
+
Parameters
|
|
43
|
+
----------
|
|
44
|
+
name:
|
|
45
|
+
Human-readable blueprint name, e.g. ``"c2004-kiosk"``.
|
|
46
|
+
device_map:
|
|
47
|
+
Previously saved DeviceMap (hardware + infra snapshot).
|
|
48
|
+
infra:
|
|
49
|
+
InfraState if already probed (skips live detection).
|
|
50
|
+
compose_files:
|
|
51
|
+
Local docker-compose YAML files to parse service specs from.
|
|
52
|
+
migration_file:
|
|
53
|
+
Local migration.yaml to extract app version and strategy from.
|
|
54
|
+
markpact_files:
|
|
55
|
+
Local markdown files with markpact fenced blocks.
|
|
56
|
+
detect_live:
|
|
57
|
+
If True and *host* is given, probe the host via SSH for a fresh
|
|
58
|
+
InfraState even if *device_map* already has one.
|
|
59
|
+
host:
|
|
60
|
+
SSH target used when ``detect_live=True``.
|
|
61
|
+
"""
|
|
62
|
+
if detect_live and host and not infra:
|
|
63
|
+
from ..detect import Detector
|
|
64
|
+
infra = infra or Detector(host).run()
|
|
65
|
+
|
|
66
|
+
from .sources.compose import merge_compose_files
|
|
67
|
+
from .sources.hardware import build_hw_requirements
|
|
68
|
+
from .sources.infra import extract_services_from_infra, infer_app_url
|
|
69
|
+
from .sources.migration import parse_migration_meta
|
|
70
|
+
|
|
71
|
+
# Prefer freshly probed infra, fall back to device_map.infra
|
|
72
|
+
_infra: Optional[InfraState] = infra or (device_map.infra if device_map else None)
|
|
73
|
+
_hw = device_map.hardware if device_map else None
|
|
74
|
+
|
|
75
|
+
services: list[ServiceSpec] = []
|
|
76
|
+
seen: set[str] = set()
|
|
77
|
+
|
|
78
|
+
# ── 1. Live InfraState ────────────────────────────────────────────────────
|
|
79
|
+
if _infra:
|
|
80
|
+
services.extend(extract_services_from_infra(_infra, seen))
|
|
81
|
+
|
|
82
|
+
# ── 2. docker-compose files ───────────────────────────────────────────────
|
|
83
|
+
merge_compose_files(list(compose_files or []), services, seen)
|
|
84
|
+
|
|
85
|
+
# ── 3. migration.yaml ─────────────────────────────────────────────────────
|
|
86
|
+
app_version = version
|
|
87
|
+
deploy_strategy = "podman_quadlet"
|
|
88
|
+
if migration_file and migration_file.exists():
|
|
89
|
+
meta = parse_migration_meta(migration_file)
|
|
90
|
+
app_version = meta.get("version", version)
|
|
91
|
+
deploy_strategy = meta.get("strategy", deploy_strategy)
|
|
92
|
+
|
|
93
|
+
# ── 4. Hardware requirements ────────────────────────────────────────────
|
|
94
|
+
hw_req = build_hw_requirements(_hw)
|
|
95
|
+
|
|
96
|
+
# ── 5. App URL ────────────────────────────────────────────────────────────
|
|
97
|
+
app_url = infer_app_url(_infra)
|
|
98
|
+
|
|
99
|
+
source = BlueprintSource(
|
|
100
|
+
device_id=device_map.id if device_map else host,
|
|
101
|
+
device_name=device_map.name if device_map else "",
|
|
102
|
+
migration_file=str(migration_file) if migration_file else None,
|
|
103
|
+
markpact_files=[str(p) for p in (markpact_files or [])],
|
|
104
|
+
compose_files=[str(p) for p in (compose_files or [])],
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
return DeviceBlueprint(
|
|
108
|
+
name=name,
|
|
109
|
+
version=app_version,
|
|
110
|
+
description=description,
|
|
111
|
+
tags=tags or (device_map.tags if device_map else []),
|
|
112
|
+
services=services,
|
|
113
|
+
hardware=hw_req,
|
|
114
|
+
app_url=app_url,
|
|
115
|
+
deploy_strategy=deploy_strategy,
|
|
116
|
+
source=source,
|
|
117
|
+
)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Blueprint source adapters — extract partial blueprints from multiple inputs.
|
|
2
|
+
|
|
3
|
+
Each module in this package exposes a narrow, single-responsibility function
|
|
4
|
+
that reads one kind of source (live infra, docker-compose, migration.yaml,
|
|
5
|
+
DeviceMap hardware, …) and returns structured data for the orchestrator in
|
|
6
|
+
:mod:`redeploy.blueprint.extractor` to merge into a ``DeviceBlueprint``.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""Parse docker-compose YAML files into :class:`ServiceSpec` objects."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
10
|
+
from ...models import ServicePort, ServiceSpec, VolumeMount
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def merge_compose_files(
|
|
14
|
+
compose_files: list[Path],
|
|
15
|
+
services: list[ServiceSpec],
|
|
16
|
+
seen: set[str],
|
|
17
|
+
) -> None:
|
|
18
|
+
"""Parse each docker-compose file and merge specs into *services* / *seen*."""
|
|
19
|
+
for cf in compose_files:
|
|
20
|
+
_merge_compose(cf, services, seen)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _merge_compose(path: Path, services: list[ServiceSpec], seen: set[str]) -> None:
|
|
24
|
+
"""Parse a single docker-compose YAML and merge service specs."""
|
|
25
|
+
try:
|
|
26
|
+
raw = yaml.safe_load(path.read_text())
|
|
27
|
+
except Exception:
|
|
28
|
+
return
|
|
29
|
+
if not isinstance(raw, dict) or "services" not in raw:
|
|
30
|
+
return
|
|
31
|
+
|
|
32
|
+
for svc_name, svc_def in raw["services"].items():
|
|
33
|
+
if not isinstance(svc_def, dict):
|
|
34
|
+
continue
|
|
35
|
+
|
|
36
|
+
existing = next((s for s in services if s.name == svc_name), None)
|
|
37
|
+
image = svc_def.get("image", "")
|
|
38
|
+
platform = svc_def.get("platform", "")
|
|
39
|
+
ports = _parse_compose_ports(svc_def.get("ports", []))
|
|
40
|
+
volumes = _parse_compose_volumes(svc_def.get("volumes", []))
|
|
41
|
+
env = _parse_compose_env(svc_def.get("environment", {}))
|
|
42
|
+
healthcheck = _parse_compose_healthcheck(svc_def.get("healthcheck", {}))
|
|
43
|
+
depends = svc_def.get("depends_on", [])
|
|
44
|
+
if isinstance(depends, dict):
|
|
45
|
+
depends = list(depends.keys())
|
|
46
|
+
command = svc_def.get("command", None)
|
|
47
|
+
if isinstance(command, list):
|
|
48
|
+
command = " ".join(str(c) for c in command)
|
|
49
|
+
restart = svc_def.get("restart", "unless-stopped")
|
|
50
|
+
privileged = bool(svc_def.get("privileged", False))
|
|
51
|
+
|
|
52
|
+
if existing:
|
|
53
|
+
if not existing.image and image:
|
|
54
|
+
existing.image = image
|
|
55
|
+
if not existing.platform and platform:
|
|
56
|
+
existing.platform = platform
|
|
57
|
+
if not existing.ports:
|
|
58
|
+
existing.ports = ports
|
|
59
|
+
if not existing.volumes:
|
|
60
|
+
existing.volumes = volumes
|
|
61
|
+
existing.env.update(env)
|
|
62
|
+
if not existing.healthcheck and healthcheck:
|
|
63
|
+
existing.healthcheck = healthcheck
|
|
64
|
+
if not existing.depends_on:
|
|
65
|
+
existing.depends_on = depends
|
|
66
|
+
existing.source_ref = str(path)
|
|
67
|
+
else:
|
|
68
|
+
seen.add(svc_name)
|
|
69
|
+
services.append(ServiceSpec(
|
|
70
|
+
name=svc_name,
|
|
71
|
+
image=image,
|
|
72
|
+
platform=platform,
|
|
73
|
+
ports=ports,
|
|
74
|
+
volumes=volumes,
|
|
75
|
+
env=env,
|
|
76
|
+
command=command,
|
|
77
|
+
healthcheck=healthcheck,
|
|
78
|
+
depends_on=depends,
|
|
79
|
+
restart=restart,
|
|
80
|
+
privileged=privileged,
|
|
81
|
+
source_ref=str(path),
|
|
82
|
+
))
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _parse_compose_ports(raw: list) -> list[ServicePort]:
|
|
86
|
+
result = []
|
|
87
|
+
for entry in raw:
|
|
88
|
+
if isinstance(entry, int):
|
|
89
|
+
result.append(ServicePort(host=entry, container=entry))
|
|
90
|
+
elif isinstance(entry, str):
|
|
91
|
+
m = re.match(r"(?:[\d.]+:)?(\d+):(\d+)(?:/(tcp|udp))?", entry)
|
|
92
|
+
if m:
|
|
93
|
+
result.append(ServicePort(
|
|
94
|
+
host=int(m.group(1)),
|
|
95
|
+
container=int(m.group(2)),
|
|
96
|
+
protocol=m.group(3) or "tcp",
|
|
97
|
+
))
|
|
98
|
+
elif isinstance(entry, dict):
|
|
99
|
+
result.append(ServicePort(
|
|
100
|
+
host=int(entry.get("published", entry.get("target", 0))),
|
|
101
|
+
container=int(entry.get("target", 0)),
|
|
102
|
+
protocol=entry.get("protocol", "tcp"),
|
|
103
|
+
))
|
|
104
|
+
return result
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _parse_compose_volumes(raw: list | dict) -> list[VolumeMount]:
|
|
108
|
+
result = []
|
|
109
|
+
items = raw if isinstance(raw, list) else [f"{k}:{v}" for k, v in raw.items()]
|
|
110
|
+
for entry in items:
|
|
111
|
+
if isinstance(entry, str) and ":" in entry:
|
|
112
|
+
parts = entry.split(":")
|
|
113
|
+
result.append(VolumeMount(
|
|
114
|
+
host=parts[0],
|
|
115
|
+
container=parts[1],
|
|
116
|
+
read_only=len(parts) > 2 and "ro" in parts[2],
|
|
117
|
+
))
|
|
118
|
+
elif isinstance(entry, dict):
|
|
119
|
+
result.append(VolumeMount(
|
|
120
|
+
host=entry.get("source", ""),
|
|
121
|
+
container=entry.get("target", ""),
|
|
122
|
+
read_only=entry.get("read_only", False),
|
|
123
|
+
))
|
|
124
|
+
return result
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _parse_compose_env(raw: dict | list) -> dict[str, str]:
|
|
128
|
+
if isinstance(raw, dict):
|
|
129
|
+
return {str(k): str(v) for k, v in raw.items() if v is not None}
|
|
130
|
+
result = {}
|
|
131
|
+
for entry in raw:
|
|
132
|
+
if isinstance(entry, str) and "=" in entry:
|
|
133
|
+
k, _, v = entry.partition("=")
|
|
134
|
+
result[k.strip()] = v.strip()
|
|
135
|
+
return result
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _parse_compose_healthcheck(raw: dict) -> Optional[str]:
|
|
139
|
+
if not raw:
|
|
140
|
+
return None
|
|
141
|
+
test = raw.get("test", [])
|
|
142
|
+
if isinstance(test, list) and len(test) > 1:
|
|
143
|
+
return " ".join(str(t) for t in test[1:])
|
|
144
|
+
if isinstance(test, str):
|
|
145
|
+
return test
|
|
146
|
+
return None
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Build ``HardwareRequirements`` from a DeviceMap hardware snapshot."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from ...models import HardwareRequirements
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def build_hw_requirements(hw) -> HardwareRequirements:
|
|
10
|
+
"""Derive hardware requirements from a probed *hw* object."""
|
|
11
|
+
if hw is None:
|
|
12
|
+
return HardwareRequirements()
|
|
13
|
+
|
|
14
|
+
display_type = None
|
|
15
|
+
display_res = None
|
|
16
|
+
features: list[str] = []
|
|
17
|
+
|
|
18
|
+
# DRM / DSI
|
|
19
|
+
for output in getattr(hw, "drm_outputs", []):
|
|
20
|
+
if "DSI" in (output.connector or ""):
|
|
21
|
+
display_type = "DSI"
|
|
22
|
+
if output.modes:
|
|
23
|
+
display_res = output.modes[0]
|
|
24
|
+
|
|
25
|
+
if getattr(hw, "backlights", []):
|
|
26
|
+
features.append("backlight")
|
|
27
|
+
if getattr(hw, "i2c_buses", []):
|
|
28
|
+
features.append("i2c")
|
|
29
|
+
|
|
30
|
+
# Infer arch from board name
|
|
31
|
+
board = getattr(hw, "board", "") or ""
|
|
32
|
+
arch = "linux/arm64" if any(x in board.lower() for x in ("rpi", "raspberry", "aarch64", "pi")) else ""
|
|
33
|
+
|
|
34
|
+
# Overlays → wayland / kiosk hints
|
|
35
|
+
for overlay in getattr(hw, "dsi_overlays", []):
|
|
36
|
+
if "dsi" in overlay.lower():
|
|
37
|
+
features.append("wayland")
|
|
38
|
+
|
|
39
|
+
return HardwareRequirements(
|
|
40
|
+
arch=arch,
|
|
41
|
+
display_type=display_type,
|
|
42
|
+
display_resolution=display_res,
|
|
43
|
+
i2c_required=bool(getattr(hw, "i2c_buses", [])),
|
|
44
|
+
features=features,
|
|
45
|
+
)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Extract service specs and app URL from a live or cached ``InfraState``."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from ...models import InfraState, ServiceSpec
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def extract_services_from_infra(infra: InfraState, seen: set[str]) -> list[ServiceSpec]:
|
|
10
|
+
"""Return :class:`ServiceSpec` objects for every service found in *infra*.
|
|
11
|
+
|
|
12
|
+
Skips entries whose ``name`` is already in *seen* and adds the new names.
|
|
13
|
+
"""
|
|
14
|
+
result: list[ServiceSpec] = []
|
|
15
|
+
for svcs in infra.services.values():
|
|
16
|
+
for svc in svcs:
|
|
17
|
+
if svc.name in seen:
|
|
18
|
+
continue
|
|
19
|
+
seen.add(svc.name)
|
|
20
|
+
result.append(ServiceSpec(
|
|
21
|
+
name=svc.name,
|
|
22
|
+
image=getattr(svc, "image", ""),
|
|
23
|
+
platform="", # unknown at this point
|
|
24
|
+
source_ref="infra:live",
|
|
25
|
+
))
|
|
26
|
+
return result
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def infer_app_url(infra: Optional[InfraState]) -> str | None:
|
|
30
|
+
"""Guess the application URL from open ports on *infra*.
|
|
31
|
+
|
|
32
|
+
Prefers ports ``80 → http://host``, ``443 → https://host``, then
|
|
33
|
+
``8100``, ``8080`` with explicit port numbers.
|
|
34
|
+
"""
|
|
35
|
+
if not infra or not infra.host:
|
|
36
|
+
return None
|
|
37
|
+
host = infra.host.split("@")[-1]
|
|
38
|
+
for port in (80, 8100, 8080, 443):
|
|
39
|
+
if port in (infra.ports or {}):
|
|
40
|
+
if port in (80, 443):
|
|
41
|
+
return f"http://{host}" if port == 80 else f"https://{host}"
|
|
42
|
+
return f"http://{host}:{port}"
|
|
43
|
+
return None
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Parse migration.yaml metadata (version, strategy hints)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import yaml
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def parse_migration_meta(path: Path) -> dict:
|
|
10
|
+
"""Read *path* and return ``{"version": "…", "strategy": "…"}`` if found."""
|
|
11
|
+
try:
|
|
12
|
+
raw = yaml.safe_load(path.read_text())
|
|
13
|
+
except Exception:
|
|
14
|
+
return {}
|
|
15
|
+
if not isinstance(raw, dict):
|
|
16
|
+
return {}
|
|
17
|
+
|
|
18
|
+
result = {}
|
|
19
|
+
if "version" in raw:
|
|
20
|
+
result["version"] = str(raw["version"])
|
|
21
|
+
if "strategy" in raw:
|
|
22
|
+
result["strategy"] = raw["strategy"]
|
|
23
|
+
|
|
24
|
+
# Walk steps looking for deploy action to infer strategy
|
|
25
|
+
for step in raw.get("steps", []):
|
|
26
|
+
action = step.get("action", "")
|
|
27
|
+
if "podman" in action or "quadlet" in action:
|
|
28
|
+
result["strategy"] = "podman_quadlet"
|
|
29
|
+
break
|
|
30
|
+
if "docker" in action:
|
|
31
|
+
result["strategy"] = "docker_compose"
|
|
32
|
+
break
|
|
33
|
+
|
|
34
|
+
return result
|
|
@@ -57,36 +57,28 @@ def _probe_hardware(host, ssh_key, console):
|
|
|
57
57
|
|
|
58
58
|
|
|
59
59
|
def _probe_hardware_op3(host: str, ssh_key: str | None, console):
|
|
60
|
-
"""Probe hardware via op3 scanner (new path).
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
registry = ProbeRegistry()
|
|
78
|
-
registry.register(RpiPhysicalDisplayProbe())
|
|
79
|
-
registry.register(OsKernelProbe())
|
|
80
|
-
registry.register(OsConfigProbe())
|
|
81
|
-
|
|
82
|
-
scanner = LinearScanner(tree)
|
|
83
|
-
scanner.probe_registry = registry
|
|
60
|
+
"""Probe hardware via op3 scanner (new path).
|
|
61
|
+
|
|
62
|
+
Delegates all the layer/probe plumbing to
|
|
63
|
+
:mod:`redeploy.integrations.op3_bridge`, which in turn calls
|
|
64
|
+
:func:`opstree.build_scanner` (0.1.8+). That upstream helper handles
|
|
65
|
+
transitive dependency resolution (e.g. ``os.kernel`` requires
|
|
66
|
+
``physical.compute``) and guarantees probe-registry isolation so
|
|
67
|
+
repeated calls in the same process don't accumulate state.
|
|
68
|
+
"""
|
|
69
|
+
from ...integrations.op3_bridge import (
|
|
70
|
+
make_scanner,
|
|
71
|
+
make_ssh_context,
|
|
72
|
+
snapshot_to_hardware_info,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
ctx = make_ssh_context(target=host, ssh_key=ssh_key)
|
|
76
|
+
scanner = make_scanner(["physical.display", "os.kernel", "os.config"])
|
|
84
77
|
|
|
85
78
|
with console.status(f"[cyan]Probing hardware on {host} via op3…[/cyan]"):
|
|
86
79
|
snapshot = scanner.scan(host, ctx.execute)
|
|
87
80
|
|
|
88
|
-
|
|
89
|
-
return hw
|
|
81
|
+
return snapshot_to_hardware_info(snapshot)
|
|
90
82
|
|
|
91
83
|
|
|
92
84
|
def _format_output(hw, output_fmt):
|
|
@@ -289,26 +281,8 @@ def _apply_transform(console, p, hw, transform):
|
|
|
289
281
|
|
|
290
282
|
def _execute_query(hw, query_expr, output_fmt):
|
|
291
283
|
"""Execute JMESPath query on hardware model and output result."""
|
|
292
|
-
import
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
data = hw.model_dump(mode="json")
|
|
296
|
-
|
|
297
|
-
try:
|
|
298
|
-
result = jmespath.search(query_expr, data)
|
|
299
|
-
except jmespath.exceptions.JMESPathError as e:
|
|
300
|
-
click.echo(f"[red]✗ JMESPath error:[/red] {e}", err=True)
|
|
301
|
-
sys.exit(1)
|
|
302
|
-
|
|
303
|
-
if result is None:
|
|
304
|
-
click.echo("[dim]No match found for query[/dim]")
|
|
305
|
-
return
|
|
306
|
-
|
|
307
|
-
if output_fmt == "json":
|
|
308
|
-
click.echo(_json.dumps(result, indent=2, default=str))
|
|
309
|
-
else:
|
|
310
|
-
import yaml
|
|
311
|
-
click.echo(yaml.safe_dump(result, sort_keys=False, default_flow_style=False))
|
|
284
|
+
from ...cli.query import execute_query
|
|
285
|
+
execute_query(hw, query_expr, output_fmt, echo=lambda msg: click.echo(msg, err=True))
|
|
312
286
|
|
|
313
287
|
|
|
314
288
|
def _apply_fix(console, p, hw, apply_fix_component, panel_id=None):
|