REST API — octavia — branch stable/2026.2
Branch
stable/2026.2@2900ed67cac2(19.0.0-19-g2900ed67) — repository https://opendev.org/openstack/octavia (local clone../octavia) — analysed on 2026-11-01
This file is the contract of the Octavia v2 API as served by the Python code. "[probe]" marks behaviour observed on the real application by _rewrite/tools/octavia_probe.py (results in _rewrite/route-probe_stable-2026.2.json, _rewrite/http-probes_stable-2026.2.json); test evidence is in _rewrite/exchanges_stable-2026.2.json. Errors are detailed in 02, what happens behind the provider call in 03.
1. Request path
1.1 WSGI pipeline
Outermost first (octavia/api/app.py:85-117):
| Order | Layer | Effect | Where |
|---|---|---|---|
| 1 | RequestBodySizeLimiter (oslo.middleware) | body larger than [oslo_middleware] max_request_body_size → 413 | octavia/api/app.py:115 |
| 2 | CORS (oslo.middleware) | CORS headers, also on errors of the inner layers | octavia/api/app.py:113 |
| 3 | HTTPProxyToWSGI | honours X-Forwarded-* when [oslo_middleware] enable_proxy_headers_parsing | octavia/api/app.py:105 |
| 4 | SkippingAuthProtocol (only auth_strategy=keystone) | keystonemiddleware auth_token, skipped for /, /load-balancer/, /healthcheck, /load-balancer/healthcheck | octavia/api/app.py:102-103, octavia/common/keystone.py:25-26, octavia/common/keystone.py:112 |
| 5 | AuditMiddleware (only [audit] enabled) | CADF audit | octavia/api/app.py:89-100 |
| 6 | RequestId | adds x-openstack-request-id to every response | octavia/api/app.py:87 |
| 7 | Pecan hooks | ContentTypeHook, ContextHook (on_route), QueryParametersHook (before, GET only) | octavia/api/config.py:21-26, octavia/api/common/hooks.py:27 |
| 8 | Pecan routing, then WSME wsexpose | body/argument conversion, handler, serialisation | octavia/api/root_controller.py:30 |
Consequences for the order of errors: 401 (Keystone) comes before 406; 406 (hook) and invalid limit/sort (hook) come before the policy check; _lookup 404s (parent pool/L7 policy missing) come before WSME validation and use a different body format (02 § 1).
1.2 Path families
Pecan routes by object traversal (octavia/api/root_controller.py:35-36, octavia/api/v2/controllers/__init__.py:33-80). Each v2 controller is mounted twice, and the whole tree twice:
| Prefix | Serves | Example |
|---|---|---|
/v2/lbaas/ and /v2.0/lbaas/ | all resources | /v2/lbaas/loadbalancers |
/v2/ and /v2.0/ | the same resources without lbaas: /v2/flavors, /v2/loadbalancers [probe] | what the functional tests use for flavors and availability zones (octavia/tests/functional/api/v2/base.py:37-50) |
/v2/octavia/, /v2.0/octavia/ | only amphorae | /v2/octavia/amphorae |
The API reference documents flavors and availability zones as /v2.0/lbaas/flavors… and everything else as /v2/lbaas/…. python-octaviaclient and openstacksdk use these. All spellings must keep working: 297 route entries = 75 resource operations × 4 prefixes (amphora: × 2) + discovery (_rewrite/routes_stable-2026.2.json; the field alias_of gives the canonical path).
Other routing facts [probe]:
- a trailing slash is accepted (
/v2/lbaas/loadbalancers/= 200); - a
.jsonsuffix is accepted; **.xmlreturns XML** with status 200 (/v2/lbaas/loadbalancers.xml→<result><loadbalancers /><loadbalancers_links /></result>), because WSME picks the renderer from the extension. No client uses this; a rewrite should answer it as JSON or 404 and document the difference; HEADon a collection → 405;OPTIONS→ 200 without body;PATCH→ 405 with the Pecan JSON body;DELETEorPUTon a collection → 400Missing argument: "id";POSTon an item → 400 (the id is parsed as the body argument);- unknown path → 404 with
{"code": 404, "description": …, "title": "Not Found"};/v3,/v2.1→ 404; - in sub-paths,
GET /v2/lbaas/quotas/{id}/default/anythingstill answers 200 because_lookupreturns''as the remainder (octavia/api/v2/controllers/quotas.py:123); the Go router need not reproduce this, it is not used by clients [inferred].
1.3 Identity
ContextHook builds octavia.common.context.RequestContext.from_environ for every routed request (octavia/api/common/hooks.py:61-64). is_admin is true when the policy rule context_is_admin passes or auth_strategy == noauth (octavia/common/context.py:29-32). With noauth every request is admin, project_id comes from the X-Project-Id-style headers if present, and quotas are skipped (octavia/db/repositories.py:455-457).
auth_strategy values: keystone (default), noauth, testing (octavia/common/constants.py:739-741, octavia/common/config.py:51). testing only means "no auth_token middleware, policy enforced"; the tests pair it with forced credentials. There is no X-Test-* header: the capture records the forced identity per exchange (harness.identity, 07 § 7).
1.4 Discovery
| Request | Answer | Where |
|---|---|---|
GET / | {"versions": [...]}: 31 entries v2.0 … v2.30, each {"id","status","updated","links":[{"href": <request URL>/v2, "rel":"self"}]}; status is SUPPORTED, and CURRENT for v2.30; no authentication | octavia/api/root_controller.py:64-160 |
GET /v2, /v2.0, /v2/lbaas, /v2/octavia | JSON string "v2" with Content-Type: application/json, not an object [probe] | octavia/api/v2/controllers/__init__.py:58, octavia/api/v2/controllers/__init__.py:70 |
GET/HEAD /healthcheck | 404 unless [api_settings] healthcheck_enabled; then oslo.middleware's healthcheck with the octavia_db_check backend, cached healthcheck_refresh_interval s. Other methods → 405. Success 200 (HEAD 204), failure 503; bypasses the content-type hook | octavia/api/root_controller.py:37-49, octavia/api/healthcheck/healthcheck_plugins.py:25-65, octavia/api/common/hooks.py:24 |
The minor versions are documentation only: there is no microversion negotiation, /v2 serves everything. The dates in updated are a client-visible constant list: copy them from octavia/api/root_controller.py:71-159. octavia-dashboard, openstacksdk and gophercloud read the highest version to enable features (tags ≥ 2.5, vip_sg_ids ≥ 2.29…) [inferred].
2. Route table
Canonical paths only. Handler lines are the method definition; policy rule names omit the prefix os_load-balancer_api:.
| Method | Path | Handler | Policy rule(s) | Success | Statuses seen in the capture (count) |
|---|---|---|---|---|---|
| GET | /v2/lbaas/loadbalancers | octavia/api/v2/controllers/load_balancer.py:84 LoadBalancersController.get_all | loadbalancer:get_allloadbalancer:get_all-global | 200 | 200 (32), 403 (2) |
| POST | /v2/lbaas/loadbalancers | octavia/api/v2/controllers/load_balancer.py:500 LoadBalancersController.post | loadbalancer:postloadbalancer:post:vip_sg_ids | 201 | 201 (1072), 400 (44), 403 (7), 409 (1), 500 (3), 501 (3) |
| GET | /v2/lbaas/loadbalancers/{loadbalancer_id} | octavia/api/v2/controllers/load_balancer.py:57 LoadBalancersController.get_one | loadbalancer:get_one | 200 | 200 (3289), 403 (1), 404 (2) |
| PUT | /v2/lbaas/loadbalancers/{loadbalancer_id} | octavia/api/v2/controllers/load_balancer.py:797 LoadBalancersController.put | loadbalancer:putloadbalancer:put:vip_sg_ids | 200 | 200 (24), 400 (3), 403 (1), 404 (2), 409 (3), 500 (1), 501 (2) |
| DELETE | /v2/lbaas/loadbalancers/{loadbalancer_id} | octavia/api/v2/controllers/load_balancer.py:877 LoadBalancersController.delete | loadbalancer:delete | 204 | 204 (23), 400 (2), 403 (1), 404 (2), 409 (3), 500 (1), 501 (2) |
| GET | /v2/lbaas/loadbalancers/{loadbalancer_id}/status | octavia/api/v2/controllers/load_balancer.py:944 StatusController.get | loadbalancer:get_status | 200 | 200 (9), 403 (1), 404 (1) |
| GET | /v2/lbaas/loadbalancers/{loadbalancer_id}/statuses | octavia/api/v2/controllers/load_balancer.py:944 StatusController.get | loadbalancer:get_status | 200 | 200 (1) |
| GET | /v2/lbaas/loadbalancers/{loadbalancer_id}/stats | octavia/api/v2/controllers/load_balancer.py:979 StatisticsController.get | loadbalancer:get_stats | 200 | 200 (2), 403 (1), 404 (1) |
| PUT | /v2/lbaas/loadbalancers/{loadbalancer_id}/failover | octavia/api/v2/controllers/load_balancer.py:1009 FailoverController.put | loadbalancer:put_failover | 202 | 202 (4), 403 (2), 404 (1), 409 (1) |
| GET | /v2/lbaas/listeners | octavia/api/v2/controllers/listener.py:82 ListenersController.get_all | listener:get_alllistener:get_all-global | 200 | 200 (21), 403 (2) |
| POST | /v2/lbaas/listeners | octavia/api/v2/controllers/listener.py:377 ListenersController.post | listener:post | 201 | 201 (951), 400 (56), 403 (2), 404 (1), 409 (4), 500 (1) |
| GET | /v2/lbaas/listeners/{listener_id} | octavia/api/v2/controllers/listener.py:54 ListenersController.get_one | listener:get_one | 200 | 200 (209), 403 (1), 404 (2) |
| PUT | /v2/lbaas/listeners/{listener_id} | octavia/api/v2/controllers/listener.py:627 ListenersController.put | listener:put | 200 | 200 (35), 400 (33), 403 (1), 404 (3), 409 (2), 500 (1) |
| DELETE | /v2/lbaas/listeners/{listener_id} | octavia/api/v2/controllers/listener.py:694 ListenersController.delete | listener:delete | 204 | 204 (3), 403 (1), 404 (2), 409 (2), 500 (1) |
| GET | /v2/lbaas/listeners/{listener_id}/stats | octavia/api/v2/controllers/listener.py:747 StatisticsController.get | listener:get_stats | 200 | 200 (2), 403 (1), 404 (1) |
| GET | /v2/lbaas/pools | octavia/api/v2/controllers/pool.py:76 PoolsController.get_all | pool:get_allpool:get_all-global | 200 | 200 (24), 403 (2) |
| POST | /v2/lbaas/pools | octavia/api/v2/controllers/pool.py:218 PoolsController.post | pool:post | 201 | 201 (1022), 400 (53), 403 (2), 409 (3), 500 (1) |
| GET | /v2/lbaas/pools/{pool_id} | octavia/api/v2/controllers/pool.py:53 PoolsController.get | pool:get_one | 200 | 200 (155), 403 (1), 404 (2) |
| PUT | /v2/lbaas/pools/{pool_id} | octavia/api/v2/controllers/pool.py:453 PoolsController.put | pool:put | 200 | 200 (21), 400 (17), 403 (1), 404 (1), 409 (2), 500 (2) |
| DELETE | /v2/lbaas/pools/{pool_id} | octavia/api/v2/controllers/pool.py:521 PoolsController.delete | pool:delete | 204 | 204 (4), 403 (1), 404 (2), 409 (3), 500 (1) |
| GET | /v2/lbaas/pools/{pool_id}/members | octavia/api/v2/controllers/member.py:74 MemberController.get_all | member:get_all | 200 | 200 (26), 403 (2) |
| POST | /v2/lbaas/pools/{pool_id}/members | octavia/api/v2/controllers/member.py:154 MemberController.post | member:post | 201 | 201 (77), 400 (6), 403 (2), 409 (4), 500 (1) |
| GET | /v2/lbaas/pools/{pool_id}/members/{member_id} | octavia/api/v2/controllers/member.py:50 MemberController.get | member:get_one | 200 | 200 (26), 403 (1), 404 (4) |
| PUT | /v2/lbaas/pools/{pool_id}/members/{member_id} | octavia/api/v2/controllers/member.py:297 MemberController.put | member:put | 200 | 200 (4), 400 (1), 403 (1), 404 (1), 409 (2), 500 (1) |
| DELETE | /v2/lbaas/pools/{pool_id}/members/{member_id} | octavia/api/v2/controllers/member.py:356 MemberController.delete | member:delete | 204 | 204 (2), 403 (1), 404 (3), 409 (2), 500 (1) |
| PUT | /v2/lbaas/pools/{pool_id}/members | octavia/api/v2/controllers/member.py:400 MembersController.put | member:postmember:putmember:delete | 202 | 202 (8), 400 (3), 404 (1) |
| GET | /v2/lbaas/healthmonitors | octavia/api/v2/controllers/health_monitor.py:67 HealthMonitorController.get_all | healthmonitor:get_allhealthmonitor:get_all-global | 200 | 200 (25), 403 (2) |
| POST | /v2/lbaas/healthmonitors | octavia/api/v2/controllers/health_monitor.py:210 HealthMonitorController.post | healthmonitor:post | 201 | 201 (100), 400 (16), 403 (2), 404 (1), 409 (4), 500 (1) |
| GET | /v2/lbaas/healthmonitors/{healthmonitor_id} | octavia/api/v2/controllers/health_monitor.py:49 HealthMonitorController.get_one | healthmonitor:get_one | 200 | 200 (40), 403 (1), 404 (2) |
| PUT | /v2/lbaas/healthmonitors/{healthmonitor_id} | octavia/api/v2/controllers/health_monitor.py:356 HealthMonitorController.put | healthmonitor:put | 200 | 200 (11), 400 (11), 403 (1), 404 (1), 409 (2), 500 (1) |
| DELETE | /v2/lbaas/healthmonitors/{healthmonitor_id} | octavia/api/v2/controllers/health_monitor.py:421 HealthMonitorController.delete | healthmonitor:delete | 204 | 204 (2), 403 (1), 404 (2), 409 (2), 500 (1) |
| GET | /v2/lbaas/l7policies | octavia/api/v2/controllers/l7policy.py:72 L7PolicyController.get_all | l7policy:get_alll7policy:get_all-global | 200 | 200 (23), 403 (2) |
| POST | /v2/lbaas/l7policies | octavia/api/v2/controllers/l7policy.py:128 L7PolicyController.post | l7policy:post | 201 | 201 (181), 400 (20), 403 (2), 404 (1), 409 (2), 500 (1) |
| GET | /v2/lbaas/l7policies/{l7policy_id} | octavia/api/v2/controllers/l7policy.py:50 L7PolicyController.get | l7policy:get_one | 200 | 200 (39), 403 (1), 404 (2) |
| PUT | /v2/lbaas/l7policies/{l7policy_id} | octavia/api/v2/controllers/l7policy.py:223 L7PolicyController.put | l7policy:put | 200 | 200 (24), 400 (19), 403 (1), 404 (2), 409 (2), 500 (1) |
| DELETE | /v2/lbaas/l7policies/{l7policy_id} | octavia/api/v2/controllers/l7policy.py:302 L7PolicyController.delete | l7policy:delete | 204 | 204 (2), 403 (1), 404 (2), 409 (2), 500 (1) |
| GET | /v2/lbaas/l7policies/{l7policy_id}/rules | octavia/api/v2/controllers/l7rule.py:68 L7RuleController.get_all | l7rule:get_all | 200 | 200 (19), 403 (2) |
| POST | /v2/lbaas/l7policies/{l7policy_id}/rules | octavia/api/v2/controllers/l7rule.py:139 L7RuleController.post | l7rule:post | 201 | 201 (64), 400 (15), 403 (2), 409 (4), 500 (2) |
| GET | /v2/lbaas/l7policies/{l7policy_id}/rules/{l7rule_id} | octavia/api/v2/controllers/l7rule.py:46 L7RuleController.get | l7rule:get_one | 200 | 200 (26), 403 (1), 404 (3) |
| PUT | /v2/lbaas/l7policies/{l7policy_id}/rules/{l7rule_id} | octavia/api/v2/controllers/l7rule.py:216 L7RuleController.put | l7rule:put | 200 | 200 (6), 400 (11), 403 (1), 404 (1), 409 (2), 500 (1) |
| DELETE | /v2/lbaas/l7policies/{l7policy_id}/rules/{l7rule_id} | octavia/api/v2/controllers/l7rule.py:286 L7RuleController.delete | l7rule:delete | 204 | 204 (2), 403 (1), 404 (2), 409 (2), 500 (1) |
| GET | /v2/lbaas/quotas | octavia/api/v2/controllers/quotas.py:51 QuotasController.get_all | quota:get_allquota:get_all-global | 200 | 200 (10), 403 (3) |
| GET | /v2/lbaas/quotas/{project_id} | octavia/api/v2/controllers/quotas.py:37 QuotasController.get | quota:get_one | 200 | 200 (15), 403 (3) |
| PUT | /v2/lbaas/quotas/{project_id} | octavia/api/v2/controllers/quotas.py:77 QuotasController.put | quota:put | 202 | 202 (42), 403 (1) |
| DELETE | /v2/lbaas/quotas/{project_id} | octavia/api/v2/controllers/quotas.py:101 QuotasController.delete | quota:delete | 202 | 202 (2), 403 (1), 404 (1) |
| GET | /v2/lbaas/quotas/{project_id}/default | octavia/api/v2/controllers/quotas.py:135 QuotasDefaultController.get | quota:get_defaults | 200 | 200 (2), 403 (1) |
| GET | /v2/lbaas/providers | octavia/api/v2/controllers/provider.py:39 ProviderController.get_all | provider:get_all | 200 | 200 (2) |
| GET | /v2/lbaas/providers/{provider}/flavor_capabilities | octavia/api/v2/controllers/provider.py:83 FlavorCapabilitiesController.get_all | provider-flavor:get_all | 200 | 200 (7), 400 (1), 403 (1), 501 (1) |
| GET | /v2/lbaas/providers/{provider}/availability_zone_capabilities | octavia/api/v2/controllers/provider.py:133 AvailabilityZoneCapabilitiesController.get_all | provider-availability-zone:get_all | 200 | 200 (8), 400 (1), 403 (1), 501 (1) |
| GET | /v2/lbaas/flavors | octavia/api/v2/controllers/flavors.py:61 FlavorsController.get_all | flavor:get_all | 200 | 200 (4), 403 (1) |
| POST | /v2/lbaas/flavors | octavia/api/v2/controllers/flavors.py:86 FlavorsController.post | flavor:post | 201 | 201 (33), 400 (5), 403 (1), 409 (1), 500 (1) |
| GET | /v2/lbaas/flavors/{flavor_id} | octavia/api/v2/controllers/flavors.py:43 FlavorsController.get_one | flavor:get_one | 200 | 200 (10), 403 (1), 404 (3) |
| PUT | /v2/lbaas/flavors/{flavor_id} | octavia/api/v2/controllers/flavors.py:119 FlavorsController.put | flavor:put | 200 | 200 (3), 400 (1), 403 (1), 404 (1), 500 (1) |
| DELETE | /v2/lbaas/flavors/{flavor_id} | octavia/api/v2/controllers/flavors.py:149 FlavorsController.delete | flavor:delete | 204 | 204 (2), 403 (1), 404 (2), 409 (1), 500 (1) |
| GET | /v2/lbaas/flavorprofiles | octavia/api/v2/controllers/flavor_profiles.py:66 FlavorProfileController.get_all | flavor-profile:get_all | 200 | 200 (4), 403 (1) |
| POST | /v2/lbaas/flavorprofiles | octavia/api/v2/controllers/flavor_profiles.py:94 FlavorProfileController.post | flavor-profile:post | 201 | 201 (69), 400 (7), 403 (1), 409 (1), 500 (1) |
| GET | /v2/lbaas/flavorprofiles/{flavorprofile_id} | octavia/api/v2/controllers/flavor_profiles.py:44 FlavorProfileController.get_one | flavor-profile:get_one | 200 | 200 (13), 403 (1), 404 (3) |
| PUT | /v2/lbaas/flavorprofiles/{flavorprofile_id} | octavia/api/v2/controllers/flavor_profiles.py:153 FlavorProfileController.put | flavor-profile:put | 200 | 200 (5), 400 (3), 403 (1), 404 (1), 409 (2) |
| DELETE | /v2/lbaas/flavorprofiles/{flavorprofile_id} | octavia/api/v2/controllers/flavor_profiles.py:215 FlavorProfileController.delete | flavor-profile:delete | 204 | 204 (2), 403 (1), 404 (2), 409 (1) |
| GET | /v2/lbaas/availabilityzones | octavia/api/v2/controllers/availability_zones.py:68 AvailabilityZonesController.get_all | availability-zone:get_all | 200 | 200 (4), 403 (1) |
| POST | /v2/lbaas/availabilityzones | octavia/api/v2/controllers/availability_zones.py:97 AvailabilityZonesController.post | availability-zone:post | 201 | 201 (29), 400 (5), 403 (1), 409 (1), 500 (1) |
| GET | /v2/lbaas/availabilityzones/{availability_zone_name} | octavia/api/v2/controllers/availability_zones.py:41 AvailabilityZonesController.get_one | availability-zone:get_one | 200 | 200 (10), 403 (1), 404 (3) |
| PUT | /v2/lbaas/availabilityzones/{availability_zone_name} | octavia/api/v2/controllers/availability_zones.py:135 AvailabilityZonesController.put | availability-zone:put | 200 | 200 (3), 400 (1), 403 (1), 404 (1), 500 (1) |
| DELETE | /v2/lbaas/availabilityzones/{availability_zone_name} | octavia/api/v2/controllers/availability_zones.py:175 AvailabilityZonesController.delete | availability-zone:delete | 204 | 204 (2), 403 (1), 404 (2), 409 (1), 500 (1) |
| GET | /v2/lbaas/availabilityzoneprofiles | octavia/api/v2/controllers/availability_zone_profiles.py:70 AvailabilityZoneProfileController.get_all | availability-zone-profile:get_all | 200 | 200 (4), 403 (1) |
| POST | /v2/lbaas/availabilityzoneprofiles | octavia/api/v2/controllers/availability_zone_profiles.py:98 AvailabilityZoneProfileController.post | availability-zone-profile:post | 201 | 201 (65), 400 (7), 403 (1), 409 (1), 500 (1) |
| GET | /v2/lbaas/availabilityzoneprofiles/{availability_zone_profile_id} | octavia/api/v2/controllers/availability_zone_profiles.py:43 AvailabilityZoneProfileController.get_one | availability-zone-profile:get_one | 200 | 200 (13), 403 (1), 404 (3) |
| PUT | /v2/lbaas/availabilityzoneprofiles/{availability_zone_profile_id} | octavia/api/v2/controllers/availability_zone_profiles.py:168 AvailabilityZoneProfileController.put | availability-zone-profile:put | 200 | 200 (5), 400 (3), 403 (1), 404 (1), 409 (2) |
| DELETE | /v2/lbaas/availabilityzoneprofiles/{availability_zone_profile_id} | octavia/api/v2/controllers/availability_zone_profiles.py:243 AvailabilityZoneProfileController.delete | availability-zone-profile:delete | 204 | 204 (2), 403 (1), 404 (2), 409 (1) |
| GET | /v2/octavia/amphorae | octavia/api/v2/controllers/amphora.py:68 AmphoraController.get_all | amphora:get_all | 200 | 200 (16), 403 (1) |
| GET | /v2/octavia/amphorae/{amphora_id} | octavia/api/v2/controllers/amphora.py:50 AmphoraController.get_one | amphora:get_one | 200 | 200 (5), 403 (1), 404 (2) |
| DELETE | /v2/octavia/amphorae/{amphora_id} | octavia/api/v2/controllers/amphora.py:94 AmphoraController.delete | amphora:delete | 204 | 204 (2), 403 (1), 404 (1), 409 (1) |
| PUT | /v2/octavia/amphorae/{amphora_id}/config | octavia/api/v2/controllers/amphora.py:188 AmphoraUpdateController.put | amphora:put_config | 202 | 202 (2), 403 (1), 404 (2), 500 (1) |
| PUT | /v2/octavia/amphorae/{amphora_id}/failover | octavia/api/v2/controllers/amphora.py:145 FailoverController.put | amphora:put_failover | 202 | 202 (2), 403 (1), 404 (2) |
| GET | /v2/octavia/amphorae/{amphora_id}/stats | octavia/api/v2/controllers/amphora.py:220 AmphoraStatsController.get | amphora:get_stats | 200 | 200 (1), 403 (1), 404 (2) |
| GET | / | octavia/api/root_controller.py:64 RootController.index | none | 200 | 200 (11), 406 (2) |
| GET | /healthcheck | octavia/api/root_controller.py:44 RootController.healthcheck | none | 200 | 200 (18), 404 (1), 503 (7) |
| HEAD | /healthcheck | octavia/api/root_controller.py:44 RootController.healthcheck | none | 200 | 204 (1), 404 (1), 503 (1) |
| GET | /v2 | octavia/api/v2/controllers/__init__.py:58 BaseV2Controller.get | none | 200 | — |
GET /v2 is the only canonical route with no test (covered by the probes). Coverage of the capture per route is the last column. Documented in the API reference but different in the code: GET /v2/lbaas/quotas/defaults (api-ref/source/v2/quota.inc), used by openstacksdk; there is no such route, it is served as GET /quotas/{project_id} with project_id = "defaults" and returns the defaults only because that project has no record, with the policy target defaults, so a non-admin gets 403 [inferred]; the real default route is GET /quotas/{project_id}/default (octavia/api/v2/controllers/quotas.py:119-124). Not documented: /loadbalancers/{id}/statuses is documented as /status only.
3. Cross-cutting rules
3.1 Bodies and types
- Every body has a root key (
loadbalancer,listener,pool,member,healthmonitor,l7policy,rule,quota,flavor,flavorprofile,availability_zone,availability_zone_profile, andmembersfor batch). - Unknown attributes are rejected with 400:
Unknown attribute for argument load_balancer.loadbalancer: bogus(orUnknown attribute for argument load_balancer: bogusat root level) [probe]. The argument name in the message is the Python parameter (load_balancer,listener_,pool_,member_,health_monitor_,l7policy_,rule_,quotas,flavor_,flavor_profile_,availability_zone_,availability_zone_profile_); it is part of the observable text (_rewrite/observed-errors_stable-2026.2.md). - Missing root object (
{}) is not validated:.listeneronUnsetraisesAttributeError→ 500'UnsetType' object has no attribute 'project_id'on the POST of LB, listener, pool, HM, L7 policy, flavor, flavor profile, AZ, AZ profile and the PUT of quota, flavor, flavor profile, AZ, AZ profile;PUT /quotas/{id}with{}→ 500'quota'[probe, 56 route entries in_rewrite/route-probe_stable-2026.2.json]. The Go API should answer 400 and list the difference: no client depends on a 500 [inferred]. - Content types [probe]:
application/jsonis decoded.text/plain→ 415Unknown mimetype: text/plain. WithoutContent-Type, or form-encoded, the body is taken as form parameters → 400Unknown argument: "<body>". A JSON array as body → 500'list' object has no attribute 'keys'. Malformed JSON → 400Request is not in valid JSON format. Empty POST body → 400Missing argument: "load_balancer". Accept(octavia/api/common/hooks.py:30-55): absent, empty, or matching JSON → JSON. Not matching → 406 with Pecan's body{"code":406,"description":"Only content type application/json is accepted.","title":"Not Acceptable"}.text/plain;q=0.1, application/json→ 200 [probe].- Query parameters on POST/PUT/DELETE are WSME arguments: unknown → 400
Unknown argument: "fields"[probe]. GET handlers declareignore_extra_args=True. - Booleans: WSME accepts
true/falseand strings"yes","true","1"… (bool_as_stringprobes reach the next validation step); integers are validated against the range;nullis accepted for any non-mandatory field and means "clear" on PUT (§ 3.2). - Messages of type errors are WSME's:
Invalid input for field/attribute <attr>. Value: '<value>'. <reason>, where<reason>isValue should be UUID format,Value should be one of: <values in Python set order>,Value should have a maximum character requirement of 255,Mandatory field missing., etc. When the attribute is the whole root,<value>is the Pythonreprof the request dictionary. Replay compares these two forms loosely (set order, repr). tenant_idis a read-write alias ofproject_idon every type havingproject_id(octavia/api/common/types.py:102-129,octavia/api/common/types.py:215-219); it is always echoed in responses but rejected as a list filter (§ 3.3) [probe].name/descriptionread asnullfrom the database are returned as""(octavia/api/common/types.py:167-168).admin_state_up: nullon POST or PUT meansfalse(octavia/api/common/types.py:220-225).- API → DB field names are per type (
_type_to_db_map), e.g.admin_state_up→enabled,vip_address→vip.ip_address, memberaddress→ip_address,default_tls_container_ref→tls_certificate_id,loadbalancer_id→load_balancer_id(octavia/api/common/types.py:182-193). They are listed with the field tables in § 5.
3.2 Unset, null, default
WSME distinguishes absent (Unset) from null. Rules:
- POST: absent → type default (
default=in the tables of § 5), else stored as NULL (to_dict(render_unsets=True)turnsUnsetintoNone) except LB (render_unsets=False,octavia/api/v2/controllers/load_balancer.py:545-546). - PUT: absent → untouched;
null→ cleared or reset to its default by_set_default_on_none(listener:octavia/api/v2/controllers/listener.py:585-623; member:octavia/api/v2/controllers/member.py:278-292; HM:octavia/api/v2/controllers/health_monitor.py:331-352; L7 ruleinvert:octavia/api/v2/controllers/l7rule.py:234-235; pooltls_versions,tls_ciphers:octavia/api/v2/controllers/pool.py:466-469). - The provider driver receives only the fields that are set:
from_dicton a dict built withrender_unsets=False. Absent ≠ null there too (03 § 2). PUTwith an empty object is a valid call: it still locks the parents, calls the driver and leaves them inPENDING_UPDATE[probeput_empty_object].
3.3 Lists: pagination, sorting, filtering, field selection
PaginationHelper is created by the before hook on every GET (octavia/api/common/hooks.py:69-75), even for item GETs, which is why a bad limit fails them too. Reserved names: limit, marker, sort, sort_key, sort_dir, fields, page_reverse (octavia/api/common/pagination.py:40-43).
| Parameter | Rule | Where |
|---|---|---|
limit | default and maximum [api_settings] pagination_max_limit (default 1000; infinite = none). 0, negative or infinite → no limit, not an error. Non-integer → 400 Supplied pagination limit 'abc' is not valid. (webob format, raised in the hook) | octavia/api/common/pagination.py:63-82, octavia/common/constants.py:755 |
marker | id of the last row of the previous page; unknown → 400 Supplied pagination marker '<x>' is not valid. | octavia/api/common/pagination.py:336-339 |
sort | k1:dir,k2; default dir asc; 3 parts → 400 Supplied sort key '['name', 'asc', 'x']' is not valid. (Python list repr, webob format); bad direction → 400 Supplied sort direction 'bogus' is not valid. (webob format) | octavia/api/common/pagination.py:84-112 |
sort_key + sort_dir | comma lists; missing dirs take the default; ignored if sort is present | octavia/api/common/pagination.py:98-110 |
| unknown sort key | 400 Supplied sort key 'bogus' is not valid. (WSME format, raised in apply) | octavia/api/common/pagination.py:328-330 |
| default order | created_at, id appended when the model has them and they are not already keys | octavia/api/common/pagination.py:301-305, octavia/common/constants.py:754 |
| nested sort | a key mapped to vip.ip_address joins the child table | octavia/api/common/pagination.py:321-327 |
page_reverse | exactly "True" flips the marker comparison, not the order of the result | octavia/api/common/pagination.py:368-376 |
| filters | any other parameter: translated with the type map, must be an attribute of the data model, else 400 One or more arguments are either duplicate or invalid (this includes tenant_id). Equality only; booleans go through bool_from_string (admin_state_up=bogus → false); vip_* filter on the VIP child | octavia/api/common/pagination.py:248-291, octavia/db/base_models.py:131-160 |
| tags | tags (all), tags-any, not-tags, not-tags-any; comma lists and repeats are merged | octavia/api/common/pagination.py:178-221, octavia/api/common/pagination.py:269-280 |
fields | repeated parameter, keeps those attributes in each object; unknown names ignored; **fields=id,name is one unknown name → {}** [probe]; root links unaffected. Honoured only when allow_field_selection. Not on /quotas, ignored (ignore_extra_args) on POST responses | octavia/api/v2/controllers/base.py:234-241 |
project_id | handler argument, not a filter: goes through _auth_get_all (§ 3.5) | octavia/api/v2/controllers/base.py:205-226 |
Result: {"<plural>": [...], "<plural>_links": [...]}. Link next appears when the page is full (len >= limit) or when no limit applies and the list is non-empty; previous appears when a marker was given; both carry limit, sort, sort_key and marker (plus page_reverse=True for previous), but not filters nor sort_dir. The base URL is [api_settings] api_base_uri when set, else the request URL (octavia/api/common/pagination.py:143-176). With limit absent and the default maximum 1000, an empty list gives no links. Flavor profile links are in flavorprofile_links and AZ profile links in availability_zone_profile_links (singular, octavia/api/v2/controllers/flavor_profiles.py:89, octavia/api/v2/controllers/availability_zone_profiles.py:93).
The kill switches allow_pagination, allow_sorting, allow_filtering, allow_field_selection default to true (octavia/common/config.py:56-63).
3.4 Locking: immutability and PENDING_*
Writes follow one pattern: test-and-set on the load balancer row inside the request transaction, call the provider, commit.
test_and_set_provisioning_statustakesSELECT … FOR UPDATEon the LB; accepted states areMUTABLE_STATUSES = (ACTIVE,), orDELETABLE_STATUSES = (ACTIVE, ERROR)when the target isPENDING_DELETE(octavia/db/repositories.py:794-825,octavia/common/constants.py:247-249).- Failover uses
FAILOVERABLE_STATUSES = (ACTIVE, ERROR)(octavia/db/repositories.py:827-853). - Child operations set LB →
PENDING_UPDATE, affected listeners →PENDING_UPDATE(orPENDING_DELETEfor the listener being deleted), the parent pool or L7 policy →PENDING_UPDATE, without re-checking listeners (octavia/db/repositories.py:392-436). - Before that, member and HM operations reject a parent pool not
ACTIVEwithImmutableObject(Pool), L7 rules a parent policy notACTIVE(octavia/api/v2/controllers/member.py:118-119,octavia/api/v2/controllers/health_monitor.py:105-106,octavia/api/v2/controllers/l7rule.py:105-107). - The object itself: created rows start
PENDING_CREATE/OFFLINE(NO_MONITORfor a member of a pool without HM:octavia/db/prepare.py:176-179), updated rows getPENDING_UPDATE, deleted rowsPENDING_DELETE. - The transaction, hence the row lock, stays open across the provider call, so the driver must be fast and must never call back into the API synchronously. A driver exception rolls everything back: statuses, new rows and quota counts.
- A 409 is
ImmutableObject(<resource> <id> is immutable and cannot be updated.) except on LB PUT, DELETE and failover, which raiseLBPendingStateError(Invalid state <status> of loadbalancer resource <id>) (octavia/api/v2/controllers/load_balancer.py:113-132). - Nothing in the API moves a resource out of
PENDING_*: only a provider status message does (03 § 2.3). With the OVN provider each child operation reports the LBACTIVEwhen it is done.
3.5 Authorisation
Every operation calls _auth_validate_action(context, project_id, action): action = RBAC_TYPE + suffix, target = {"project_id": <owner>} (octavia/api/v2/controllers/base.py:228-232). The owner comes from the database for existing objects: the load balancer's project_id, or the object's own. A project-less check uses context.project_id as target (flavors, providers, AZ, amphora GET: any authenticated project passes the owner part). Lists use _auth_get_all (octavia/api/v2/controllers/base.py:205-226):
<type>:get_all-globalwith target{project_id: <query value or None>}: if allowed, no project filter unlessproject_idwas given;- otherwise
project_idfalls back to the context's,None→ 403, and<type>:get_allis enforced with that target, which is how another project'sproject_idgets a 403.
Member and L7 rule lists enforce the parent's owner without a global variant. PolicyForbidden → 403 Policy does not allow this request to be performed. (octavia/common/policy.py:71-121). Rule list: § 6.
3.6 Response envelope on write
POST → 201 with the object; LB POST uses LoadBalancerFullResponse, so listeners and pools are complete objects on a graph create (octavia/api/v2/types/load_balancer.py:117-123), but IDs only on GET. PUT → 200 with the object re-read (statuses already PENDING_UPDATE), except quota PUT/DELETE → 202 with body (DELETE too: body with the defaults although documented as 202, octavia/api/v2/controllers/quotas.py:100-117). DELETE → 204. LB failover, amphora failover/config, member batch → 202 without body. tenant_id always echoed. Timestamps are naive UTC, ISO 8601 without zone (2026-10-31T23:57:54.439059), updated_at null until first update.
4. Resources
4.1 Load balancers
- Purpose: declare a virtual IP served by a provider; optionally the whole tree of listeners, pools, members, monitors and L7 objects in one call.
- Actor: project member (or admin creating for a project through
project_id).
POST (octavia/api/v2/controllers/load_balancer.py:500-684). Order of checks, all before any write unless stated:
project_id: body value, else context; none → 400Validation failure: Missing project ID in request where one is required. An administrator should check the keystone settings in the Octavia configuration.(octavia/api/v2/controllers/load_balancer.py:505-512).- Policy
loadbalancer:poston that project, andpost:vip_sg_idsifvip_sg_idsis in the body (octavia/api/v2/controllers/load_balancer.py:514-519). - VIP request object (
octavia/api/v2/controllers/load_balancer.py:284-344):allow_vip_port_id,allow_vip_network_id,allow_vip_subnet_idfalse → 400Validation failure: use of vip_port_id is disallowed by this deployment's configuration.…;- none of the three →
VIPValidationException400Validation failure: VIP must contain one of: vip_port_id, vip_network_id, vip_subnet_id.; - port given (
_validate_port_and_fill_or_validate_subnet,octavia/api/v2/controllers/load_balancer.py:203-252): port must exist (InvalidSubresource400Port <id> not found.), must have nodevice_id(400Port <id> is already used by device <d>with trailing space,octavia/common/validate.py:345-348); network taken from the port; the port'sqos_policy_idfillsvip_qos_policy_idif absent; then with a subnet: subnet exists, first fixed IP in that subnet gives the address, none →No VIP address found on the specified VIP port within the specified subnet.; with an address: matches a fixed IP (compressed form), elseSpecified VIP address not found on the specified VIP port.; alone: exactly one fixed IP, elseVIP port's subnet could not be determined. Please specify either a VIP subnet or address.; - network given (
octavia/api/v2/controllers/load_balancer.py:135-200): must exist; a given subnet must be innetwork.subnets, elseSubnet <id> not found.; no subnet + address: first subnet whose CIDR holds the address, elseSupplied network does not contain a subnet for VIP address specified.; no subnet, no address:get_network_ip_availability; subnets with at least 2 free IPs (3 underACTIVE_STANDBYtopology), prefer the first IPv4, else the first; messagesSupplied network does not contain a subnet.,Could not verify IP availability for network <id>.,Subnet(s) in the supplied network do not contain enough available IPs.; - subnet only: must exist, gives the network;
vip_qos_policy_id: QoS extension enabled (VIP QoS policy is not allowed in this deployment.) and policy exists (octavia/common/validate.py:363-378);- additional VIPs: subnets unique including the main one (
Duplicate VIP subnet(s) specified. Only one IP can be bound per subnet.), all exist, all in one network (All VIP subnets must belong to the same network.),network_idfilled (octavia/api/v2/controllers/load_balancer.py:255-282); vip_sg_ids: each must exist (Security Group <id> not found.). Network lookups use the user's token unless[networking] allow_invisible_resource_usage(03 § 3).
- Flavor: exists (
Validation failure: Invalid flavor_id.), enabled (DisabledOption400The selected flavor is not allowed in this deployment: <id>) (octavia/api/v2/controllers/load_balancer.py:443-455). - Availability zone: exists (
Invalid availability zone.), enabled (octavia/api/v2/controllers/load_balancer.py:485-496). - Provider (
octavia/api/v2/controllers/load_balancer.py:388-416): flavor's provider, else bodyprovider, elsedefault_provider_driver; both and different →ProviderFlavorMismatchError400.get_driver: not inenabled_provider_drivers→ProviderNotEnabled400; cannot load →ProviderNotFound501 (octavia/api/drivers/driver_factory.py:30-52). - Transaction opens. Quota (§ 4.8): over →
QuotaException403Quota has been met for resources: load_balancer… (octavia/api/v2/controllers/load_balancer.py:536-541). create_load_balancerprepares id,PENDING_CREATE,OFFLINE, topology (octavia/db/prepare.py:26-37);provider= driver name.- Flavor metadata:
validate_flavoron the driver if the flavor has data;loadbalancer_topologyfrom it, else config (octavia/api/v2/controllers/load_balancer.py:418-441). - AZ metadata:
validate_availability_zoneif the AZ has data;NotImplementedError→ 501… This provider does not support availability zones.(octavia/api/v2/controllers/load_balancer.py:457-483). network_allowed_by_config(AZvalid_vip_networks, else[networking] valid_vip_networks),network_denylist_by_config→ 400Supplied VIP network_id is not allowed by the configuration of this deployment.(octavia/common/validate.py:412-430).- vNIC:
directwhen the flavor setssriov_vip, elsenormal;direct+vip_sg_ids→VIP Security Groups are not allowed with VNIC direct type(octavia/api/v2/controllers/load_balancer.py:570-578,octavia/api/v2/controllers/load_balancer.py:346-352). - Rows
load_balancer,vip,additional_vipinserted (octavia/db/repositories.py:299-335). - **
create_vip_port** on the driver with the provider VIP dictionary and additional VIPs. The driver may return a complete VIP (the OVN provider does).ProviderNotImplementedError→ the API creates the port itself withallocate_vip(octavia/api/v2/controllers/load_balancer.py:603-625).octavia_ownedtrue if the API made the port or the driver says so. - Ports created in this request and not given by the user are remembered, so that any later failure deletes them (
octavia/api/v2/controllers/load_balancer.py:582-585,octavia/api/v2/controllers/load_balancer.py:371-386); this is the recent fix at HEAD (2900ed67). vip/additional_viprows updated from the result.- Graph (below), if
listenersorpools. loadbalancer_createon the driver with the full provider object, then commit; a duplicate →IDAlreadyExists409; response re-read withLoadBalancerFullResponse(octavia/api/v2/controllers/load_balancer.py:658-684).
Graph creation (octavia/api/v2/controllers/load_balancer.py:686-792). Same transaction; no status test because the LB is new:
- a listener's
default_poolor a policy'sredirect_poolwith more than 3 keys (name + the two defaulted fieldsadmin_state_up,tls_enabled) is a full definition moved to the top-level pool list; otherwise it is a reference by name. No name →Pools must be named when creating a fully populated loadbalancer.; - pool names unique:
Pool names must be unique when creating a fully populated loadbalancer.; reference without definition:Pool '<name>' was referenced but no full definition was found.; the mandatoryprotocol,lb_algorithmare checked by hand:Pool definition for '<name>' missing required attribute: <attr>; - quotas are consumed per kind, with
count(pools, then for each pool HM and members; listeners; L7 policies and rules are not quota-checked in the graph); - pools → members and HM → listeners → L7 policies → rules; the same per-object validators as the single calls, except that no driver call happens per object: the driver gets one
loadbalancer_createwith nested objects. - PROMETHEUS listeners cannot have a default pool or L7 policies:
ListenerNoChildren. - Evidence:
TestLoadBalancerGraphexchanges.
PUT (octavia/api/v2/controllers/load_balancer.py:797-874): editable fields name, description, admin_state_up, tags, vip_qos_policy_id, vip_sg_ids. Policy put, put:vip_sg_ids; QoS validated if changed; vip_sg_ids: null → []; _validate_vnic_type. Status test → PENDING_UPDATE. loadbalancer_update(old, new): old is the full object from the database with for_delete=True, new only the changed fields and the id. DB update after the driver accepted.
DELETE (octavia/api/v2/controllers/load_balancer.py:877-904): ?cascade= parsed by bool_from_string. Children (listeners or pools) without cascade → 400 Validation failure: Cannot delete Load Balancer <id> - it has children. Status → PENDING_DELETE; loadbalancer_delete(full_object, cascade). Children statuses are not touched by the API on a cascade: the driver reports every object DELETED.
Status tree GET …/status and …/statuses (octavia/api/v2/controllers/load_balancer.py:944-967): {"statuses": {"loadbalancer": {id, name, provisioning_status, operating_status, listeners: [{…, l7policies:[{…, rules:[…]}], pools:[{…, health_monitor:{…}, members:[{…, address, protocol_port}]}]}], pools: […]}}}. Pools appear under the listeners using them and at LB level. Policy get_status.
Stats GET …/stats (octavia/api/v2/controllers/load_balancer.py:979-999): sum of the listeners (03 § 2.5); {"stats": {active_connections, bytes_in, bytes_out, request_errors, total_connections}}.
Failover PUT …/failover (octavia/api/v2/controllers/load_balancer.py:1009-1027): 202, body ignored (**kwargs), status ACTIVE or ERROR → PENDING_UPDATE, loadbalancer_failover(id). The OVN provider raises UnsupportedOptionError → 501 Provider 'ovn' does not support a requested option: OVN provider does not support loadbalancer failover and rollback (provider spec 02).
Soft delete: GET-style lookups use show_deleted=False, the LB row stays with status DELETED until housekeeping purges it after load_balancer_expiry_age (octavia/db/repositories.py:126-135, octavia/controller/housekeeping/house_keeping.py:66). Non-LB rows are removed by the status path (03 § 2.3).
4.2 Listeners
POST (octavia/api/v2/controllers/listener.py:377-445). Project and provider from the LB (NotFound if unknown); policy; driver; quota; default pool: PROMETHEUS → ListenerNoChildren 400, must belong to the LB (404 Pool <id> not found.) and be protocol-compatible (octavia/common/constants.py:210-218: The pool protocol 'X' is invalid while the listener protocol is 'Y'.); LB test-and-set; then _validate_create_listener (octavia/api/v2/controllers/listener.py:193-373):
| Rule | Message (faultstring) |
|---|---|
insert_headers only on HTTP, TERMINATED_HTTPS | insert-headers is not a valid option for a <P> protocol listener. |
| header name known; SSL headers only with TERMINATED_HTTPS | <list repr> is not a valid option for insert_headers |
| UDP, SCTP + TLS or headers | Validation failure: <P> protocol listener does not support TLS. |
allow_tls_terminated_listeners off | The selected protocol is not allowed in this deployment: TERMINATED_HTTPS |
allow_prometheus_listeners off | The selected protocol is not allowed in this deployment: PROMETHEUS |
| certificate refs on non-TLS | Certificate container references are not allowed on <P> protocol listeners. |
| TLS without certificate | An SNI or default certificate container reference must be provided for <P> protocol listeners. |
| client CA without base cert; client auth without CA; CRL without CA | An SNI or default…, Client authentication setting <X> requires a client CA container reference., A client authentication CA reference is required to specify a client authentication revocation list. |
| ciphers | Invalid characters in cipher string.…, The following ciphers have been prohibited by an administrator: … |
| certificates readable | Could not retrieve certificate: <refs>; PEM errors for CA and CRL (octavia/api/v2/controllers/base.py:249-316) |
| same L4 port on the LB | 409 Another Listener on this Load Balancer is already using protocol <P> and protocol_port <n> (L4 map octavia/common/constants.py:866-875) |
allowed_cidrs | Allowed CIDRs are not allowed when using custom VIP Security Groups.; CIDR <c> IP version incompatible with all VIPs <list repr> IP version. |
TLS versions, ALPN, HSTS (octavia/common/validate.py:512-597) | Empty TLS version list.…, Invalid TLS versions: …, Requested TLS versions are less than the minimum: …, Empty ALPN protocol list.…, Invalid ALPN protocol: …, HSTS configuration options…, The HSTS feature can only be used… |
Defaults: timeout_* from [haproxy_amphora] (timeout_client_data 50000, timeout_member_connect 5000, timeout_member_data 50000, timeout_tcp_inspect 0), tls_ciphers, tls_versions, alpn_protocols from [api_settings] for TERMINATED_HTTPS (octavia/db/prepare.py:40-97). A listener with protocol PROMETHEUS reports protocol_port-based stats on the amphora only, out of OVN scope. The driver call receives secrets read from the certificate manager: default_tls_container_data, sni_container_data, client_ca_tls_container_data, client_crl_container_data (octavia/api/drivers/utils.py:227-295).
PUT (octavia/api/v2/controllers/listener.py:627-691): _validate_listener_PUT is the same list adapted (octavia/api/v2/controllers/listener.py:484-583), with one text difference: … does not support TLS or header insertion.; default_pool_id re-validated, null removes it; LB and listener → PENDING_UPDATE; listener_update(old, new); insert_headers, allowed_cidrs, sni_container_refs are replaced as a whole. DELETE (octavia/api/v2/controllers/listener.py:694-724): LB PENDING_UPDATE, listener PENDING_DELETE. Stats octavia/api/v2/controllers/listener.py:747-772.
4.3 Pools
POST (octavia/api/v2/controllers/pool.py:218-318). Requires loadbalancer_id or listener_id (Must provide at least one of: loadbalancer_id, listener_id; a bad listener_id raises an internal error → 500, AttributeError on None **[inferred from octavia/api/v2/controllers/pool.py:238-240]**). Rules: listener protocol compatibility; PROMETHEUS parent; UDP/SCTP session persistence: only SOURCE_IP with persistence_timeout and persistence_granularity, messages at octavia/api/v2/controllers/pool.py:188-214; non-UDP: persistence_timeout and persistence_granularity is only for UDP and SCTP protocol pools. (POST) or … only for UDP protocol pools. (PUT, octavia/api/v2/controllers/pool.py:386-388); check_session_persistence (octavia/common/validate.py:439-462); listener already having a default pool → DuplicatePoolEntry 409; TLS refs, CA, CRL, ciphers, versions, ALPN (only when tls_enabled) (octavia/api/v2/controllers/pool.py:118-170). listener_id sets the listener's default_pool_id in the same transaction (octavia/db/repositories.py:337-364). lb_algorithm SOURCE_IP_PORT… is free for the API; the OVN provider accepts SOURCE_IP_PORT and SOURCE_IP only (others → 501 from the driver). PUT octavia/api/v2/controllers/pool.py:453-518. DELETE octavia/api/v2/controllers/pool.py:521-556: used by an L7 policy → PoolInUseByL7Policy 409; affects every listener using it (_get_affected_listener_ids).
4.4 Members
Sub-resource of a pool: _lookup checks the pool exists (webob-format 404, title "Bad Request") and returns MemberController for an item, MembersController for the collection (octavia/api/v2/controllers/pool.py:559-583).
POST (octavia/api/v2/controllers/member.py:154-246): ip_not_reserved ([networking] reserved_ips: <IP upper exploded> is not a valid option for member address), subnet exists, quota, request_sriov needs flavor allow_member_sriov (MemberSRIOVDisabled 400), operating status NO_MONITOR without HM else OFFLINE; duplicate (pool, address, port) → 409 Another member on this pool is already using ip <a> on protocol_port <p>. PUT octavia/api/v2/controllers/member.py:297-353: name, weight, admin_state_up, backup, monitor_*, tags. DELETE octavia/api/v2/controllers/member.py:356-390. A member of another pool → 404 Member <id> not found. (octavia/api/v2/controllers/member.py:148-150).
Batch PUT /pools/{id}/members (octavia/api/v2/controllers/member.py:400-546, 202 without body). additive_only query parameter. Policies post, put, and delete unless additive. Algorithm:
- Key = (address, port). In the body and not in the DB: new (
ip_not_reserved). In both: update; the same key twice →Member must be updated only once in the same request.. In the DB and not in the body: delete, unlessadditive_only(then passed through unchanged). - All empty → logged, rollback, return 202 without driver call and with statuses unchanged.
- Quota on
new - deleted(newif additive) when positive. - New subnets validated once each. New members via
_graph_create; updates →PENDING_UPDATE; deletes →PENDING_DELETE. member_batch_update(pool_id, provider_members): the list is the target state: new + updated + (when additive) untouched; deleted ones are simply absent. **In the batch,request_sriovon an update is silently ignored**.
4.5 Health monitors
POST octavia/api/v2/controllers/health_monitor.py:210-286. PING can be disabled (allow_ping_health_monitors); pool UDP/SCTP: only UDP-CONNECT, SCTP, TCP, HTTP; UDP-CONNECT delay ≥ udp_connect_min_interval_health_monitor (default 3); other pools refuse UDP-CONNECT, SCTP. Non-HTTP types refuse http_method, url_path, expected_codes: http_method is not a valid option for health monitors of type TCP; HTTP(S) defaults GET, /, 200 (octavia/api/v2/controllers/health_monitor.py:129-152). domain_name requires http_version ≥ 1.1. One per pool: DuplicateHealthMonitor 409. max_retries is stored as rise_threshold, max_retries_down as fall_threshold (default 3). Member operating statuses are not changed by HM creation [inferred: not in the controller]. PUT octavia/api/v2/controllers/health_monitor.py:356-418: type not editable; delay, timeout, max_retries can't be null (None is not a valid option for delay). DELETE octavia/api/v2/controllers/health_monitor.py:421-450.
4.6 L7 policies and rules
Policy POST octavia/api/v2/controllers/l7policy.py:128-200: listener exists; PROMETHEUS parent refused; redirect_pool_id exists and is protocol-compatible; sanitize_l7policy_api_args makes action and arguments consistent (octavia/common/validate.py:250-330): REJECT clears redirects, REDIRECT_TO_URL, REDIRECT_PREFIX, REDIRECT_TO_POOL need their argument (redirect_url must not be None…), pool and URL together → Cannot specify redirect_pool_id and redirect_url or redirect_prefix at the same time; and the argument **overrides action**. redirect_http_code defaults to 302 for URL and prefix. position 1..2147483647 (default last): the list is renumbered from 1 without holes, a too-large position means last (octavia/db/repositories.py:1920-1960, octavia/db/models.py:597). PUT octavia/api/v2/controllers/l7policy.py:223-299; a moved position renumbers siblings (octavia/db/repositories.py:1905-1916). DELETE octavia/api/v2/controllers/l7policy.py:302-337.
Rule (under /l7policies/{id}/rules, _lookup as members): POST octavia/api/v2/controllers/l7rule.py:139-201, PUT octavia/api/v2/controllers/l7rule.py:216-283 merges the row with the body and validates the result, DELETE octavia/api/v2/controllers/l7rule.py:286-324. Maximum 50 rules per policy: TooManyL7RulesOnL7Policy 409 Too many rules on L7 policy <id> (octavia/common/constants.py:270). validate.l7rule_data (octavia/common/validate.py:129-247), wrapped in L7RuleValidation 400 Error parsing L7Rule: Invalid L7 Rule: <msg>; header and cookie regexes in octavia/common/constants.py (HTTP_HEADER_NAME_REGEX…). In the OVN provider, all L7 calls are NotImplementedError → 501 after the same validation.
4.7 Amphorae (read mostly; empty in OpenKeel)
Request when no amphora exists [probe, _rewrite/amphora-empty_stable-2026.2.json] | Status | Body |
|---|---|---|
GET /v2/octavia/amphorae | 200 | {"amphorae": [], "amphorae_links": []} |
GET /v2/octavia/amphorae/{id} | 404 | faultstring: Amphora <id> not found. |
DELETE /v2/octavia/amphorae/{id} | 404 | same (octavia/api/v2/controllers/amphora.py:101-106) |
PUT …/{id}/failover, PUT …/{id}/config | 404 | same |
GET …/{id}/stats | 404 | faultstring: Amphora stats for <id> not found. (octavia/api/v2/controllers/amphora.py:229-231) |
The policy is checked first on list, DELETE and stats (target: the caller's own project), after the lookup on GET one, failover and config: under keystone, a non-admin sees 403 on list, DELETE and stats, and 404 elsewhere for an unknown id (octavia/api/v2/controllers/amphora.py:50-58, octavia/api/v2/controllers/amphora.py:73-74, octavia/api/v2/controllers/amphora.py:98-99, octavia/api/v2/controllers/amphora.py:150-155). With amphorae present these routes cast RPC to the worker, out of scope; the Go API serves only the table above. Amphora rows filter project_id through the LB (octavia/api/common/pagination.py:253-254, octavia/api/common/pagination.py:292-294).
4.8 Quotas
GET /quotas lists explicit records only (not defaults); GET /quotas/{project} merges the record with [quotas] default_*_quota (default -1, unlimited) (octavia/api/v2/controllers/base.py:163-203, octavia/common/config.py:834-852). GET /quotas/{project}/default is config only. PUT (202): body fields loadbalancer/load_balancer, healthmonitor/health_monitor, listener, member, pool, l7policy, l7rule, range -1..2000000000; null means "use the default". DELETE (202 with body) nulls the limits, keeps in_use_* (404 if no record, octavia/db/repositories.py:2014-2030). Responses carry both spellings (loadbalancer and load_balancer; healthmonitor and health_monitor).
Accounting (octavia/db/repositories.py:438-624): check_quota_met runs inside the create transaction: skipped under noauth; ensure_project_exists commits a blank record in its own transaction with deadlock retries (octavia/db/repositories.py:2005-2012); SELECT … FOR UPDATE on the project's row; limit = record or config default; if in_use_x is 0 or NULL, it is recomputed by COUNT(*) of non-DELETED rows ("upgrade case"); else in_use + count; allowed if <= limit or limit = -1, then stored. DBDeadlock → ProjectBusyException 503. The counter is decremented only by a provider status DELETED (03 § 2.3): delete-then-create immediately may be refused while the provider has not reported.
4.9 Providers
GET /providers lists [api_settings] enabled_provider_drivers as {name, description}, minus drivers that failed to load at start-up (octavia/api/v2/controllers/provider.py:39-53, octavia/api/app.py:45-60). GET /providers/{p}/flavor_capabilities, …/availability_zone_capabilities: get_supported_*_metadata() as {name, description} list, filters name, description (exact), fields; NotImplementedError → 501 (octavia/api/v2/controllers/provider.py:83-120, octavia/api/v2/controllers/provider.py:133-172). A direct library exception bypasses call_provider here: other driver exceptions → 500 with their text.
4.10 Flavors, flavor profiles, availability zones, AZ profiles
Administrator-managed catalogues with the same shape.
- Reads for everybody (
rule:load-balancer:read-style, see § 6); profiles admin-only. - **The placeholder rows with id or name
00000000-0000-0000-0000-000000000000are hidden from every list and refused as id** with 404 (octavia/db/repositories.py:2033-2097,octavia/api/v2/controllers/flavors.py:48-49). - Flavor: POST takes an existing
flavor_profile_id(FK; a bad id → 500 from the DB error, no explicit validation,octavia/api/v2/controllers/flavors.py:93;nameunique →RecordAlreadyExists409A flavor of <name> already exists.). PUT: onlyname,description,enabled. DELETE (serialisable transaction, 5 deadlock retries): re-pointsDELETEDLBs to the NIL id, then deletes; FK from live LBs →ObjectInUse409Flavor <id> is in use and cannot be modified.(octavia/api/v2/controllers/flavors.py:147-176,octavia/db/repositories.py:2119-2135). NotePUTwith the Python argument namedidandDELETEwithflavor_id: invisible for clients. - Flavor profile:
flavor_datamust be JSON (<value> is not a valid option for flavor_data),validate_flavoron the driver; PUT refuses changingflavor_dataorprovider_namewhile in use;nullrefused (None is not a valid option for name); DELETE refused while used (octavia/api/v2/controllers/flavor_profiles.py:94-237). Theflavor_datais returned as a string. - AZ: key is
name(not a UUID),availability_zone_profile_id; PUT onlydescription,enabled; DELETE same as flavor (octavia/api/v2/controllers/availability_zones.py:175-206,octavia/db/repositories.py:2177-2197). - AZ profile: same as flavor profile; a bad JSON on PUT reports
flavor_data, notavailability_zone_data(octavia/api/v2/controllers/availability_zone_profiles.py:190-192), a client-visible typo to keep.
5. Field tables
Generated from the WSME classes (_rewrite/request-schemas_stable-2026.2.json). uuid, ip, cidr, url, url-path are the validators of octavia/api/common/types.py.
Load balancer
**LoadBalancerPOST** (octavia/api/v2/types/load_balancer.py:139)
| Field | Type | Req. | Default |
|---|---|---|---|
additional_vips | [AdditionalVipsType] | [] | |
admin_state_up | boolean | true | |
availability_zone | string ≤255 | ||
description | string ≤255 | ||
flavor_id | uuid | ||
listeners | [ListenerSingleCreate] | [] | |
name | string ≤255 | ||
pools | [PoolSingleCreate] | [] | |
project_id | string ≤36 | ||
provider | string ≤64 | ||
tags | [string ≤255] | ||
tenant_id | string ≤36 | ||
vip_address | ip | ||
vip_network_id | uuid | ||
vip_port_id | uuid | ||
vip_qos_policy_id | uuid | ||
vip_sg_ids | [uuid] | ||
vip_subnet_id | uuid |
API → DB names: admin_state_up→enabled, vip_address→vip.ip_address, vip_network_id→vip.network_id, vip_port_id→vip.port_id, vip_qos_policy_id→vip.qos_policy_id, vip_sg_ids→vip.sg_ids, vip_subnet_id→vip.subnet_id, vip_vnic_type→vip.vnic_type
**LoadBalancerPUT** (octavia/api/v2/types/load_balancer.py:166)
| Field | Type | Req. | Default |
|---|---|---|---|
admin_state_up | boolean | ||
description | string ≤255 | ||
name | string ≤255 | ||
tags | [string ≤255] | ||
vip_qos_policy_id | uuid | ||
vip_sg_ids | [uuid] |
API → DB names: admin_state_up→enabled, vip_address→vip.ip_address, vip_network_id→vip.network_id, vip_port_id→vip.port_id, vip_qos_policy_id→vip.qos_policy_id, vip_sg_ids→vip.sg_ids, vip_subnet_id→vip.subnet_id, vip_vnic_type→vip.vnic_type
**AdditionalVipsType** (octavia/api/v2/types/load_balancer.py:41)
| Field | Type | Req. | Default |
|---|---|---|---|
ip_address | ip | ||
port_id | uuid | ||
subnet_id | uuid | yes |
**LoadBalancerResponse** (octavia/api/v2/types/load_balancer.py:48)
| Field | Type | Req. | Default |
|---|---|---|---|
additional_vips | [AdditionalVipsType] | ||
admin_state_up | boolean | ||
availability_zone | string | ||
created_at | date-time | ||
description | string | ||
flavor_id | uuid | ||
id | uuid | ||
listeners | [IdOnlyType] | ||
name | string | ||
operating_status | string | ||
pools | [IdOnlyType] | ||
project_id | string | ||
provider | string | ||
provisioning_status | string | ||
tags | [string] | ||
tenant_id | string ≤36 | ||
updated_at | date-time | ||
vip_address | ip | ||
vip_network_id | uuid | ||
vip_port_id | uuid | ||
vip_qos_policy_id | uuid | ||
vip_sg_ids | [uuid] | ||
vip_subnet_id | uuid | ||
vip_vnic_type | string |
API → DB names: admin_state_up→enabled, vip_address→vip.ip_address, vip_network_id→vip.network_id, vip_port_id→vip.port_id, vip_qos_policy_id→vip.qos_policy_id, vip_sg_ids→vip.sg_ids, vip_subnet_id→vip.subnet_id, vip_vnic_type→vip.vnic_type
**LoadBalancerStatisticsResponse** (octavia/api/v2/types/load_balancer.py:212)
| Field | Type | Req. | Default |
|---|---|---|---|
active_connections | integer | ||
bytes_in | integer | ||
bytes_out | integer | ||
request_errors | integer | ||
total_connections | integer |
API → DB names: admin_state_up→enabled, vip_address→vip.ip_address, vip_network_id→vip.network_id, vip_port_id→vip.port_id, vip_qos_policy_id→vip.qos_policy_id, vip_sg_ids→vip.sg_ids, vip_subnet_id→vip.subnet_id, vip_vnic_type→vip.vnic_type
Listener
**ListenerPOST** (octavia/api/v2/types/listener.py:119)
| Field | Type | Req. | Default |
|---|---|---|---|
admin_state_up | boolean | true | |
allowed_cidrs | [cidr] | ||
alpn_protocols | [string] | ||
client_authentication | MANDATORY | NONE | OPTIONAL | "NONE" | |
client_ca_tls_container_ref | string ≤255 | ||
client_crl_container_ref | string ≤255 | ||
connection_limit | integer [-1..] | -1 | |
default_pool | PoolSingleCreate | ||
default_pool_id | uuid | ||
default_tls_container_ref | string ≤255 | ||
description | string ≤255 | ||
hsts_include_subdomains | boolean | false | |
hsts_max_age | integer [0..] | ||
hsts_preload | boolean | false | |
insert_headers | map | ||
l7policies | [L7PolicySingleCreate] | [] | |
loadbalancer_id | uuid | yes | |
name | string ≤255 | ||
project_id | string ≤36 | ||
protocol | HTTP | HTTPS | PROMETHEUS | SCTP | TCP | TERMINATED_HTTPS | UDP | yes | |
protocol_port | integer [1..65535] | yes | |
sni_container_refs | [string ≤255] | ||
tags | [string ≤255] | ||
tenant_id | string ≤36 | ||
timeout_client_data | integer [0..2073600000] | ||
timeout_member_connect | integer [0..2073600000] | ||
timeout_member_data | integer [0..2073600000] | ||
timeout_tcp_inspect | integer [0..2073600000] | ||
tls_ciphers | string ≤2048 | ||
tls_versions | [string ≤32] |
API → DB names: admin_state_up→enabled, client_ca_tls_container_ref→client_ca_tls_certificate_id, client_crl_container_ref→client_crl_container_id, default_tls_container_ref→tls_certificate_id, sni_container_refs→sni_containers
**ListenerPUT** (octavia/api/v2/types/listener.py:177)
| Field | Type | Req. | Default |
|---|---|---|---|
admin_state_up | boolean | ||
allowed_cidrs | [cidr] | ||
alpn_protocols | [string] | ||
client_authentication | MANDATORY | NONE | OPTIONAL | ||
client_ca_tls_container_ref | string ≤255 | ||
client_crl_container_ref | string ≤255 | ||
connection_limit | integer [-1..] | ||
default_pool_id | uuid | ||
default_tls_container_ref | string ≤255 | ||
description | string ≤255 | ||
hsts_include_subdomains | boolean | ||
hsts_max_age | integer [0..] | ||
hsts_preload | boolean | ||
insert_headers | map | ||
name | string ≤255 | ||
sni_container_refs | [string ≤255] | ||
tags | [string ≤255] | ||
timeout_client_data | integer [0..2073600000] | ||
timeout_member_connect | integer [0..2073600000] | ||
timeout_member_data | integer [0..2073600000] | ||
timeout_tcp_inspect | integer [0..2073600000] | ||
tls_ciphers | string ≤2048 | ||
tls_versions | [string ≤32] |
API → DB names: admin_state_up→enabled, client_ca_tls_container_ref→client_ca_tls_certificate_id, client_crl_container_ref→client_crl_container_id, default_tls_container_ref→tls_certificate_id, sni_container_refs→sni_containers
**ListenerResponse** (octavia/api/v2/types/listener.py:34)
| Field | Type | Req. | Default |
|---|---|---|---|
admin_state_up | boolean | ||
allowed_cidrs | [cidr] | ||
alpn_protocols | [string] | ||
client_authentication | string | ||
client_ca_tls_container_ref | string | ||
client_crl_container_ref | string | ||
connection_limit | integer | ||
created_at | date-time | ||
default_pool_id | uuid | ||
default_tls_container_ref | string | ||
description | string | ||
hsts_include_subdomains | boolean | ||
hsts_max_age | integer | ||
hsts_preload | boolean | ||
id | uuid | ||
insert_headers | map | ||
l7policies | [IdOnlyType] | ||
loadbalancers | [IdOnlyType] | ||
name | string | ||
operating_status | string | ||
project_id | string | ||
protocol | string | ||
protocol_port | integer | ||
provisioning_status | string | ||
sni_container_refs | [string] | ||
tags | [string] | ||
tenant_id | string ≤36 | ||
timeout_client_data | integer | ||
timeout_member_connect | integer | ||
timeout_member_data | integer | ||
timeout_tcp_inspect | integer | ||
tls_ciphers | string | ||
tls_versions | [string] | ||
updated_at | date-time |
API → DB names: admin_state_up→enabled, client_ca_tls_container_ref→client_ca_tls_certificate_id, client_crl_container_ref→client_crl_container_id, default_tls_container_ref→tls_certificate_id, sni_container_refs→sni_containers
**ListenerStatisticsResponse** (octavia/api/v2/types/listener.py:295)
| Field | Type | Req. | Default |
|---|---|---|---|
active_connections | integer | ||
bytes_in | integer | ||
bytes_out | integer | ||
request_errors | integer | ||
total_connections | integer |
API → DB names: admin_state_up→enabled, client_ca_tls_container_ref→client_ca_tls_certificate_id, client_crl_container_ref→client_crl_container_id, default_tls_container_ref→tls_certificate_id, sni_container_refs→sni_containers
Pool and session persistence
**PoolPOST** (octavia/api/v2/types/pool.py:161)
| Field | Type | Req. | Default |
|---|---|---|---|
admin_state_up | boolean | true | |
alpn_protocols | [string] | ||
ca_tls_container_ref | string ≤255 | ||
crl_container_ref | string ≤255 | ||
description | string ≤255 | ||
healthmonitor | HealthMonitorSingleCreate | ||
lb_algorithm | LEAST_CONNECTIONS | ROUND_ROBIN | SOURCE_IP | SOURCE_IP_PORT | yes | |
listener_id | uuid | ||
loadbalancer_id | uuid | ||
members | [MemberSingleCreate] | ||
name | string ≤255 | ||
project_id | string ≤36 | ||
protocol | HTTP | HTTPS | PROXY | PROXYV2 | SCTP | TCP | UDP | yes | |
session_persistence | SessionPersistencePOST | ||
tags | [string ≤255] | ||
tenant_id | string ≤36 | ||
tls_ciphers | string ≤2048 | ||
tls_container_ref | string ≤255 | ||
tls_enabled | boolean | false | |
tls_versions | [string ≤32] |
API → DB names: admin_state_up→enabled, ca_tls_container_ref→ca_tls_certificate_id, crl_container_ref→crl_container_id, healthmonitor→health_monitor, healthmonitor_id→health_monitor.id, tls_container_ref→tls_certificate_id
**PoolPUT** (octavia/api/v2/types/pool.py:196)
| Field | Type | Req. | Default |
|---|---|---|---|
admin_state_up | boolean | ||
alpn_protocols | [string] | ||
ca_tls_container_ref | string ≤255 | ||
crl_container_ref | string ≤255 | ||
description | string ≤255 | ||
lb_algorithm | LEAST_CONNECTIONS | ROUND_ROBIN | SOURCE_IP | SOURCE_IP_PORT | ||
name | string ≤255 | ||
session_persistence | SessionPersistencePUT | ||
tags | [string ≤255] | ||
tls_ciphers | string ≤2048 | ||
tls_container_ref | string ≤255 | ||
tls_enabled | boolean | ||
tls_versions | [string ≤32] |
API → DB names: admin_state_up→enabled, ca_tls_container_ref→ca_tls_certificate_id, crl_container_ref→crl_container_id, healthmonitor→health_monitor, healthmonitor_id→health_monitor.id, tls_container_ref→tls_certificate_id
**SessionPersistencePOST** (octavia/api/v2/types/pool.py:32)
| Field | Type | Req. | Default |
|---|---|---|---|
cookie_name | string ≤255 pattern ^[^\s,;\\]+$ | null | |
persistence_granularity | ip | null | |
persistence_timeout | integer | null | |
type | APP_COOKIE | HTTP_COOKIE | SOURCE_IP | yes |
**SessionPersistencePUT** (octavia/api/v2/types/pool.py:46)
| Field | Type | Req. | Default |
|---|---|---|---|
cookie_name | string ≤255 pattern ^[^\s,;\\]+$ | null | |
persistence_granularity | ip | null | |
persistence_timeout | integer | null | |
type | APP_COOKIE | HTTP_COOKIE | SOURCE_IP |
**PoolResponse** (octavia/api/v2/types/pool.py:70)
| Field | Type | Req. | Default |
|---|---|---|---|
admin_state_up | boolean | ||
alpn_protocols | [string] | ||
ca_tls_container_ref | string | ||
created_at | date-time | ||
crl_container_ref | string | ||
description | string | ||
healthmonitor_id | uuid | ||
id | uuid | ||
lb_algorithm | string | ||
listeners | [IdOnlyType] | ||
loadbalancers | [IdOnlyType] | ||
members | [IdOnlyType] | ||
name | string | ||
operating_status | string | ||
project_id | string | ||
protocol | string | ||
provisioning_status | string | ||
session_persistence | SessionPersistenceResponse | ||
tags | [string] | ||
tenant_id | string ≤36 | ||
tls_ciphers | string | ||
tls_container_ref | string | ||
tls_enabled | boolean | ||
tls_versions | [string] | ||
updated_at | date-time |
API → DB names: admin_state_up→enabled, ca_tls_container_ref→ca_tls_certificate_id, crl_container_ref→crl_container_id, healthmonitor→health_monitor, healthmonitor_id→health_monitor.id, tls_container_ref→tls_certificate_id
Member
**MemberPOST** (octavia/api/v2/types/member.py:63)
| Field | Type | Req. | Default |
|---|---|---|---|
address | ip | yes | |
admin_state_up | boolean | true | |
backup | boolean | false | |
monitor_address | ip | null | |
monitor_port | integer [1..65535] | null | |
name | string ≤255 | ||
project_id | string ≤36 | ||
protocol_port | integer [1..65535] | yes | |
request_sriov | boolean | false | |
subnet_id | uuid | ||
tags | [string ≤255] | ||
tenant_id | string ≤36 | ||
weight | integer [0..256] | 1 |
API → DB names: address→ip_address, admin_state_up→enabled
**MemberPUT** (octavia/api/v2/types/member.py:90)
| Field | Type | Req. | Default |
|---|---|---|---|
admin_state_up | boolean | ||
backup | boolean | ||
monitor_address | ip | ||
monitor_port | integer [1..65535] | ||
name | string ≤255 | ||
tags | [string ≤255] | ||
weight | integer [0..256] |
API → DB names: address→ip_address, admin_state_up→enabled
**MembersRootPUT** (octavia/api/v2/types/member.py:107)
| Field | Type | Req. | Default |
|---|---|---|---|
members | [MemberPOST] |
**MemberResponse** (octavia/api/v2/types/member.py:27)
| Field | Type | Req. | Default |
|---|---|---|---|
address | ip | ||
admin_state_up | boolean | ||
backup | boolean | ||
created_at | date-time | ||
id | uuid | ||
monitor_address | ip | ||
monitor_port | integer | ||
name | string | ||
operating_status | string | ||
project_id | string | ||
protocol_port | integer | ||
provisioning_status | string | ||
subnet_id | uuid | ||
tags | [string] | ||
tenant_id | string ≤36 | ||
updated_at | date-time | ||
vnic_type | string | ||
weight | integer |
API → DB names: address→ip_address, admin_state_up→enabled
Health monitor
**HealthMonitorPOST** (octavia/api/v2/types/health_monitor.py:76)
| Field | Type | Req. | Default |
|---|---|---|---|
admin_state_up | boolean | true | |
delay | integer [0..] | yes | |
domain_name | string ≤255 ≥1 pattern ^(?=.{1,253}\.?$)(?:(?!-|[^.]+_)[A-Za-z0-9-_]{1,63}(?<!-)(?:\.|$))+$ | ||
expected_codes | string pattern ^(\d{3}(\s*,\s*\d{3})*)$|^(\d{3}-\d{3})$ | ||
http_method | CONNECT | DELETE | GET | HEAD | OPTIONS | PATCH | POST | PUT | TRACE | ||
http_version | 1.0 | 1.1 | ||
max_retries | integer [1..10] | yes | |
max_retries_down | integer [1..10] | 3 | |
name | string ≤255 | ||
pool_id | uuid | yes | |
project_id | string ≤36 | ||
tags | [string ≤255] | ||
tenant_id | string ≤36 | ||
timeout | integer [0..] | yes | |
type | HTTP | HTTPS | PING | SCTP | TCP | TLS-HELLO | UDP-CONNECT | yes | |
url_path | url-path |
API → DB names: admin_state_up→enabled, max_retries→rise_threshold, max_retries_down→fall_threshold
**HealthMonitorPUT** (octavia/api/v2/types/health_monitor.py:114)
| Field | Type | Req. | Default |
|---|---|---|---|
admin_state_up | boolean | ||
delay | integer [0..] | ||
domain_name | string ≤255 ≥1 pattern ^(?=.{1,253}\.?$)(?:(?!-|[^.]+_)[A-Za-z0-9-_]{1,63}(?<!-)(?:\.|$))+$ | ||
expected_codes | string pattern ^(\d{3}(\s*,\s*\d{3})*)$|^(\d{3}-\d{3})$ | ||
http_method | CONNECT | DELETE | GET | HEAD | OPTIONS | PATCH | POST | PUT | TRACE | ||
http_version | 1.0 | 1.1 | ||
max_retries | integer [1..10] | ||
max_retries_down | integer [1..10] | ||
name | string ≤255 | ||
tags | [string ≤255] | ||
timeout | integer [0..] | ||
url_path | url-path |
API → DB names: admin_state_up→enabled, max_retries→rise_threshold, max_retries_down→fall_threshold
**HealthMonitorResponse** (octavia/api/v2/types/health_monitor.py:28)
| Field | Type | Req. | Default |
|---|---|---|---|
admin_state_up | boolean | ||
created_at | date-time | ||
delay | integer | ||
domain_name | string | ||
expected_codes | string | ||
http_method | string | ||
http_version | number | ||
id | uuid | ||
max_retries | integer | ||
max_retries_down | integer | ||
name | string | ||
operating_status | string | ||
pools | [IdOnlyType] | ||
project_id | string | ||
provisioning_status | string | ||
tags | [string] | ||
tenant_id | string ≤36 | ||
timeout | integer | ||
type | string | ||
updated_at | date-time | ||
url_path | string |
API → DB names: admin_state_up→enabled, max_retries→rise_threshold, max_retries_down→fall_threshold
L7 policy and rule
**L7PolicyPOST** (octavia/api/v2/types/l7policy.py:84)
| Field | Type | Req. | Default |
|---|---|---|---|
action | REDIRECT_PREFIX | REDIRECT_TO_POOL | REDIRECT_TO_URL | REJECT | yes | |
admin_state_up | boolean | true | |
description | string ≤255 | ||
listener_id | uuid | yes | |
name | string ≤255 | ||
position | integer [1..2147483647] | 2147483647 | |
project_id | string ≤36 | ||
redirect_http_code | 301 | 302 | 303 | 307 | 308 | ||
redirect_pool_id | uuid | ||
redirect_prefix | url | ||
redirect_url | url | ||
rules | [L7RuleSingleCreate] | ||
tags | [string ≤255] | ||
tenant_id | string ≤36 |
API → DB names: admin_state_up→enabled
**L7PolicyPUT** (octavia/api/v2/types/l7policy.py:112)
| Field | Type | Req. | Default |
|---|---|---|---|
action | REDIRECT_PREFIX | REDIRECT_TO_POOL | REDIRECT_TO_URL | REJECT | ||
admin_state_up | boolean | ||
description | string ≤255 | ||
name | string ≤255 | ||
position | integer [1..2147483647] | ||
redirect_http_code | 301 | 302 | 303 | 307 | 308 | ||
redirect_pool_id | uuid | ||
redirect_prefix | url | ||
redirect_url | url | ||
tags | [string ≤255] |
API → DB names: admin_state_up→enabled
**L7PolicyResponse** (octavia/api/v2/types/l7policy.py:28)
| Field | Type | Req. | Default |
|---|---|---|---|
action | string | ||
admin_state_up | boolean | ||
created_at | date-time | ||
description | string | ||
id | uuid | ||
listener_id | uuid | ||
name | string | ||
operating_status | string | ||
position | integer | ||
project_id | string | ||
provisioning_status | string | ||
redirect_http_code | integer | ||
redirect_pool_id | uuid | ||
redirect_prefix | string | ||
redirect_url | string | ||
rules | [IdOnlyType] | ||
tags | [string] | ||
tenant_id | string ≤36 | ||
updated_at | date-time |
API → DB names: admin_state_up→enabled
**L7RulePOST** (octavia/api/v2/types/l7rule.py:58)
| Field | Type | Req. | Default |
|---|---|---|---|
admin_state_up | boolean | true | |
compare_type | CONTAINS | ENDS_WITH | EQUAL_TO | REGEX | STARTS_WITH | yes | |
invert | boolean | false | |
key | string ≤255 pattern ^[^\r\n]*$ | ||
project_id | string ≤36 | ||
tags | [string ≤255] | ||
tenant_id | string ≤36 | ||
type | COOKIE | FILE_TYPE | HEADER | HOST_NAME | PATH | SSL_CONN_HAS_CERT | SSL_DN_FIELD | SSL_VERIFY_RESULT | yes | |
value | string ≤255 pattern ^[^\r\n]*$ | yes |
API → DB names: admin_state_up→enabled
**L7RulePUT** (octavia/api/v2/types/l7rule.py:82)
| Field | Type | Req. | Default |
|---|---|---|---|
admin_state_up | boolean | ||
compare_type | CONTAINS | ENDS_WITH | EQUAL_TO | REGEX | STARTS_WITH | ||
invert | boolean | ||
key | string ≤255 pattern ^[^\r\n]*$ | ||
tags | [string ≤255] | ||
type | COOKIE | FILE_TYPE | HEADER | HOST_NAME | PATH | SSL_CONN_HAS_CERT | SSL_DN_FIELD | SSL_VERIFY_RESULT | ||
value | string ≤255 pattern ^[^\r\n]*$ |
API → DB names: admin_state_up→enabled
**L7RuleResponse** (octavia/api/v2/types/l7rule.py:26)
| Field | Type | Req. | Default |
|---|---|---|---|
admin_state_up | boolean | ||
compare_type | string | ||
created_at | date-time | ||
id | uuid | ||
invert | boolean | ||
key | string | ||
operating_status | string | ||
project_id | string | ||
provisioning_status | string | ||
tags | [string] | ||
tenant_id | string ≤36 | ||
type | string | ||
updated_at | date-time | ||
value | string |
API → DB names: admin_state_up→enabled
Quota
**QuotaPUT** (octavia/api/v2/types/quotas.py:60)
| Field | Type | Req. | Default |
|---|---|---|---|
health_monitor | integer [-1..2000000000] | ||
healthmonitor | integer [-1..2000000000] | ||
l7policy | integer [-1..2000000000] | ||
l7rule | integer [-1..2000000000] | ||
listener | integer [-1..2000000000] | ||
load_balancer | integer [-1..2000000000] | ||
loadbalancer | integer [-1..2000000000] | ||
member | integer [-1..2000000000] | ||
pool | integer [-1..2000000000] |
API → DB names: healthmonitor→health_monitor, loadbalancer→load_balancer
**QuotaResponse** (octavia/api/v2/types/quotas.py:27)
| Field | Type | Req. | Default |
|---|---|---|---|
health_monitor | integer [-1..2000000000] | ||
healthmonitor | integer [-1..2000000000] | ||
l7policy | integer [-1..2000000000] | ||
l7rule | integer [-1..2000000000] | ||
listener | integer [-1..2000000000] | ||
load_balancer | integer [-1..2000000000] | ||
loadbalancer | integer [-1..2000000000] | ||
member | integer [-1..2000000000] | ||
pool | integer [-1..2000000000] | ||
project_id | string | ||
tenant_id | string ≤36 |
API → DB names: healthmonitor→health_monitor, loadbalancer→load_balancer
Flavor and profile
**FlavorPOST** (octavia/api/v2/types/flavors.py:43)
| Field | Type | Req. | Default |
|---|---|---|---|
description | string ≤255 | ||
enabled | boolean | true | |
flavor_profile_id | uuid | yes | |
name | string ≤255 | yes |
**FlavorPUT** (octavia/api/v2/types/flavors.py:55)
| Field | Type | Req. | Default |
|---|---|---|---|
description | string ≤255 | ||
enabled | boolean | ||
name | string ≤255 |
**FlavorResponse** (octavia/api/v2/types/flavors.py:25)
| Field | Type | Req. | Default |
|---|---|---|---|
description | string | ||
enabled | boolean | ||
flavor_profile_id | string | ||
id | uuid | ||
name | string |
**FlavorProfilePOST** (octavia/api/v2/types/flavor_profile.py:42)
| Field | Type | Req. | Default |
|---|---|---|---|
flavor_data | string ≤4096 | yes | |
name | string ≤255 | yes | |
provider_name | string ≤255 | yes |
**FlavorProfilePUT** (octavia/api/v2/types/flavor_profile.py:55)
| Field | Type | Req. | Default |
|---|---|---|---|
flavor_data | string ≤4096 | ||
name | string ≤255 | ||
provider_name | string ≤255 |
**FlavorProfileResponse** (octavia/api/v2/types/flavor_profile.py:25)
| Field | Type | Req. | Default |
|---|---|---|---|
flavor_data | string | ||
id | uuid | ||
name | string | ||
provider_name | string |
Availability zone and profile
**AvailabilityZonePOST** (octavia/api/v2/types/availability_zones.py:42)
| Field | Type | Req. | Default |
|---|---|---|---|
availability_zone_profile_id | uuid | yes | |
description | string ≤255 | ||
enabled | boolean | true | |
name | string ≤255 | yes |
**AvailabilityZonePUT** (octavia/api/v2/types/availability_zones.py:55)
| Field | Type | Req. | Default |
|---|---|---|---|
description | string ≤255 | ||
enabled | boolean |
**AvailabilityZoneResponse** (octavia/api/v2/types/availability_zones.py:25)
| Field | Type | Req. | Default |
|---|---|---|---|
availability_zone_profile_id | string | ||
description | string | ||
enabled | boolean | ||
name | string |
**AvailabilityZoneProfilePOST** (octavia/api/v2/types/availability_zone_profile.py:43)
| Field | Type | Req. | Default |
|---|---|---|---|
availability_zone_data | string ≤4096 | yes | |
name | string ≤255 | yes | |
provider_name | string ≤255 | yes |
**AvailabilityZoneProfilePUT** (octavia/api/v2/types/availability_zone_profile.py:56)
| Field | Type | Req. | Default |
|---|---|---|---|
availability_zone_data | string ≤4096 | ||
name | string ≤255 | ||
provider_name | string ≤255 |
**AvailabilityZoneProfileResponse** (octavia/api/v2/types/availability_zone_profile.py:25)
| Field | Type | Req. | Default |
|---|---|---|---|
availability_zone_data | string | ||
id | uuid | ||
name | string | ||
provider_name | string |
Amphora
**AmphoraResponse** (octavia/api/v2/types/amphora.py:25)
| Field | Type | Req. | Default |
|---|---|---|---|
cached_zone | string | ||
cert_busy | boolean | ||
cert_expiration | date-time | ||
compute_flavor | string | ||
compute_id | uuid | ||
created_at | date-time | ||
ha_ip | ip | ||
ha_port_id | uuid | ||
id | uuid | ||
image_id | uuid | ||
lb_network_ip | ip | ||
loadbalancer_id | uuid | ||
role | string | ||
status | string | ||
updated_at | date-time | ||
vrrp_id | integer | ||
vrrp_interface | string | ||
vrrp_ip | ip | ||
vrrp_port_id | uuid | ||
vrrp_priority | integer |
API → DB names: loadbalancer_id→load_balancer_id
**AmphoraStatisticsResponse** (octavia/api/v2/types/amphora.py:58)
| Field | Type | Req. | Default |
|---|---|---|---|
active_connections | integer | ||
bytes_in | integer | ||
bytes_out | integer | ||
id | uuid | ||
listener_id | uuid | ||
loadbalancer_id | uuid | ||
request_errors | integer | ||
total_connections | integer |
API → DB names: loadbalancer_id→load_balancer_id
Objects inside a graph create
**ListenerSingleCreate** (octavia/api/v2/types/listener.py:221)
| Field | Type | Req. | Default |
|---|---|---|---|
admin_state_up | boolean | true | |
allowed_cidrs | [cidr] | ||
alpn_protocols | [string] | ||
client_authentication | MANDATORY | NONE | OPTIONAL | "NONE" | |
client_ca_tls_container_ref | string ≤255 | ||
client_crl_container_ref | string ≤255 | ||
connection_limit | integer [-1..] | -1 | |
default_pool | PoolSingleCreate | ||
default_pool_id | uuid | ||
default_tls_container_ref | string ≤255 | ||
description | string ≤255 | ||
hsts_include_subdomains | boolean | false | |
hsts_max_age | integer | ||
hsts_preload | boolean | false | |
insert_headers | map | ||
l7policies | [L7PolicySingleCreate] | [] | |
name | string ≤255 | ||
protocol | HTTP | HTTPS | PROMETHEUS | SCTP | TCP | TERMINATED_HTTPS | UDP | yes | |
protocol_port | integer [1..65535] | yes | |
sni_container_refs | [string ≤255] | ||
tags | [string ≤255] | ||
timeout_client_data | integer [0..2073600000] | ||
timeout_member_connect | integer [0..2073600000] | ||
timeout_member_data | integer [0..2073600000] | ||
timeout_tcp_inspect | integer [0..2073600000] | ||
tls_ciphers | string ≤2048 | ||
tls_versions | [string ≤32] |
API → DB names: admin_state_up→enabled, client_ca_tls_container_ref→client_ca_tls_certificate_id, client_crl_container_ref→client_crl_container_id, default_tls_container_ref→tls_certificate_id, sni_container_refs→sni_containers
**PoolSingleCreate** (octavia/api/v2/types/pool.py:219)
| Field | Type | Req. | Default |
|---|---|---|---|
admin_state_up | boolean | true | |
alpn_protocols | [string] | ||
ca_tls_container_ref | string ≤255 | ||
crl_container_ref | string ≤255 | ||
description | string ≤255 | ||
healthmonitor | HealthMonitorSingleCreate | ||
lb_algorithm | LEAST_CONNECTIONS | ROUND_ROBIN | SOURCE_IP | SOURCE_IP_PORT | ||
members | [MemberSingleCreate] | ||
name | string ≤255 | ||
protocol | HTTP | HTTPS | PROXY | PROXYV2 | SCTP | TCP | UDP | ||
session_persistence | SessionPersistencePOST | ||
tags | [string ≤255] | ||
tls_ciphers | string ≤2048 | ||
tls_container_ref | string ≤255 | ||
tls_enabled | boolean | false | |
tls_versions | [string ≤32] |
API → DB names: admin_state_up→enabled, ca_tls_container_ref→ca_tls_certificate_id, crl_container_ref→crl_container_id, healthmonitor→health_monitor, healthmonitor_id→health_monitor.id, tls_container_ref→tls_certificate_id
**MemberSingleCreate** (octavia/api/v2/types/member.py:111)
| Field | Type | Req. | Default |
|---|---|---|---|
address | ip | yes | |
admin_state_up | boolean | true | |
backup | boolean | false | |
monitor_address | ip | ||
monitor_port | integer [1..65535] | ||
name | string ≤255 | ||
protocol_port | integer [1..65535] | yes | |
request_sriov | boolean | false | |
subnet_id | uuid | ||
tags | [string ≤255] | ||
weight | integer [0..256] | 1 |
API → DB names: address→ip_address, admin_state_up→enabled
**HealthMonitorSingleCreate** (octavia/api/v2/types/health_monitor.py:143)
| Field | Type | Req. | Default |
|---|---|---|---|
admin_state_up | boolean | true | |
delay | integer [0..] | yes | |
domain_name | string ≤255 ≥1 pattern ^(?=.{1,253}\.?$)(?:(?!-|[^.]+_)[A-Za-z0-9-_]{1,63}(?<!-)(?:\.|$))+$ | ||
expected_codes | string pattern ^(\d{3}(\s*,\s*\d{3})*)$|^(\d{3}-\d{3})$ | ||
http_method | CONNECT | DELETE | GET | HEAD | OPTIONS | PATCH | POST | PUT | TRACE | ||
http_version | 1.0 | 1.1 | ||
max_retries | integer [1..10] | yes | |
max_retries_down | integer [1..10] | 3 | |
name | string ≤255 | ||
tags | [string ≤255] | ||
timeout | integer [0..] | yes | |
type | HTTP | HTTPS | PING | SCTP | TCP | TLS-HELLO | UDP-CONNECT | yes | |
url_path | url-path |
API → DB names: admin_state_up→enabled, max_retries→rise_threshold, max_retries_down→fall_threshold
**L7PolicySingleCreate** (octavia/api/v2/types/l7policy.py:134)
| Field | Type | Req. | Default |
|---|---|---|---|
action | REDIRECT_PREFIX | REDIRECT_TO_POOL | REDIRECT_TO_URL | REJECT | yes | |
admin_state_up | boolean | true | |
description | string ≤255 | ||
name | string ≤255 | ||
position | integer [1..2147483647] | 2147483647 | |
redirect_http_code | 301 | 302 | 303 | 307 | 308 | ||
redirect_pool | PoolSingleCreate | ||
redirect_prefix | url | ||
redirect_url | url | ||
rules | [L7RuleSingleCreate] | ||
tags | [string ≤255] |
API → DB names: admin_state_up→enabled
**L7RuleSingleCreate** (octavia/api/v2/types/l7rule.py:103)
| Field | Type | Req. | Default |
|---|---|---|---|
admin_state_up | boolean | true | |
compare_type | CONTAINS | ENDS_WITH | EQUAL_TO | REGEX | STARTS_WITH | yes | |
invert | boolean | false | |
key | string ≤255 | ||
tags | [string ≤255] | ||
type | COOKIE | FILE_TYPE | HEADER | HOST_NAME | PATH | SSL_CONN_HAS_CERT | SSL_DN_FIELD | SSL_VERIFY_RESULT | yes | |
value | string ≤255 | yes |
API → DB names: admin_state_up→enabled
6. Policies
97 default rules: 81 operation rules on the API and 16 base rules (octavia/policies/__init__.py:23-27). The rule evaluated by each route is in § 2; _rewrite/policies_stable-2026.2.json, .yaml have them in machine form.
| Rule | Default check string | Description |
|---|---|---|
load-balancer:read | rule:load-balancer:observer_and_owner or rule:load-balancer:global_observer or rule:load-balancer:member_and_owner or rule:load-balancer:admin | |
load-balancer:read-global | rule:load-balancer:global_observer or rule:load-balancer:admin | |
load-balancer:write | rule:load-balancer:member_and_owner or rule:load-balancer:admin | |
load-balancer:read-quota | rule:load-balancer:observer_and_owner or rule:load-balancer:global_observer or rule:load-balancer:member_and_owner or rule:load-balancer:quota-admin or rule:load-balancer:admin | |
load-balancer:read-quota-global | rule:load-balancer:global_observer or rule:load-balancer:quota-admin or rule:load-balancer:admin | |
load-balancer:write-quota | rule:load-balancer:quota-admin or rule:load-balancer:admin | |
project-member | role:member and project_id:%(project_id)s | |
project-reader | role:reader and project_id:%(project_id)s | |
context_is_admin | role:admin | |
load-balancer:admin | is_admin:True or role:admin | |
service | role:service | |
load-balancer:global_observer | role:admin | |
load-balancer:member_and_owner | rule:project-member | |
load-balancer:observer_and_owner | rule:project-reader | |
load-balancer:quota-admin | role:admin | |
load-balancer:owner | project_id:%(project_id)s | |
os_load-balancer_api:flavor:get_all | rule:load-balancer:read | List Flavors |
os_load-balancer_api:flavor:post | rule:load-balancer:admin | Create a Flavor |
os_load-balancer_api:flavor:put | rule:load-balancer:admin | Update a Flavor |
os_load-balancer_api:flavor:get_one | rule:load-balancer:read | Show Flavor details |
os_load-balancer_api:flavor:delete | rule:load-balancer:admin | Remove a Flavor |
os_load-balancer_api:flavor-profile:get_all | rule:load-balancer:admin | List Flavor Profiles |
os_load-balancer_api:flavor-profile:post | rule:load-balancer:admin | Create a Flavor Profile |
os_load-balancer_api:flavor-profile:put | rule:load-balancer:admin | Update a Flavor Profile |
os_load-balancer_api:flavor-profile:get_one | rule:load-balancer:admin | Show Flavor Profile details |
os_load-balancer_api:flavor-profile:delete | rule:load-balancer:admin | Remove a Flavor Profile |
os_load-balancer_api:availability-zone:get_all | rule:load-balancer:read | List Availability Zones |
os_load-balancer_api:availability-zone:post | rule:load-balancer:admin | Create an Availability Zone |
os_load-balancer_api:availability-zone:put | rule:load-balancer:admin | Update an Availability Zone |
os_load-balancer_api:availability-zone:get_one | rule:load-balancer:read | Show Availability Zone details |
os_load-balancer_api:availability-zone:delete | rule:load-balancer:admin | Remove an Availability Zone |
os_load-balancer_api:availability-zone-profile:get_all | rule:load-balancer:admin | List Availability Zones |
os_load-balancer_api:availability-zone-profile:post | rule:load-balancer:admin | Create an Availability Zone |
os_load-balancer_api:availability-zone-profile:put | rule:load-balancer:admin | Update an Availability Zone |
os_load-balancer_api:availability-zone-profile:get_one | rule:load-balancer:admin | Show Availability Zone details |
os_load-balancer_api:availability-zone-profile:delete | rule:load-balancer:admin | Remove an Availability Zone |
os_load-balancer_api:healthmonitor:get_all | rule:load-balancer:read | List Health Monitors of a Pool |
os_load-balancer_api:healthmonitor:get_all-global | rule:load-balancer:read-global | List Health Monitors including resources owned by others |
os_load-balancer_api:healthmonitor:post | rule:load-balancer:write | Create a Health Monitor |
os_load-balancer_api:healthmonitor:get_one | rule:load-balancer:read | Show Health Monitor details |
os_load-balancer_api:healthmonitor:put | rule:load-balancer:write | Update a Health Monitor |
os_load-balancer_api:healthmonitor:delete | rule:load-balancer:write | Remove a Health Monitor |
os_load-balancer_api:l7policy:get_all | rule:load-balancer:read | List L7 Policys |
os_load-balancer_api:l7policy:get_all-global | rule:load-balancer:read-global | List L7 Policys including resources owned by others |
os_load-balancer_api:l7policy:post | rule:load-balancer:write | Create a L7 Policy |
os_load-balancer_api:l7policy:get_one | rule:load-balancer:read | Show L7 Policy details |
os_load-balancer_api:l7policy:put | rule:load-balancer:write | Update a L7 Policy |
os_load-balancer_api:l7policy:delete | rule:load-balancer:write | Remove a L7 Policy |
os_load-balancer_api:l7rule:get_all | rule:load-balancer:read | List L7 Rules |
os_load-balancer_api:l7rule:post | rule:load-balancer:write | Create a L7 Rule |
os_load-balancer_api:l7rule:get_one | rule:load-balancer:read | Show L7 Rule details |
os_load-balancer_api:l7rule:put | rule:load-balancer:write | Update a L7 Rule |
os_load-balancer_api:l7rule:delete | rule:load-balancer:write | Remove a L7 Rule |
os_load-balancer_api:listener:get_all | rule:load-balancer:read | List Listeners |
os_load-balancer_api:listener:get_all-global | rule:load-balancer:read-global | List Listeners including resources owned by others |
os_load-balancer_api:listener:post | rule:load-balancer:write | Create a Listener |
os_load-balancer_api:listener:get_one | rule:load-balancer:read | Show Listener details |
os_load-balancer_api:listener:put | rule:load-balancer:write | Update a Listener |
os_load-balancer_api:listener:delete | rule:load-balancer:write | Remove a Listener |
os_load-balancer_api:listener:get_stats | rule:load-balancer:read | Show Listener statistics |
os_load-balancer_api:loadbalancer:get_all | rule:load-balancer:read | List Load Balancers |
os_load-balancer_api:loadbalancer:get_all-global | rule:load-balancer:read-global | List Load Balancers including resources owned by others |
os_load-balancer_api:loadbalancer:post | rule:load-balancer:write | Create a Load Balancer |
os_load-balancer_api:loadbalancer:post:vip_sg_ids | rule:load-balancer:write | Create a Load Balancer with VIP Security Groups |
os_load-balancer_api:loadbalancer:get_one | rule:load-balancer:read | Show Load Balancer details |
os_load-balancer_api:loadbalancer:put | rule:load-balancer:write | Update a Load Balancer |
os_load-balancer_api:loadbalancer:put:vip_sg_ids | rule:load-balancer:write | Update the VIP Security Groups of a Load Balancer |
os_load-balancer_api:loadbalancer:delete | rule:load-balancer:write | Remove a Load Balancer |
os_load-balancer_api:loadbalancer:get_stats | rule:load-balancer:read | Show Load Balancer statistics |
os_load-balancer_api:loadbalancer:get_status | rule:load-balancer:read | Show Load Balancer status |
os_load-balancer_api:loadbalancer:put_failover | rule:load-balancer:admin | Failover a Load Balancer |
os_load-balancer_api:member:get_all | rule:load-balancer:read or rule:service | List Members of a Pool |
os_load-balancer_api:member:post | rule:load-balancer:write | Create a Member |
os_load-balancer_api:member:get_one | rule:load-balancer:read | Show Member details |
os_load-balancer_api:member:put | rule:load-balancer:write | Update a Member |
os_load-balancer_api:member:delete | rule:load-balancer:write | Remove a Member |
os_load-balancer_api:pool:get_all | rule:load-balancer:read | List Pools |
os_load-balancer_api:pool:get_all-global | rule:load-balancer:read-global | List Pools including resources owned by others |
os_load-balancer_api:pool:post | rule:load-balancer:write | Create a Pool |
os_load-balancer_api:pool:get_one | rule:load-balancer:read | Show Pool details |
os_load-balancer_api:pool:put | rule:load-balancer:write | Update a Pool |
os_load-balancer_api:pool:delete | rule:load-balancer:write | Remove a Pool |
os_load-balancer_api:provider:get_all | rule:load-balancer:read | List enabled providers |
os_load-balancer_api:quota:get_all | rule:load-balancer:read-quota | List Quotas |
os_load-balancer_api:quota:get_all-global | rule:load-balancer:read-quota-global | List Quotas including resources owned by others |
os_load-balancer_api:quota:get_one | rule:load-balancer:read-quota | Show Quota details |
os_load-balancer_api:quota:put | rule:load-balancer:write-quota | Update a Quota |
os_load-balancer_api:quota:delete | rule:load-balancer:write-quota | Reset a Quota |
os_load-balancer_api:quota:get_defaults | rule:load-balancer:read-quota | Show Default Quota for a Project |
os_load-balancer_api:amphora:get_all | rule:load-balancer:admin | List Amphorae |
os_load-balancer_api:amphora:get_one | rule:load-balancer:admin | Show Amphora details |
os_load-balancer_api:amphora:delete | rule:load-balancer:admin | Delete an Amphora |
os_load-balancer_api:amphora:put_config | rule:load-balancer:admin | Update Amphora Agent Configuration |
os_load-balancer_api:amphora:put_failover | rule:load-balancer:admin | Failover Amphora |
os_load-balancer_api:amphora:get_stats | rule:load-balancer:admin | Show Amphora statistics |
os_load-balancer_api:provider-flavor:get_all | rule:load-balancer:admin | List the provider flavor capabilities. |
os_load-balancer_api:provider-availability-zone:get_all | rule:load-balancer:admin | List the provider availability zone capabilities. |
Policy modes
Four things decide who can do what: the code defaults, oslo.policy's [oslo_policy] enforce_new_defaults and enforce_scope (both default true in the oslo.policy shipped by the 2026.2 constraints; Octavia does not set them), and a policy file.
- Code defaults:
member+ owner writes,reader+ owner reads,admineverything. **Theload-balancer_*roles grant nothing.** Quota-related admin isadmin. All rules arescope_types=[project], so system-scoped tokens are refused (InvalidScope). (octavia/policies/keystone_default_roles.py:18-82) - **
enforce_new_defaults=False**: each deprecated rule (role:load-balancer_member and rule:load-balancer:owner,role:load-balancer_observer…,load-balancer_global_observer,load-balancer_quota_admin,load-balancer_admin) is OR-ed with the new one: legacy advanced RBAC (octavia/policies/advanced_rbac.py:35-84). - **
etc/policy/octavia-advanced-rbac-policy.yaml**: only theload-balancer_*roles (andadmin). - **
etc/policy/keystone_default_roles-policy.yaml**: Keystone personas. - **
etc/policy/admin_or_owner-policy.yaml**: anyone in the project;admineverything.
is_admin:True in a check string is true when context_is_admin passes, or always under noauth (octavia/common/context.py:31-32, octavia/common/policy.py:103-104, octavia/common/policy.py:143-157).
Samples from _rewrite/rbac-matrix_stable-2026.2.json (all 81 rules × 9 personas × 5 modes, computed by the real enforcer; "own" = target in the caller's project):
code defaults (enforce_new_defaults=True, enforce_scope=True)
| Rule | admin | lb_admin | lb_global_observer | lb_member | lb_observer | lb_quota_admin | member | other_role | reader |
|---|---|---|---|---|---|---|---|---|---|
loadbalancer:get_one | own+other | — | — | — | — | — | own | — | own |
loadbalancer:get_all-global | own+other | — | — | — | — | — | — | — | — |
loadbalancer:post | own+other | — | — | — | — | — | own | — | — |
quota:get_one | own+other | — | — | — | — | — | own | — | own |
quota:put | own+other | — | — | — | — | — | — | — | — |
flavor:get_all | own+other | — | — | — | — | — | own | — | own |
flavor:post | own+other | — | — | — | — | — | — | — | — |
amphora:get_all | own+other | — | — | — | — | — | — | — | — |
code defaults with [oslo_policy] enforce_new_defaults=False (deprecated rules OR-ed)
| Rule | admin | lb_admin | lb_global_observer | lb_member | lb_observer | lb_quota_admin | member | other_role | reader |
|---|---|---|---|---|---|---|---|---|---|
loadbalancer:get_one | own+other | own+other | own+other | own | own | — | own | — | own |
loadbalancer:get_all-global | own+other | own+other | own+other | — | — | — | — | — | — |
loadbalancer:post | own+other | own+other | — | own | — | — | own | — | — |
quota:get_one | own+other | own+other | own+other | own | own | own+other | own | — | own |
quota:put | own+other | own+other | — | — | — | own+other | — | — | — |
flavor:get_all | own+other | own+other | own+other | own | own | — | own | — | own |
flavor:post | own+other | own+other | — | — | — | — | — | — | — |
amphora:get_all | own+other | own+other | — | — | — | — | — | — | — |
etc/policy/admin_or_owner-policy.yaml
| Rule | admin | lb_admin | lb_global_observer | lb_member | lb_observer | lb_quota_admin | member | other_role | reader |
|---|---|---|---|---|---|---|---|---|---|
loadbalancer:get_one | own+other | own+other | own | own | own | own | own | own | own |
loadbalancer:get_all-global | own+other | own+other | — | — | — | — | — | — | — |
loadbalancer:post | own+other | own+other | own | own | own | own | own | own | own |
quota:get_one | own+other | own+other | own | own | own | own | own | own | own |
quota:put | own+other | own+other | — | — | — | — | — | — | — |
flavor:get_all | own+other | own+other | own | own | own | own | own | own | own |
flavor:post | own+other | own+other | — | — | — | — | — | — | — |
amphora:get_all | own+other | own+other | — | — | — | — | — | — | — |
etc/policy/keystone_default_roles-policy.yaml
| Rule | admin | lb_admin | lb_global_observer | lb_member | lb_observer | lb_quota_admin | member | other_role | reader |
|---|---|---|---|---|---|---|---|---|---|
loadbalancer:get_one | own+other | — | — | — | — | — | — | — | own |
loadbalancer:get_all-global | own+other | — | — | — | — | — | — | — | — |
loadbalancer:post | own+other | — | — | — | — | — | own | — | — |
quota:get_one | own+other | — | — | — | — | — | — | — | own |
quota:put | own+other | — | — | — | — | — | — | — | — |
flavor:get_all | own+other | — | — | — | — | — | — | — | own |
flavor:post | own+other | own+other | — | — | — | — | — | — | — |
amphora:get_all | own+other | own+other | — | — | — | — | — | — | — |
etc/policy/octavia-advanced-rbac-policy.yaml
| Rule | admin | lb_admin | lb_global_observer | lb_member | lb_observer | lb_quota_admin | member | other_role | reader |
|---|---|---|---|---|---|---|---|---|---|
loadbalancer:get_one | own+other | own+other | own+other | own | own | — | — | — | — |
loadbalancer:get_all-global | own+other | own+other | own+other | — | — | — | — | — | — |
loadbalancer:post | own+other | own+other | — | own | — | — | — | — | — |
quota:get_one | own+other | own+other | own+other | own | own | own+other | — | — | — |
quota:put | own+other | own+other | — | — | — | own+other | — | — | — |
flavor:get_all | own+other | own+other | own+other | own | own | — | — | — | — |
flavor:post | own+other | own+other | — | — | — | — | — | — | — |
amphora:get_all | own+other | own+other | — | — | — | — | — | — | — |
7. Lifecycles
stateDiagram-v2 [*] --> PENDING_CREATE: POST (API, in the request transaction) PENDING_CREATE --> ACTIVE: provider status message ACTIVE PENDING_CREATE --> ERROR: provider status message ERROR ACTIVE --> PENDING_UPDATE: PUT, any child write, failover (API) ERROR --> PENDING_UPDATE: failover only (LB) PENDING_UPDATE --> ACTIVE: provider status ACTIVE PENDING_UPDATE --> ERROR: provider status ERROR ACTIVE --> PENDING_DELETE: DELETE (API) ERROR --> PENDING_DELETE: DELETE (LB) PENDING_DELETE --> DELETED: provider status DELETED (LB row kept) PENDING_DELETE --> [*]: provider status DELETED (child row removed) DELETED --> [*]: housekeeping after load_balancer_expiry_age (absent from Go unless ported)
Child objects use the same states, but ERROR children can only be deleted through the parent's rules: for example a member in ERROR is writable as long as the LB and pool are ACTIVE, since the test is on the parents (octavia/api/v2/controllers/member.py:112-129). Operating status is free-form from the provider: ONLINE, OFFLINE, DEGRADED, ERROR, DRAINING, NO_MONITOR (FK to operating_status).
Source: reverse-spec-octavia/01-rest-api_stable-2026.2.md. The _rewrite/ and _inventory/ files are not published on the site.