kronos-openstack 0.1.0__tar.gz → 0.2.0__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 (113) hide show
  1. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/.github/workflows/ci.yml +20 -0
  2. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/.github/workflows/release.yml +12 -1
  3. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/CLAUDE.md +27 -2
  4. kronos_openstack-0.2.0/CONTRIBUTING.md +71 -0
  5. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/PKG-INFO +10 -17
  6. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/README.md +8 -15
  7. kronos_openstack-0.2.0/docs/configuration/index.rst +98 -0
  8. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/docs/configuration/kronos.conf.sample +21 -2
  9. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/etc/kronos/kronos.conf.sample +1 -0
  10. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/kronos/clients/placement.py +26 -0
  11. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/kronos/cmd/record.py +5 -0
  12. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/kronos/cmd/replay.py +32 -1
  13. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/kronos/common/config.py +9 -0
  14. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/kronos/common/snapshot.py +24 -2
  15. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/kronos/engine/constraints.py +46 -2
  16. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/kronos/engine/loop.py +52 -3
  17. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/kronos_openstack.egg-info/PKG-INFO +10 -17
  18. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/kronos_openstack.egg-info/SOURCES.txt +4 -0
  19. kronos_openstack-0.2.0/kronos_openstack.egg-info/scm_file_list.json +106 -0
  20. kronos_openstack-0.2.0/kronos_openstack.egg-info/scm_version.json +8 -0
  21. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/pyproject.toml +1 -1
  22. kronos_openstack-0.2.0/tests/unit/engine/test_cpu_compatibility.py +382 -0
  23. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/tests/unit/engine/test_loop.py +4 -0
  24. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/tools/generate_fake_snapshot.py +2 -2
  25. kronos_openstack-0.1.0/docs/configuration/index.rst +0 -45
  26. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/.dockerignore +0 -0
  27. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/.github/workflows/container.yml +0 -0
  28. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/.gitignore +0 -0
  29. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/.python-version +0 -0
  30. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/.readthedocs.yaml +0 -0
  31. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/LICENSE +0 -0
  32. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/docker/Dockerfile +0 -0
  33. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/docs/conf.py +0 -0
  34. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/docs/deployment/container.rst +0 -0
  35. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/docs/deployment/systemd.rst +0 -0
  36. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/docs/index.rst +0 -0
  37. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/docs/installation.rst +0 -0
  38. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/docs/operations/runbook.rst +0 -0
  39. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/etc/kolla/kronos-engine.json.example +0 -0
  40. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/etc/kolla/kronos-executor.json.example +0 -0
  41. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/etc/kronos/policies.yaml.sample +0 -0
  42. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/etc/oslo-config-generator/kronos.conf +0 -0
  43. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/etc/systemd/kronos-engine@.service +0 -0
  44. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/etc/systemd/kronos-executor@.service +0 -0
  45. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/kronos/__init__.py +0 -0
  46. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/kronos/clients/__init__.py +0 -0
  47. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/kronos/clients/nova.py +0 -0
  48. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/kronos/clients/prometheus.py +0 -0
  49. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/kronos/cmd/__init__.py +0 -0
  50. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/kronos/cmd/engine.py +0 -0
  51. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/kronos/cmd/executor.py +0 -0
  52. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/kronos/cmd/test_config.py +0 -0
  53. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/kronos/common/__init__.py +0 -0
  54. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/kronos/common/exceptions.py +0 -0
  55. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/kronos/common/messaging.py +0 -0
  56. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/kronos/engine/__init__.py +0 -0
  57. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/kronos/engine/_sim.py +0 -0
  58. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/kronos/engine/affinity_enforcer.py +0 -0
  59. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/kronos/engine/cooldown.py +0 -0
  60. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/kronos/engine/evacuator.py +0 -0
  61. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/kronos/engine/placement.py +0 -0
  62. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/kronos/engine/planner.py +0 -0
  63. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/kronos/engine/profiler.py +0 -0
  64. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/kronos/engine/result_listener.py +0 -0
  65. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/kronos/engine/scorer.py +0 -0
  66. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/kronos/engine/types.py +0 -0
  67. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/kronos/executor/__init__.py +0 -0
  68. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/kronos/executor/migrate.py +0 -0
  69. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/kronos/executor/scheduler.py +0 -0
  70. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/kronos/executor/worker.py +0 -0
  71. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/kronos/policies/__init__.py +0 -0
  72. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/kronos/policies/loader.py +0 -0
  73. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/kronos/policies/models.py +0 -0
  74. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/kronos/version.py +0 -0
  75. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/kronos_openstack.egg-info/dependency_links.txt +0 -0
  76. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/kronos_openstack.egg-info/entry_points.txt +0 -0
  77. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/kronos_openstack.egg-info/requires.txt +0 -0
  78. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/kronos_openstack.egg-info/top_level.txt +0 -0
  79. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/setup.cfg +0 -0
  80. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/tests/__init__.py +0 -0
  81. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/tests/conftest.py +0 -0
  82. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/tests/fixtures/policies_invalid.yaml +0 -0
  83. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/tests/fixtures/policies_valid.yaml +0 -0
  84. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/tests/fixtures/prometheus_responses/healthy.json +0 -0
  85. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/tests/fixtures/prometheus_responses/partial.json +0 -0
  86. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/tests/fixtures/prometheus_responses/stale.json +0 -0
  87. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/tests/unit/__init__.py +0 -0
  88. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/tests/unit/clients/__init__.py +0 -0
  89. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/tests/unit/clients/test_nova.py +0 -0
  90. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/tests/unit/clients/test_prometheus.py +0 -0
  91. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/tests/unit/cmd/__init__.py +0 -0
  92. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/tests/unit/cmd/test_executor.py +0 -0
  93. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/tests/unit/cmd/test_replay.py +0 -0
  94. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/tests/unit/common/__init__.py +0 -0
  95. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/tests/unit/common/test_snapshot.py +0 -0
  96. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/tests/unit/engine/__init__.py +0 -0
  97. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/tests/unit/engine/test_affinity_enforcer.py +0 -0
  98. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/tests/unit/engine/test_constraints.py +0 -0
  99. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/tests/unit/engine/test_cooldown.py +0 -0
  100. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/tests/unit/engine/test_evacuator.py +0 -0
  101. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/tests/unit/engine/test_placement.py +0 -0
  102. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/tests/unit/engine/test_planner.py +0 -0
  103. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/tests/unit/engine/test_profiler.py +0 -0
  104. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/tests/unit/engine/test_result_listener.py +0 -0
  105. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/tests/unit/engine/test_scorer.py +0 -0
  106. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/tests/unit/executor/__init__.py +0 -0
  107. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/tests/unit/executor/test_migrate.py +0 -0
  108. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/tests/unit/executor/test_scheduler.py +0 -0
  109. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/tests/unit/executor/test_worker.py +0 -0
  110. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/tests/unit/policies/__init__.py +0 -0
  111. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/tests/unit/policies/test_loader.py +0 -0
  112. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/tests/unit/policies/test_models.py +0 -0
  113. {kronos_openstack-0.1.0 → kronos_openstack-0.2.0}/tox.ini +0 -0
