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

Reverse-spec · Services

cloudkitty — REST API

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)BehaviourWhere
Paste urlmap/healthcheck → oslo.middleware Healthcheck (disable_by_file /etc/cloudkitty/healthcheck_disable); / → the API pipelineetc/cloudkitty/api_paste.ini:1-9, etc/cloudkitty/api_paste.ini:31-34
Pipeline selectioncloudkitty+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 → authtokenoslo.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 + WSMEhooks put the RPC client, the storage driver and a RequestContext on the requestcloudkitty/api/v1/__init__.py:39-57, cloudkitty/api/v1/hooks.py:22-41
v2: Flask + flask-restfulone blueprint per module (/v2/scope, /v2/dataframes, /v2/summary, /v2/task, /v2/rating); request context set before each request; JSON output through cloudkitty.utils.jsoncloudkitty/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

MethodPathHandlerPolicySuccessErrors
GET/cloudkitty/api/root.py:81 CloudkittyAPIRoot.getnone (public route)200 {"versions":[…]}—
GET/v1/rating/modulescloudkitty/api/v1/controllers/rating.py:72rating:list_modules (admin)200403
GET/v1/rating/modules/{module_id}cloudkitty/api/v1/controllers/rating.py:91rating:get_module (admin)200403, 404
PUT/v1/rating/modules/{module_id}cloudkitty/api/v1/controllers/rating.py:112rating:update_module (admin)302 + Location403, 404
POST/v1/rating/quotecloudkitty/api/v1/controllers/rating.py:192rating:quote (open)200, a JSON number400, 500/504 on RPC failure [inferred]
GET/v1/rating/reload_modulescloudkitty/api/v1/controllers/rating.py:213rating:module_config (admin)204 **[inferred: WSME None return]**403
GET/v1/collector/mappingscloudkitty/api/v1/controllers/collector.py:53collector:list_mappings (admin)200403
GET/v1/collector/mappings/{service}cloudkitty/api/v1/controllers/collector.py:37collector:get_mapping (admin)200403, 404
POST/v1/collector/mappings?collector=&service=cloudkitty/api/v1/controllers/collector.py:70collector:manage_mapping (admin)200403
DELETE/v1/collector/mappings/{service}cloudkitty/api/v1/controllers/collector.py:86collector:manage_mapping (admin)204403, 404
GET/v1/collector/states/{name}cloudkitty/api/v1/controllers/collector.py:106collector:get_state (admin)200403
PUT/v1/collector/states/{name}cloudkitty/api/v1/controllers/collector.py:121collector:update_state (admin)200403
GET/v1/info/metricscloudkitty/api/v1/controllers/info.py:86info:list_metrics_info (open)200405
GET/v1/info/metrics/{metric_name}cloudkitty/api/v1/controllers/info.py:94info:get_metric_info (open)200404, 405
GET/v1/info/services (deprecated)cloudkitty/api/v1/controllers/info.py:106info:list_metrics_info200405
GET/v1/info/services/{name} (deprecated)cloudkitty/api/v1/controllers/info.py:116info:get_metric_info200404, 405
GET/v1/info/configcloudkitty/api/v1/controllers/info.py:137info:get_config (open)200, the raw metric file ([collect]/metrics_conf)—
GET/v1/report/tenantscloudkitty/api/v1/controllers/report.py:56report:list_tenants (admin)200 ["id",…]403
GET/v1/report/total (deprecated)cloudkitty/api/v1/controllers/report.py:77report:get_total (reader, target project)200, a decimal403
GET/v1/report/summarycloudkitty/api/v1/controllers/report.py:121report:get_summary (reader, target project)200403
GET/v1/storage/dataframescloudkitty/api/v1/controllers/storage.py:43storage:list_data_frames (reader)200403

/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).

