Skip to content

Quantum: Add 'az quantum suite-offer quotas' command - #10285

Draft
v-elegacheva wants to merge 23 commits into
Azure:mainfrom
v-elegacheva:ekat/quantum-suite-offer-quotas
Draft

Quantum: Add 'az quantum suite-offer quotas' command#10285
v-elegacheva wants to merge 23 commits into
Azure:mainfrom
v-elegacheva:ekat/quantum-suite-offer-quotas

Conversation

@v-elegacheva

@v-elegacheva v-elegacheva commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

🤖 PR Validation — ⚠️ Review suggested

Breaking Changes
⚠️ None
⚠️Azure CLI Extensions Breaking Change Test
⚠️quantum
rule cmd_name rule_message suggest_message
⚠️ 1001 - CmdAdd quantum suite-offer quotas cmd quantum suite-offer quotas added
⚠️ 1011 - SubgroupAdd quantum suite-offer target sub group quantum suite-offer target added

Summary

Adds suite-offer and V2 target-quota support to the quantum extension:

  • az quantum suite-offer list lists 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 quotas now includes V2 target allocations and usages while preserving the existing V1 response format.
  • az quantum workspace create/update --quota now 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

  • Added subscription-level suite-offer listing.
  • Added per-target quota allocation and usage output:
    • JSON retains the service contract's minute-based values.
    • Table output displays allocation and usage in hours.
    • Missing allocation or usage values are displayed as 0.
  • Added suite-offer target status listing without requiring a workspace.
  • Consolidated the data-plane suite-offer client factories into cf_suite_offers_data_plane.

Workspace quotas

  • Preserved the existing flat V1 quota response fields.
  • Added optional targetId to identify V2 target quota rows.
  • Added separate StandardMinutesLifetime and HighMinutesLifetime rows for each V2 target.
  • Combined control-plane target allocations with data-plane usage.
  • Included usage-only targets so historical usage is not hidden when an allocation is absent.
  • Used the V1 regional endpoint for legacy quota dimensions and the V2 regional endpoint for target usage.
  • Returned missing V2 allocation and usage values as 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 allocation

  • Equality is allowed at both boundaries.
  • Standard and High allocations are validated independently.
  • Create validates against the suite target allocation, using zero as initial workspace usage.
  • Update validates against live workspace usage and the suite target allocation.
  • Priority values omitted during update are preserved and validated.
  • Validation completes before the workspace write.
  • A missing workspace usage row is treated as zero; other failures retrieving required validation data prevent the operation.
  • No quota add, --validate-only, or validation bypass option was added.

Provider status parsing

The providerStatus endpoint returns a single ProviderStatus object 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 averageQueueTimeHighPriority and averageQueueTimeStandardPriority, but those fields are not currently declared by the vendored TargetStatus model 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

  • Full Quantum test suite: 70 passed, 14 skipped.
  • Focused workspace quota validation tests: 13 passed.
  • Suite-offer tests cover request construction, deserialization, table transformation, single-object provider-status handling, and quota merge behavior.
  • PPE suite-offer E2E verified table and JSON output against active provider accounts.
  • PPE workspace validation E2E:
    • Rejected an allocation below fractional workspace usage.
    • Rejected an allocation above the suite target allocation.
    • Accepted a valid idempotent allocation update.
    • Confirmed that an omitted High allocation was preserved.
  • Azure CLI linter, changed-file Flake8, help rendering, VS Code diagnostics, and git diff --check: passed.

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.
@azure-client-tools-bot-prd

Copy link
Copy Markdown

Hi v-elegacheva,
Please write the description of changes which can be perceived by customers into HISTORY.rst.
If you want to release a new extension version, please update the version in pyproject.toml (or setup.py, if the extension has not migrated yet) as well.

@v-elegacheva v-elegacheva changed the title [Quantum] Add 'az quantum suite-offer quotas' command Quantum: Add 'az quantum suite-offer quotas' command Sep 1, 2026
@microsoft-github-policy-service microsoft-github-policy-service Bot added the customer-reported Issues that are reported by GitHub users external to the Azure organization. label Sep 1, 2026
@microsoft-github-policy-service

Copy link
Copy Markdown
Contributor

Thank you for your contribution v-elegacheva! We will review the pull request and get back to you soon.

Comment thread src/quantum/azext_quantum/operations/suite_offers.py Outdated
Comment thread src/quantum/azext_quantum/tests/latest/test_quantum_suite_offers.py Outdated
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()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
}
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@yonzhan

Copy link
Copy Markdown
Collaborator

Quantum

}

rows = []
for target_quota in sorted(offer.properties.target_quotas or [], key=lambda q: q.target_id or ""):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the reason of sorting target quotas here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can reuse usage.Scope here instead of magic string

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think in UI we show allocation and usage in hours? align with it

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@a0x1ab

Copy link
Copy Markdown
Member

/azp run

@azure-pipelines

Copy link
Copy Markdown
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))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and it is always requesting DPv1 even if workspace can be v2, which seems to be incorrect

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +574 to +575
v2_client = cf_quotas(
cmd.cli_ctx, info.subscription, info.resource_group, info.name, base_url_v2(workspace.location))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +587 to +592

_WORKSPACE_QUOTA_SCOPE = "Workspace"
_WORKSPACE_QUOTA_PERIOD = "None"
_TARGET_QUOTA_DIMENSIONS = (
("StandardMinutesLifetime", "standard_minutes_lifetime"),
("HighMinutesLifetime", "high_minutes_lifetime"),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wondering if we could reuse values from autogenerated part for these

Comment on lines +281 to +286
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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we really need this? could we just use usage.standard_minutes_lifetime?

Comment on lines +329 to +333
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."
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

act-codegen-extensibility-squad Auto-Assign Auto assign by bot customer-reported Issues that are reported by GitHub users external to the Azure organization. Quantum az quantum

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants