)]}'
{"/PATCHSET_LEVEL":[{"author":{"_account_id":13425,"name":"Simon Dodsley","email":"simon@everpuredata.com","username":"sdodsley"},"change_message_id":"316e83c13a13aee0cc55f639e894391c0ed38449","unresolved":true,"context_lines":[],"source_content_type":"","patch_set":1,"id":"16033695_7647404d","updated":"2026-06-25 13:39:10.000000000","message":"Two blocking items: the \"incomplete blocking\" justification is backwards (service-disable already blocks extend/snapshot/retype today), and the pool-name key diverges from what the filter actually matches under active/active. \nFour smaller items as well. \nThe filter is more comprehensive than the spec claims — migrate, manage_existing, and group-create all funnel through backend_passes_filters, so they\u0027re covered too; worth stating explicitly.\nAlso this should be moved to 2026.2 now","commit_id":"faec9e23cf9923def9d312512b434292658fe0e5"},{"author":{"_account_id":13425,"name":"Simon Dodsley","email":"simon@everpuredata.com","username":"sdodsley"},"change_message_id":"1b845aff4eb59fd3c84fe0024043a722295eb85c","unresolved":true,"context_lines":[],"source_content_type":"","patch_set":2,"id":"ee15148c_d35fe808","updated":"2026-09-17 16:49:29.000000000","message":"Missing behaviours that need to be documented:\nClone, create-from-snapshot, and create-into-group are pinned to the source pool via ``request_spec[\u0027resource_backend\u0027]`` (``filter_scheduler._schedule()``), so they cannot be redirected — on a disabled pool they fail hard with ``NoValidBackend`` rather than landing elsewhere. That\u0027s probably what you want while draining, but it\u0027s operator-visible and belongs in the admin guide section. An explicit \"unaffected operations\" list would also help: deletes, attach/detach and revert-to-snapshot never touch the scheduler, so they continue to work on a drained pool.","commit_id":"f8e31d8ab7c57e1fbd309d49197032cab15c8153"},{"author":{"_account_id":13425,"name":"Simon Dodsley","email":"simon@everpuredata.com","username":"sdodsley"},"change_message_id":"c52b59891316a822d7c57fb6d6033431ef2cc29e","unresolved":false,"context_lines":[],"source_content_type":"","patch_set":2,"id":"6aa9a653_9f6d272b","updated":"2026-09-23 18:00:31.000000000","message":"please move to 2027.1","commit_id":"f8e31d8ab7c57e1fbd309d49197032cab15c8153"}],"specs/2026.1/pool-disable-draining.rst":[{"author":{"_account_id":5997,"name":"Walt","display_name":"Hemna","email":"waboring@hemna.com","username":"walter-boring","status":"SAP"},"change_message_id":"4aa6b0167153cf9fa8db9817ca99733c5f71163f","unresolved":false,"context_lines":[{"line_number":34,"context_line":"  and health reporting, as a disabled service appears unhealthy even though"},{"line_number":35,"context_line":"  it is still running normally."},{"line_number":36,"context_line":""},{"line_number":37,"context_line":"* **Incomplete blocking**: Disabling a service only prevents the scheduler from"},{"line_number":38,"context_line":"  selecting that service as a *destination* for new volume creates. It does"},{"line_number":39,"context_line":"  **not** block extend, snapshot, or retype operations on volumes already"},{"line_number":40,"context_line":"  residing on that service, since those operations validate the existing backend"}],"source_content_type":"text/x-rst","patch_set":1,"id":"3ceda395_5ef591dd","line":37,"updated":"2026-07-24 20:03:16.000000000","message":"Done. Removed the \"Incomplete blocking\" bullet entirely. You\u0027re right that service-disable already drops backends from the state map via the disabled\u003dFalse filter in get_all(), blocking all those operations. Problem Description now only has granularity and semantic mismatch.","commit_id":"faec9e23cf9923def9d312512b434292658fe0e5"},{"author":{"_account_id":13425,"name":"Simon Dodsley","email":"simon@everpuredata.com","username":"sdodsley"},"change_message_id":"316e83c13a13aee0cc55f639e894391c0ed38449","unresolved":true,"context_lines":[{"line_number":34,"context_line":"  and health reporting, as a disabled service appears unhealthy even though"},{"line_number":35,"context_line":"  it is still running normally."},{"line_number":36,"context_line":""},{"line_number":37,"context_line":"* **Incomplete blocking**: Disabling a service only prevents the scheduler from"},{"line_number":38,"context_line":"  selecting that service as a *destination* for new volume creates. It does"},{"line_number":39,"context_line":"  **not** block extend, snapshot, or retype operations on volumes already"},{"line_number":40,"context_line":"  residing on that service, since those operations validate the existing backend"}],"source_content_type":"text/x-rst","patch_set":1,"id":"85bd7c58_daddd5df","line":37,"range":{"start_line":37,"start_character":2,"end_line":37,"end_character":25},"updated":"2026-06-25 13:39:10.000000000","message":"This is backwards against master. host_manager._update_backend_state_map builds the state map from ServiceList.get_all(context, {\u0027topic\u0027: topic, \u0027disabled\u0027: False, \u0027frozen\u0027: False}), so a disabled service\u0027s backends are dropped from the map entirely. backend_passes_filters then raises NoValidBackend for extend/snapshot/retype/migrate on volumes residing there — i.e. service-disable already blocks all of those today, not just new-create destination selection. Please drop this justification and lean on the two valid ones: granularity (one service disables all its pools) and the health/semantic conflation of the service disabled flag.","commit_id":"faec9e23cf9923def9d312512b434292658fe0e5"},{"author":{"_account_id":5997,"name":"Walt","display_name":"Hemna","email":"waboring@hemna.com","username":"walter-boring","status":"SAP"},"change_message_id":"4aa6b0167153cf9fa8db9817ca99733c5f71163f","unresolved":false,"context_lines":[{"line_number":109,"context_line":"* **Volume extend**: Same as snapshot -- the scheduler validates the existing"},{"line_number":110,"context_line":"  pool via ``backend_passes_filters()``. The filter rejects it. An API-level"},{"line_number":111,"context_line":"  pre-check also returns HTTP 409 immediately."},{"line_number":112,"context_line":""},{"line_number":113,"context_line":"* **Retype**: The scheduler runs the full filter/weigh pipeline. If the"},{"line_number":114,"context_line":"  volume\u0027s current pool is disabled:"},{"line_number":115,"context_line":""}],"source_content_type":"text/x-rst","patch_set":1,"id":"b05db351_ac649570","line":112,"updated":"2026-07-24 20:03:16.000000000","message":"Done. Added explicit entries for migrate, manage_existing, manage_existing_snapshot, and group validate_host_capacity to the Scheduler Filter Behavior section, noting they all route through backend_passes_filters(). Also noted that draining off a disabled pool works since the filter evaluates the destination.","commit_id":"faec9e23cf9923def9d312512b434292658fe0e5"},{"author":{"_account_id":13425,"name":"Simon Dodsley","email":"simon@everpuredata.com","username":"sdodsley"},"change_message_id":"316e83c13a13aee0cc55f639e894391c0ed38449","unresolved":true,"context_lines":[{"line_number":109,"context_line":"* **Volume extend**: Same as snapshot -- the scheduler validates the existing"},{"line_number":110,"context_line":"  pool via ``backend_passes_filters()``. The filter rejects it. An API-level"},{"line_number":111,"context_line":"  pre-check also returns HTTP 409 immediately."},{"line_number":112,"context_line":""},{"line_number":113,"context_line":"* **Retype**: The scheduler runs the full filter/weigh pipeline. If the"},{"line_number":114,"context_line":"  volume\u0027s current pool is disabled:"},{"line_number":115,"context_line":""}],"source_content_type":"text/x-rst","patch_set":1,"id":"e26240cb_941a8a69","line":112,"updated":"2026-06-25 13:39:10.000000000","message":"Worth noting the filter coverage is broader than listed here: scheduler/manager.py routes migrate_volume, manage_existing, manage_existing_snapshot, and group validate_host_capacity all through backend_passes_filters, so a disabled pool is also blocked as a migrate destination and for manage/group-create. Recommend listing these so reviewers don\u0027t assume they\u0027re gaps. (Draining off a disabled pool still works — backend_passes_filters runs against the destination, not the source.)","commit_id":"faec9e23cf9923def9d312512b434292658fe0e5"},{"author":{"_account_id":5997,"name":"Walt","display_name":"Hemna","email":"waboring@hemna.com","username":"walter-boring","status":"SAP"},"change_message_id":"4aa6b0167153cf9fa8db9817ca99733c5f71163f","unresolved":false,"context_lines":[{"line_number":179,"context_line":"A new table ``pool_disabled_states`` is introduced:"},{"line_number":180,"context_line":""},{"line_number":181,"context_line":".. code-block:: sql"},{"line_number":182,"context_line":""},{"line_number":183,"context_line":"    CREATE TABLE pool_disabled_states ("},{"line_number":184,"context_line":"        id              INTEGER PRIMARY KEY AUTOINCREMENT,"},{"line_number":185,"context_line":"        pool_name       VARCHAR(255) NOT NULL UNIQUE,"}],"source_content_type":"text/x-rst","patch_set":1,"id":"8e5d14d6_843c7540","line":182,"updated":"2026-07-24 20:03:16.000000000","message":"Done. DDL is now marked as illustrative with a note that the real implementation uses SQLAlchemy/alembic. Changed UNIQUE(pool_name) to UNIQUE(pool_name, deleted) and switched deleted to INTEGER DEFAULT 0 (set to row id on delete) per Cinder conventions. Insert new row on disable, set deleted\u003did on enable — preserves audit trail.","commit_id":"faec9e23cf9923def9d312512b434292658fe0e5"},{"author":{"_account_id":13425,"name":"Simon Dodsley","email":"simon@everpuredata.com","username":"sdodsley"},"change_message_id":"316e83c13a13aee0cc55f639e894391c0ed38449","unresolved":true,"context_lines":[{"line_number":179,"context_line":"A new table ``pool_disabled_states`` is introduced:"},{"line_number":180,"context_line":""},{"line_number":181,"context_line":".. code-block:: sql"},{"line_number":182,"context_line":""},{"line_number":183,"context_line":"    CREATE TABLE pool_disabled_states ("},{"line_number":184,"context_line":"        id              INTEGER PRIMARY KEY AUTOINCREMENT,"},{"line_number":185,"context_line":"        pool_name       VARCHAR(255) NOT NULL UNIQUE,"}],"source_content_type":"text/x-rst","patch_set":1,"id":"88b99453_8c78a9b4","line":182,"updated":"2026-06-25 13:39:10.000000000","message":"The illustrative DDL is SQLite-flavored (AUTOINCREMENT); the real migration should follow Cinder\u0027s SQLAlchemy/alembic soft-delete conventions. Also, UNIQUE(pool_name) is what forces the restore-the-soft-deleted-row dance on re-disable and erases the audit trail of repeated disable events — consider UNIQUE(pool_name, deleted) instead.","commit_id":"faec9e23cf9923def9d312512b434292658fe0e5"},{"author":{"_account_id":5997,"name":"Walt","display_name":"Hemna","email":"waboring@hemna.com","username":"walter-boring","status":"SAP"},"change_message_id":"4aa6b0167153cf9fa8db9817ca99733c5f71163f","unresolved":false,"context_lines":[{"line_number":248,"context_line":""},{"line_number":249,"context_line":"    GET /scheduler-stats/get_pools?name\u003dhost%40backend%23pool2\u0026detail\u003dtrue"},{"line_number":250,"context_line":""},{"line_number":251,"context_line":"**2. New endpoint: PUT /pool-states/down**"},{"line_number":252,"context_line":""},{"line_number":253,"context_line":"Mark a pool as disabled. Admin-only."},{"line_number":254,"context_line":""}],"source_content_type":"text/x-rst","patch_set":1,"id":"841865c4_19316a98","line":251,"updated":"2026-07-24 20:03:16.000000000","message":"Done. Renamed to PUT /pool-states/disable and PUT /pool-states/enable, matching the clusters API vocabulary. Added a GET /pool-states endpoint that returns all currently disabled pools, so read and write live on the same resource.","commit_id":"faec9e23cf9923def9d312512b434292658fe0e5"},{"author":{"_account_id":13425,"name":"Simon Dodsley","email":"simon@everpuredata.com","username":"sdodsley"},"change_message_id":"316e83c13a13aee0cc55f639e894391c0ed38449","unresolved":true,"context_lines":[{"line_number":248,"context_line":""},{"line_number":249,"context_line":"    GET /scheduler-stats/get_pools?name\u003dhost%40backend%23pool2\u0026detail\u003dtrue"},{"line_number":250,"context_line":""},{"line_number":251,"context_line":"**2. New endpoint: PUT /pool-states/down**"},{"line_number":252,"context_line":""},{"line_number":253,"context_line":"Mark a pool as disabled. Admin-only."},{"line_number":254,"context_line":""}],"source_content_type":"text/x-rst","patch_set":1,"id":"b415b72b_e2c9b216","line":251,"updated":"2026-06-25 13:39:10.000000000","message":"up/down is exactly the service-state vocabulary you\u0027re trying to disentangle from admin state, and the References section already cites PUT /clusters/disable as the precedent — match it with /disable and /enable. Separately, there\u0027s no GET on /pool-states: state is written here but read back from scheduler-stats/get_pools. A read endpoint, or folding this into a coherent /pools resource, would be cleaner than splitting write and read across two surfaces.","commit_id":"faec9e23cf9923def9d312512b434292658fe0e5"},{"author":{"_account_id":13425,"name":"Simon Dodsley","email":"simon@everpuredata.com","username":"sdodsley"},"change_message_id":"316e83c13a13aee0cc55f639e894391c0ed38449","unresolved":true,"context_lines":[{"line_number":321,"context_line":"Security impact"},{"line_number":322,"context_line":"---------------"},{"line_number":323,"context_line":""},{"line_number":324,"context_line":"No new security concerns. The new API endpoints use the existing"},{"line_number":325,"context_line":"``RULE_ADMIN_API`` policy, restricting access to cloud administrators only."},{"line_number":326,"context_line":"The disable reason is a free-form string limited to 255 characters."},{"line_number":327,"context_line":""},{"line_number":328,"context_line":""},{"line_number":329,"context_line":"Active/Active HA impact"}],"source_content_type":"text/x-rst","patch_set":1,"id":"706a973a_83f86684","line":326,"range":{"start_line":324,"start_character":0,"end_line":326,"end_character":67},"updated":"2026-06-25 13:39:10.000000000","message":"Define an explicit policy module under cinder/policies/ (with registered rule names and defaults) rather than inlining RULE_ADMIN_API. That\u0027s the current convention for new admin APIs and lets operators override the rules without code changes.","commit_id":"faec9e23cf9923def9d312512b434292658fe0e5"},{"author":{"_account_id":5997,"name":"Walt","display_name":"Hemna","email":"waboring@hemna.com","username":"walter-boring","status":"SAP"},"change_message_id":"4aa6b0167153cf9fa8db9817ca99733c5f71163f","unresolved":false,"context_lines":[{"line_number":323,"context_line":""},{"line_number":324,"context_line":"No new security concerns. The new API endpoints use the existing"},{"line_number":325,"context_line":"``RULE_ADMIN_API`` policy, restricting access to cloud administrators only."},{"line_number":326,"context_line":"The disable reason is a free-form string limited to 255 characters."},{"line_number":327,"context_line":""},{"line_number":328,"context_line":""},{"line_number":329,"context_line":"Active/Active HA impact"}],"source_content_type":"text/x-rst","patch_set":1,"id":"600c3b21_dacc640e","line":326,"updated":"2026-07-24 20:03:16.000000000","message":"Done. Added cinder/policies/pool_states.py with three registered rules: pool_states:disable, pool_states:enable, pool_states:get. Default is system_admin_api, overridable via policy.yaml.","commit_id":"faec9e23cf9923def9d312512b434292658fe0e5"},{"author":{"_account_id":5997,"name":"Walt","display_name":"Hemna","email":"waboring@hemna.com","username":"walter-boring","status":"SAP"},"change_message_id":"4aa6b0167153cf9fa8db9817ca99733c5f71163f","unresolved":false,"context_lines":[{"line_number":326,"context_line":"The disable reason is a free-form string limited to 255 characters."},{"line_number":327,"context_line":""},{"line_number":328,"context_line":""},{"line_number":329,"context_line":"Active/Active HA impact"},{"line_number":330,"context_line":"-----------------------"},{"line_number":331,"context_line":""},{"line_number":332,"context_line":"The disabled pool state is stored in the database, which is shared across all"}],"source_content_type":"text/x-rst","patch_set":1,"id":"80dd432c_3f90bf60","line":329,"updated":"2026-07-24 20:03:16.000000000","message":"Done. Added a full \"Pool Name Canonicalization (Active/Active)\" section. The key is canonicalized on service_topic_queue (cluster@backend#pool in A/A). The filter resolves each candidate\u0027s name to the same canonical form. Also defined orphan behavior: stale rows are harmless (match nothing), can be cleaned up via the enable endpoint.","commit_id":"faec9e23cf9923def9d312512b434292658fe0e5"},{"author":{"_account_id":13425,"name":"Simon Dodsley","email":"simon@everpuredata.com","username":"sdodsley"},"change_message_id":"316e83c13a13aee0cc55f639e894391c0ed38449","unresolved":true,"context_lines":[{"line_number":326,"context_line":"The disable reason is a free-form string limited to 255 characters."},{"line_number":327,"context_line":""},{"line_number":328,"context_line":""},{"line_number":329,"context_line":"Active/Active HA impact"},{"line_number":330,"context_line":"-----------------------"},{"line_number":331,"context_line":""},{"line_number":332,"context_line":"The disabled pool state is stored in the database, which is shared across all"}],"source_content_type":"text/x-rst","patch_set":1,"id":"07a0725a_712604aa","line":329,"range":{"start_line":329,"start_character":0,"end_line":329,"end_character":23},"updated":"2026-06-25 13:39:10.000000000","message":"The state-key naming won\u0027t match what the filter evaluates under A/A. get_pools (what the admin sees and what this API validates against) builds the name via append_host(backend_key, pool_name) with backend_key \u003d service.service_topic_queue → cluster@backend#pool. But the candidate the filter sees is a PoolState whose .host is append_host(service.host, pool_name) → host@backend#pool. These coincide only when service_topic_queue \u003d\u003d host (non-clustered); in A/A they diverge, so a row keyed on the get_pools name won\u0027t match the filter\u0027s candidate. Please canonicalize on the service_topic_queue-based name and state it explicitly, plus define behavior for rows orphaned by a backend/pool rename.","commit_id":"faec9e23cf9923def9d312512b434292658fe0e5"},{"author":{"_account_id":13425,"name":"Simon Dodsley","email":"simon@everpuredata.com","username":"sdodsley"},"change_message_id":"316e83c13a13aee0cc55f639e894391c0ed38449","unresolved":true,"context_lines":[{"line_number":386,"context_line":"---------------------"},{"line_number":387,"context_line":""},{"line_number":388,"context_line":"* The ``PoolDisabledFilter`` is added to ``scheduler_default_filters``. On"},{"line_number":389,"context_line":"  upgrade, the filter is active by default. Since the ``pool_disabled_states``"},{"line_number":390,"context_line":"  table starts empty, no pools are affected until an admin explicitly marks"},{"line_number":391,"context_line":"  one as disabled."},{"line_number":392,"context_line":""}],"source_content_type":"text/x-rst","patch_set":1,"id":"73625c1a_66e35266","line":389,"range":{"start_line":389,"start_character":25,"end_line":389,"end_character":42},"updated":"2026-06-25 13:39:10.000000000","message":"\"Active by default\" only holds for deployments using the default scheduler_default_filters ([\u0027AvailabilityZoneFilter\u0027, \u0027CapacityFilter\u0027, \u0027CapabilitiesFilter\u0027]). Operators who override that list won\u0027t get PoolDisabledFilter and won\u0027t enforce disable on creates unless they add it manually. Please call this out.","commit_id":"faec9e23cf9923def9d312512b434292658fe0e5"},{"author":{"_account_id":5997,"name":"Walt","display_name":"Hemna","email":"waboring@hemna.com","username":"walter-boring","status":"SAP"},"change_message_id":"4aa6b0167153cf9fa8db9817ca99733c5f71163f","unresolved":false,"context_lines":[{"line_number":386,"context_line":"---------------------"},{"line_number":387,"context_line":""},{"line_number":388,"context_line":"* The ``PoolDisabledFilter`` is added to ``scheduler_default_filters``. On"},{"line_number":389,"context_line":"  upgrade, the filter is active by default. Since the ``pool_disabled_states``"},{"line_number":390,"context_line":"  table starts empty, no pools are affected until an admin explicitly marks"},{"line_number":391,"context_line":"  one as disabled."},{"line_number":392,"context_line":""}],"source_content_type":"text/x-rst","patch_set":1,"id":"95fe5d4f_6a9e68d3","line":389,"updated":"2026-07-24 20:03:16.000000000","message":"Done. Added a .. note:: block in both the filter behavior section and the deployer impact section explicitly calling out that operators who override scheduler_default_filters won\u0027t get PoolDisabledFilter automatically and must add it manually.","commit_id":"faec9e23cf9923def9d312512b434292658fe0e5"},{"author":{"_account_id":5997,"name":"Walt","display_name":"Hemna","email":"waboring@hemna.com","username":"walter-boring","status":"SAP"},"change_message_id":"4aa6b0167153cf9fa8db9817ca99733c5f71163f","unresolved":false,"context_lines":[{"line_number":418,"context_line":"* Add database migration for ``pool_disabled_states`` table"},{"line_number":419,"context_line":"* Create ``PoolDisabledState`` Oslo Versioned Object and DB API methods"},{"line_number":420,"context_line":"* Implement ``PoolDisabledFilter`` scheduler filter"},{"line_number":421,"context_line":"* Register filter in ``pyproject.toml`` entry points"},{"line_number":422,"context_line":"* Add filter to ``scheduler_default_filters``"},{"line_number":423,"context_line":"* Create ``/pool-states`` API controller, schema, view builder, and policy"},{"line_number":424,"context_line":"* Register routes in ``cinder/api/v3/router.py``"}],"source_content_type":"text/x-rst","patch_set":1,"id":"fd1ed1cb_58ea3039","line":421,"updated":"2026-07-24 20:03:16.000000000","message":"Done. Removed the entry-points work item. The filter work item now notes it lives under cinder/scheduler/filters/ and is discovered by class name via BackendFilterHandler.","commit_id":"faec9e23cf9923def9d312512b434292658fe0e5"},{"author":{"_account_id":13425,"name":"Simon Dodsley","email":"simon@everpuredata.com","username":"sdodsley"},"change_message_id":"316e83c13a13aee0cc55f639e894391c0ed38449","unresolved":true,"context_lines":[{"line_number":418,"context_line":"* Add database migration for ``pool_disabled_states`` table"},{"line_number":419,"context_line":"* Create ``PoolDisabledState`` Oslo Versioned Object and DB API methods"},{"line_number":420,"context_line":"* Implement ``PoolDisabledFilter`` scheduler filter"},{"line_number":421,"context_line":"* Register filter in ``pyproject.toml`` entry points"},{"line_number":422,"context_line":"* Add filter to ``scheduler_default_filters``"},{"line_number":423,"context_line":"* Create ``/pool-states`` API controller, schema, view builder, and policy"},{"line_number":424,"context_line":"* Register routes in ``cinder/api/v3/router.py``"}],"source_content_type":"text/x-rst","patch_set":1,"id":"46b78a27_41ff3df2","line":421,"range":{"start_line":421,"start_character":2,"end_line":421,"end_character":52},"updated":"2026-06-25 13:39:10.000000000","message":"Filters aren\u0027t entry-point registered. HostManager discovers them via BackendFilterHandler(\u0027cinder.scheduler.filters\u0027).get_all_classes() and selects by cls.__name__ in _choose_backend_filters. The filter just needs to live under cinder/scheduler/filters/ and be named in the default list — drop this work item.","commit_id":"faec9e23cf9923def9d312512b434292658fe0e5"}],"specs/2026.2/pool-disable-draining.rst":[{"author":{"_account_id":13425,"name":"Simon Dodsley","email":"simon@everpuredata.com","username":"sdodsley"},"change_message_id":"1b845aff4eb59fd3c84fe0024043a722295eb85c","unresolved":true,"context_lines":[{"line_number":131,"context_line":"   Operators who override ``scheduler_default_filters`` in their configuration"},{"line_number":132,"context_line":"   will **not** automatically get ``PoolDisabledFilter``. Such operators must"},{"line_number":133,"context_line":"   add it to their custom filter list manually to enforce pool disable on"},{"line_number":134,"context_line":"   create operations. The API pre-checks for extend/snapshot and the"},{"line_number":135,"context_line":"   ``backend_passes_filters()`` path remain effective regardless of filter"},{"line_number":136,"context_line":"   configuration, as those use the filter directly."},{"line_number":137,"context_line":""},{"line_number":138,"context_line":"Pool Name Canonicalization (Active/Active)"},{"line_number":139,"context_line":"-------------------------------------------"}],"source_content_type":"text/x-rst","patch_set":2,"id":"9900c0cd_a9575209","line":136,"range":{"start_line":134,"start_character":22,"end_line":136,"end_character":51},"updated":"2026-09-17 16:49:29.000000000","message":"**Blocker**\n``backend_passes_filters()`` does not use the filter directly. It calls ``_get_weighted_candidates()``, which calls ``host_manager.get_filtered_backends(backends, filter_properties)``, which uses ``self.enabled_filters`` — built by ``_choose_backend_filters(CONF.scheduler_default_filters)``. So an operator who overrides ``scheduler_default_filters`` and omits ``PoolDisabledFilter`` loses all scheduler-side enforcement: creates, retype, migrate-destination, manage_existing, manage_existing_snapshot and group capacity validation. Only the API pre-check for extend/snapshot survives, because that one is a direct DB lookup.\n\nThat makes the Use Cases bullet (\"prevent all new provisioning ... on a pool being drained\") conditional on operator config.","commit_id":"f8e31d8ab7c57e1fbd309d49197032cab15c8153"},{"author":{"_account_id":13425,"name":"Simon Dodsley","email":"simon@everpuredata.com","username":"sdodsley"},"change_message_id":"1b845aff4eb59fd3c84fe0024043a722295eb85c","unresolved":true,"context_lines":[{"line_number":135,"context_line":"   ``backend_passes_filters()`` path remain effective regardless of filter"},{"line_number":136,"context_line":"   configuration, as those use the filter directly."},{"line_number":137,"context_line":""},{"line_number":138,"context_line":"Pool Name Canonicalization (Active/Active)"},{"line_number":139,"context_line":"-------------------------------------------"},{"line_number":140,"context_line":""},{"line_number":141,"context_line":"The pool name used as the key in the ``pool_disabled_states`` table is"},{"line_number":142,"context_line":"canonicalized on the ``service_topic_queue``-based name (i.e.,"},{"line_number":143,"context_line":"``cluster@backend#pool`` in clustered / Active/Active deployments, or"},{"line_number":144,"context_line":"``host@backend#pool`` in non-clustered deployments)."},{"line_number":145,"context_line":""},{"line_number":146,"context_line":"This matches the name reported by ``GET /scheduler-stats/get_pools``, which"},{"line_number":147,"context_line":"builds pool names via ``append_host(backend_key, pool_name)`` where"},{"line_number":148,"context_line":"``backend_key`` is derived from ``service.service_topic_queue``."},{"line_number":149,"context_line":""},{"line_number":150,"context_line":"The ``PoolDisabledFilter`` resolves each candidate\u0027s pool name to the same"},{"line_number":151,"context_line":"``service_topic_queue``-based canonical form before checking the disabled set,"},{"line_number":152,"context_line":"ensuring consistent matching regardless of whether the candidate PoolState\u0027s"},{"line_number":153,"context_line":"``.host`` uses the per-node hostname or the cluster name."},{"line_number":154,"context_line":""},{"line_number":155,"context_line":"**Behavior for orphaned rows:** If a backend or pool is renamed or removed,"},{"line_number":156,"context_line":"rows in ``pool_disabled_states`` referencing the old name become orphaned (they"},{"line_number":157,"context_line":"match no active pool). These rows are harmless -- they block nothing since no"},{"line_number":158,"context_line":"pool reports the old name. Administrators can clean them up by calling the"},{"line_number":159,"context_line":"enable endpoint for the old name, or they can be left in place. A future"},{"line_number":160,"context_line":"enhancement could add a periodic cleanup task, but it is not required for"},{"line_number":161,"context_line":"correctness."},{"line_number":162,"context_line":""},{"line_number":163,"context_line":"Data Flow: How the Scheduler Learns Pool State"},{"line_number":164,"context_line":"------------------------------------------------"}],"source_content_type":"text/x-rst","patch_set":2,"id":"8f43787b_ba434bfd","line":161,"range":{"start_line":138,"start_character":0,"end_line":161,"end_character":12},"updated":"2026-09-17 16:49:29.000000000","message":"**canonicalization — delete most of it.** ``PoolState.__init__`` already sets ``new_cluster \u003d append_host(cluster_name, pool_name)``, and ``BackendState.backend_id`` returns ``cluster_name or host. get_pools()`` keys on ``append_host(service.service_topic_queue, pool.pool_name)``, and ``service_topic_queue`` is ``cluster_name or host``. Those are the same string. So the filter needs no \"resolution\" logic — it compares ``backend_state.backend_id``. Precedent: ``filter_scheduler._schedule()`` already matches ``backend.obj.backend_id`` against ``volume.resource_backend`` (which is an alias of ``service_topic_queue``) for exactly this reason. One sentence replaces the section; keep the orphaned-rows paragraph.","commit_id":"f8e31d8ab7c57e1fbd309d49197032cab15c8153"},{"author":{"_account_id":13425,"name":"Simon Dodsley","email":"simon@everpuredata.com","username":"sdodsley"},"change_message_id":"1b845aff4eb59fd3c84fe0024043a722295eb85c","unresolved":true,"context_lines":[{"line_number":160,"context_line":"enhancement could add a periodic cleanup task, but it is not required for"},{"line_number":161,"context_line":"correctness."},{"line_number":162,"context_line":""},{"line_number":163,"context_line":"Data Flow: How the Scheduler Learns Pool State"},{"line_number":164,"context_line":"------------------------------------------------"},{"line_number":165,"context_line":""},{"line_number":166,"context_line":"The ``PoolDisabledFilter`` queries the ``pool_disabled_states`` database table"}],"source_content_type":"text/x-rst","patch_set":2,"id":"eeabe23d_2f89824b","line":163,"updated":"2026-09-17 16:49:29.000000000","message":"``BaseFilterHandler`` invokes ``_filter_one()`` per candidate, so a DB read inside ``backend_passes()`` is one query per pool per pass, not one per pass — that is precisely why ``AffinityFilter`` is a known cost, so citing it as precedent cuts against you. Commit to fetching once per pass: override ``filter_all()``, or memoize on ``filter_properties``. (Moot if you take the ``get_all_backend_states()`` route above.)","commit_id":"f8e31d8ab7c57e1fbd309d49197032cab15c8153"},{"author":{"_account_id":13425,"name":"Simon Dodsley","email":"simon@everpuredata.com","username":"sdodsley"},"change_message_id":"1b845aff4eb59fd3c84fe0024043a722295eb85c","unresolved":true,"context_lines":[{"line_number":218,"context_line":"Data model impact"},{"line_number":219,"context_line":"-----------------"},{"line_number":220,"context_line":""},{"line_number":221,"context_line":"A new table ``pool_disabled_states`` is introduced. The illustrative DDL below"},{"line_number":222,"context_line":"shows the logical schema; the actual implementation uses a SQLAlchemy model"},{"line_number":223,"context_line":"with an alembic migration following Cinder\u0027s standard soft-delete conventions:"},{"line_number":224,"context_line":""},{"line_number":225,"context_line":".. code-block:: sql"},{"line_number":226,"context_line":""}],"source_content_type":"text/x-rst","patch_set":2,"id":"a8c4e78e_8f9457ed","line":223,"range":{"start_line":221,"start_character":0,"end_line":223,"end_character":78},"updated":"2026-09-17 16:49:29.000000000","message":"**my PS1 suggestion needs a caveat I didn\u0027t give you.** ``UNIQUE(pool_name, deleted)`` only buys the audit trail if ``deleted`` is id-valued. ``CinderBase`` declares ``deleted \u003d Column(Boolean, default\u003dFalse)`` with ``delete_values()`` returning ``deleted\u003dTrue``, so under the base class only one soft-deleted row per pool can exist and the second enable after a re-disable violates the constraint. To get what the text now claims, the model must explicitly override ``deleted \u003d Column(Integer, default\u003d0)`` and ``delete_values()`` — there is precedent (``VolumeTypeProjects``, ``GroupTypeProjects`` both use an integer ``deleted`` inside a unique constraint), but it is a deliberate departure from ``CinderBase``, not \"Cinder\u0027s standard soft-delete conventions\". Either say that explicitly, or drop the audit-trail goal and update one row in place. Relatedly, ``disabled BOOLEAN NOT NULL DEFAULT TRUE`` is dead weight: under the sparse model, row existence is the disabled state, so the column can never be false.","commit_id":"f8e31d8ab7c57e1fbd309d49197032cab15c8153"},{"author":{"_account_id":13425,"name":"Simon Dodsley","email":"simon@everpuredata.com","username":"sdodsley"},"change_message_id":"1b845aff4eb59fd3c84fe0024043a722295eb85c","unresolved":true,"context_lines":[{"line_number":259,"context_line":"REST API impact"},{"line_number":260,"context_line":"---------------"},{"line_number":261,"context_line":""},{"line_number":262,"context_line":"All changes are gated by a new microversion (e.g., ``3.72``; the actual"},{"line_number":263,"context_line":"version number will be determined at merge time)."},{"line_number":264,"context_line":""},{"line_number":265,"context_line":"**1. Amended existing endpoint: GET /scheduler-stats/get_pools**"}],"source_content_type":"text/x-rst","patch_set":2,"id":"6165274c_a77180a2","line":262,"range":{"start_line":262,"start_character":53,"end_line":262,"end_character":57},"updated":"2026-09-17 16:49:29.000000000","message":"3.72 already used in master - just say \"next available microversion\"","commit_id":"f8e31d8ab7c57e1fbd309d49197032cab15c8153"},{"author":{"_account_id":13425,"name":"Simon Dodsley","email":"simon@everpuredata.com","username":"sdodsley"},"change_message_id":"1b845aff4eb59fd3c84fe0024043a722295eb85c","unresolved":true,"context_lines":[{"line_number":262,"context_line":"All changes are gated by a new microversion (e.g., ``3.72``; the actual"},{"line_number":263,"context_line":"version number will be determined at merge time)."},{"line_number":264,"context_line":""},{"line_number":265,"context_line":"**1. Amended existing endpoint: GET /scheduler-stats/get_pools**"},{"line_number":266,"context_line":""},{"line_number":267,"context_line":"When the request microversion is \u003e\u003d the new microversion, the response for"},{"line_number":268,"context_line":"each pool includes two additional fields:"},{"line_number":269,"context_line":""},{"line_number":270,"context_line":".. code-block:: json"},{"line_number":271,"context_line":""},{"line_number":272,"context_line":"    {"},{"line_number":273,"context_line":"        \"pools\": ["},{"line_number":274,"context_line":"            {"},{"line_number":275,"context_line":"                \"name\": \"host@backend#pool1\","},{"line_number":276,"context_line":"                \"capabilities\": {},"},{"line_number":277,"context_line":"                \"disabled\": false,"},{"line_number":278,"context_line":"                \"disabled_reason\": null"},{"line_number":279,"context_line":"            },"},{"line_number":280,"context_line":"            {"},{"line_number":281,"context_line":"                \"name\": \"host@backend#pool2\","},{"line_number":282,"context_line":"                \"capabilities\": {},"},{"line_number":283,"context_line":"                \"disabled\": true,"},{"line_number":284,"context_line":"                \"disabled_reason\": \"decommissioning rack 5\""},{"line_number":285,"context_line":"            }"},{"line_number":286,"context_line":"        ]"},{"line_number":287,"context_line":"    }"},{"line_number":288,"context_line":""},{"line_number":289,"context_line":"For requests below the new microversion, the response is unchanged."},{"line_number":290,"context_line":""},{"line_number":291,"context_line":"Individual pool state can be queried by filtering with the ``name`` query"},{"line_number":292,"context_line":"parameter:"},{"line_number":293,"context_line":""},{"line_number":294,"context_line":".. code-block::"},{"line_number":295,"context_line":""},{"line_number":296,"context_line":"    GET /scheduler-stats/get_pools?name\u003dhost%40backend%23pool2\u0026detail\u003dtrue"},{"line_number":297,"context_line":""},{"line_number":298,"context_line":"**2. New endpoint: PUT /pool-states/disable**"},{"line_number":299,"context_line":""}],"source_content_type":"text/x-rst","patch_set":2,"id":"8080a11c_1383ef2c","line":296,"range":{"start_line":265,"start_character":0,"end_line":296,"end_character":74},"updated":"2026-09-17 16:49:29.000000000","message":"The view builder receives only ``{name, capabilities}`` dicts from the scheduler RPC, so amending the view builder isn\u0027t sufficient on its own — say where the state comes from. API-side DB read is the better answer (keeps your \"no RPC changes\" property); scheduler-side means the ``get_pools`` RPC response shape changes and the API must tolerate an older scheduler omitting the keys during a rolling upgrade. Also note the summary view (``detail\u003dfalse``) returns only name today — state whether the new fields appear there too.","commit_id":"f8e31d8ab7c57e1fbd309d49197032cab15c8153"},{"author":{"_account_id":13425,"name":"Simon Dodsley","email":"simon@everpuredata.com","username":"sdodsley"},"change_message_id":"1b845aff4eb59fd3c84fe0024043a722295eb85c","unresolved":true,"context_lines":[{"line_number":320,"context_line":"        }"},{"line_number":321,"context_line":"    }"},{"line_number":322,"context_line":""},{"line_number":323,"context_line":"If the pool name does not correspond to a known pool reported by the"},{"line_number":324,"context_line":"scheduler, the request returns HTTP 404."},{"line_number":325,"context_line":""},{"line_number":326,"context_line":"**3. New endpoint: PUT /pool-states/enable**"},{"line_number":327,"context_line":""}],"source_content_type":"text/x-rst","patch_set":2,"id":"55f79d2d_3e768ba5","line":324,"range":{"start_line":323,"start_character":0,"end_line":324,"end_character":40},"updated":"2026-09-17 16:49:29.000000000","message":"Validating the pool name requires the API to call ``scheduler_rpcapi.get_pools()``; worth saying so. Consequence worth a line: a pool whose service is down or disabled is absent from the scheduler\u0027s state map, so you cannot pre-disable it. Enable-as-no-op-200 keeps orphan cleanup working, so the two are consistent — just make the validation source explicit.","commit_id":"f8e31d8ab7c57e1fbd309d49197032cab15c8153"},{"author":{"_account_id":13425,"name":"Simon Dodsley","email":"simon@everpuredata.com","username":"sdodsley"},"change_message_id":"1b845aff4eb59fd3c84fe0024043a722295eb85c","unresolved":true,"context_lines":[{"line_number":364,"context_line":"                \"pool_name\": \"host@backend#pool2\","},{"line_number":365,"context_line":"                \"disabled\": true,"},{"line_number":366,"context_line":"                \"disabled_reason\": \"decommissioning rack 5\","},{"line_number":367,"context_line":"                \"disabled_at\": \"2026-06-15T10:30:00Z\""},{"line_number":368,"context_line":"            }"},{"line_number":369,"context_line":"        ]"},{"line_number":370,"context_line":"    }"}],"source_content_type":"text/x-rst","patch_set":2,"id":"0fc73cb0_e5938691","line":367,"range":{"start_line":367,"start_character":0,"end_line":367,"end_character":53},"updated":"2026-09-17 16:49:29.000000000","message":"Not a column in the data model; presumably ``created_at`` of the active row. Say which.","commit_id":"f8e31d8ab7c57e1fbd309d49197032cab15c8153"},{"author":{"_account_id":13425,"name":"Simon Dodsley","email":"simon@everpuredata.com","username":"sdodsley"},"change_message_id":"1b845aff4eb59fd3c84fe0024043a722295eb85c","unresolved":true,"context_lines":[{"line_number":372,"context_line":"An optional ``pool_name`` query parameter filters to a single pool."},{"line_number":373,"context_line":""},{"line_number":374,"context_line":"This provides a dedicated read surface for disabled pool state, complementing"},{"line_number":375,"context_line":"the disable/enable write endpoints. The ``GET /scheduler-stats/get_pools``"},{"line_number":376,"context_line":"endpoint continues to include disabled state in its response as well, giving"},{"line_number":377,"context_line":"operators two access paths: the pool-states resource for admin state"},{"line_number":378,"context_line":"management, and scheduler-stats for a unified view of pool capabilities and"},{"line_number":379,"context_line":"state."},{"line_number":380,"context_line":""},{"line_number":381,"context_line":"**5. Error responses for blocked operations**"},{"line_number":382,"context_line":""}],"source_content_type":"text/x-rst","patch_set":2,"id":"67bee80e_f7518a54","line":379,"range":{"start_line":375,"start_character":35,"end_line":379,"end_character":6},"updated":"2026-09-17 16:49:29.000000000","message":"see blocker comment at 480","commit_id":"f8e31d8ab7c57e1fbd309d49197032cab15c8153"},{"author":{"_account_id":13425,"name":"Simon Dodsley","email":"simon@everpuredata.com","username":"sdodsley"},"change_message_id":"1b845aff4eb59fd3c84fe0024043a722295eb85c","unresolved":true,"context_lines":[{"line_number":393,"context_line":"    }"},{"line_number":394,"context_line":""},{"line_number":395,"context_line":""},{"line_number":396,"context_line":"Security impact"},{"line_number":397,"context_line":"---------------"},{"line_number":398,"context_line":""},{"line_number":399,"context_line":"No new security concerns. The new API endpoints are protected by a dedicated"},{"line_number":400,"context_line":"policy rule registered under ``cinder/policies/pool_states.py``, following"},{"line_number":401,"context_line":"Cinder\u0027s current convention for new admin APIs. The default rule restricts"},{"line_number":402,"context_line":"access to system-scope admin (``rule:system_admin_api``), allowing operators"},{"line_number":403,"context_line":"to override the policy without code changes via ``policy.yaml``."},{"line_number":404,"context_line":""},{"line_number":405,"context_line":"Registered policy rules:"},{"line_number":406,"context_line":""},{"line_number":407,"context_line":"* ``volume_extension:pool_states:disable`` -- mark a pool as disabled"},{"line_number":408,"context_line":"* ``volume_extension:pool_states:enable`` -- mark a pool as enabled"},{"line_number":409,"context_line":"* ``volume_extension:pool_states:get`` -- list/get disabled pool states"},{"line_number":410,"context_line":""},{"line_number":411,"context_line":"The disable reason is a free-form string limited to 255 characters."},{"line_number":412,"context_line":""},{"line_number":413,"context_line":""},{"line_number":414,"context_line":"Active/Active HA impact"}],"source_content_type":"text/x-rst","patch_set":2,"id":"e384fc77_125fcb27","line":411,"range":{"start_line":396,"start_character":0,"end_line":411,"end_character":67},"updated":"2026-09-17 16:49:29.000000000","message":"``rule:system_admin_api`` doesn\u0027t exist in ``cinder/policies/base.py``. The convention for new admin APIs is ``base.RULE_ADMIN_API (\"rule:admin_api\")`` — see ``cinder/policies/clusters.py``. Separately, the spec body names the rules ``volume_extension:pool_states:*`` while your Gerrit reply says ``pool_states:*. volume_extension:`` is the legacy contrib-extension prefix; a new resource in the v3 router should use the bare form, like ``clusters:*`` and ``workers:*``. The bare names are right — fix the body.","commit_id":"f8e31d8ab7c57e1fbd309d49197032cab15c8153"},{"author":{"_account_id":13425,"name":"Simon Dodsley","email":"simon@everpuredata.com","username":"sdodsley"},"change_message_id":"1b845aff4eb59fd3c84fe0024043a722295eb85c","unresolved":true,"context_lines":[{"line_number":477,"context_line":"Other deployer impact"},{"line_number":478,"context_line":"---------------------"},{"line_number":479,"context_line":""},{"line_number":480,"context_line":"* The ``PoolDisabledFilter`` is added to ``scheduler_default_filters``. On"},{"line_number":481,"context_line":"  upgrade, the filter is active by default. Since the ``pool_disabled_states``"},{"line_number":482,"context_line":"  table starts empty, no pools are affected until an admin explicitly marks"},{"line_number":483,"context_line":"  one as disabled."}],"source_content_type":"text/x-rst","patch_set":2,"id":"3b7d4d26_660d391d","line":480,"updated":"2026-09-17 16:49:29.000000000","message":"**Blocker**\n``host_manager._filter_pools_by_volume_type()`` calls ``get_filtered_backends()`` over the pool list. That path serves ``GET /scheduler-stats/get_pools?volume_type\u003dX`` (MV 3.35). With ``PoolDisabledFilter`` in the default list, disabled pools silently disappear from that query — contradicting both the spec\u0027s premise (\"keeping them visible for monitoring\") and the new ``disabled: true`` / ``disabled_reason`` fields you\u0027re adding to the same endpoint.\n\n**Recommendation covering both blockers:** enforce in ``host_manager.get_all_backend_states()`` instead of in a filter. That is the scheduling-side candidate source, it is a separate method from ``get_pools()``, and it mirrors exactly how service-disable is enforced today (``_update_backend_state_map()`` querying with ``disabled: False``). It is unconditional, it leaves the monitoring view untouched, and it removes the config-default change, both caveat notes, and the per-pass DB query from filter code. If you\u0027d rather keep the filter, the spec needs to say plainly that enforcement is best-effort and operator-defeatable, and that ``_filter_pools_by_volume_type()`` must skip this filter.","commit_id":"f8e31d8ab7c57e1fbd309d49197032cab15c8153"},{"author":{"_account_id":13425,"name":"Simon Dodsley","email":"simon@everpuredata.com","username":"sdodsley"},"change_message_id":"1b845aff4eb59fd3c84fe0024043a722295eb85c","unresolved":true,"context_lines":[{"line_number":527,"context_line":"* Register policy rules under ``cinder/policies/pool_states.py``"},{"line_number":528,"context_line":"* Register routes in ``cinder/api/v3/router.py``"},{"line_number":529,"context_line":"* Add new microversion constant and bump max API version"},{"line_number":530,"context_line":"* Amend ``scheduler_stats`` view builder to include disabled state fields"},{"line_number":531,"context_line":"* Add API pre-checks in ``cinder/volume/api.py`` for extend and snapshot"},{"line_number":532,"context_line":"* Add unit tests for filter, API, OVO, and DB layer"},{"line_number":533,"context_line":"* Add tempest tests for the new API endpoints"}],"source_content_type":"text/x-rst","patch_set":2,"id":"01a80295_0ad163f9","line":530,"range":{"start_line":530,"start_character":0,"end_line":530,"end_character":73},"updated":"2026-09-17 16:49:29.000000000","message":"see comments on line 265","commit_id":"f8e31d8ab7c57e1fbd309d49197032cab15c8153"},{"author":{"_account_id":13425,"name":"Simon Dodsley","email":"simon@everpuredata.com","username":"sdodsley"},"change_message_id":"1b845aff4eb59fd3c84fe0024043a722295eb85c","unresolved":true,"context_lines":[{"line_number":528,"context_line":"* Register routes in ``cinder/api/v3/router.py``"},{"line_number":529,"context_line":"* Add new microversion constant and bump max API version"},{"line_number":530,"context_line":"* Amend ``scheduler_stats`` view builder to include disabled state fields"},{"line_number":531,"context_line":"* Add API pre-checks in ``cinder/volume/api.py`` for extend and snapshot"},{"line_number":532,"context_line":"* Add unit tests for filter, API, OVO, and DB layer"},{"line_number":533,"context_line":"* Add tempest tests for the new API endpoints"},{"line_number":534,"context_line":"* Update ``python-cinderclient`` with new commands and amended pool-list"}],"source_content_type":"text/x-rst","patch_set":2,"id":"8137f57f_11cc953e","line":531,"range":{"start_line":531,"start_character":0,"end_line":531,"end_character":72},"updated":"2026-09-17 16:49:29.000000000","message":"It has the same naming problem the filter had: ``volume.host`` is the per-node form, so the pre-check must use ``volume.service_topic_queue``. Also worth one sentence saying the pre-check is deliberately only for extend/snapshot and that retype/migrate/manage fail later via ``NoValidBackend``.","commit_id":"f8e31d8ab7c57e1fbd309d49197032cab15c8153"},{"author":{"_account_id":13425,"name":"Simon Dodsley","email":"simon@everpuredata.com","username":"sdodsley"},"change_message_id":"1b845aff4eb59fd3c84fe0024043a722295eb85c","unresolved":true,"context_lines":[{"line_number":556,"context_line":"  confirming that creates, extends, snapshots, retypes, migrates,"},{"line_number":557,"context_line":"  manage_existing, and group-create are blocked."},{"line_number":558,"context_line":""},{"line_number":559,"context_line":"* **Tempest tests**: Integration tests exercising the new API endpoints and"},{"line_number":560,"context_line":"  verifying scheduler behavior with disabled pools."},{"line_number":561,"context_line":""},{"line_number":562,"context_line":""}],"source_content_type":"text/x-rst","patch_set":2,"id":"93f4e8e2_4bb98063","line":559,"range":{"start_line":559,"start_character":4,"end_line":559,"end_character":17},"updated":"2026-09-17 16:49:29.000000000","message":"\"Tempest tests\" should say ``cinder-tempest-plugin``, and the A/A name-matching case needs a clustered fixture to be meaningful.","commit_id":"f8e31d8ab7c57e1fbd309d49197032cab15c8153"}]}