MethodPathHandlerSuccessErrors
GET…/hashmap/typescloudkitty/rating/hash/controllers/root.py:42200 ["flat","rate"]403
GET…/hashmap/servicescloudkitty/rating/hash/controllers/service.py:36200403
GET…/hashmap/services/{service_id}cloudkitty/rating/hash/controllers/service.py:52200400 (bad UUID), 404
POST…/hashmap/servicescloudkitty/rating/hash/controllers/service.py:67201 + Location409
DELETE…/hashmap/services/{service_id}cloudkitty/rating/hash/controllers/service.py:85204 (cascade)404
GET…/hashmap/fields?service_id=cloudkitty/rating/hash/controllers/field.py:33200400
GET…/hashmap/fields/{field_id}cloudkitty/rating/hash/controllers/field.py:52200404
POST…/hashmap/fieldscloudkitty/rating/hash/controllers/field.py:67201 + Location400 (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:91204 (cascade)404
GET…/hashmap/groupscloudkitty/rating/hash/controllers/group.py:70200—
GET…/hashmap/groups/{group_id}cloudkitty/rating/hash/controllers/group.py:87200404
POST…/hashmap/groupscloudkitty/rating/hash/controllers/group.py:102201 + Location400, 409
DELETE…/hashmap/groups/{group_id}?recursive=cloudkitty/rating/hash/controllers/group.py:125204404
GET…/hashmap/groups/mappings?group_id=cloudkitty/rating/hash/controllers/group.py:37200—
GET…/hashmap/groups/thresholds?group_id=cloudkitty/rating/hash/controllers/group.py:54200—
GET…/hashmap/mappings (+ filters)cloudkitty/rating/hash/controllers/mapping.py:69200400 (types)
GET…/hashmap/mappings/{mapping_id}cloudkitty/rating/hash/controllers/mapping.py:135200404
POST…/hashmap/mappings?force=cloudkitty/rating/hash/controllers/mapping.py:152201 + Location400, 409
PUT…/hashmap/mappings/{mapping_id}cloudkitty/rating/hash/controllers/mapping.py:191302 + Location400, 404, 409
DELETE…/hashmap/mappings/{mapping_id}cloudkitty/rating/hash/controllers/mapping.py:227204 (soft delete)404
GET…/hashmap/mappings/group?mapping_id=cloudkitty/rating/hash/controllers/mapping.py:39200404
GET…/hashmap/thresholds (+ filters)cloudkitty/rating/hash/controllers/threshold.py:57200400
GET…/hashmap/thresholds/{threshold_id}cloudkitty/rating/hash/controllers/threshold.py:95200404
POST…/hashmap/thresholdscloudkitty/rating/hash/controllers/threshold.py:111201 + Location400, 409
PUT…/hashmap/thresholds/{threshold_id}cloudkitty/rating/hash/controllers/threshold.py:141302 + Location400, 404, 409
DELETE…/hashmap/thresholds/{threshold_id}cloudkitty/rating/hash/controllers/threshold.py:168204 (hard delete)404
GET…/hashmap/thresholds/group?threshold_id=cloudkitty/rating/hash/controllers/threshold.py:36200404

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)

MethodPathHandlerSuccessErrors
GET…/scripts (+ no_data, audit filters)cloudkitty/rating/pyscripts/controllers/script.py:55200403
GET…/scripts/{script_id}cloudkitty/rating/pyscripts/controllers/script.py:100200404
POST…/scripts?force=cloudkitty/rating/pyscripts/controllers/script.py:116201 + Location400, 409
PUT…/scripts/{script_id}cloudkitty/rating/pyscripts/controllers/script.py:147201 + Location400, 404
DELETE…/scripts/{script_id}cloudkitty/rating/pyscripts/controllers/script.py:180204 (soft delete)404

2.4 v2

MethodPathHandlerPolicySuccessErrors
GET/v2/summarycloudkitty/api/v2/summary/summary.py:50summary:get_summary (reader, own project)200400, 403, 500
GET/v2/dataframescloudkitty/api/v2/dataframes/dataframes.py:65dataframes:get (reader)200400, 403, 404 when empty
POST/v2/dataframescloudkitty/api/v2/dataframes/dataframes.py:36dataframes:add (admin)204400, 403
GET/v2/scopecloudkitty/api/v2/scope/state.py:61scope:get_state (admin)200400, 403, 404
PUT/v2/scopecloudkitty/api/v2/scope/state.py:105scope:reset_state (admin)202400, 403, 404
PATCH/v2/scopecloudkitty/api/v2/scope/state.py:174scope:patch_state (admin)200400, 403, 404
POST/v2/scopecloudkitty/api/v2/scope/state.py:241scope:post_state (admin)200400, 403, 404 when it already exists
POST/v2/task/reprocessescloudkitty/api/v2/task/reprocess.py:53schedule:task_reprocesses (admin)202400, 403
GET/v2/task/reprocessescloudkitty/api/v2/task/reprocess.py:265schedule:get_task_reprocesses (admin)200400, 403
GET/v2/task/reprocesses/{path_scope_id}cloudkitty/api/v2/task/reprocess.py:265 (subclass at cloudkitty/api/v2/task/reprocess.py:310)same200400, 403
GET/v2/rating/modulescloudkitty/api/v2/rating/modules.py:107none (rule v2_rating:list_modules registered but unused)200—
GET/v2/rating/modules/{module_id}cloudkitty/api/v2/rating/modules.py:68v2_rating:get_module (admin)200403, 404
PUT/v2/rating/modules/{module_id}cloudkitty/api/v2/rating/modules.py:88v2_rating:update_module (admin)204400, 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

