OpenKeel: the sovereign French Cloud solution of the inherent group, with a European reach Discover inherent

Reverse-spec · Services

masakari — Rewrite pack

Rewrite pack — masakari — branch stable/2026.2

Branch stable/2026.2 @ ae41528e1ebc — repository https://opendev.org/openstack/masakari — analysed on 2026-10-28

Clients read for § 4: masakari-monitors stable/2026.2 @ b792a7ae040a (prefix masakari-monitors:), openstacksdk stable/2026.2 @ fbdc40381 (prefix openstacksdk:), python-masakariclient stable/2026.2 @ 073cd2c (prefix python-masakariclient:).

1. Contents of _rewrite/

FileUse in a rewrite
[routes_stable-2026.2.json](_rewrite/routes_stable-2026.2.json)the 21 implemented operations (including GET / and /v1/ aliases), handler file:line, policy rule
[openapi_stable-2026.2.json](_rewrite/openapi_stable-2026.2.json)OpenAPI 3.0.3. It merges the api-ref parameters, the request schemas, x-schema-selection (schema per microversion), x-policy and the response schemas inferred from the scenarios. x-notes lists what OpenAPI cannot say (project-id aliases, .json suffix, 405 routes). It is not the authority on behaviour
[request-schemas_stable-2026.2.json](_rewrite/request-schemas_stable-2026.2.json), [schema-selection_stable-2026.2.json](_rewrite/schema-selection_stable-2026.2.json)the 10 JSON schemas, and which one applies for which microversion range or notification type
[api-ref_stable-2026.2.json](_rewrite/api-ref_stable-2026.2.json)the 17 documented operations, with parameters and samples
[response-schemas_stable-2026.2.json](_rewrite/response-schemas_stable-2026.2.json) / [.md](_rewrite/response-schemas_stable-2026.2.md)response JSON schemas per route, status and microversion, inferred from the 201 scenario exchanges
[observed-errors_stable-2026.2.md](_rewrite/observed-errors_stable-2026.2.md)every (operation, status, fault name, message) seen; messages that clients may match
[exchanges_stable-2026.2.json](_rewrite/exchanges_stable-2026.2.json)the 201 HTTP exchanges of the 23 scenarios, with their expected and actual outcomes: the reference for differential testing
[nova-calls_stable-2026.2.json](_rewrite/nova-calls_stable-2026.2.json)the 143 requests Masakari sent to Nova (path, query, body, microversion), per scenario: the recovery algorithm as observable behaviour
[rpc-frames_stable-2026.2.json](_rewrite/rpc-frames_stable-2026.2.json)the 29 RPC payloads (cast and call) as serialised for the bus, and the exceptions the engine raised in cast handlers
[schema-mysql_stable-2026.2.sql](_rewrite/schema-mysql_stable-2026.2.sql), [schema-postgresql_stable-2026.2.sql](_rewrite/schema-postgresql_stable-2026.2.sql)DDL generated from the models
[schema-sqlite-migrated_stable-2026.2.sql](_rewrite/schema-sqlite-migrated_stable-2026.2.sql), [schema-diff_stable-2026.2.md](_rewrite/schema-diff_stable-2026.2.md)schema after db sync; models and migrations are identical
[schema-sqlite-taskflow_stable-2026.2.sql](_rewrite/schema-sqlite-taskflow_stable-2026.2.sql)the taskflow persistence schema (second database)
[policies_stable-2026.2.json](_rewrite/policies_stable-2026.2.json), [policies_stable-2026.2.yaml](_rewrite/policies_stable-2026.2.yaml)27 default rules (oslopolicy-sample-generator)
[config_stable-2026.2.conf](_rewrite/config_stable-2026.2.conf)masakari.conf sample (oslo-config-generator, namespace masakari.conf and the oslo libraries). The customized_recovery_flow_opts namespace ([taskflow_driver_recovery_flows]) is generated by the separate etc/masakari/masakari-customized-recovery-flow-config-generator.conf, and its defaults are in § 3.2
[tools/](_rewrite/tools/)masakari_routes.py (route table from the live mapper), adapter_masakari.py (scenarios for conformance_capture.py scenarios), masakari_postprocess.py (policies, schema selection, error table, OpenAPI merge, schema diff)

