kronos-openstack 0.1.1__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.
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/.github/workflows/ci.yml +20 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/CLAUDE.md +27 -2
- kronos_openstack-0.2.0/CONTRIBUTING.md +71 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/PKG-INFO +7 -3
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/README.md +6 -2
- kronos_openstack-0.2.0/docs/configuration/index.rst +98 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/docs/configuration/kronos.conf.sample +21 -2
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/etc/kronos/kronos.conf.sample +1 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/kronos/clients/placement.py +26 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/kronos/cmd/record.py +5 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/kronos/cmd/replay.py +32 -1
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/kronos/common/config.py +9 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/kronos/common/snapshot.py +24 -2
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/kronos/engine/constraints.py +46 -2
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/kronos/engine/loop.py +52 -3
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/kronos_openstack.egg-info/PKG-INFO +7 -3
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/kronos_openstack.egg-info/SOURCES.txt +4 -0
- kronos_openstack-0.2.0/kronos_openstack.egg-info/scm_file_list.json +106 -0
- kronos_openstack-0.2.0/kronos_openstack.egg-info/scm_version.json +8 -0
- kronos_openstack-0.2.0/tests/unit/engine/test_cpu_compatibility.py +382 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/tests/unit/engine/test_loop.py +4 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/tools/generate_fake_snapshot.py +2 -2
- kronos_openstack-0.1.1/docs/configuration/index.rst +0 -45
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/.dockerignore +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/.github/workflows/container.yml +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/.github/workflows/release.yml +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/.gitignore +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/.python-version +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/.readthedocs.yaml +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/LICENSE +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/docker/Dockerfile +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/docs/conf.py +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/docs/deployment/container.rst +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/docs/deployment/systemd.rst +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/docs/index.rst +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/docs/installation.rst +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/docs/operations/runbook.rst +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/etc/kolla/kronos-engine.json.example +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/etc/kolla/kronos-executor.json.example +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/etc/kronos/policies.yaml.sample +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/etc/oslo-config-generator/kronos.conf +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/etc/systemd/kronos-engine@.service +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/etc/systemd/kronos-executor@.service +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/kronos/__init__.py +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/kronos/clients/__init__.py +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/kronos/clients/nova.py +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/kronos/clients/prometheus.py +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/kronos/cmd/__init__.py +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/kronos/cmd/engine.py +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/kronos/cmd/executor.py +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/kronos/cmd/test_config.py +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/kronos/common/__init__.py +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/kronos/common/exceptions.py +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/kronos/common/messaging.py +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/kronos/engine/__init__.py +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/kronos/engine/_sim.py +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/kronos/engine/affinity_enforcer.py +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/kronos/engine/cooldown.py +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/kronos/engine/evacuator.py +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/kronos/engine/placement.py +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/kronos/engine/planner.py +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/kronos/engine/profiler.py +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/kronos/engine/result_listener.py +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/kronos/engine/scorer.py +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/kronos/engine/types.py +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/kronos/executor/__init__.py +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/kronos/executor/migrate.py +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/kronos/executor/scheduler.py +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/kronos/executor/worker.py +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/kronos/policies/__init__.py +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/kronos/policies/loader.py +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/kronos/policies/models.py +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/kronos/version.py +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/kronos_openstack.egg-info/dependency_links.txt +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/kronos_openstack.egg-info/entry_points.txt +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/kronos_openstack.egg-info/requires.txt +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/kronos_openstack.egg-info/top_level.txt +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/pyproject.toml +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/setup.cfg +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/tests/__init__.py +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/tests/conftest.py +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/tests/fixtures/policies_invalid.yaml +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/tests/fixtures/policies_valid.yaml +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/tests/fixtures/prometheus_responses/healthy.json +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/tests/fixtures/prometheus_responses/partial.json +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/tests/fixtures/prometheus_responses/stale.json +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/tests/unit/__init__.py +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/tests/unit/clients/__init__.py +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/tests/unit/clients/test_nova.py +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/tests/unit/clients/test_prometheus.py +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/tests/unit/cmd/__init__.py +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/tests/unit/cmd/test_executor.py +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/tests/unit/cmd/test_replay.py +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/tests/unit/common/__init__.py +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/tests/unit/common/test_snapshot.py +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/tests/unit/engine/__init__.py +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/tests/unit/engine/test_affinity_enforcer.py +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/tests/unit/engine/test_constraints.py +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/tests/unit/engine/test_cooldown.py +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/tests/unit/engine/test_evacuator.py +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/tests/unit/engine/test_placement.py +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/tests/unit/engine/test_planner.py +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/tests/unit/engine/test_profiler.py +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/tests/unit/engine/test_result_listener.py +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/tests/unit/engine/test_scorer.py +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/tests/unit/executor/__init__.py +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/tests/unit/executor/test_migrate.py +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/tests/unit/executor/test_scheduler.py +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/tests/unit/executor/test_worker.py +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/tests/unit/policies/__init__.py +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/tests/unit/policies/test_loader.py +0 -0
- {kronos_openstack-0.1.1 → kronos_openstack-0.2.0}/tests/unit/policies/test_models.py +0 -0
- {kronos_openstack-0.1.1 → 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.
|
|
@@ -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`
|
|
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,
|
|
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.
|
|
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
|
|
@@ -261,6 +261,11 @@ enforce_placement_claims = true
|
|
|
261
261
|
# ephemeral root disk is genuinely local.
|
|
262
262
|
enforce_placement_disk = false
|
|
263
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
|
+
|
|
264
269
|
[prometheus]
|
|
265
270
|
url = http://prometheus:9090
|
|
266
271
|
|
|
@@ -532,8 +537,7 @@ enforcer, planner) so you can see where cycles are spent.
|
|
|
532
537
|
|
|
533
538
|
Planned work:
|
|
534
539
|
|
|
535
|
-
- **Richer constraints** - NUMA topology,
|
|
536
|
-
flavor extra specs as additional move filters.
|
|
540
|
+
- **Richer constraints** - NUMA topology, and flavor extra specs as additional move filters.
|
|
537
541
|
|
|
538
542
|
Suggestions and contributions are welcome - open an issue.
|
|
539
543
|
|
|
@@ -213,6 +213,11 @@ enforce_placement_claims = true
|
|
|
213
213
|
# ephemeral root disk is genuinely local.
|
|
214
214
|
enforce_placement_disk = false
|
|
215
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
|
+
|
|
216
221
|
[prometheus]
|
|
217
222
|
url = http://prometheus:9090
|
|
218
223
|
|
|
@@ -484,8 +489,7 @@ enforcer, planner) so you can see where cycles are spent.
|
|
|
484
489
|
|
|
485
490
|
Planned work:
|
|
486
491
|
|
|
487
|
-
- **Richer constraints** - NUMA topology,
|
|
488
|
-
flavor extra specs as additional move filters.
|
|
492
|
+
- **Richer constraints** - NUMA topology, and flavor extra specs as additional move filters.
|
|
489
493
|
|
|
490
494
|
Suggestions and contributions are welcome - open an issue.
|
|
491
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:
|
|
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
|
|
@@ -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
|
-
) ->
|
|
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__(
|
|
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
|