GapEvidence
The v1 reference documents the metric controller under /v1/info/metric; the code mounts it at /v1/info/metricsapi-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 documentedcloudkitty/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 unprotectedcloudkitty/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:quote is 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):
    1. The API groups the resources by service and 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).
    2. RPC call quote(res_data={"usage": {...}}), namespace rating, topic cloudkitty, to any processor (cloudkitty/api/v1/controllers/rating.py:209).
    3. The processor builds a DataFrame over [now, now + period); for each entry, groupby = desc.get('groupby', []) and metadata = desc.get('metadata', []) (cloudkitty/orchestrator.py:145-175).
    4. An APIWorker loads the enabled rating modules (no tenant) and runs their quote() (= process()) in decreasing priority, then sums the prices (cloudkitty/orchestrator.py:268-280).
    5. 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).
  • Business rules / limitations:
    • Attributes must be nested as desc.groupby / desc.metadata; a flat desc is 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).

3.2 GET/PUT /v1/rating/modules — rating module state

  • Purpose: list the installed rating modules (stevedore namespace cloudkitty.rating.processors) with enabled, priority, hot-config, description; enable/disable a module or change its priority.
  • Flow of PUT: set_state casts enable_module/disable_module with fanout=True and writes modules_state.state; set_priority casts reload_module and writes modules_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 Worker reloads EnabledExtensionManager from 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

  • metrics and services re-read and validate the metric file ([collect]/metrics_conf, sample etc/cloudkitty/metrics.yml) on every call; an invalid file yields 405 Invalid endpoint: no metrics in current configuration. (cloudkitty/api/v1/controllers/info.py:36-75).
  • Each metric is returned with metric_id = its alt_name (defaulted to the metric name, cloudkitty/collector/__init__.py:351-360), unit, metadata.
  • config returns 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

EndpointBehaviourWhere
tenantsscopes known in cloudkitty_storage_states (v2 storage) — not filtered by periodcloudkitty/api/v1/controllers/report.py:56-69, cloudkitty/storage/v2/__init__.py:166-167, cloudkitty/storage_state/__init__.py:241-244
totalsum of rate over the period for tenant_id (default: caller's project; all_tenants=true drops the filter); service filters the metric typecloudkitty/api/v1/controllers/report.py:77-112
summarylist of {res_type, tenant_id, begin, end, rate}; groupby accepts tenant_id and/or res_typecloudkitty/api/v1/controllers/report.py:121-165
storage/dataframesrated points of the period: admins see all (or tenant_id), others only their project; not paginatedcloudkitty/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

ObjectMeaningKey constraintsWhere
Servicea metric type, matched by name against the DataFrame key (the metric's alt_name)name uniquecloudkitty/rating/hash/db/sqlalchemy/models.py:62-100
Fieldan attribute of the service's data points (flavor_id, image_id…)unique (service_id, name)cloudkitty/rating/hash/db/sqlalchemy/models.py:103-154
Groupa calculation unit: rules of a group combine, groups add upname uniquecloudkitty/rating/hash/db/sqlalchemy/models.py:157-190
Mappingflat 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 deleteunique (value, field_id, tenant_id) and (value, service_id, tenant_id)cloudkitty/rating/hash/db/sqlalchemy/models.py:193-269
Thresholdflat 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):

  1. Service mappings (no value): rate multiplies rate; flat keeps the highest flat (cloudkitty/rating/hash/__init__.py:236-243, cloudkitty/rating/hash/__init__.py:200-207).
  2. Service thresholds: every threshold with level <= qty is a candidate; the one with the highest level wins, scope service (cloudkitty/rating/hash/__init__.py:244-247, cloudkitty/rating/hash/__init__.py:221-234, cloudkitty/rating/hash/__init__.py:193-199).
  3. Field mappings: for each configured field present in point.desc (metadata ∪ groupby), the mapping whose value == str(desc[field]) applies as in step 1 (cloudkitty/rating/hash/__init__.py:249-261, cloudkitty/rating/hash/__init__.py:209-219).
  4. Field thresholds: compared with Decimal(desc[field]), scope field (cloudkitty/rating/hash/__init__.py:262-265).
  5. Price per group (cloudkitty/rating/hash/__init__.py:159-176):
    • field-scope threshold: flat += cost (flat) or rate *= cost (rate);
    • res = rate × flat × qty;
    • service-scope threshold: res += cost (flat) or res *= cost (rate);
    • point.price += res, summed over groups.

Consequences worth knowing:

  • With a rate mapping only (no flat), flat stays 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 are Decimal.

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: start must not be in the past unless force=true; end must be after start. Missing start = now (cloudkitty/rating/hash/db/sqlalchemy/api.py:324-326); missing name = random hex UUID (cloudkitty/rating/hash/db/sqlalchemy/api.py:309-310).
  • Update of a running rule (start in the past, no end): only end may 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 takes SELECT … FOR UPDATE.
  • Parent rules: exactly one of service_id/field_id; field mappings require a value, service mappings forbid one (cloudkitty/rating/hash/db/sqlalchemy/api.py:311-322).
  • Audit: created_by, updated_by, deleted_by are filled with the Keystone user id resolved from the request token (cloudkitty/common/custom_session.py:38-58), or unknown when 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

ObjectDeleteWhere
Servicehard delete, cascades to fields, mappings, thresholds (FK ON DELETE CASCADE)cloudkitty/rating/hash/db/sqlalchemy/api.py:586-600
Fieldhard delete, cascadescloudkitty/rating/hash/db/sqlalchemy/api.py:602-610
Grouphard delete; with recursive=true its mappings and thresholds are hard-deleted, otherwise their group_id becomes NULLcloudkitty/rating/hash/db/sqlalchemy/api.py:612-627
Mappingsoft delete (deleted, deleted_by)cloudkitty/rating/hash/db/sqlalchemy/api.py:629-640
Thresholdhard deletecloudkitty/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 data and must leave the result in data (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 — name unique, code zlib-compressed in data, SHA-512 checksum (cloudkitty/rating/pyscripts/db/sqlalchemy/models.py:64-106); same validity/audit columns as hashmap mappings.
  • Create/update: same start/end/force rules 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: exec with 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 qty and rate (price) over a period, grouped by any groupby attribute or by type, filtered by attributes.
  • Query: begin, end (ISO 8601), groupby (comma list, may repeat; time, time-d|w|m|y for time buckets), filters (key:value, may repeat a key), response_format (table default, or object), 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, so filters=project_id:<other> cannot widen access.
  • **type filter** is turned into metric_types (cloudkitty/api/v2/summary/summary.py:80-82).
  • Time group-bys: time-d, time-w, time-m, time-y map to the day_of_the_year, week_of_the_year, month, year attributes that every collector adds to every point (cloudkitty/storage/v2/__init__.py:169-199, cloudkitty/collector/__init__.py:295-306); bare time groups by collection period on InfluxDB (cloudkitty/storage/v2/influx.py:243-245) and by start/end on 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_format raises voluptuous.Invalid inside 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 ≤ limit lines (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

MethodBehaviourWhere
GETlist cloudkitty_storage_states rows filtered by scope_id, scope_key, fetcher, collector, active (default: active only), paginated; 404 if nonecloudkitty/api/v2/scope/state.py:39-89
PUTreset: 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 donecloudkitty/api/v2/scope/state.py:91-150
PATCHchange scope_key, fetcher, collector or active of one scope; toggling active stamps scope_activation_toggle_datecloudkitty/api/v2/scope/state.py:152-216, cloudkitty/storage_state/__init__.py:246-282
POSTcreate a scope with an explicit last_processed_timestamp (and optionally active=false); 404 if the id existscloudkitty/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 or ALL), start_reprocess_time, end_reprocess_time (ISO, read as UTC), reason (non-blank) (cloudkitty/api/v2/task/reprocess.py:45-93).
  • Validation (all 400): ALL mixed 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's last_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_ids or path id, order asc|desc (case-insensitive), paginated; current_reprocess_time is "" 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)

HelperBehaviourWhere
SingleQueryParam(T)exactly one occurrence, coerced to Tcloudkitty/api/v2/utils.py:27-45
MultiQueryParam(T)repeated and/or comma-separated values → listcloudkitty/api/v2/utils.py:48-64
DictQueryParamkey:value tokens, comma-separated; Single… forbids a repeated key, Multi… collects listscloudkitty/api/v2/utils.py:67-136
paginatedoffset ≥ 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.