REST API — CloudKitty — branch stable/2026.2
Branch
stable/2026.2@0685cc330e83— repository https://opendev.org/openstack/cloudkitty — analysed on 2026-11-09
1. Serving stack
| Layer (outermost first) | Behaviour | Where |
|---|---|---|
Paste urlmap | /healthcheck → oslo.middleware Healthcheck (disable_by_file /etc/cloudkitty/healthcheck_disable); / → the API pipeline | etc/cloudkitty/api_paste.ini:1-9, etc/cloudkitty/api_paste.ini:31-34 |
| Pipeline selection | cloudkitty+keystone or cloudkitty+noauth, from [DEFAULT]/auth_strategy (default keystone) | cloudkitty/api/app.py:38-42, cloudkitty/api/app.py:88-91 |
cors → http_proxy_to_wsgi → request_id → authtoken | oslo.middleware filters, then AuthTokenMiddleware, a keystonemiddleware subclass that skips authentication for the public paths /, /v1, /v2 (after stripping a trailing /) | etc/cloudkitty/api_paste.ini:11-29, cloudkitty/api/middleware.py:19-45 |
Werkzeug DispatcherMiddleware | / → Flask root app, /v1 → Pecan app, /v2 → Flask app; **/v2 is removed when [storage]/version < 2** | cloudkitty/api/app.py:58-74 |
| v1: Pecan + WSME | hooks put the RPC client, the storage driver and a RequestContext on the request | cloudkitty/api/v1/__init__.py:39-57, cloudkitty/api/v1/hooks.py:22-41 |
| v2: Flask + flask-restful | one blueprint per module (/v2/scope, /v2/dataframes, /v2/summary, /v2/task, /v2/rating); request context set before each request; JSON output through cloudkitty.utils.json | cloudkitty/api/v2/__init__.py:33-53, cloudkitty/api/v2/utils.py:285-327 |
Context and admin flag. RequestContext.is_admin is computed by the policy rule context_is_admin (role:admin) (cloudkitty/common/context.py:21-26, cloudkitty/common/policy.py:127-142, cloudkitty/common/policies/base.py:41-43). With auth_strategy = noauth every policy check passes and every caller is admin (cloudkitty/common/policy.py:100-101, cloudkitty/common/policy.py:131-132).
Policy defaults (cloudkitty/common/policies/base.py:40-75): project_reader_or_admin = role:reader and project_id:%(project_id)s or role:admin; the deprecated admin_or_owner is still the fallback of the new rules. No rule sets scope_types.
Time conventions. Datetimes without a zone are taken as UTC; defaults for begin/end are the start of the current month and the start of the next month (cloudkitty/utils/tz.py:121-150).
2. Endpoint table
Policy names are those passed to policy.authorize; admin = role:admin, reader = project_reader_or_admin, open = empty rule.
2.1 Root and v1 core
| Method | Path | Handler | Policy | Success | Errors |
|---|---|---|---|---|---|
| GET | / | cloudkitty/api/root.py:81 CloudkittyAPIRoot.get | none (public route) | 200 {"versions":[…]} | — |
| GET | /v1/rating/modules | cloudkitty/api/v1/controllers/rating.py:72 | rating:list_modules (admin) | 200 | 403 |
| GET | /v1/rating/modules/{module_id} | cloudkitty/api/v1/controllers/rating.py:91 | rating:get_module (admin) | 200 | 403, 404 |
| PUT | /v1/rating/modules/{module_id} | cloudkitty/api/v1/controllers/rating.py:112 | rating:update_module (admin) | 302 + Location | 403, 404 |
| POST | /v1/rating/quote | cloudkitty/api/v1/controllers/rating.py:192 | rating:quote (open) | 200, a JSON number | 400, 500/504 on RPC failure [inferred] |
| GET | /v1/rating/reload_modules | cloudkitty/api/v1/controllers/rating.py:213 | rating:module_config (admin) | 204 **[inferred: WSME None return]** | 403 |
| GET | /v1/collector/mappings | cloudkitty/api/v1/controllers/collector.py:53 | collector:list_mappings (admin) | 200 | 403 |
| GET | /v1/collector/mappings/{service} | cloudkitty/api/v1/controllers/collector.py:37 | collector:get_mapping (admin) | 200 | 403, 404 |
| POST | /v1/collector/mappings?collector=&service= | cloudkitty/api/v1/controllers/collector.py:70 | collector:manage_mapping (admin) | 200 | 403 |
| DELETE | /v1/collector/mappings/{service} | cloudkitty/api/v1/controllers/collector.py:86 | collector:manage_mapping (admin) | 204 | 403, 404 |
| GET | /v1/collector/states/{name} | cloudkitty/api/v1/controllers/collector.py:106 | collector:get_state (admin) | 200 | 403 |
| PUT | /v1/collector/states/{name} | cloudkitty/api/v1/controllers/collector.py:121 | collector:update_state (admin) | 200 | 403 |
| GET | /v1/info/metrics | cloudkitty/api/v1/controllers/info.py:86 | info:list_metrics_info (open) | 200 | 405 |
| GET | /v1/info/metrics/{metric_name} | cloudkitty/api/v1/controllers/info.py:94 | info:get_metric_info (open) | 200 | 404, 405 |
| GET | /v1/info/services (deprecated) | cloudkitty/api/v1/controllers/info.py:106 | info:list_metrics_info | 200 | 405 |
| GET | /v1/info/services/{name} (deprecated) | cloudkitty/api/v1/controllers/info.py:116 | info:get_metric_info | 200 | 404, 405 |
| GET | /v1/info/config | cloudkitty/api/v1/controllers/info.py:137 | info:get_config (open) | 200, the raw metric file ([collect]/metrics_conf) | — |
| GET | /v1/report/tenants | cloudkitty/api/v1/controllers/report.py:56 | report:list_tenants (admin) | 200 ["id",…] | 403 |
| GET | /v1/report/total (deprecated) | cloudkitty/api/v1/controllers/report.py:77 | report:get_total (reader, target project) | 200, a decimal | 403 |
| GET | /v1/report/summary | cloudkitty/api/v1/controllers/report.py:121 | report:get_summary (reader, target project) | 200 | 403 |
| GET | /v1/storage/dataframes | cloudkitty/api/v1/controllers/storage.py:43 | storage:list_data_frames (reader) | 200 | 403 |
/v1/billing/… is an alias of /v1/rating/…: the same RatingController class is mounted twice (cloudkitty/api/v1/controllers/__init__.py:30-32).
2.2 HashMap module (/v1/rating/module_config/hashmap)
Every request under module_config is authorised with **rating:module_config (admin)**: once by ModulesController.route (cloudkitty/api/v1/controllers/rating.py:64-69) **[inferred: this override is never called by Pecan's REST routing, which uses _route]**, and effectively by RatingRestControllerBase._route, which turns a refusal into a 403 (cloudkitty/rating/__init__.py:137-145). A module without a controller answers 409 Module is not configurable (cloudkitty/api/v1/controllers/rating.py:133-141).
| Method | Path | Handler | Success | Errors |
|---|---|---|---|---|
| GET | …/hashmap/types | cloudkitty/rating/hash/controllers/root.py:42 | 200 ["flat","rate"] | 403 |
| GET | …/hashmap/services | cloudkitty/rating/hash/controllers/service.py:36 | 200 | 403 |
| GET | …/hashmap/services/{service_id} | cloudkitty/rating/hash/controllers/service.py:52 | 200 | 400 (bad UUID), 404 |
| POST | …/hashmap/services | cloudkitty/rating/hash/controllers/service.py:67 | 201 + Location | 409 |
| DELETE | …/hashmap/services/{service_id} | cloudkitty/rating/hash/controllers/service.py:85 | 204 (cascade) | 404 |
| GET | …/hashmap/fields?service_id= | cloudkitty/rating/hash/controllers/field.py:33 | 200 | 400 |
| GET | …/hashmap/fields/{field_id} | cloudkitty/rating/hash/controllers/field.py:52 | 200 | 404 |
| POST | …/hashmap/fields | cloudkitty/rating/hash/controllers/field.py:67 | 201 + Location | 400 (also for an unknown service: NoSuchService is a ClientHashMapError, cloudkitty/rating/hash/db/api.py:42), 409 |
| DELETE | …/hashmap/fields/{field_id} | cloudkitty/rating/hash/controllers/field.py:91 | 204 (cascade) | 404 |
| GET | …/hashmap/groups | cloudkitty/rating/hash/controllers/group.py:70 | 200 | — |
| GET | …/hashmap/groups/{group_id} | cloudkitty/rating/hash/controllers/group.py:87 | 200 | 404 |
| POST | …/hashmap/groups | cloudkitty/rating/hash/controllers/group.py:102 | 201 + Location | 400, 409 |
| DELETE | …/hashmap/groups/{group_id}?recursive= | cloudkitty/rating/hash/controllers/group.py:125 | 204 | 404 |
| GET | …/hashmap/groups/mappings?group_id= | cloudkitty/rating/hash/controllers/group.py:37 | 200 | — |
| GET | …/hashmap/groups/thresholds?group_id= | cloudkitty/rating/hash/controllers/group.py:54 | 200 | — |
| GET | …/hashmap/mappings (+ filters) | cloudkitty/rating/hash/controllers/mapping.py:69 | 200 | 400 (types) |
| GET | …/hashmap/mappings/{mapping_id} | cloudkitty/rating/hash/controllers/mapping.py:135 | 200 | 404 |
| POST | …/hashmap/mappings?force= | cloudkitty/rating/hash/controllers/mapping.py:152 | 201 + Location | 400, 409 |
| PUT | …/hashmap/mappings/{mapping_id} | cloudkitty/rating/hash/controllers/mapping.py:191 | 302 + Location | 400, 404, 409 |
| DELETE | …/hashmap/mappings/{mapping_id} | cloudkitty/rating/hash/controllers/mapping.py:227 | 204 (soft delete) | 404 |
| GET | …/hashmap/mappings/group?mapping_id= | cloudkitty/rating/hash/controllers/mapping.py:39 | 200 | 404 |
| GET | …/hashmap/thresholds (+ filters) | cloudkitty/rating/hash/controllers/threshold.py:57 | 200 | 400 |
| GET | …/hashmap/thresholds/{threshold_id} | cloudkitty/rating/hash/controllers/threshold.py:95 | 200 | 404 |
| POST | …/hashmap/thresholds | cloudkitty/rating/hash/controllers/threshold.py:111 | 201 + Location | 400, 409 |
| PUT | …/hashmap/thresholds/{threshold_id} | cloudkitty/rating/hash/controllers/threshold.py:141 | 302 + Location | 400, 404, 409 |
| DELETE | …/hashmap/thresholds/{threshold_id} | cloudkitty/rating/hash/controllers/threshold.py:168 | 204 (hard delete) | 404 |
| GET | …/hashmap/thresholds/group?threshold_id= | cloudkitty/rating/hash/controllers/threshold.py:36 | 200 | 404 |
The service controller also mounts fields and mappings sub-controllers (cloudkitty/rating/hash/controllers/service.py:32-33), so …/services/{id}/fields and …/services/{id}/mappings are reachable [inferred: Pecan nested REST routing passes the service id as first argument].
2.3 PyScripts module (/v1/rating/module_config/pyscripts/scripts)
| Method | Path | Handler | Success | Errors |
|---|---|---|---|---|
| GET | …/scripts (+ no_data, audit filters) | cloudkitty/rating/pyscripts/controllers/script.py:55 | 200 | 403 |
| GET | …/scripts/{script_id} | cloudkitty/rating/pyscripts/controllers/script.py:100 | 200 | 404 |
| POST | …/scripts?force= | cloudkitty/rating/pyscripts/controllers/script.py:116 | 201 + Location | 400, 409 |
| PUT | …/scripts/{script_id} | cloudkitty/rating/pyscripts/controllers/script.py:147 | 201 + Location | 400, 404 |
| DELETE | …/scripts/{script_id} | cloudkitty/rating/pyscripts/controllers/script.py:180 | 204 (soft delete) | 404 |
2.4 v2
| Method | Path | Handler | Policy | Success | Errors |
|---|---|---|---|---|---|
| GET | /v2/summary | cloudkitty/api/v2/summary/summary.py:50 | summary:get_summary (reader, own project) | 200 | 400, 403, 500 |
| GET | /v2/dataframes | cloudkitty/api/v2/dataframes/dataframes.py:65 | dataframes:get (reader) | 200 | 400, 403, 404 when empty |
| POST | /v2/dataframes | cloudkitty/api/v2/dataframes/dataframes.py:36 | dataframes:add (admin) | 204 | 400, 403 |
| GET | /v2/scope | cloudkitty/api/v2/scope/state.py:61 | scope:get_state (admin) | 200 | 400, 403, 404 |
| PUT | /v2/scope | cloudkitty/api/v2/scope/state.py:105 | scope:reset_state (admin) | 202 | 400, 403, 404 |
| PATCH | /v2/scope | cloudkitty/api/v2/scope/state.py:174 | scope:patch_state (admin) | 200 | 400, 403, 404 |
| POST | /v2/scope | cloudkitty/api/v2/scope/state.py:241 | scope:post_state (admin) | 200 | 400, 403, 404 when it already exists |
| POST | /v2/task/reprocesses | cloudkitty/api/v2/task/reprocess.py:53 | schedule:task_reprocesses (admin) | 202 | 400, 403 |
| GET | /v2/task/reprocesses | cloudkitty/api/v2/task/reprocess.py:265 | schedule:get_task_reprocesses (admin) | 200 | 400, 403 |
| GET | /v2/task/reprocesses/{path_scope_id} | cloudkitty/api/v2/task/reprocess.py:265 (subclass at cloudkitty/api/v2/task/reprocess.py:310) | same | 200 | 400, 403 |
| GET | /v2/rating/modules | cloudkitty/api/v2/rating/modules.py:107 | none (rule v2_rating:list_modules registered but unused) | 200 | — |
| GET | /v2/rating/modules/{module_id} | cloudkitty/api/v2/rating/modules.py:68 | v2_rating:get_module (admin) | 200 | 403, 404 |
| PUT | /v2/rating/modules/{module_id} | cloudkitty/api/v2/rating/modules.py:88 | v2_rating:update_module (admin) | 204 | 400, 403, 404 |
Routes are declared per module (cloudkitty/api/v2/scope/__init__.py:19-23, cloudkitty/api/v2/dataframes/__init__.py:19-23, cloudkitty/api/v2/summary/__init__.py:19-23, cloudkitty/api/v2/task/__init__.py:18-32, cloudkitty/api/v2/rating/__init__.py:19-28). POST and GET /v2/task/reprocesses are two resources registered on the same URL; Werkzeug picks the one whose methods match.
2.5 Gaps between the API reference and the code
| Gap | Evidence |
|---|---|
The v1 reference documents the metric controller under /v1/info/metric; the code mounts it at /v1/info/metrics | api-ref/source/v1/v1.rst:35-36, cloudkitty/api/v1/controllers/info.py:126-130 |
/v1/billing alias, /v1/rating/reload_modules and hashmap nested sub-controllers are not documented | cloudkitty/api/v1/controllers/__init__.py:30, cloudkitty/api/v1/controllers/rating.py:182-185 |
GET /v2/rating/modules is documented as admin-only by its policy description but is unprotected | cloudkitty/common/policies/v2/rating.py:22-26, cloudkitty/api/v2/rating/modules.py:104-118 |
3. v1 core — functional descriptions
3.1 POST /v1/rating/quote — instant price
- Purpose: price a hypothetical set of resources with the current rating rules, without storing anything.
- Actor: anyone with a valid token (rule
rating:quoteis empty,cloudkitty/common/policies/v1/rating.py:43-48). - Request:
{"resources": [{"service": "<metric name>", "desc": {...}, "volume": "<decimal>"}]}(cloudkitty/api/v1/datamodels/rating.py:29-67). - Flow (synchronous):
- The API groups the resources by
serviceand turns each into{"desc": desc, "vol": {"qty": str(volume), "unit": "undef"}}(cloudkitty/api/v1/controllers/rating.py:200-206,cloudkitty/api/v1/datamodels/rating.py:46-52). - RPC call
quote(res_data={"usage": {...}}), namespacerating, topiccloudkitty, to any processor (cloudkitty/api/v1/controllers/rating.py:209). - The processor builds a DataFrame over
[now, now + period); for each entry,groupby = desc.get('groupby', [])andmetadata = desc.get('metadata', [])(cloudkitty/orchestrator.py:145-175). - An
APIWorkerloads the enabled rating modules (no tenant) and runs theirquote()(=process()) in decreasing priority, then sums the prices (cloudkitty/orchestrator.py:268-280). - The result travels back as a string and is returned as a JSON number (
cloudkitty/orchestrator.py:180,cloudkitty/api/v1/controllers/rating.py:190-191).
- The API groups the resources by
- Business rules / limitations:
- Attributes must be nested as
desc.groupby/desc.metadata; a flatdescis silently dropped (see 00, point 1). - Tenant-specific hashmap rules are not applied: the quote worker is created with
tenant_id=None(cloudkitty/orchestrator.py:147,cloudkitty/rating/hash/__init__.py:117-126). - The price is for one period of
volume; the period length is not returned. - Rating modules are instantiated for each quote, so every quote reloads all rules from SQL (
cloudkitty/orchestrator.py:257-265).
- Attributes must be nested as
3.2 GET/PUT /v1/rating/modules — rating module state
- Purpose: list the installed rating modules (stevedore namespace
cloudkitty.rating.processors) withenabled,priority,hot-config,description; enable/disable a module or change its priority. - Flow of PUT:
set_statecastsenable_module/disable_modulewithfanout=Trueand writesmodules_state.state;set_prioritycastsreload_moduleand writesmodules_state.priority(cloudkitty/rating/__init__.py:71-97,cloudkitty/rating/__init__.py:131-134). Response: 302 to the same path (cloudkitty/api/v1/controllers/rating.py:108-130). - Effect on rating: the RPC messages are ignored by the processor (05 § 2); the change is effective at the next scope cycle, because each
WorkerreloadsEnabledExtensionManagerfrom SQL (cloudkitty/extension_manager.py:19-40,cloudkitty/orchestrator.py:290-306). - Priority: higher runs first (
cloudkitty/orchestrator.py:265); default 1 when no row (cloudkitty/db/sqlalchemy/api.py:83-93). A module with no row is disabled (cloudkitty/rating/__init__.py:52-60).
3.3 GET /v1/rating/reload_modules
Rebuilds the API's own stevedore managers and re-exposes module controllers, then casts reload_modules (version 1.1) (cloudkitty/api/v1/controllers/rating.py:212-220, cloudkitty/api/v1/controllers/rating.py:33-53). Useful only after installing a new module package.
3.4 /v1/collector/* — deprecated, no effect
Mappings (service_to_collector_mappings) and per-collector states (modules_state rows named collector_<name>) are stored and returned (cloudkitty/api/v1/controllers/collector.py:29-133) but no code of the processor reads them: the collector is fixed by [collect]/collector (cloudkitty/collector/__init__.py:122-132). Every call logs a deprecation warning.
3.5 /v1/info — metric catalogue
metricsandservicesre-read and validate the metric file ([collect]/metrics_conf, sampleetc/cloudkitty/metrics.yml) on every call; an invalid file yields 405Invalid endpoint: no metrics in current configuration.(cloudkitty/api/v1/controllers/info.py:36-75).- Each metric is returned with
metric_id= itsalt_name(defaulted to the metric name,cloudkitty/collector/__init__.py:351-360),unit,metadata. configreturns the whole YAML file as JSON (cloudkitty/api/v1/controllers/info.py:137-140) to any authenticated user.
3.6 /v1/report/* and /v1/storage/dataframes — legacy reporting
| Endpoint | Behaviour | Where |
|---|---|---|
tenants | scopes known in cloudkitty_storage_states (v2 storage) — not filtered by period | cloudkitty/api/v1/controllers/report.py:56-69, cloudkitty/storage/v2/__init__.py:166-167, cloudkitty/storage_state/__init__.py:241-244 |
total | sum of rate over the period for tenant_id (default: caller's project; all_tenants=true drops the filter); service filters the metric type | cloudkitty/api/v1/controllers/report.py:77-112 |
summary | list of {res_type, tenant_id, begin, end, rate}; groupby accepts tenant_id and/or res_type | cloudkitty/api/v1/controllers/report.py:121-165 |
storage/dataframes | rated points of the period: admins see all (or tenant_id), others only their project; not paginated | cloudkitty/api/v1/controllers/storage.py:43-102 |
For total and summary, the policy target is the requested tenant_id, so a reader of project A asking for project B gets 403; with all_tenants=true the target is None and only admin passes **[inferred from the rule project_id:%(project_id)s]**.
4. HashMap — functional description
4.1 Model
| Object | Meaning | Key constraints | Where |
|---|---|---|---|
| Service | a metric type, matched by name against the DataFrame key (the metric's alt_name) | name unique | cloudkitty/rating/hash/db/sqlalchemy/models.py:62-100 |
| Field | an attribute of the service's data points (flavor_id, image_id…) | unique (service_id, name) | cloudkitty/rating/hash/db/sqlalchemy/models.py:103-154 |
| Group | a calculation unit: rules of a group combine, groups add up | name unique | cloudkitty/rating/hash/db/sqlalchemy/models.py:157-190 |
| Mapping | flat or rate cost, attached to a service (no value) or to a field (with value); optional tenant_id, group; validity window start/end; audit fields; soft delete | unique (value, field_id, tenant_id) and (value, service_id, tenant_id) | cloudkitty/rating/hash/db/sqlalchemy/models.py:193-269 |
| Threshold | flat or rate cost applied when a level is reached (service: on quantity; field: on the attribute's numeric value) | unique (level, field_id, tenant_id) and (level, service_id, tenant_id) | cloudkitty/rating/hash/db/sqlalchemy/models.py:272-348 |
4.2 Rating algorithm (per data point)
Rules are loaded per processor cycle with reload_config(processing_date): mappings active at the start of the period being rated (start <= t < end, not deleted), for tenant NULL and for the scope being rated (cloudkitty/rating/hash/__init__.py:91-157, cloudkitty/rating/common/db/filters.py:23-30, cloudkitty/orchestrator.py:329-339). Thresholds have no time filter.
For each (service, point) of the frame (cloudkitty/rating/hash/__init__.py:267-276), with one accumulator per group {flat: 0, rate: 1, threshold: {level: -1, cost: 0, type: flat, scope: field}} (cloudkitty/rating/hash/__init__.py:185-192):
- Service mappings (no value):
ratemultipliesrate;flatkeeps the highest flat (cloudkitty/rating/hash/__init__.py:236-243,cloudkitty/rating/hash/__init__.py:200-207). - Service thresholds: every threshold with
level <= qtyis a candidate; the one with the highest level wins, scopeservice(cloudkitty/rating/hash/__init__.py:244-247,cloudkitty/rating/hash/__init__.py:221-234,cloudkitty/rating/hash/__init__.py:193-199). - Field mappings: for each configured field present in
point.desc(metadata ∪ groupby), the mapping whosevalue == str(desc[field])applies as in step 1 (cloudkitty/rating/hash/__init__.py:249-261,cloudkitty/rating/hash/__init__.py:209-219). - Field thresholds: compared with
Decimal(desc[field]), scopefield(cloudkitty/rating/hash/__init__.py:262-265). - Price per group (
cloudkitty/rating/hash/__init__.py:159-176):- field-scope threshold:
flat += cost(flat) orrate *= cost(rate); res = rate × flat × qty;- service-scope threshold:
res += cost(flat) orres *= cost(rate); point.price += res, summed over groups.
- field-scope threshold:
Consequences worth knowing:
- With a
ratemapping only (no flat),flatstays 0 and the price is 0 (rate × 0 × qty) — unless a flat mapping exists in the same group. - Mappings without group fall into the implicit group
_DEFAULT_(cloudkitty/rating/hash/__init__.py:55-58). - A point whose service has no hashmap service keeps its incoming price (
cloudkitty/rating/hash/__init__.py:237-238). - The output frame keeps the period and every point (
cloudkitty/rating/hash/__init__.py:268-276); prices areDecimal.
4.3 Mapping lifecycle
stateDiagram-v2 [*] --> Scheduled: POST mappings (start in the future) [*] --> Running: POST mappings (no start, or force=true with start in the past) Scheduled --> Scheduled: PUT (any attribute) Scheduled --> Running: time reaches start Running --> Running: PUT end only (end in the future) Running --> Expired: time reaches end Scheduled --> Deleted: DELETE (sets deleted, deleted_by) Running --> Deleted: DELETE Expired --> Deleted: DELETE
Rules (cloudkitty/rating/common/validations/fields.py:40-87):
- Create:
startmust not be in the past unlessforce=true;endmust be afterstart. Missingstart= now (cloudkitty/rating/hash/db/sqlalchemy/api.py:324-326); missingname= random hex UUID (cloudkitty/rating/hash/db/sqlalchemy/api.py:309-310). - Update of a running rule (start in the past, no end): only
endmay change, and it must be in the future; otherwise 400. - Update of a rule with an end that already started: always 400
Cannot update a rule that was already processed and has a defined end date.. - Update of a scheduled rule:
cost,start,end,description(cloudkitty/rating/hash/controllers/mapping.py:197-215);value,type,tenant_id, parent and group are not updatable through PUT. - Overlap check at creation: a new mapping is refused (409) when a non-deleted mapping has the same
name, or the same value/tenant/parent/group with an overlapping window (cloudkitty/rating/hash/db/sqlalchemy/api.py:340-405). The check takesSELECT … FOR UPDATE. - Parent rules: exactly one of
service_id/field_id; field mappings require avalue, service mappings forbid one (cloudkitty/rating/hash/db/sqlalchemy/api.py:311-322). - Audit:
created_by,updated_by,deleted_byare filled with the Keystone user id resolved from the request token (cloudkitty/common/custom_session.py:38-58), orunknownwhen there is no token.
4.4 Listing filters
GET …/mappings accepts service_id, field_id, group_id, no_group, tenant_id + filter_tenant, deleted, start (>=), end (<), updated_by, created_by, deleted_by, description (case-insensitive substring), is_active, all (default true) (cloudkitty/rating/hash/controllers/mapping.py:52-131, cloudkitty/rating/common/db/filters.py:19-53). Thresholds accept only service_id, field_id, group_id, no_group, tenant_id + filter_tenant (cloudkitty/rating/hash/controllers/threshold.py:49-91).
4.5 Deletions
| Object | Delete | Where |
|---|---|---|
| Service | hard delete, cascades to fields, mappings, thresholds (FK ON DELETE CASCADE) | cloudkitty/rating/hash/db/sqlalchemy/api.py:586-600 |
| Field | hard delete, cascades | cloudkitty/rating/hash/db/sqlalchemy/api.py:602-610 |
| Group | hard delete; with recursive=true its mappings and thresholds are hard-deleted, otherwise their group_id becomes NULL | cloudkitty/rating/hash/db/sqlalchemy/api.py:612-627 |
| Mapping | soft delete (deleted, deleted_by) | cloudkitty/rating/hash/db/sqlalchemy/api.py:629-640 |
| Threshold | hard delete | cloudkitty/rating/hash/db/sqlalchemy/api.py:642-650 |
5. PyScripts — functional description
- Purpose: rate with arbitrary Python. A script receives the frame as a mutable dict in the global
dataand must leave the result indata(cloudkitty/rating/pyscripts/__init__.py:98-115). Every active script runs in turn on the output of the previous one; order is the dict order of script UUIDs [inferred: SQL return order]. - Storage:
pyscripts_scripts—nameunique, code zlib-compressed indata, SHA-512checksum(cloudkitty/rating/pyscripts/db/sqlalchemy/models.py:64-106); same validity/audit columns as hashmap mappings. - Create/update: same
start/end/forcerules as hashmap mappings (cloudkitty/rating/pyscripts/controllers/script.py:116-177). PUT answers 201. - Activation: scripts active at the period being rated are (re)compiled on each processor cycle (
cloudkitty/rating/pyscripts/__init__.py:47-96). - Security:
execwith full builtins in the processor (cloudkitty/rating/pyscripts/__init__.py:98-101); only admins can write scripts.
6. v2 — functional descriptions
6.1 GET /v2/summary — aggregated cost
- Purpose: totals of
qtyandrate(price) over a period, grouped by anygroupbyattribute or bytype, filtered by attributes. - Query:
begin,end(ISO 8601),groupby(comma list, may repeat;time,time-d|w|m|yfor time buckets),filters(key:value, may repeat a key),response_format(tabledefault, orobject),custom_fields(InfluxDB only),offset(≥0, default 0),limit(≥1, default 100) (cloudkitty/api/v2/summary/summary.py:37-52,cloudkitty/api/v2/utils.py:204-230). - Scoping: non-admins are forced to their own project (
filters[scope_key] = project_id); an unscoped non-admin gets an empty result (cloudkitty/api/v2/summary/summary.py:66-78). The policy target is always the caller's project, sofilters=project_id:<other>cannot widen access. - **
typefilter** is turned intometric_types(cloudkitty/api/v2/summary/summary.py:80-82). - Time group-bys:
time-d,time-w,time-m,time-ymap to theday_of_the_year,week_of_the_year,month,yearattributes that every collector adds to every point (cloudkitty/storage/v2/__init__.py:169-199,cloudkitty/collector/__init__.py:295-306); baretimegroups by collection period on InfluxDB (cloudkitty/storage/v2/influx.py:243-245) and bystart/endon Elasticsearch (cloudkitty/storage/v2/elasticsearch/client.py:107-111). - Response (
cloudkitty/api/v2/summary/summary.py:101-117):table→{"total": n, "columns": [...], "results": [[...]], "format": "table"};object→{"total": n, "results": [{...}], "format": "object"}. Columns come from the first result's keys (begin,end,qty,rate, then group-bys). - Limitations: an invalid
response_formatraisesvoluptuous.Invalidinside the handler, outside the validation wrapper → 500 [inferred] (cloudkitty/api/v2/summary/summary.py:54-57,cloudkitty/api/v2/utils.py:181-196). Pagination is applied in memory after the full query on InfluxDB (cloudkitty/storage/v2/influx.py:845-858). Loki: totals over ≤limitlines (00, point 3).
6.2 GET /v2/dataframes — rated data points
begin,end,filters(single value per key;type:<metric>selects the metric),offset,limit(cloudkitty/api/v2/dataframes/dataframes.py:51-70).- Same project scoping as summary (
cloudkitty/api/v2/dataframes/dataframes.py:86-97). - Response
{"total": n, "dataframes": [{"period": {"begin","end"}, "usage": {"<type>": [{"vol": {"unit","qty"}, "rating": {"price"}, "groupby": {...}, "metadata": {...}}]}}]}(cloudkitty/dataframe.py:68-118,cloudkitty/dataframe.py:197-205). - 404
No resource found for provided filters.when the total is 0 (cloudkitty/api/v2/dataframes/dataframes.py:106-108) — clients must treat 404 as "empty".
6.3 POST /v2/dataframes — inject rated data
Admin-only. Body {"dataframes": [<DataFrame dict>]}; each frame is validated by DataFrame.from_dict (cloudkitty/api/v2/dataframes/dataframes.py:33-49, cloudkitty/dataframe.py:162-234). Points are pushed as given — no rating module runs; prices must already be set. Empty list → 400. This is the hook an external rater (such as openkeel-pricing) could use to write into CloudKitty's storage.
6.4 /v2/scope — scope state
| Method | Behaviour | Where |
|---|---|---|
| GET | list cloudkitty_storage_states rows filtered by scope_id, scope_key, fetcher, collector, active (default: active only), paginated; 404 if none | cloudkitty/api/v2/scope/state.py:39-89 |
| PUT | reset: requires all_scopes xor scope_id, and last_processed_timestamp; finds matching scopes (404 if none) and casts reset_state to a processor; answers 202 before anything is done | cloudkitty/api/v2/scope/state.py:91-150 |
| PATCH | change scope_key, fetcher, collector or active of one scope; toggling active stamps scope_activation_toggle_date | cloudkitty/api/v2/scope/state.py:152-216, cloudkitty/storage_state/__init__.py:246-282 |
| POST | create a scope with an explicit last_processed_timestamp (and optionally active=false); 404 if the id exists | cloudkitty/api/v2/scope/state.py:218-284 |
Deactivated scopes are skipped by the processor (cloudkitty/orchestrator.py:406-411). The reset is asynchronous; see 04 § 5 and 05.
6.5 /v2/task/reprocesses — re-rate a past window
- POST body:
scope_ids(list orALL),start_reprocess_time,end_reprocess_time(ISO, read as UTC),reason(non-blank) (cloudkitty/api/v2/task/reprocess.py:45-93). - Validation (all 400):
ALLmixed with ids; blank reason;end < start; start or end not aligned on[collect]/period(the message proposes the nearest aligned window); unknown scope ids; start or end after the scope'slast_processed_timestamp; overlap with an existing unfinished schedule for the same scope (cloudkitty/api/v2/task/reprocess.py:95-241). - Effect: one row per scope in
storage_scope_reprocessing_schedule, 202. The reprocessing workers pick them up (05 § 3). - GET: list schedules (finished ones included),
scope_idsor path id,orderasc|desc(case-insensitive), paginated;current_reprocess_timeis""until started (cloudkitty/api/v2/task/reprocess.py:252-307).
6.6 /v2/rating/modules
Same information as v1 with hot_config instead of hot-config; PUT takes enabled and/or priority (≥1) and answers 204 (cloudkitty/api/v2/rating/modules.py:30-118). The stevedore manager is loaded once per API process (cloudkitty/api/v2/rating/modules.py:54-62, cloudkitty/api/v2/base.py:41-53).
7. Query parameter parsing (v2)
| Helper | Behaviour | Where |
|---|---|---|
SingleQueryParam(T) | exactly one occurrence, coerced to T | cloudkitty/api/v2/utils.py:27-45 |
MultiQueryParam(T) | repeated and/or comma-separated values → list | cloudkitty/api/v2/utils.py:48-64 |
DictQueryParam | key:value tokens, comma-separated; Single… forbids a repeated key, Multi… collects lists | cloudkitty/api/v2/utils.py:67-136 |
paginated | offset ≥ 0 (0), limit ≥ 1 (100) | cloudkitty/api/v2/utils.py:204-230 |
Unknown query or body keys are rejected (voluptuous default extra=PREVENT_EXTRA) → 400 Invalid data '<args>' : <msg> (path: '<path>') (cloudkitty/api/v2/utils.py:189-195) [inferred: voluptuous default]. Note that a : inside a filter value is not supported (cloudkitty/api/v2/utils.py:104-108).
Source: reverse-spec-cloudkitty/01-rest-api_stable-2026.2.md. The _rewrite/ and _inventory/ files are not published on the site.