@@ -67,6 +67,26 @@ jobs:
67
67
  - run: pip install -e ".[docs]"
68
68
  - run: sphinx-build -W -b html docs docs/_build/html
69
69
 
70
+ dco:
71
+ # Every commit must carry a DCO Signed-off-by line (see CONTRIBUTING.md)
72
+ runs-on: ubuntu-latest
73
+ if: github.event_name == 'pull_request'
74
+ steps:
75
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
76
+ with:
77
+ fetch-depth: 0
78
+ - name: Check Signed-off-by on every commit
79
+ run: |
80
+ missing=0
81
+ for sha in $(git rev-list --no-merges \
82
+ ${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}); do
83
+ if ! git log -1 --format=%B "$sha" | grep -q "^Signed-off-by: "; then
84
+ echo "::error::commit $sha is missing a Signed-off-by line"
85
+ missing=1
86
+ fi
87
+ done
88
+ exit "$missing"
89
+
70
90
  genconfig:
71
91
  # The generated config reference must be regenerated and committed
72
92
  # whenever options change.
@@ -60,4 +60,15 @@ jobs:
60
60
  with:
61
61
  inputs: dist/*.tar.gz dist/*.whl
62
62
  upload-signing-artifacts: true
63
- release-signing-artifacts: true
63
+ # The sigstore action only attaches files to a release that
64
+ # already exists, so create the release here with the dist files
65
+ # and the .sigstore.json bundles the previous step wrote.
66
+ - name: Create GitHub release
67
+ env:
68
+ GH_TOKEN: ${{ github.token }}
69
+ run: |
70
+ gh release create "$GITHUB_REF_NAME" \
71
+ --repo "$GITHUB_REPOSITORY" \
72
+ --title "Kronos ${GITHUB_REF_NAME#v}" \
73
+ --generate-notes \
74
+ dist/*.tar.gz dist/*.whl dist/*.sigstore.json
@@ -217,7 +217,7 @@ at load time because the query has to run against live Prometheus.
217
217
  **Adding a new policy field:**
218
218
  1. Add to `PolicyConfig` in `models.py` with `Field(...)`
219
219
  2. Add cross-field validation via `@model_validator` if needed
220
- 3. Update the sample `policies.yaml` in `internal-documentation/`
220
+ 3. Update the sample `etc/kronos/policies.yaml.sample`
221
221
  4. Add tests in `tests/unit/policies/test_models.py`
222
222
  5. Update this section
223
223
 
@@ -454,7 +454,7 @@ of the three may veto a candidate destination.
454
454
 
455
455
  The cache is invalidated each engine cycle.
456
456
 
457
- Future: NUMA, CPU feature flags, flavor extra specs, soft-rule
457
+ Future: NUMA, flavor extra specs, soft-rule
458
458
  penalties in the planner -
459
459
  https://docs.openstack.org/nova/latest/user/server-groups.html
460
460
 
@@ -798,3 +798,28 @@ Engine notification listener updates cooldown/quarantine state
798
798
  4. Hard timeout on migration polling
799
799
  5. Post-flight verification (host + status)
800
800
  6. Idempotent: pre-flight catches duplicate or stale tasks
801
+
802
+
803
+ ## CPU compatibility gate
804
+
805
+ `[engine] require_cpu_compatibility` defaults to false and is independent
806
+ of the Placement claims gate. `PlacementClient.fetch_cpu_traits(hosts)`
807
+ reads `HW_CPU_*` traits using Placement microversion 1.6. The engine
808
+ resolves aggregate/AZ scope before fetching the union once per cycle.
809
+
810
+ `ConstraintChecker` requires source traits to be a subset of destination
811
+ traits after availability and claims checks, before server-group checks.
812
+ All four movers inherit the check. Pair verdicts and trait data reset
813
+ each cycle. Missing data fails closed. A known empty source set warns
814
+ and passes when the destination also has known data.
815
+
816
+ Both snapshot entry points capture `placement/cpu_traits.json` when
817
+ enabled. `ReplayPlacementClient` reads it without network access.
818
+ Missing or invalid snapshots fail closed. Claims remain unrecorded,
819
+ so CPU replay requires `enforce_placement_claims = false`.
820
+
821
+ The host-level rule is approximate. `HW_CPU_*` includes hyperthreading
822
+ as well as mapped instruction flags. Current host configuration can
823
+ differ from a running guest's retained CPU model. Nova remains the
824
+ authoritative migration compatibility check. See the configuration
825
+ reference for behavior and limitations.
@@ -0,0 +1,71 @@
1
+ # Contributing to Kronos
2
+
3
+ Thanks for your interest in Kronos. Contributions of every kind are
4
+ welcome: bug reports, documentation fixes, new constraint checks,
5
+ policy ideas, and operational feedback from real clouds.
6
+
7
+ ## Reporting issues
8
+
9
+ Open an issue at https://github.com/kronos-openstack/kronos/issues.
10
+ For bugs, include the Kronos version, the OpenStack release, and -
11
+ when planning behaves unexpectedly - a `kronos-record` snapshot if
12
+ you can share one (scrub hostnames if needed). A snapshot lets us
13
+ replay your exact planning cycle offline with `kronos-replay`.
14
+
15
+ ## Development setup
16
+
17
+ Python 3.12+ is required.
18
+
19
+ ```bash
20
+ git clone https://github.com/kronos-openstack/kronos.git
21
+ cd kronos
22
+ python3 -m venv .venv
23
+ . .venv/bin/activate
24
+ pip install -e ".[dev]"
25
+ ```
26
+
27
+ ## Before you submit
28
+
29
+ All four gates must pass; CI runs the same commands:
30
+
31
+ ```bash
32
+ pytest tests/
33
+ ruff check kronos/ tests/
34
+ mypy kronos/
35
+ pyright kronos/ tests/
36
+ ```
37
+
38
+ New code needs type hints on public APIs and tests for new behavior.
39
+ Follow the conventions of the module you are editing: the project
40
+ uses oslo.config, oslo.log, and oslo.messaging throughout, and
41
+ ASCII-only punctuation in code, comments, and documentation. If you
42
+ change configuration options, regenerate the config reference:
43
+
44
+ ```bash
45
+ oslo-config-generator --config-file etc/oslo-config-generator/kronos.conf
46
+ ```
47
+
48
+ ## Sign your work (DCO)
49
+
50
+ Every commit must carry a Developer Certificate of Origin sign-off:
51
+
52
+ ```bash
53
+ git commit -s
54
+ ```
55
+
56
+ This appends a `Signed-off-by:` line with your name and email,
57
+ certifying that you have the right to submit the change under the
58
+ project license (see https://developercertificate.org/). CI checks
59
+ every commit in a pull request and fails on missing sign-offs.
60
+
61
+ ## Pull requests
62
+
63
+ - Keep each PR to one logical change.
64
+ - Describe the operational motivation, not just the code change.
65
+ - The project targets OpenStack ecosystem conventions; expect review
66
+ feedback aimed at keeping it that way.
67
+
68
+ ## License
69
+
70
+ By contributing you agree that your contributions are licensed under
71
+ the Apache License 2.0, the same license as the project.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kronos-openstack
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: PromQL-driven VM placement engine for OpenStack
5
5
  Author-email: Luca Del Monte <luca.delmonte5@gmail.com>, Michele Palazzi <sysdadmin@m1k.cloud>
6
6
  License: Apache-2.0
@@ -9,7 +9,7 @@ Project-URL: Repository, https://github.com/kronos-openstack/kronos
9
9
  Project-URL: Issues, https://github.com/kronos-openstack/kronos/issues
10
10
  Project-URL: Documentation, https://kronos-openstack.readthedocs.io
11
11
  Keywords: openstack,nova,prometheus,live-migration,scheduler,placement,rebalancing
12
- Classifier: Development Status :: 2 - Pre-Alpha
12
+ Classifier: Development Status :: 4 - Beta
13
13
  Classifier: Environment :: OpenStack
14
14
  Classifier: Intended Audience :: System Administrators
15
15
  Classifier: License :: OSI Approved :: Apache Software License
@@ -59,8 +59,6 @@ When dry-run is disabled, the engine casts migration tasks to a per-aggregate RP
59
59
  topic via oslo.messaging. A dedicated executor daemon consumes the tasks and carries
60
60
  them out through the Nova live-migrate API.
61
61
 
62
- > **Status:** Pre-alpha. Not yet ready for production.
63
-
64
62
  ## Features
65
63
 
66
64
  - **Combined multi-policy scoring** - every policy contributes a
@@ -263,6 +261,11 @@ enforce_placement_claims = true
263
261
  # ephemeral root disk is genuinely local.
264
262
  enforce_placement_disk = false
265
263
 
264
+ # Require destination Placement HW_CPU_* traits to contain all source traits.
265
+ # Missing data blocks moves. Empty source sets warn and pass. Independent of
266
+ # enforce_placement_claims. Nova still checks migrations. (boolean value)
267
+ require_cpu_compatibility = false
268
+
266
269
  [prometheus]
267
270
  url = http://prometheus:9090
268
271
 
@@ -532,19 +535,9 @@ enforcer, planner) so you can see where cycles are spent.
532
535
 
533
536
  ## Roadmap
534
537
 
535
- Planned work, in rough priority order:
536
-
537
- - **Pack mode rework** - the current First Fit Decreasing drain order
538
- is mechanically correct but the drain decisions need rethinking.
539
- Once the semantics are right: put fully drained hosts into
540
- maintenance, refuse to drain below a configurable spare-host
541
- reserve, and bring drained hosts back when load grows.
542
- - **Soft affinity as planner penalties** - soft server-group rules
543
- currently veto moves just like hard ones; they should become
544
- weighted penalties so a mild soft-rule violation can still win when
545
- it resolves a much larger imbalance.
546
- - **Richer constraints** - NUMA topology, CPU feature flags, and
547
- flavor extra specs as additional move filters.
538
+ Planned work:
539
+
540
+ - **Richer constraints** - NUMA topology, and flavor extra specs as additional move filters.
548
541
 
549
542
  Suggestions and contributions are welcome - open an issue.
550
543
 
@@ -11,8 +11,6 @@ When dry-run is disabled, the engine casts migration tasks to a per-aggregate RP
11
11
  topic via oslo.messaging. A dedicated executor daemon consumes the tasks and carries
12
12
  them out through the Nova live-migrate API.
13
13
 
14
- > **Status:** Pre-alpha. Not yet ready for production.
15
-
16
14
  ## Features
17
15
 
18
16
  - **Combined multi-policy scoring** - every policy contributes a
@@ -215,6 +213,11 @@ enforce_placement_claims = true
215
213
  # ephemeral root disk is genuinely local.
216
214
  enforce_placement_disk = false
217
215
 
216
+ # Require destination Placement HW_CPU_* traits to contain all source traits.
217
+ # Missing data blocks moves. Empty source sets warn and pass. Independent of
218
+ # enforce_placement_claims. Nova still checks migrations. (boolean value)
219
+ require_cpu_compatibility = false
220
+
218
221
  [prometheus]
219
222
  url = http://prometheus:9090
220
223
 
@@ -484,19 +487,9 @@ enforcer, planner) so you can see where cycles are spent.
484
487
 
485
488
  ## Roadmap
486
489
 
487
- Planned work, in rough priority order:
488
-
489
- - **Pack mode rework** - the current First Fit Decreasing drain order
490
- is mechanically correct but the drain decisions need rethinking.
491
- Once the semantics are right: put fully drained hosts into
492
- maintenance, refuse to drain below a configurable spare-host
493
- reserve, and bring drained hosts back when load grows.
494
- - **Soft affinity as planner penalties** - soft server-group rules
495
- currently veto moves just like hard ones; they should become
496
- weighted penalties so a mild soft-rule violation can still win when
497
- it resolves a much larger imbalance.
498
- - **Richer constraints** - NUMA topology, CPU feature flags, and
499
- flavor extra specs as additional move filters.
490
+ Planned work:
491
+
492
+ - **Richer constraints** - NUMA topology, and flavor extra specs as additional move filters.
500
493
 
501
494
  Suggestions and contributions are welcome - open an issue.
502
495
 
@@ -0,0 +1,98 @@
1
+ Configuration reference
2
+ =======================
3
+
4
+ Config split
5
+ ------------
6
+
7
+ - ``/etc/kronos/kronos.conf`` - oslo.config INI for daemon settings.
8
+ Everything the daemons need to run: evaluation interval, dry-run,
9
+ aggregate scope, availability zone, cooldowns, messaging transport,
10
+ Prometheus endpoint, Keystone auth.
11
+ - ``/etc/kronos/policies.yaml`` - Pydantic-validated YAML describing
12
+ the scheduling policies: PromQL queries, thresholds, weights,
13
+ per-VM profiling queries and fallbacks.
14
+
15
+ The split is deliberate: policies are a rich, validated document with
16
+ cross-field rules (weights summing to 1.0, one mode per file); daemon
17
+ settings are flat key-value pairs that fit oslo.config.
18
+
19
+ Full option reference
20
+ ---------------------
21
+
22
+ The complete generated reference - every option of every group,
23
+ including the inherited oslo.log and oslo.messaging options - is
24
+ checked into the repository at ``docs/configuration/kronos.conf.sample``
25
+ and regenerated with:
26
+
27
+ .. code-block:: console
28
+
29
+ oslo-config-generator --config-file etc/oslo-config-generator/kronos.conf
30
+
31
+ .. literalinclude:: kronos.conf.sample
32
+ :language: ini
33
+
34
+ Policies file
35
+ -------------
36
+
37
+ See ``etc/kronos/policies.yaml.sample`` in the repository for a
38
+ commented example. Load-time invariants:
39
+
40
+ - policy names are unique;
41
+ - all policies in one file share a ``mode`` (``spread`` or ``pack``);
42
+ - enabled policy ``weight`` values sum to 1.0;
43
+ - ``imbalance_query`` must return per-host values in [0, 1] - enforced
44
+ at runtime by the scorer, which skips the policy for the cycle (with
45
+ an error logged) on out-of-range data.
46
+
47
+
48
+ CPU compatibility
49
+ -----------------
50
+
51
+ Set ``[engine] require_cpu_compatibility = true`` to require a
52
+ destination's Placement ``HW_CPU_*`` traits to contain every CPU trait
53
+ reported for the source host. The default is false. This option is
54
+ independent of ``enforce_placement_claims``.
55
+
56
+ The shared constraint checker applies this rule to spread, pack,
57
+ disabled-host evacuation, and affinity repair. Checks run after host
58
+ availability and capacity claims, before server-group checks. A DEBUG
59
+ message names the rejected host pair and missing traits.
60
+
61
+ The engine lists Placement providers and reads traits once per in-scope
62
+ host per cycle, deduplicating overlapping aggregates and filtering to
63
+ the configured availability zone. The service user needs
64
+ ``placement:resource_providers:list`` and
65
+ ``placement:resource_providers:traits:list`` access. The traits endpoint
66
+ uses Placement microversion 1.6. Provider names must match the Nova host
67
+ names used by the engine. Missing source or destination data blocks a
68
+ move. Any traits fetch failure blocks all moves for that cycle.
69
+
70
+ A successful response containing no ``HW_CPU_*`` traits is different
71
+ from unavailable data. An empty source set permits moves to destinations
72
+ with known data and logs a warning once per cycle that the check is
73
+ blind for that source.
74
+
75
+ This is a host-level approximation, not a per-instance CPU model check.
76
+ Nova's libvirt driver reports host features for host-model and
77
+ host-passthrough, and a union of configured models for custom mode.
78
+ The prefix also includes ``HW_CPU_HYPERTHREADING``, so the check can
79
+ reject moves for more than instruction-set differences. It can reject
80
+ moves Nova would accept and miss incompatibilities involving unmapped
81
+ flags or guests retaining an older CPU model after reconfiguration.
82
+ Nova's migration pre-check remains authoritative.
83
+
84
+ Producer source:
85
+ `Nova libvirt CPU trait reporting <https://github.com/openstack/nova/blob/stable/2025.2/nova/virt/libvirt/driver.py>`_.
86
+ API source:
87
+ `Placement provider traits <https://github.com/openstack/placement/blob/stable/2025.2/placement/handlers/trait.py>`_.
88
+
89
+ When the option is enabled, both ``kronos-record`` and engine SIGUSR1
90
+ snapshots write ``placement/cpu_traits.json``, a mapping from host names
91
+ to sorted trait lists. Failed collection writes an empty mapping.
92
+ Replay reads this file without contacting Placement. Missing files in
93
+ older snapshots fail closed when CPU checking is enabled.
94
+
95
+ For CPU compatibility replay, enable ``require_cpu_compatibility`` and
96
+ set ``enforce_placement_claims = false``. Capacity claims are not
97
+ recorded by the existing snapshot format. Leaving that gate enabled
98
+ pauses replay planning with an explicit error in the logs.
@@ -292,6 +292,11 @@
292
292
  # aggregate. (boolean value)
293
293
  #enforce_placement_disk = false
294
294
 
295
+ # Require destination Placement HW_CPU_* traits to contain all source traits.
296
+ # Missing data blocks moves. Empty source sets warn and pass. Independent of
297
+ # enforce_placement_claims. Nova still checks migrations. (boolean value)
298
+ #require_cpu_compatibility = false
299
+
295
300
 
296
301
  [executor]
297
302
 
@@ -387,6 +392,21 @@
387
392
  # Log requests to multiple loggers. (boolean value)
388
393
  #split_loggers = false
389
394
 
395
+ # An OpenSSL cipher string to set the allowed ciphers for TLS connections. If
396
+ # not specified, the default ciphers from the underlying OpenSSL library are
397
+ # used. (string value)
398
+ #tls_ciphers = <None>
399
+
400
+ # The minimum TLS version to require for TLS connections. When not specified,
401
+ # the behavior is undefined and defers to the system cryptographic policy.
402
+ # (string value)
403
+ # Possible values:
404
+ # <None> - None - the absence of a setting, defers to the system cryptographic
405
+ # policy
406
+ # 1.2 - TLS 1.2 - enforce minimum TLS version 1.2
407
+ # 1.3 - TLS 1.3 - enforce minimum TLS version 1.3
408
+ #tls_min_version = <None>
409
+
390
410
  # The default service_type for endpoint URL discovery. (string value)
391
411
  #service_type = <None>
392
412
 
@@ -584,8 +604,7 @@
584
604
  # (boolean value)
585
605
  # This option is deprecated for removal.
586
606
  # Its value may be silently ignored in the future.
587
- # Reason: Hostname verification should remain enabled once operators have
588
- # completed the migration.
607
+ # Reason: Verification is always enabled now
589
608
  #ssl_enforce_hostname_verification = true
590
609
 
591
610
  # DEPRECATED: Global toggle for enforcing the OpenSSL FIPS mode. This feature
@@ -2,6 +2,7 @@
2
2
  # debug = false
3
3
 
4
4
  [engine]
5
+ require_cpu_compatibility = false
5
6
  # Seconds between policy evaluation cycles.
6
7
  # evaluation_interval = 60
7
8
 
@@ -206,3 +206,29 @@ class PlacementClient:
206
206
  for rc, used in raw.items()
207
207
  if rc in TRACKED_RESOURCE_CLASSES
208
208
  }
209
+
210
+ def fetch_cpu_traits(self, hosts: set[str]) -> dict[str, frozenset[str]]:
211
+ if not hosts:
212
+ return {}
213
+ try:
214
+ traits: dict[str, frozenset[str]] = {}
215
+ for rp in self._placement.resource_providers():
216
+ if rp.name not in hosts:
217
+ continue
218
+ response = self._placement.get(
219
+ f"/resource_providers/{rp.id}/traits", microversion="1.6",
220
+ )
221
+ response.raise_for_status()
222
+ raw = response.json()["traits"]
223
+ if not isinstance(raw, list) or not all(
224
+ isinstance(trait, str) for trait in raw
225
+ ):
226
+ raise ValueError(f"Invalid CPU traits response for {rp.name}")
227
+ traits[rp.name] = frozenset(
228
+ trait for trait in raw if trait.startswith("HW_CPU_")
229
+ )
230
+ return traits
231
+ except Exception as exc:
232
+ raise PlacementClientError(
233
+ reason=f"Failed to fetch CPU traits: {exc}",
234
+ ) from exc
@@ -15,6 +15,7 @@ from oslo_config import cfg
15
15
  from oslo_log import log as logging
16
16
 
17
17
  from kronos.clients.nova import NovaClient
18
+ from kronos.clients.placement import PlacementClient
18
19
  from kronos.clients.prometheus import PrometheusClient
19
20
  from kronos.common.config import register_opts
20
21
  from kronos.common.snapshot import write_snapshot
@@ -72,8 +73,12 @@ def main() -> int:
72
73
 
73
74
  nova = NovaClient(CONF)
74
75
  prometheus = PrometheusClient(CONF)
76
+ placement = (
77
+ PlacementClient(CONF) if CONF.engine.require_cpu_compatibility else None
78
+ )
75
79
  target = write_snapshot(
76
80
  parent_dir, nova, prometheus, policies, aggregate_names,
81
+ placement=placement,
77
82
  )
78
83
 
79
84
  LOG.info("Snapshot written to %s", target)
@@ -22,9 +22,10 @@ from oslo_config import cfg
22
22
  from oslo_log import log as logging
23
23
 
24
24
  from kronos.clients.nova import ComputeService, Instance
25
+ from kronos.clients.placement import PlacementClient, ProviderSnapshot
25
26
  from kronos.clients.prometheus import PrometheusHealth, QueryResult
26
27
  from kronos.common.config import register_opts
27
- from kronos.common.exceptions import AggregateNotFound
28
+ from kronos.common.exceptions import AggregateNotFound, PlacementClientError
28
29
  from kronos.common.messaging import UNASSIGNED_TOPIC_MARKER
29
30
  from kronos.engine.cooldown import CooldownTracker
30
31
  from kronos.engine.loop import EngineLoop
@@ -295,6 +296,7 @@ def main() -> int:
295
296
  prometheus=prometheus, # type: ignore[arg-type]
296
297
  cooldown=cooldown,
297
298
  timings=timings,
299
+ placement=ReplayPlacementClient(snapshot_dir),
298
300
  )
299
301
 
300
302
  started = time.perf_counter()
@@ -307,3 +309,32 @@ def main() -> int:
307
309
  _log_timings(timings, total)
308
310
 
309
311
  return 0
312
+
313
+
314
+ class ReplayPlacementClient(PlacementClient):
315
+ def __init__(self, snapshot_dir: Path) -> None:
316
+ self._traits_path = snapshot_dir / "placement" / "cpu_traits.json"
317
+
318
+ def fetch_cpu_traits(self, hosts: set[str]) -> dict[str, frozenset[str]]:
319
+ if not self._traits_path.exists():
320
+ raise PlacementClientError(reason="Snapshot has no CPU traits")
321
+ raw = json.loads(self._traits_path.read_text())
322
+ if not isinstance(raw, dict):
323
+ raise PlacementClientError(reason="Invalid CPU traits snapshot")
324
+ traits: dict[str, frozenset[str]] = {}
325
+ for host, flags in raw.items():
326
+ if not isinstance(flags, list) or not all(
327
+ isinstance(flag, str) for flag in flags
328
+ ):
329
+ raise PlacementClientError(reason=f"Invalid CPU traits for {host}")
330
+ if host in hosts:
331
+ traits[host] = frozenset(
332
+ flag for flag in flags if flag.startswith("HW_CPU_")
333
+ )
334
+ return traits
335
+
336
+ def fetch_snapshots(self) -> dict[str, ProviderSnapshot]:
337
+ raise PlacementClientError(
338
+ reason="Placement claims are not recorded. Disable "
339
+ "enforce_placement_claims for offline CPU compatibility replay.",
340
+ )
@@ -250,6 +250,15 @@ engine_opts: list[cfg.Opt] = [
250
250
  "identical across all hosts in the aggregate."
251
251
  ),
252
252
  ),
253
+ cfg.BoolOpt(
254
+ "require_cpu_compatibility",
255
+ default=False,
256
+ help=(
257
+ "Require destination Placement HW_CPU_* traits to contain all source "
258
+ "traits. Missing data blocks moves. Empty source sets warn and pass. "
259
+ "Independent of enforce_placement_claims. Nova still checks migrations."
260
+ ),
261
+ ),
253
262
  ]
254
263
 
255
264
  prometheus_opts: list[cfg.Opt] = [
@@ -27,6 +27,7 @@ from pathlib import Path
27
27
  from oslo_log import log as logging
28
28
 
29
29
  from kronos.clients.nova import NovaClient
30
+ from kronos.clients.placement import PlacementClient
30
31
  from kronos.clients.prometheus import PrometheusClient
31
32
  from kronos.common.messaging import UNASSIGNED_TOPIC_MARKER
32
33
  from kronos.policies.models import PoliciesConfig
@@ -43,6 +44,8 @@ def write_snapshot(
43
44
  prometheus: PrometheusClient,
44
45
  policies: PoliciesConfig,
45
46
  aggregate_names: list[str | None],
47
+ *,
48
+ placement: PlacementClient | None = None,
46
49
  ) -> Path:
47
50
  """Write a complete snapshot under ``parent_dir`` and return its path.
48
51
 
@@ -77,7 +80,9 @@ def write_snapshot(
77
80
  ),
78
81
  )
79
82
 
80
- _write_nova(nova, aggregate_names, target)
83
+ hosts = _write_nova(nova, aggregate_names, target)
84
+ if placement is not None:
85
+ _write_cpu_traits(placement, hosts, target)
81
86
  _write_prometheus(prometheus, policies, target)
82
87
 
83
88
  (target / "cooldowns.json").write_text(
@@ -98,7 +103,7 @@ def _write_nova(
98
103
  nova: NovaClient,
99
104
  aggregate_names: list[str | None],
100
105
  output_dir: Path,
101
- ) -> None:
106
+ ) -> set[str]:
102
107
  nova_dir = output_dir / "nova"
103
108
  nova_dir.mkdir()
104
109
 
@@ -163,6 +168,8 @@ def _write_nova(
163
168
  LOG.error("Failed to list compute services", exc_info=True)
164
169
  (nova_dir / "services.json").write_text("[]")
165
170
 
171
+ return all_hosts
172
+
166
173
 
167
174
  def _write_prometheus(
168
175
  prometheus: PrometheusClient,
@@ -234,3 +241,18 @@ def _write_prometheus(
234
241
  policy.name,
235
242
  exc_info=True,
236
243
  )
244
+
245
+
246
+ def _write_cpu_traits(
247
+ placement: PlacementClient, hosts: set[str], output_dir: Path,
248
+ ) -> None:
249
+ directory = output_dir / "placement"
250
+ directory.mkdir()
251
+ try:
252
+ traits = placement.fetch_cpu_traits(hosts)
253
+ except Exception:
254
+ LOG.error("Failed to record CPU traits", exc_info=True)
255
+ traits = {}
256
+ (directory / "cpu_traits.json").write_text(
257
+ json.dumps({host: sorted(flags) for host, flags in traits.items()}, indent=2),
258
+ )
@@ -104,13 +104,17 @@ class ConstraintChecker:
104
104
 
105
105
  Future:
106
106
  - NUMA topology
107
- - CPU feature flags
108
107
  - Flavor extra specs / traits
109
108
  - Promote soft rules to planner-side penalties instead of vetoes
110
109
  """
111
110
 
112
- def __init__(self, nova: NovaClient) -> None:
111
+ def __init__(
112
+ self, nova: NovaClient, *, require_cpu_compatibility: bool = False,
113
+ ) -> None:
113
114
  self._nova = nova
115
+ self._require_cpu_compatibility = require_cpu_compatibility
116
+ self._cpu_traits: dict[str, frozenset[str]] = {}
117
+ self._cpu_compatibility: dict[tuple[str, str], bool] = {}
114
118
  # Lazy-loaded cache; populated on first check() in a cycle.
115
119
  self._groups: list[ServerGroup] | None = None
116
120
  # Per-cycle nova-compute service map (host -> ComputeService).
@@ -209,6 +213,9 @@ class ConstraintChecker:
209
213
  ):
210
214
  return False
211
215
 
216
+ if not self._check_cpu_compatibility(vm.host, dest_host):
217
+ return False
218
+
212
219
  groups = self._get_groups()
213
220
  if not groups:
214
221
  return True
@@ -382,5 +389,42 @@ class ConstraintChecker:
382
389
  """
383
390
  self._groups = None
384
391
  self._services = None
392
+ self._cpu_traits = {}
393
+ self._cpu_compatibility.clear()
385
394
  if self._placement_gate is not None:
386
395
  self._placement_gate.invalidate()
396
+
397
+ def set_cpu_traits(self, traits: dict[str, frozenset[str]]) -> None:
398
+ self._cpu_traits = dict(traits)
399
+ self._cpu_compatibility.clear()
400
+ for host, features in traits.items():
401
+ if not features:
402
+ LOG.warning(
403
+ "Host %s reports no HW_CPU_* traits. CPU compatibility "
404
+ "checking is blind for moves from this host.", host,
405
+ )
406
+
407
+ def _check_cpu_compatibility(self, source: str, destination: str) -> bool:
408
+ if not self._require_cpu_compatibility:
409
+ return True
410
+ pair = (source, destination)
411
+ if pair in self._cpu_compatibility:
412
+ return self._cpu_compatibility[pair]
413
+ source_traits = self._cpu_traits.get(source)
414
+ destination_traits = self._cpu_traits.get(destination)
415
+ if source_traits is None or destination_traits is None:
416
+ compatible = False
417
+ LOG.debug(
418
+ "CPU compatibility: %s -> %s rejected (missing host traits).",
419
+ source, destination,
420
+ )
421
+ else:
422
+ missing = source_traits - destination_traits
423
+ compatible = not missing
424
+ if missing:
425
+ LOG.debug(
426
+ "CPU compatibility: %s -> %s rejected (missing traits: %s).",
427
+ source, destination, ", ".join(sorted(missing)),
428
+ )
429
+ self._cpu_compatibility[pair] = compatible
430
+ return compatible