Extraction: Python 3.12.7, upper-constraints stable/2026.2 (taskflow 6.4.0, oslo.db 18.1.1, python-novaclient 18.13.1). psycopg2 was left out of the test requirements because it does not build without pg_config; it is not needed.

2. HTTP behaviour that the contracts do not show

2.1 Middleware pipeline (outermost first)

cors → http_proxy_to_wsgi → request_id → faultwrap → sizelimit → authtoken → keystonecontext → router (etc/masakari/api-paste.ini). Consequences:

  • An invalid token is a keystonemiddleware 401 (plain text), before any routing: an unknown path with a bad token is 401, not 404.
  • MasakariKeystoneContext answers a 401 without a JSON body when no user id is present (masakari/api/auth.py:90).
  • Microversion parsing happens in the router's Resource (masakari/api/openstack/wsgi.py:178), so a bad version header on an unknown path is 404.
  • GET / is served without authentication. GET /v1 requires it.

2.2 Error body

{"<faultName>": {"code": <int>, "message": "<text>"}} with the fault names of masakari/api/openstack/wsgi.py:1006, Content-Type: application/json; charset=UTF-8, plus OpenStack-API-Version and Vary when a version was parsed. 401/404 from the middlewares are webob plain-text pages. Messages are gettext-translated per Accept-Language, so clients must not parse them; masakari-monitors only looks at the status (§ 4).

2.3 Other cross-cutting rules

  • 405 badMethod for every route that Routes' resource() generates but the controller does not implement (PUT/DELETE on notifications, POST/PUT/DELETE on vmoves and extensions, new, edit, action). 404 for paths outside the mapper.
  • /v1/{project_id}/… aliases and the .json/.xml suffix are accepted everywhere, and the project id is ignored.
  • The id (integer primary key) is exposed in every body and is the pagination marker.
  • deleted, deleted_at, created_at and updated_at are in every body. Timestamps are YYYY-MM-DDTHH:MM:SS.000000 (microseconds zeroed by the models), except vmove start_time/end_time.
  • Responses never carry links/next.
  • Booleans in bodies accept 22 spellings (§ 01). The response always carries JSON booleans.
  • enabled is returned on segments at every microversion, but only accepted in requests from 1.2.
  • vmoves are served at every microversion although documented as 1.3.

3. The recovery engine: formal specification

3.1 Input model and validation

  • Notification types and payloads: masakari/api/openstack/ha/schemas/payload.py:18. All payload values are strings ≤ 255 (masakari/api/validation/parameter_types.py:166).
  • The handled VM event pairs: masakari/engine/instance_events.py:22.
  • Host flags: reserved, on_maintenance; segment: recovery_method ∈ {auto, reserved_host, auto_priority, rh_priority}, enabled.

3.2 Configuration that changes the algorithm

