Quantum: Add 'az quantum suite-offer quotas' command - #10285
Quantum: Add 'az quantum suite-offer quotas' command#10285v-elegacheva wants to merge 23 commits into
Conversation
List the Quantum suite offers available to the subscription (provider, location, and subscription-level quota allocations) via the control-plane SuiteOffers API. Bumps the extension to 1.0.0b24.
…fer-list # Conflicts: # src/quantum/HISTORY.rst # src/quantum/setup.py
Adds 'az quantum suite-offer quotas --provider-id' which returns v2 quota allocations merged with their consumed usages for a suite offer provider account. Combines the control-plane suite offer allocations with the data-plane (-v2 endpoint) quota usages, reporting allocated/used/remaining standard and high priority minutes per subscription and target scope.
|
Hi v-elegacheva, |
|
Thank you for your contribution v-elegacheva! We will review the pull request and get back to you soon. |
| std_used = usage_values.standard_minutes_lifetime if usage_values is not None else None | ||
| high_used = usage_values.high_minutes_lifetime if usage_values is not None else None | ||
|
|
||
| row = OrderedDict() |
There was a problem hiding this comment.
Did you consider creating a structure for return type to improve readability? and probably we can skip remaining and stick to initial structure like this:
{
"providerId" : "atom-dev",
"scope" : "SubscriptionTarget",
"targetId" : "msft.sim.ac1000.physical",
"allocation" : {
"standardMinutesLifetime" : 600,
"highMinutesLifetime" : 60
},
"usage" : {
"standardMinutesLifetime" : 120,
"highMinutesLifetime" : 12
}
}
There was a problem hiding this comment.
I restructured to match your proposed shape. Each row is now:
{
"providerId": "...",
"scope": "SubcriptionTarget",
"targetId": "...",
"allocation": { "standardMinutesLifetime": 0, "highMinutesLifetime": 0 },
"usage": {"standardMinutesLifetime": 0, "highMinutesLifetime": 0}
}
remaining and lastModifiedTime are dropped. I added a small _minutes() helper to build the nested blocks. The table transformer and help text were also updated to match
There was a problem hiding this comment.
thanks, but it still dict, do you think there will be benefit of creating a type with all of these fields and have dot access to the fields?
There was a problem hiding this comment.
The return value of a CLI custom command is serialized straight to the user-facing output, and the CLI's todict serializer uses each object's raw attribute names (vars()). So a plain class/dataclass would emit snake_case keys (provider_id, standard_minutes_lifetime) instead of the providerId / standardMinutesLifetime contract, and a namedTuple serializes as a JSON array. OrderedDict gives exact control over the camelCase keys and ordering that define this command's output, and it is consistent with the rest of the quantum extension (all handlers/ transformers return dicts or SDK models). The dot-access benefit would only apply inside this ~ 15 line builder, which _minutes() already simplifies. If you'd like the shape documented in code, i can switch the row to a TypedDict. That gives type-checking + editor hints and still serializes correctly as a dict. A full dataclass would need custom camelCase serialization to avoid changing the output. Which way would you prefer?
…age output Per review: build one row per targetQuota (SubscriptionTarget scope only), restructure each row into nested 'allocation' and 'usage' blocks, and drop the computed 'remaining' field.
Non-functional follow-ups from code review: add the canary branch to base_url_v2 for parity with base_url, add a @live_only scenario test for 'suite-offer quotas', correct the 'suite-offer list' help summary, and comment the unused factory args.
|
Quantum |
| } | ||
|
|
||
| rows = [] | ||
| for target_quota in sorted(offer.properties.target_quotas or [], key=lambda q: q.target_id or ""): |
There was a problem hiding this comment.
what is the reason of sorting target quotas here?
There was a problem hiding this comment.
It is just to give deterministic, stable output ordering. The service does not guarantee an order for targetQuotas (or the usages list), so sorting by targetId keeps the JSON/ table rows consistent across runs. Which also keeps diffs and the live test stable. I can drop it if you'd rather preserve the service's order :)
|
|
||
| row = OrderedDict() | ||
| row["providerId"] = provider_id | ||
| row["scope"] = "SubscriptionTarget" |
There was a problem hiding this comment.
we can reuse usage.Scope here instead of magic string
There was a problem hiding this comment.
I agree! I did find something worth attention however: a target row can have no matching usage (usage is None for targets with no consumption), so usage.scope isn't always available to read. Since every target row is SubscriptionTarget - scoped by definition, I'll lift the literal into a named constant so it is not a magic string and stays independent of whether a usage row exists. If you would prefer, I can instead read usage.scope when present and fall back to the constant. Please let me know what you would prefer!
| row["providerId"] = provider_id | ||
| row["scope"] = "SubscriptionTarget" | ||
| row["targetId"] = target_quota.target_id | ||
| row["allocation"] = _minutes( |
There was a problem hiding this comment.
I think in UI we show allocation and usage in hours? align with it
There was a problem hiding this comment.
The underlying ARM / DP fields are standardMinutesLifetime / highMinutesLifetime. The values are minutes by contract and the key names literally say "Minutes". The CLI mirrors the service payload, so converting to hours would make the value diagree with its own filed name and with ARM. My instinct was keeping the JSON in minutes (true to contract) and if it helps parity with the UI, adding hours to the table view only. If you would prefer to fully match the UI, we would need new hour-names fields (like standardHoursLifetime) rather than silently dividing the existing ones. Please let me know how you'd like to proceed on this one :)
Lists targets and their status for a suite offer provider account via the data plane, without requiring a workspace. Fixes single-object ProviderStatus parsing, consolidates the data-plane suite-offer client factory, and bumps the extension to 1.0.0b27.
Address review: the data-plane getProviderStatus endpoint returns a single ProviderStatus object per the spec, not a list. Rename the vendored list_provider_status to get_provider_status (sync + async) returning a single ProviderStatus, drop the wrap-in-list workaround, and have the target-list handler wrap the result for the shared table transformer. Update tests accordingly.
…ota allocations merged with usages Replaces the legacy data-plane quotas listing with v2 workspace target quota allocations (from ARM) merged with their consumed quota usages from the data-plane v2 quotaUsages endpoint. The workspace quotaUsages endpoint requires a providerId query parameter, so usages are fetched per provider. Adds a table transformer and unit tests.
|
/azp run |
|
Azure Pipelines: Successfully started running 2 pipeline(s). |
| providers = properties.providers if properties is not None else None | ||
|
|
||
| legacy_client = cf_quotas( | ||
| cmd.cli_ctx, info.subscription, info.resource_group, info.name, base_url(workspace.location)) |
There was a problem hiding this comment.
what is the reason of construction url instead of using info.endpoint here?
there is endpoint field in workspace resource property that contains url to DP, and if it's workspace V2 it will contain url to Data Plane v2
There was a problem hiding this comment.
and it is always requesting DPv1 even if workspace can be v2, which seems to be incorrect
There was a problem hiding this comment.
I agree. There was no need to reconstruct the dp url from the workspace location. I updated this to use workspace.properties.endpoint_uri, which contains the correct endpoint for the workspace's dp version.
I also updated the quota flow to branch by workspace kind. v1 workspaces call the existing /quotas endpoint, while v2 workspaces skip that call and use only the v2 workspace usage API through endpointUri
| v2_client = cf_quotas( | ||
| cmd.cli_ctx, info.subscription, info.resource_group, info.name, base_url_v2(workspace.location)) |
There was a problem hiding this comment.
this seems to be making call to DPv2 to {{workspaceName}}/quotas endpoint which will return 404.
we should get target quota allocations from workspace resource itself
There was a problem hiding this comment.
oh, I see that it is used to list workspace usages only later. But anyway I think we could just reuse workspace's endpoint instead of constructing DPv2 url by ourselves
There was a problem hiding this comment.
Fixed! For v2 workspaces, target quota allocations now come from workspace.properties.providers[].target_quotas on the ARM resource. The only dp request is /quotaUsages for current usage, so the v2 flow no longer calls /quotas. I also added tests that enforce the separate v1 and v2 routing paths
|
|
||
| _WORKSPACE_QUOTA_SCOPE = "Workspace" | ||
| _WORKSPACE_QUOTA_PERIOD = "None" | ||
| _TARGET_QUOTA_DIMENSIONS = ( | ||
| ("StandardMinutesLifetime", "standard_minutes_lifetime"), | ||
| ("HighMinutesLifetime", "high_minutes_lifetime"), |
There was a problem hiding this comment.
wondering if we could reuse values from autogenerated part for these
| def _target_quota_usage_value(usage, attribute): | ||
| if usage is None: | ||
| return None | ||
| if hasattr(usage, "get"): | ||
| return usage.get(_TARGET_QUOTA_USAGE_FIELDS[attribute]) | ||
| return getattr(usage, attribute, None) |
There was a problem hiding this comment.
do we really need this? could we just use usage.standard_minutes_lifetime?
| raise InvalidArgumentValueError( | ||
| f"Cannot validate --quota because target '{target_quota.target_id}' was not found in the " | ||
| f"suite offer for provider '{provider.provider_id}'. Run 'az quantum suite-offer quotas " | ||
| f"--provider-id {provider.provider_id}' to view available target allocations." | ||
| ) |
There was a problem hiding this comment.
if no corresponding target quota allocation found in suite offer, let's assume it is set to 0, so we show error to the user that target quota allocation should be set on subscription level first
🤖 PR Validation —⚠️ Review suggested
Summary
Adds suite-offer and V2 target-quota support to the
quantumextension:az quantum suite-offer listlists the suite offers available to the subscription.az quantum suite-offer quotas --provider-id <id>lists per-target suite allocations merged with consumed Standard and High usage.az quantum suite-offer target list --provider-id <id>lists targets available through a suite-offer provider account without requiring a workspace.az quantum workspace quotasnow includes V2 target allocations and usages while preserving the existing V1 response format.az quantum workspace create/update --quotanow validates final V2 target allocations against current workspace usage and suite target allocations.The suite-offer data-plane commands resolve the provider account's region from its control-plane suite offer and call the corresponding regional V2 endpoint.
Changes
Suite offers
0.cf_suite_offers_data_plane.Workspace quotas
targetIdto identify V2 target quota rows.StandardMinutesLifetimeandHighMinutesLifetimerows for each V2 target.0.Workspace quota validation
Added always-on validation for V2 workspace target allocations on create and update.
For each target and priority, the final absolute allocation must satisfy:
current workspace usage <= final workspace allocation <= suite target allocationquota add,--validate-only, or validation bypass option was added.Provider status parsing
The
providerStatusendpoint returns a singleProviderStatusobject rather than a paged{ "value": [...] }response. The command wraps that object for table transformation while the generated client continues to tolerate paged and array responses.Known model drift
The DataPlaneV2 backend returns
averageQueueTimeHighPriorityandaverageQueueTimeStandardPriority, but those fields are not currently declared by the vendoredTargetStatusmodel and are dropped during deserialization.The public specification and generated data-plane client are being updated separately. The regenerated client will be integrated before the priority-specific queue times are added to the CLI table.
Testing
git diff --check: passed.