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

Reverse-spec · Services

cloudkitty — Scenarios

End-to-end scenarios — CloudKitty — branch stable/2026.2

Branch stable/2026.2 @ 0685cc330e83 — repository https://opendev.org/openstack/cloudkitty — analysed on 2026-11-09

1. Rating cycle of one scope (the core flow)

Default deployment: Keystone fetcher, Gnocchi collector, hashmap, InfluxDB storage. Trigger: the processor loop (05 § 1).

sequenceDiagram
  participant P as cloudkitty-processor worker
  participant KS as Keystone
  participant TZ as tooz back end
  participant DB as MariaDB
  participant G as Gnocchi
  participant I as InfluxDB
  P->>KS: GET /v3/projects + role check per project (fetcher)
  KS-->>P: scopes with role "rating"
  loop each scope (shuffled)
    P->>TZ: acquire lock sha256(cloudkitty-<scope>-…) (non-blocking)
    alt lock busy
      Note over P: skip scope this loop
    else acquired
      P->>DB: SELECT cloudkitty_storage_states (last_processed_timestamp)
      Note over P: next = last + period, if next + wait_periods×period < now<br/>new scope: start of current month
      loop while a period is ready
        P->>DB: SELECT modules_state, hashmap_* / pyscripts_scripts active at period start
        par one thread per metric (max_threads)
          P->>G: POST /v1/aggregates (metric, scope, period, groupby)
          G-->>P: measures per group
          P->>G: POST /v1/search/resource/<type> (only if metadata)
        end
        alt every metric empty
          Note over P: WARNING "No usage data…"; nothing stored
        else data
          Note over P: hashmap/pyscripts set prices<br/>skip_datapoints_expression filters points
          P->>I: write_points measurement "dataframes" (chunks of 500)
        end
        P->>DB: UPDATE cloudkitty_storage_states SET last_processed_timestamp = period start
      end
      P->>TZ: release lock
    end
  end
  Note over P: sleep [collect]/period, then start again

Key references: fetcher cloudkitty/fetcher/keystone.py:84-105; lock cloudkitty/orchestrator.py:700-735; next period cloudkitty/utils/__init__.py:200-223; collection cloudkitty/orchestrator.py:341-391; rating cloudkitty/orchestrator.py:509-521; storage cloudkitty/orchestrator.py:445-460, cloudkitty/storage/v2/influx.py:766-773; state cloudkitty/orchestrator.py:523-524.

Where to look when it fails: a scope whose last_processed_timestamp stays behind (GET /v2/scope) and the processor log Error processing scope %s, we will try it again later. (cloudkitty/orchestrator.py:719-723). A collection error aborts the whole period for that scope; a bad rule or script blocks it until fixed (02 § 3).

Latency: a period [T, T+p) is rated no earlier than T + p + wait_periods × p (3 h with defaults), then up to one more loop (sleep(period)) later.

2. Instant quote

sequenceDiagram
  participant C as Client (Skyline, CLI)
  participant A as cloudkitty-api
  participant MQ as RabbitMQ
  participant P as cloudkitty-processor
  participant DB as MariaDB
  C->>A: POST /v1/rating/quote {"resources":[{"service","desc","volume"}]}
  A->>A: policy rating:quote (open)
  A->>MQ: RPC call quote(res_data={"usage":{…}}) topic cloudkitty, ns rating
  MQ->>P: quote
  P->>DB: load enabled modules + all rules (tenant NULL only)
  Note over P: DataFrame [now, now+period)<br/>DataPoint(groupby=desc.groupby, metadata=desc.metadata)<br/>modules in priority order, sum of prices
  P-->>MQ: "0.042"
  MQ-->>A: reply
  alt no processor / timeout
    A-->>C: 500 (MessagingTimeout) [inferred]
  else
    A-->>C: 200 0.042
  end

References: cloudkitty/api/v1/controllers/rating.py:190-210, cloudkitty/orchestrator.py:145-180, cloudkitty/orchestrator.py:268-280. Trap: a flat desc loses every attribute (00, point 1).

3. Changing a price (hashmap mapping)

