Interactions — masakari — branch stable/2026.2
Branch
stable/2026.2@ae41528e1ebc— repository https://opendev.org/openstack/masakari — analysed on 2026-10-28
| System | Protocol | Library | Configuration | Used by | Usage |
|---|---|---|---|---|---|
| Nova | HTTPS REST, microversion 2.53 | python-novaclient over a keystoneauth session | [DEFAULT]/os_privileged_user_*, nova_catalog_admin_info, os_region_name, nova_ca_certificates_file, nova_api_insecure (masakari/conf/nova.py:19) | API (host check), engine (recovery) | the recovery actions |
| Keystone | HTTPS REST v3 | keystoneauth1 (password plugin of [keystone_authtoken]/auth_type) | os_privileged_user_auth_url, os_user_domain_name, os_project_domain_name, os_system_scope | API, engine | service token for Nova; keystonemiddleware validates incoming tokens |
| RabbitMQ | AMQP 0-9-1 | oslo.messaging | [DEFAULT]/transport_url, [oslo_messaging_notifications] | API → engine (RPC), API and engine → listeners (notifications) | cast, call, versioned notifications |
| MariaDB/MySQL | SQL | oslo.db, SQLAlchemy | [database]/connection | API, engine | segments, hosts, notifications, vmoves |
| MariaDB/MySQL (taskflow) | SQL | taskflow persistence (SQLAlchemy backend) | [taskflow]/connection (required by the engine, masakari/engine/drivers/taskflow/driver.py:50) | engine, read by GET /notifications/{id} 1.1 through the engine | workflow logbooks and per-task progress |
| tooz backend (etcd, Redis, ZooKeeper…) | backend-specific | tooz | [coordination]/backend_url (default none, masakari/conf/coordination.py:19) | API only | distributed lock masakari-create_host_notification-<hostname> |
Outbound REST calls
Authentication
novaclient() (masakari/compute/nova.py:86) builds a new keystoneauth session for every Nova call, with password auth for os_privileged_user_name/os_privileged_user_password in project os_privileged_user_tenant (or system_scope). Each call therefore costs a POST /v3/auth/tokens [inferred: no session or token cache is kept between calls]. The Nova endpoint is the catalogue entry compute/nova/publicURL of os_region_name, unless the request context already carries a catalogue. The X-OpenStack-Request-ID of the incoming request is passed as global_request_id. No timeout is set (timeout=None, masakari/compute/nova.py:131), so a hung Nova blocks a green thread until TCP gives up.
Calls to Nova
All calls carry X-Auth-Token, X-OpenStack-Nova-API-Version: 2.53 and OpenStack-API-Version: compute 2.53. Recorded with the real novaclient in [_rewrite/nova-calls_stable-2026.2.json](_rewrite/nova-calls_stable-2026.2.json).
| Method + path | Body | When | Expected answer | Errors handled | Defined at |
|---|---|---|---|---|---|
GET /v2.1/os-services?binary=nova-compute&host=<name> | — | host create / rename (API); before every enable/disable (engine) | services[] with id (UUID at 2.53), status | empty list → ComputeNotFoundByName (API 400); in the engine [0] on an empty list → IndexError, which escapes the translation → generic MasakariException → notification error [inferred] | masakari/compute/nova.py:256, masakari/compute/nova.py:162 |
PUT /v2.1/os-services/{service_uuid} | {"status": "disabled", "disabled_reason": "Masakari detected host failed."} | DisableComputeServiceTask, DisableComputeNodeTask | 200 service | translated (masakari/compute/nova.py:52) | masakari/compute/nova.py:167 |
PUT /v2.1/os-services/{service_uuid} | {"status": "enabled"} | reserved host before evacuation | 200 | idem | masakari/compute/nova.py:172 |
GET /v2.1/servers/detail?all_tenants=true&host=<name> | — | PrepareHAEnabledInstancesTask | servers[] with metadata, OS-EXT-STS:vm_state | single page (no limit/marker loop) | masakari/compute/nova.py:147 |
GET /v2.1/servers/{id} | — | before every action, and every verify_interval while polling | server with OS-EXT-STS:vm_state, OS-EXT-STS:task_state, OS-EXT-STS:power_state, OS-EXT-SRV-ATTR:hypervisor_hostname, locked, metadata | 404 → NotFound | masakari/compute/nova.py:200 |
POST /v2.1/servers/{id}/action | {"lock": null} / {"unlock": null} | around each evacuation, if not already locked | 202 | unlock failure only logged | masakari/compute/nova.py:240, masakari/compute/nova.py:248 |
POST /v2.1/servers/{id}/action | {"os-resetState": {"state": "error"}} (or "active" for a resized VM in instance recovery) | state normalisation before evacuation; after the evacuation of an error VM | 202 | — | masakari/compute/nova.py:191 |
POST /v2.1/servers/{id}/action | {"evacuate": {}} (auto) or {"evacuate": {"host": "<reserved host>"}} | EvacuateInstancesTask | 200 | Nova 400/409 → vmove failed | masakari/compute/nova.py:182 |
POST /v2.1/servers/{id}/action | {"os-stop": null} / {"os-start": null} | instance recovery; re-stop after evacuating a stopped VM | 202 | 409 on stop tolerated if already stopped | masakari/compute/nova.py:208, masakari/compute/nova.py:216 |
GET /v2.1/os-aggregates | — | reserved host, if add_reserved_host_to_aggregate | aggregates[] with hosts | — | masakari/compute/nova.py:224 |
POST /v2.1/os-aggregates/{id}/action | {"add_host": {"host": "<reserved host>"}} | idem, for each aggregate containing the failed host | 200 | 409 ignored ("already been added") | masakari/compute/nova.py:231 |
Error translation (translate_nova_exception, masakari/compute/nova.py:52):
Timeout,CommandError, keystoneauthConnectionError,DiscoveryFailure→MasakariException;- 400 →
InvalidInput; - 403 →
Forbidden; - 404 →
NotFound; - 409 →
Conflict; - **a 5xx (
ClientException) is not translated** and escapes as a novaclient exception (observed: API 500 "…<class 'novaclient.exceptions.ClientException'>").
Sample frame (host failure, auto method), as recorded:
PUT /v2.1/os-services/9811decb-80f0-5552-99d1-1d2f6f5bea9a HTTP/1.1
X-Auth-Token: <service token>
X-OpenStack-Nova-API-Version: 2.53
Content-Type: application/json
{"status": "disabled", "disabled_reason": "Masakari detected host failed."}
POST /v2.1/servers/aa7ba2b1-b24c-5227-90dc-49925583e0f3/action HTTP/1.1
X-OpenStack-Nova-API-Version: 2.53
{"evacuate": {}}
RabbitMQ
Topology
| Exchange | Type | Routing key / queue | Producer | Consumer |
|---|---|---|---|---|
openstack (control exchange) | topic | ha_engine (shared, round-robin) and ha_engine.<host> | masakari-api (EngineAPI, masakari/engine/rpcapi.py:43) | masakari-engine (masakari/service.py:91) |
ha_engine_fanout | fanout | ha_engine_fanout_<uuid> | nobody (oslo.messaging creates it) | masakari-engine |
openstack | topic | versioned_notifications.info / .error | masakari-api, masakari-engine (masakari/rpc.py:55) | external listeners (none in Masakari) |
reply queues reply_<uuid> | direct | — | masakari-engine | masakari-api (for the call) |
RPC methods
| Method | Direction | Mode | Topic / server | Version | Arguments | Response | Defined at |
|---|---|---|---|---|---|---|---|
process_notification | api → engine | cast | ha_engine (any engine) | 1.0 | notification: Notification object 1.3 | none | masakari/engine/rpcapi.py:48 |
get_notification_recovery_workflow_details | api → engine | call (rpc_response_timeout, 60 s by default) | ha_engine | 1.1 | notification | the same Notification with recovery_workflow_details (list of NotificationProgressDetails 1.0) | masakari/engine/rpcapi.py:53 |
Objects travel as oslo.versionedobjects primitives through MasakariObjectSerializer. The request context is serialised with every message, including auth_token, service_catalog, roles, user_id, project_id, request_id and global_request_id (context_keys in [_rewrite/rpc-frames_stable-2026.2.json](_rewrite/rpc-frames_stable-2026.2.json)).
Sample frame (process_notification, oslo.messaging message body, as recorded, timestamps shortened):
{"method": "process_notification", "version": "1.0",
"args": {"notification": {
"masakari_object.name": "Notification", "masakari_object.namespace": "masakari",
"masakari_object.version": "1.3",
"masakari_object.data": {"id": 1, "notification_uuid": "7882ccb9-ebb1-43b3-848b-f2333ae4b79c",
"generated_time": "2026-10-28T10:00:00Z", "type": "PROCESS", "status": "new",
"payload": {"event": "STARTED", "process_name": "nova-compute"},
"source_host_uuid": "e3fe016c-f32c-4971-8e7f-8ca518e30f72",
"failover_segment_uuid": "44f3f05c-8e15-4fac-9e0b-994d3c1f4991", "message": null,
"created_at": "2026-10-28T22:43:57Z", "updated_at": null, "deleted": false, "deleted_at": null}}},
"_context_auth_token": "…", "_context_user_id": "…", "_context_project_id": "…",
"_context_request_id": "req-…", "_context_roles": ["admin"]}
Versioned notifications
They are emitted only when [oslo_messaging_notifications]/driver is set (the oslo default is none, which makes them no-ops). They use topic versioned_notifications (masakari/rpc.py:57), publisher_id masakari-api:<hostname> or masakari-engine:<hostname>, and priority INFO (or ERROR with a fault payload). Samples are in doc/notification_samples/.
| event_type | Emitted when | Payload | Emitted at |
|---|---|---|---|
segment.create.start / .end | around the INSERT | SegmentApiPayload (segment fields) | masakari/objects/segment.py:80 |
segment.update.start / .end, segment.delete.start / .end | around the update / soft delete | idem | masakari/objects/segment.py:104, masakari/objects/segment.py:122 |
segment.create.error (and update/delete) | the database operation failed | idem + fault (exception, function, module, message) | masakari/ha/api.py:105 |
host.create.*, host.update.*, host.delete.* | idem for hosts | HostApiPayload (with the segment) | masakari/objects/host.py:91, masakari/objects/host.py:123, masakari/objects/host.py:144 |
notification.create.start / .end / .error | around the INSERT of a notification, and an error of the insert or of the cast | NotificationApiPayload | masakari/objects/notification.py:90, masakari/ha/api.py:337 |
notification.process.start | the engine starts a notification | idem | masakari/engine/manager.py:328 |
notification.process.end / .error | at the end of the type handler | idem (+ fault, traceback) | masakari/engine/manager.py:116, masakari/engine/manager.py:257 |
Host updates made by the engine (on_maintenance, reserved) go through Host.save() and therefore also emit host.update.start/end, with publisher masakari-api (the default binary of masakari/api/utils.py:63) even though the engine sends them.
Database
Tables (main database)
| Table | Role | Written by | Read by |
|---|---|---|---|
failover_segments | segments; unique (name, deleted), (uuid); recovery_method ENUM | API | API, engine |
hosts | members of a segment; FK failover_segment_id → failover_segments.uuid; unique (name, deleted), i.e. a name is unique across segments | API; engine (on_maintenance, reserved) | API, engine |
notifications | failure reports and their status; unique (notification_uuid); status ENUM; payload JSON text (dict of strings); generated_time set by the sender | API (insert), engine (status, message) | API, engine (periodic tasks) |
vmoves | one row per instance evacuation; no FK, linked by notification_uuid | engine | API (1.3) |
The schema is identical between the models and the alembic migrations ([_rewrite/schema-diff_stable-2026.2.md](_rewrite/schema-diff_stable-2026.2.md)). All tables are soft-deleted (deleted = id when deleted, oslo.db SoftDeleteMixin).
Taskflow persistence database
Tables logbooks, flowdetails, atomdetails (and their own alembic_version) are created by TaskFlowDriver.upgrade_backend (masakari/engine/drivers/taskflow/driver.py:250) ([_rewrite/schema-sqlite-taskflow_stable-2026.2.sql](_rewrite/schema-sqlite-taskflow_stable-2026.2.sql)). There is one logbook per notification, named after the notification UUID (masakari/engine/drivers/taskflow/base.py:132), one flow detail per run (retries add flow details), and one atom detail per task, whose meta holds progress and progress_details (written by update_details, masakari/engine/drivers/taskflow/base.py:58). Nothing ever purges this database.
Notable transactions
- Every DB API function is its own transaction (
@context_manager.reader/writer), retried 5 times on deadlock (masakari/db/sqlalchemy/api.py:171). There is **noSELECT … FOR UPDATE** and no row versioning. - The duplicate check (
SELECTthenINSERT) is not atomic. It relies on the tooz lock (COMPUTE_HOST only) or on the random sleep. - The "in use" check of segment/host update and delete is a separate
SELECT COUNT(*)before the write (masakari/db/sqlalchemy/api.py:304): racy, but harmless. - Segment delete soft-deletes the segment and its hosts in one transaction (
masakari/db/sqlalchemy/api.py:289).
Representative queries (reconstructed):
-- duplicate detection (masakari/ha/api.py:277)
SELECT * FROM notifications WHERE deleted = 0 AND type = :type AND source_host_uuid = :host
AND generated_time >= :generated_time - INTERVAL 180 SECOND ORDER BY created_at DESC, id DESC;
-- segment under recovery (masakari/db/sqlalchemy/api.py:304)
SELECT count(id) FROM notifications WHERE deleted = 0 AND status IN ('new','running','error')
AND source_host_uuid IN (SELECT uuid FROM hosts WHERE deleted = 0 AND failover_segment_id = :seg);
-- reserved hosts (masakari/engine/manager.py:223)
SELECT * FROM hosts WHERE deleted = 0 AND failover_segment_id = :seg AND reserved = 1 AND on_maintenance = 0
ORDER BY created_at DESC, id DESC;
Other
- No subprocess, rootwrap or privsep. The engine needs no access to the compute nodes: everything goes through Nova.
GET /healthcheck(oslo.middleware,disable_by_filebackend,/etc/masakari/healthcheck_disable) is served by the API paste composite.
Source: reverse-spec-masakari/03-interactions_stable-2026.2.md. The _rewrite/ and _inventory/ files are not published on the site.