OptionDefaultEffect
[DEFAULT]/duplicate_notification_detection_interval180duplicate window (API)
[DEFAULT]/notification_delay_max5random sleep before each POST /notifications when [coordination]/backend_url is unset
[DEFAULT]/wait_period_after_service_update180sleep after disabling a compute service; timeout of the "disabled" poll
[DEFAULT]/wait_period_after_evacuation90evacuation confirmation timeout
[DEFAULT]/wait_period_after_power_off / wait_period_after_power_on180 / 60stop / start confirmation timeouts
[DEFAULT]/verify_interval1polling interval of all confirmations
[DEFAULT]/host_failure_recovery_threads3parallel evacuations per notification
[DEFAULT]/process_unfinished_notifications_interval / retry_notification_new_status_interval120 / 60retry loop
[DEFAULT]/check_expired_notifications_interval / notifications_expired_interval600 / 86400expiry loop
[host_failure]/evacuate_all_instancestrueevacuate non-HA instances too
[host_failure]/ha_enabled_instance_metadata_keyHA_Enabledmetadata key, read with bool_from_string
[host_failure]/ignore_instances_in_error_statefalseskip error instances
[host_failure]/add_reserved_host_to_aggregatefalsecopy the failed host's aggregates to the reserved host
[host_failure]/service_disable_reason, [process_failure]/service_disable_reason"Masakari detected host failed." / "…process failed."Nova disabled_reason
[instance_failure]/process_all_instances, ha_enabled_instance_metadata_keyfalse, HA_EnabledVM recovery scope
[taskflow_driver_recovery_flows]/*_recovery_taskssee masakari/conf/engine_driver.py:128the task lists (stevedore names from setup.cfg masakari.task_flow.tasks; third-party tasks may be plugged in)

3.3 Algorithm (pseudo-code)

on POST /notifications(n):                                  # masakari/ha/api.py:348
  if not coordination: sleep(uniform(0, notification_delay_max))
  with lock("create_host_notification-"+n.hostname) if n.type == COMPUTE_HOST and coordination:
    h = host_by_name(n.hostname) or 400                     # masakari/ha/api.py:301
    if h.on_maintenance: 409
    if exists m: m.type==n.type and m.source_host==h and m.generated_time >= n.generated_time - window
                 and m.payload == n.payload (any status): 409   # masakari/ha/api.py:277
    insert n(status=new, failover_segment_uuid=h.segment); cast process_notification(n); 202

process_notification(n):                                    # masakari/engine/manager.py:335
  if not segment(n).enabled: n.status=ignored; return
  with local_lock(n.source_host_uuid):                     # masakari/engine/manager.py:274
    if n.status==new and db(n).status!=new: return
    n.status=running
    n.status = {PROCESS: process(n), VM: instance(n), COMPUTE_HOST: host(n)}[n.type]

host(n):                                                    # masakari/engine/manager.py:174
  if n.payload.host_status is None or upper != NORMAL: return ignored
  if event == STARTED: return finished
  h.on_maintenance = true; if h.reserved: h.reserved = false
  R = [x.name for x in hosts(segment, reserved, not on_maintenance) order by created_at desc, id desc] if method != auto
  try: run(method)  ->  finished ; except Skip: finished ; except Failure: error
     auto            = flow(pre, main, post of host_auto_failure_recovery_tasks)
     reserved_host   = R empty ? ReservedHostsUnavailable : flow_rh(R)    # main retried per R[i]
     auto_priority   = try auto except (not Skip) -> reserved_host
     rh_priority     = try reserved_host except (not Skip) -> auto

DisableComputeService(H): svc=GET os-services(host=H, binary=nova-compute)[0]; PUT svc {disabled, reason}; sleep(180)
PrepareHAEnabledInstances(H): S = GET servers/detail?all_tenants&host=H (1 page)
     ha = [s | bool(s.metadata[key])] minus error ones if ignore_error ; rest likewise
     L = ha + (rest if evacuate_all) ; if L empty: raise Skip
     insert vmove(pending, evacuation) for s in L                          # masakari/engine/drivers/taskflow/host_failure.py:136
Evacuate(H, R?):                                            # masakari/engine/drivers/taskflow/host_failure.py:349
  V = vmoves(n, status=pending) order by created_at desc, id desc   # => last inserted first
  if R: lock_nonblocking(R) else LockAlreadyAcquired; [aggregates]; enable R; host R.reserved=false
  pool(3).each v in V: evacuate_and_confirm(v, R)
  if any vmove(n) failed or any not in (succeeded, failed): raise HostRecoveryFailure   # counts ALL attempts

evacuate_and_confirm(v, R):                                 # masakari/engine/drivers/taskflow/host_failure.py:193
  s=GET server; locked=GET server.locked; if not locked: lock
  st=s.vm_state; stop_after=true
  if st not in (active,error,stopped): resetState(error); s=GET; if st==resized and power!=4: stop_after=false
  elif st==stopped and task_state is None: stop_after=false
  elif task_state is not None: resetState(error); s=GET; if st==active: stop_after=false
  v.ongoing; POST evacuate(host=R)
  poll(1s, 90s): s'=GET; if s'.vm_state==error != st: raise ; if host(s')!=H and (st==error and s'==active or s'.vm_state==st): done
  if st != active and stop_after: os-stop; poll until stopped (180s); if st==error: resetState(error)
  v.succeeded(dest=host(GET)) ; on timeout: v.failed("Failed reason: timeout.") ; on exception e: v.failed(str(e))
  finally: v.end_time; if not locked: unlock (errors logged)

instance(n):                                                # masakari/engine/manager.py:130
  if (event, vir_domain_event) not in INSTANCE_EVENTS: return ignored
  s=GET; if not HA and not process_all: finished
  if s.vm_state in (paused, rescued): ignored
  if not stopped: (resized -> resetState(active)); os-stop (409 ok if stopped); poll stopped (180s) else error
  if GET.vm_state != stopped: error ; os-start ; poll active (60s) else error ; finished

process(n):                                                 # masakari/engine/manager.py:67
  if event==STARTED: finished
  if event==STOPPED: h.on_maintenance=true
     if process_name != nova-compute: finished
     if not disabled(H): PUT os-services {disabled, reason}; poll disabled (180s) else error ; finished
  else ignored

3.4 Ordering and determinism

  • The vmove order (last inserted first, so non-HA before HA by default) is visible in Nova's evacuation order and in the progress messages. A rewrite that "fixes" it should say so.
  • The evacuations run in a green thread pool, but with the fakes the call sequence was identical across two runs (143 Nova calls, same order). With a real Nova, network latency interleaves the threads. Compare the sets of calls per instance, not the global order.
  • recovery_workflow_details order comes from the configured task lists, not from execution time.

3.5 Recorded behaviour of the algorithm

[nova-calls_stable-2026.2.json](_rewrite/nova-calls_stable-2026.2.json) holds, per scenario, the exact Nova requests:

  • the auto, reserved_host (with aggregates, an error VM and a resized VM) and auto_priority fallback flows;
  • the VM recovery for an HA instance, a non-HA instance, a paused instance, an ignored event pair and an IO error on a stopped instance;
  • the process recovery for nova-compute and for another process;
  • the periodic retry.

Together with the final notification status, the vmoves and the recovery_workflow_details (in exchanges), they are the test vectors of the engine. They do not encode real-time behaviour: the sleeps were set to 0 and the timeouts to 3 s.

3.6 Defects a rewrite must decide on

DefectWhereRecommendation
failed vmoves of an earlier attempt make every fallback or retry failmasakari/engine/drivers/taskflow/host_failure.py:424count only the vmoves of the current attempt, or reuse the pending/failed rows instead of inserting new ones
non-HA instances are evacuated firstmasakari/engine/drivers/taskflow/host_failure.py:350evacuate in the documented order
AUTO_PRIORITY/RH_PRIORITY compared in upper casemasakari/engine/drivers/taskflow/driver.py:319compare the stored values
GET …/vmoves/{id} 500 for an unknown notification; vmove not checked against its notificationmasakari/api/openstack/ha/vmoves.py:77, masakari/ha/api.py:436404
vmoves microversion 1.3 not enforcedmasakari/api/openstack/ha/vmoves.py:38keep it lenient: clients send 1.3 anyway (§ 4)
FailoverSegmentDisabled raised with an unformatted tuplemasakari/engine/manager.py:346—
periodic tasks run in every engine without coordination; per-host lock is per processmasakari/engine/manager.py:356, masakari/engine/manager.py:274one leader (Kubernetes Lease) or row claims (UPDATE … WHERE status='new')
a new keystone token for every Nova call, and no Nova timeoutmasakari/compute/nova.py:86cache the session; set timeouts
a PROCESS STOPPED for an unhandled process still puts the host on maintenancemasakari/engine/manager.py:90keep it for compatibility, or document it

4. What the clients expect

ExpectationWhere the client relies on it
POST /v1/notifications accepts {"notification": {type, hostname, generated_time, payload}} with no microversion header and a naive generated_time (datetime.isoformat(), no zone)masakari-monitors:masakarimonitors/ha/masakari.py:66, openstacksdk:openstack/instance_ha/v1/_proxy.py:65
400 and 409 are final: the monitor logs and drops the event; any other error (5xx, connection) is retried api_retry_max times every api_retry_interval s (host monitor: 12 × 10 s)masakari-monitors:masakarimonitors/ha/masakari.py:76, masakari-monitors:masakarimonitors/conf/host.py:32
hence 409 for duplicates and for hosts on maintenance must stay 409, and unknown hosts must stay 400, or the monitors will retry for 2 minutesmasakari/api/openstack/ha/notifications.py:79
host monitor (pacemaker/corosync): COMPUTE_HOST, event STARTED/STOPPED, cluster_status ONLINE/OFFLINE, host_status NORMAL, or UNKNOWN when IPMI cannot confirm the power-off (Masakari then ignores it)masakari-monitors:masakarimonitors/hostmonitor/host_handler/handle_host.py:272
Kubernetes host check: COMPUTE_HOST, {"event": "STOPPED", "host_status": "NORMAL"} **without cluster_status**, hostname = Kubernetes node name (so the node name must equal the Masakari host name and the Nova service host), sent when the Ready condition is not True in the last monitoring_samples samplesmasakari-monitors:masakarimonitors/hostmonitor/kubernetes_check/manager.py:88, masakari-monitors:masakarimonitors/hostmonitor/kubernetes_check/manager.py:102
process monitor: PROCESS, {"event": "STOPPED", "process_name": <basename>}, sent after local restarts failedmasakari-monitors:masakarimonitors/processmonitor/process_handler/handle_process.py:123
instance monitor: VM, {"event": <LIFECYCLE…>, "instance_uuid", "vir_domain_event": <detail>} for the libvirt events of its filter tablemasakari-monitors:masakarimonitors/instancemonitor/libvirt_handler/callback.py:61, masakari-monitors:masakarimonitors/instancemonitor/libvirt_handler/eventfilter_table.py:23
after a processed host failure the host is on_maintenance, so the next STOPPED from another monitor gets 409 and is dropped (observed in masakari-monitors-payloads)masakari/engine/manager.py:216
openstacksdk sends segment requests at microversion 1.2 (is_enabled)openstacksdk:openstack/instance_ha/v1/segment.py:36
python-masakariclient (openstack segment/host/notification …) defaults to microversion 1.3 and supports 1.0–1.3python-masakariclient:masakariclient/plugin.py:22
notification show shows notification_uuid, generated_time, status, type, source_host_uuid, payloadpython-masakariclient:masakariclient/osc/v1/notification.py:64
Masakari, as a client of Nova, needs microversion ≥ 2.53 (service UUIDs), os-services PUT by UUID, server actions evacuate (without host = scheduler choice), lock/unlock, os-resetState, os-stop/os-start, aggregates add_host, and the admin attributes OS-EXT-SRV-ATTR:hypervisor_hostname, OS-EXT-STS:*, lockedmasakari/compute/nova.py:41, [nova-calls_stable-2026.2.json](_rewrite/nova-calls_stable-2026.2.json)

5. Database strategy

  • Reuse the schema ([schema-sqlite-migrated_stable-2026.2.sql](_rewrite/schema-sqlite-migrated_stable-2026.2.sql); models = migrations). Keep the alembic table masakari_alembic_version at the head 13adff5efb9a if the Python tools must keep working on the same database.
  • Integrity kept by code: vmoves → notification, and notification → host/segment, have no foreign keys; hosts → segment uses the UUID FK. Soft delete everywhere, with deleted = id and unique keys (name, deleted).
  • Integer ids are client-visible (markers): keep AUTO_INCREMENT.
  • No locking, no optimistic versioning. Notification status transitions should become conditional updates (UPDATE … SET status='running' WHERE notification_uuid=? AND status IN ('new','error')) to make multi-engine processing safe. That is a behaviour improvement, compatible with the API.
  • The taskflow persistence database exists only to serve recovery_workflow_details. A rewrite may store the progress in its own table, as long as it returns the same {name, progress, progress_details[], state} list (state values are taskflow's: PENDING, RUNNING, SUCCESS, FAILURE, REVERTING, REVERTED).
  • db purge semantics in 05; orphan vmoves are a known gap.

6. Authentication, policies, configuration and CLI

  • keystonemiddleware auth_token (incoming), keystoneauth password (outgoing to Nova) with [keystone_authtoken]/auth_type reused as the plugin name (masakari/compute/nova.py:113).
  • oslo.policy: 27 rules, every API rule rule:admin_api → is_admin:True ← role:admin. A rewrite can implement role:admin plus the policy file override mechanism, and needs no scope logic to be equivalent.
  • oslo.config: the options of § 3.2, [database], [taskflow]/connection, [coordination]/backend_url, [oslo_messaging_*], [keystone_authtoken], [DEFAULT]/os_privileged_user_* (secret: os_privileged_user_password, [taskflow]/connection).
  • CLI to keep for deployment tools: masakari-manage db sync|version|purge, masakari-status upgrade check, masakari-api (or the WSGI module masakari/wsgi/api.py), masakari-engine.

7. Conformance strategy

  • Scenario suite: tools/adapter_masakari.py drives the real API (keystone pipeline without authtoken, SQLite, in-process RPC delivering to a real MasakariManager) with fake Keystone and Nova behind requests-mock and the real novaclient.
    • Proof: 23/23 scenarios as expected, and a second run gave the same 201 statuses and the same 143 Nova requests in the same order.
    • To check a rewrite: start it with a fake Nova that implements the same small state machine (FakeNova in the adapter, ~150 lines), replay the requests of exchanges_stable-2026.2.json scenario by scenario, and compare statuses, bodies (ignoring UUIDs and timestamps) and the Nova request sequence of nova-calls_stable-2026.2.json.
    • Engine scenarios: deliver the cast synchronously after the 202, or wait for the notification status.
  • Project tests: masakari/tests/functional/ drive a real deployment with openstacksdk (segments, hosts, process and VM notifications). They need devstack and were not run. masakari/tests/unit/engine/drivers/taskflow/ hold the per-task unit tests (mocked Nova), useful as reading material for edge cases.
  • Not covered: real Nova timing (service-down detection, evacuation duration), keystonemiddleware's token handling, tooz locking, multi-engine races, MySQL-specific behaviour (ENUM, DeleteFromSelect purge).

8. Gaps found while building the pack

  • rewrite_pack.py cannot read Masakari's routes (Routes mapper built from stevedore extensions) or its lower-case schema constants. Hence tools/masakari_routes.py and --schemas-modules. Policy operations are declared without /v1, so the post-processor maps them.
  • The api-ref documents 17 operations. It does not document the /v1/{project_id} aliases or /v1/extensions. It says vmoves are 1.3 and that markers are "the ID of the last-seen item" without saying it is the integer id.
  • conformance_capture.py's error table does not read Nova-style fault bodies, so observed-errors_stable-2026.2.md is produced by the post-processor.
  • No gabbi or Tempest-like suite runs in-process. The scenarios are the conformance base.

9. Non-functional requirements

  • API latency: POST /notifications sleeps 0–5 s by default without tooz. A rewrite with a real lock (or a unique constraint) should drop the sleep.
  • Recovery time of a host failure is at least wait_period_after_service_update (180 s) plus the evacuation (≤ 90 s per instance, 3 in parallel).
  • One periodic loop can block for many minutes (sequential retries).
  • Scale: the server list is one page (≤ 1000 per host); the periodic queries are unbounded.

10. Rewrite checklist

  • [ ] /v1 routes, the /v1/{project_id} aliases, the .json suffix and 405 on unimplemented routes
  • [ ] microversion header instance-ha, 1.0–1.3, latest, 406/400 messages
  • [ ] fault body {"<faultName>": {code, message}}, plain-text 401 from the auth layer
  • [ ] admin-only policies with file overrides
  • [ ] segment/host/notification/vmove bodies including id, deleted* and nested failover_segment
  • [ ] integer-id markers, sort keys and directions, filters, generated-since
  • [ ] Nova nova-compute check on host create and rename (400; 403 passthrough)
  • [ ] duplicate detection (any status, sender clock), maintenance 409, unknown host 400, 202 + async
  • [ ] engine: the four methods, three flows, vmoves, on_maintenance/reserved side effects, ignored cases
  • [ ] retry and expiry loops, single-instance execution (leader election)
  • [ ] recovery_workflow_details at 1.1
  • [ ] versioned notifications (optional, off by default)
  • [ ] masakari-manage db sync/purge, masakari-status upgrade check
  • [ ] decide on each defect of § 3.6

Source: reverse-spec-masakari/07-rewrite-pack_stable-2026.2.md. The _rewrite/ and _inventory/ files are not published on the site.