sequenceDiagram
  participant Op as Administrator
  participant A as cloudkitty-api
  participant KS as Keystone
  participant DB as MariaDB
  participant P as cloudkitty-processor
  Op->>A: POST /v1/rating/module_config/hashmap/mappings {"field_id","value":"m1.small","type":"flat","cost":"0.05","start":"2026-12-01"}
  A->>A: policy rating:module_config (admin)
  A->>A: validate start/end (start in the past → 400 unless force=true)
  A->>KS: GET /v3/auth/tokens (X-Subject-Token = caller token, verify=False)
  KS-->>A: user id → created_by
  A->>DB: SELECT … FOR UPDATE (same name or overlapping window)
  alt conflict
    A-->>Op: 409 Mapping '…' already exists for field '…', tenant: '…'
  else
    A->>DB: INSERT hashmap_mappings
    A-->>Op: 201 Location: …/mappings/<uuid>
  end
  Note over P: at each scope cycle, rules active at the period start are reloaded:<br/>periods from 2026-12-01 use 0.05, earlier periods keep the old mapping

References: cloudkitty/rating/hash/controllers/mapping.py:152-185, cloudkitty/rating/common/validations/fields.py:73-87, cloudkitty/common/custom_session.py:38-58, cloudkitty/rating/hash/db/sqlalchemy/api.py:296-445, cloudkitty/orchestrator.py:329-339. To end the old price, the operator sets its end (PUT, end only) to 2026-12-01; without it, both rules are active from that date and both contribute to the group (flat: the highest wins; rate: they multiply) (cloudkitty/rating/hash/__init__.py:200-207).

Already-rated periods are not re-priced: that needs a reprocessing (§ 4).

4. Reprocessing a past window

sequenceDiagram
  participant Op as Administrator
  participant A as cloudkitty-api
  participant DB as MariaDB
  participant R as cloudkitty-processor (reprocessor worker)
  participant TZ as tooz
  participant S as Storage back end
  participant Col as Collector back end
  Op->>A: POST /v2/task/reprocesses {"scope_ids":["p1"],"start_reprocess_time","end_reprocess_time","reason"}
  A->>DB: SELECT scope state, existing schedules
  alt misaligned / beyond last processed / overlapping
    A-->>Op: 400 …
  else
    A->>DB: INSERT storage_scope_reprocessing_schedule
    A-->>Op: 202
  end
  loop reprocessor loop (every period)
    R->>DB: SELECT unfinished schedules
    R->>TZ: acquire lock (schedule)
    R->>DB: next = start or current + period (≤ end)
    alt first period of the schedule
      R->>S: delete [start, end) for {scope_key: p1}
    end
    R->>Col: collect, rate with rules active at that period
    R->>S: push rated frame
    R->>DB: UPDATE current_reprocess_time
  end

References: cloudkitty/api/v2/task/reprocess.py:53-93, cloudkitty/orchestrator.py:527-631, cloudkitty/orchestrator.py:768-806.

Failure points: with Elasticsearch/OpenSearch the delete erases every scope's data in the window (00, point 2); the reprocessing re-rates only the scheduled scopes. The schedule processes the period that starts at end_reprocess_time too (00, point 13).

5. Resetting a scope (PUT /v2/scope)

sequenceDiagram
  participant Op as Administrator
  participant A as cloudkitty-api
  participant DB as MariaDB
  participant MQ as RabbitMQ
  participant P as cloudkitty-processor (any worker)
  participant TZ as tooz
  participant S as Storage back end
  Op->>A: PUT /v2/scope {"scope_id":"p1","last_processed_timestamp":"2026-10-01T00:00:00"}
  A->>DB: SELECT matching scopes
  A->>MQ: cast reset_state(res_data)
  A-->>Op: 202
  MQ->>P: reset_state
  loop each scope (shuffled)
    P->>TZ: acquire lock (blocking)
    P->>S: delete from last_processed_timestamp to now for {scope_key: id}
    P->>DB: UPDATE last_processed_timestamp
    P->>TZ: release
  end
  Note over P: the next rating loop re-rates from last_processed_timestamp + period

References: cloudkitty/api/v2/scope/state.py:105-150, cloudkitty/orchestrator.py:221-246, cloudkitty/storage_state/__init__.py:148-179. The client gets no completion signal: it polls GET /v2/scope. InfluxDB v2's delete_api.delete(begin, None, …) with end=None is untested here [inferred].

Source: reverse-spec-cloudkitty/04-scenarios_stable-2026.2.md. The _rewrite/ and _inventory/ files are not published on the site.