Ceph S3 Storage Plugin for Waldur Site Agent
Integrates Waldur Mastermind with Ceph object storage. Each marketplace resource creates one RGW user, with its access keys managed as Waldur resource API keys and its ordered ceiling applied as an RGW quota.
Two flavours reach the same RGW concepts by different routes:
croit (default) |
radosgw |
|
|---|---|---|
| Talks to | croit's management API | RadosGW Admin Ops on the gateway |
| Authenticates with | bearer token or basic auth | SigV4, as an RGW user with caps |
| Users, keys, quotas | yes | yes |
| GB-day metering | yes | no — see Usage Reporting |
The flavours differ only inside the client. Everything above it — key lifecycle, quota mapping, metadata, Waldur plumbing — is shared.
Features
- Automatic S3 User Creation: One S3 user per marketplace resource with slug-based naming
- Usage-Based Billing: storage billed in GB-days, integrated from croit's metrics over the billing period. Object counts are not billed
- Safety Quota Enforcement: an ordered ceiling is applied as both a RadosGW user quota (the tenant total) and a bucket quota (per bucket)
- Credential Management: two S3 access keys per resource, encrypted in Waldur and managed from the portal (reveal / rotate). The count is fixed at provisioning — rotation replaces a key's value in place, so there is no add or revoke
- Bearer Token Authentication: Secure API access with configurable SSL verification
Installation
Add the plugin to your UV workspace:
1 2 | |
Configuration
Basic Configuration
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | |
The radosgw flavour
Talks Admin Ops directly to the gateway, for a Ceph cluster croit does not manage.
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
api_url, token, username and password belong to the croit flavour and are
rejected here rather than ignored — an offering carrying both sets is a
half-finished migration, and silently picking one would authenticate somewhere the
operator did not intend.
Two behaviours that differ from croit
- No GB-day metering. Admin Ops exposes no per-user stored-bytes series:
/admin/usageis bandwidth and operation counts, and bucket stats are a point-in-time size. There is nothing to integrate, so leavereporting_backendunset and bill this offering on a fixed component. - Quota enforcement overshoots. RGW syncs bucket statistics into the
user-quota check periodically rather than on every write. Measured on squid: a
6 MiB ceiling against 5 MiB already stored accepted a further 7 MiB before
answering
QuotaExceeded. The ceiling bounds the invoice approximately, not exactly.
Configuration Options
Backend Settings
flavour(optional, default:"croit"):croitorradosgwapi_url(croit only, required): croit management API base URL (/api is appended)s3_endpoint(required): the S3 data endpoint tenants connect to. This is a different host fromapi_url— RadosGW runs on the storage nodes while the management API runs on the croit VM, and croit exposes no VIP or DNS name to derive it from. Point it at a load balancer or DNS name in front of the RGW nodess3_region(optional, default:"default"): the region S3 clients sign with. Maps to the RadosGW zonegroup'sapi_name; croit exposes no endpoint to read it back, and"default"is Ceph's out-of-the-box zonegroup name. Set it if yours was namedtoken(croit only): bearer token for API authenticationusername(croit only): API username (alternative to token)password(croit only): API password (alternative to token)admin_access_key(radosgw only, required): access key of the RGW user holdingusers=*;buckets=readadmin_secret_key(radosgw only, required): its secretadmin_path(radosgw only, default:"admin"): the value ofrgw_admin_entry, if the operator renamed itverify_ssl(optional, default:true): Enable/disable SSL certificate verificationtimeout(optional, default:30): Request timeout in secondsallocation_prefix(optional, default:"waldur-"): namespace every uid this agent creates is built under — the only setting that changes a provisioned uid. A core setting, but the core defaults it to"", which this plugin overrides: an empty namespace puts a consumer-chosen resource name straight into the cluster's global one. See Username Generationdefault_tenant(croit only, optional): Default RadosGW tenant. Rejected on the radosgw flavour: a tenanted uid istenant$uid, and the client's path validator refuses$default_placement(optional): Default placement rule. Honoured by both flavoursdefault_storage_class(croit only, optional): Default storage class. Rejected on the radosgw flavour: Admin Ops user-create takes a placement but no storage class
The billing period boundary for GB-day reporting follows the agent's global
timezone, which the reporting processor injects into the backend — the same value
it uses to file the usage, so the two cannot drift. The configuration validator
rejects a timezone that does not parse: the backend used to fall back to UTC while
the processor fell back to naive system local time, so near a month boundary a typo
filed a whole period's usage against the wrong month.
Set it to whatever Mastermind computes billing periods in. The agent derives both
the integration bounds and the billing_period it files against from its own clock,
so an agent on Europe/Tallinn reporting to a UTC Mastermind integrates the first
hours of each month into the period Mastermind has already closed. A fixed component
would not notice; GB-days move storage-hours onto the wrong invoice.
Component Types
Usage-Based Storage (s3_storage)
The only billed component. Reports GB-days — storage integrated over the billing period — with optional safety quota enforcement:
1 2 3 4 5 6 | |
unit_factor is mandatory here and must be above 1. The core config model
defaults it to 1.0 and normalises every component through that default, so an
omitted value arrives at the plugin as a real one — nothing downstream can tell
"not configured" from "configured as 1". At 1 both directions break silently: the
quota path sends an ordered 5 GB ceiling as 5 bytes, capping the tenant at
nothing, and the metering path divides by 1, reporting byte-days against a GB-day
price. The backend therefore refuses to construct rather than start and misbill,
so a missing unit_factor fails at agent startup with the component named.
Setting limit_amount/limit_period on this component is harmless but noisy.
validate_amount() only raises for non-usage components; for a usage component
it logs a warning and the report lands anyway. Two things make that warning fire
early: a GB-denominated threshold is compared against a GB-day amount, and the
running total includes the row about to be overwritten, so cumulative reporting
roughly double-counts. Leave it unset unless you want the log noise.
Username Generation
Provisioned resources are named by the order processor, not by this plugin.
_get_resource_backend_id() in the core produces
f"{allocation_prefix}{resource_slug}".lower(), and Waldur caps every slug at 10
characters (SLUG_NAME_LIMIT), appending -2, -3, … on collision. So a resource
named cust-0-proj-0-ceph-s3-of becomes the uid cust-0-pro.
Format: {allocation_prefix}{resource_slug} — Example: cust-0-pro
This is the only naming scheme the plugin has. A second one — user_prefix,
slug_separator, max_username_length and a create_resource() that read them —
existed alongside it and was never reached, since the order processor calls
create_resource_with_id(). It has been removed; an offering still carrying those
three keys is unaffected, they are simply ignored.
Usage Reporting
Croit flavour only, and it needs its own backend. Set
1 | |
Metering reads croit's statistics subsystem, which has no RadosGW equivalent, so
it lives in a separate backend class rather than as a method that works on one
flavour and fails on the other. croit_usage refuses to start on a
radosgw-flavoured offering.
Leaving reporting_backend at ceph_s3 reports nothing — deliberately, not
zeros. A zero is a legitimate usage value and would overwrite the period's
accrued total; silence leaves the last good figure in place.
Storage is billed in GB-days — the area under the storage curve over the billing period, not a reading taken at report time. Object counts are not billed.
Usage is metered per resource, not per key: the series is the S3 user's, so the resource's two access keys never enter the calculation and rotating one cannot change a bill.
Where the number comes from
GET /api/stats?graph=s3-user-data&template-s3-user-name=<uid> returns croit's
per-user storage series as {"t": unix seconds, "v": bytes} datapoints at 180 s
native resolution. Each pass asks for the period so far and integrates:
1 | |
Each datapoint is one rectangle — the reading, held until the next sample. A null
v is missing telemetry, so the previous reading is carried across it; dropping
the interval would bill it at zero and turn a metrics outage into a silent
discount.
Recomputed from the period start every time rather than accumulated, so the value
is absolute: reporting is idempotent, and an agent that was down for two days
returns the correct figure on its next pass with no catch-up logic. It also only
rises within a period, so Waldur's anomaly detection stays useful and
supports_decreasing_usage stays at the default False.
Why GB-days rather than GB
Waldur bills a usage component as a flat unit_price × quantity — the PER_DAY
proration in invoicing applies only to fixed components. So for a price list
denominated per GB per day, the days have to be inside the quantity. Reporting
plain GB against a per-day price under-bills by roughly the number of days in the
month.
Precision
The integral is computed in Decimal and rounded half-up to two places, which is
what the reporter's "%.2f" wire format preserves. The report leaves the backend
as plain floats, like every other plugin.
Report Format
The reporter reads TOTAL_ACCOUNT_USAGE and skips any resource whose report
lacks it. There is exactly one S3 user per resource, so the account total is
that user's usage; per-user entries would key on Waldur offering users, which
this backend does not manage.
A resource whose series cannot be read is absent from the report rather than present with a zero — zero is a valid usage value and would overwrite the period's accrued total.
1 2 3 4 5 6 7 | |
Values are plain numbers already in Waldur units, with unit_factor applied —
not {"usage": n} wrappers.
Resource Metadata
Each S3 user resource exposes comprehensive metadata:
S3 Connection
Credentials are not in resource metadata. Each resource owns two S3 access keys, stored
encrypted in Waldur and revealed or rotated per key from the portal (see
waldur-mastermind/docs/resource-api-keys.md). Metadata carries only the non-secret
connection info, and the S3 endpoint is also published as a resource access endpoint:
1 2 3 4 5 | |
These are deliberately flat keys. An offering's Getting started text is a template
interpolating {backend_metadata_<key>} one level deep, so a nested block would render as
[object Object]. Flat keys let a provider write usable instructions once, for example:
1 2 3 4 5 | |
The agent mints both halves of each pair, applies them to the S3 user, and only then reports them to Waldur — so a stored key is always one RadosGW already accepts. Rotation replaces the access key as well as the secret, applying the new pair before dropping the old one, so the resource's other key keeps working throughout.
Every credential that works is one Waldur can rotate. Two things would otherwise break that:
- RadosGW mints its own key when a user is created, and Waldur never sees its secret. It is deleted right after the user is created, while it is that user's only key and before anyone holds credentials. This happens only on the branch that actually created the user. An existing uid is adopted with its keys left in place at provisioning time, but they are not in Waldur's known set, so the resource's first rotation prunes them.
- A rotation whose reply to Waldur is lost has already replaced the access key at the
backend, so re-issuing it rotates from a client_id croit no longer has and would leave
the intermediate key live and invisible. Each rotation therefore receives the resource's
known client_ids and drops anything outside that set. If Waldur cannot be listed the set
is
None— unknown, so nothing is pruned; treating it as empty would delete every credential the resource has.
Known limitation: croit's create-key endpoint takes the secret as a query parameter
(PUT /s3/users/{uid}/keys/{accessKey}?secretKey=…), so it appears in croit's access log.
The bulk alternative (PUT /s3/users/{uid}/keys) keeps the secret in the body but replaces
the user's entire key set asynchronously, which can drop a sibling key when two rotations
overlap.
Storage Summary
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
Quota Information
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
Safety Quota Enforcement
Ordering a ceiling is the only signal needed — there is no enable switch. The plugin applies the ordered limits as RadosGW quotas:
- Create Resource: apply the ordered ceilings (
max_storage_limit,max_object_limit) — on the create path only; quotas are not re-asserted afterwards, so an existing resource keeps whatever it was given - Prevent Overages: the quota is what bounds the tenant, and therefore the invoice — usage billing itself has no ceiling
Quota Types
Both are set from the same ordered ceiling:
- User quota (
PUT /s3/users/{uid}/quota): the aggregate cap across everything the user owns. This is the one that bounds a tenant — croit exposes no way to cap the bucket count, so a per-bucket quota alone bounds nothing - Bucket quota (
PUT /s3/users/{uid}/bucket-quota): a per-bucket guard maxSizein bytes (max_storage_limit×unit_factor),maxObjectsas a count
How Safety Limits Work
- User Configuration: Users set
max_storage_limitandmax_object_limitvia Waldur marketplace form - Resource Options: Waldur passes these as resource attributes to the site agent
- Quota Application: applied as user and bucket quotas during S3 user creation. A resource that reaches provisioning with no ceiling is logged as a warning naming the attributes that were present — an unbounded resource is one with no bound on its invoice
- Usage Billing: actual consumption is measured and billed separately from the quota, which is a cap rather than a billing basis
Waldur Marketplace Integration
Creating the Matching Offering
To create a matching offering in Waldur Mastermind, run the setup script:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 | |
Alternative: Save the above code as setup_ceph_s3_offering.py and run:
1 | |
Offering Configuration
The created Waldur offering will have:
- Type:
SITE_AGENT_OFFERING("Marketplace.Slurm") - Components:
s3_storageonly (usage-based, billed in GB-days) - Options:
max_storage_limitandmax_object_limitfor user input (safety limits) - Pricing: €0.0010 per GB-day for storage; objects are not billed
Order Payload Example
1 2 3 4 5 6 7 8 9 10 11 12 | |
Testing
Run the test suite:
1 2 | |
Development
Adding New Components
- Define component in site agent configuration:
1 2 3 4 5 6 | |
- Add usage collection logic in
_get_usage_report() - Add safety limit handling in
_apply_bucket_quotas()if needed - Add corresponding field in Waldur offering options for user input
Error Handling
The plugin includes comprehensive error handling:
CroitS3AuthenticationError: API authentication failuresCroitS3UserNotFoundError: User doesn't existCroitS3UserExistsError: User already existsCroitS3GraphNotFoundError: the statistics graph behind usage reporting is missingCroitS3APIError: General API errorsCroitS3Error: Base exception class
Troubleshooting
SSL Certificate Issues
1 2 | |
Connection Timeouts
1 2 | |
Username Length Issues
The uid is {allocation_prefix}{resource_slug}, and Waldur already caps the slug at
10 characters — so only allocation_prefix is yours to shorten. See Username
Generation.
Debug Logging
Use standard Python logging configuration or waldur-site-agent logging settings to enable debug output for the plugin modules:
waldur_site_agent_ceph_s3.client- HTTP API interactionswaldur_site_agent_ceph_s3.backend- Backend operations
Resource Lifecycle
- Order Creation: User submits order with
max_storage_limitandmax_object_limit - User Creation: Plugin creates the S3 user under the backend id the order processor generated
- Quota Application: ceilings applied as user and bucket quotas
- Key Provisioning: Two access keys minted, applied, and reported to Waldur encrypted; RadosGW's auto-generated key is removed
- Usage Tracking: storage integrated into GB-days per billing period
- Limit Updates: not supported. The ceiling is an order attribute, fixed at
creation. Waldur can carry an
update_optionsorder, but the agent does not act onnew_options, and quotas are applied only on the create path — so changing a ceiling today means terminating and re-ordering - Resource Deletion: the S3 user is removed — but only once it owns no
buckets. croit refuses to delete a user that still holds data
(
500 Unable to remove user with buckets.), so termination of a tenant with data fails until the buckets are emptied and removed. The failure reason reaches the portal's order history and resource activity log.