Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/quantum/HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
Release History
===============

1.0.0b22
+++++++++++++++
* Added ``--quota`` support to ``az quantum workspace create`` and ``az quantum workspace update`` for managing V2 provider target quota allocations.
* Updated control plane related commands to use API version 2026-06-15-preview.
Comment thread
v-elegacheva marked this conversation as resolved.

1.0.0b21
+++++++++++++++
* Added the ``az quantum job update`` command to update a submitted job's name, priority, and tags.
Expand Down
10 changes: 10 additions & 0 deletions src/quantum/azext_quantum/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,12 @@
-r "MyProvider1 / MySKU1, MyProvider2 / MySKU2" --skip-autoadd -a MyStorageAccountName\n
To display a list of available providers and their SKUs, use the following command:
az quantum offerings list -l MyLocation -o table
- name: Create a V2 workspace with quota allocations for provider targets.
text: |-
az quantum workspace create -g MyResourceGroup -w MyWorkspace -l MyLocation \\
--workspace-kind V2 -r "MyProvider/default" --skip-autoadd -a MyStorageAccountName \\
--quota provider-id=MyProvider target-id=MyProvider.Target1 standard-minutes-lifetime=500 high-minutes-lifetime=50 \\
--quota provider-id=MyProvider target-id=MyProvider.Target2 standard-minutes-lifetime=250
"""

helps['quantum workspace delete'] = """
Expand Down Expand Up @@ -352,6 +358,10 @@
- name: Disable a provided Azure Quantum workspace api keys.
text: |-
az quantum workspace update --enable-api-key False
- name: Update a target quota allocation on a V2 workspace.
text: |-
az quantum workspace update -g MyResourceGroup -w MyWorkspace \\
--quota provider-id=MyProvider target-id=MyProvider.Target1 standard-minutes-lifetime=1000
"""

helps['quantum workspace keys'] = """
Expand Down
125 changes: 125 additions & 0 deletions src/quantum/azext_quantum/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
# pylint: disable=line-too-long,protected-access,too-many-statements

import argparse
import re
from knack.arguments import CLIArgumentType
from azure.cli.core.azclierror import InvalidArgumentValueError, CLIError
from azure.cli.core.commands.parameters import get_enum_type
Expand All @@ -32,6 +33,127 @@ def get_action(self, values, option_string):
return params


class QuotaAction(argparse._AppendAction):
# targetId: leading alphanumeric followed by up to 199 more alphanumeric, '-', '.', or '_' characters.
_TARGET_ID_PATTERN = re.compile(r'[a-zA-Z0-9][-._a-zA-Z0-9]{0,199}')
# Maximum value accepted for minutes-lifetime fields (Int32 max, matching the service contract).
_MAX_MINUTES_LIFETIME = 2147483647

_allowed_keys = {
'providerId',
'targetId',
'standardMinutesLifetime',
'highMinutesLifetime'
Comment thread
v-elegacheva marked this conversation as resolved.
}

# Accept az-style kebab-case and snake_case keys (case-insensitive) as aliases of the camelCase keys.
_key_aliases = {
'providerid': 'providerId',
'targetid': 'targetId',
'standardminuteslifetime': 'standardMinutesLifetime',
'highminuteslifetime': 'highMinutesLifetime'
}

@classmethod
def _canonical_key(cls, key):
if not isinstance(key, str):
return key
normalized = key.strip().lower().replace('-', '').replace('_', '')
return cls._key_aliases.get(normalized, key.strip())

def __call__(self, parser, namespace, values, option_string=None):
allocations = list(getattr(namespace, self.dest, None) or [])
parsed_values = []
current = {}

def add_to_current(key, value):
canonical = self._canonical_key(key)
if canonical in current:
raise InvalidArgumentValueError(
f'{option_string} got multiple values for "{key}" in a single allocation. '
f'Specify a separate {option_string} for each target.'
)
current[canonical] = value

for item in values:
try:
parsed = shell_safe_json_parse(item)
if isinstance(parsed, list):
parsed_values.extend(parsed)
elif isinstance(parsed, dict):
for key, value in parsed.items():
add_to_current(key, value)
else:
raise InvalidArgumentValueError(
f'Usage error: {option_string} expects key=value pairs, a JSON object or array, or @file.'
)
except CLIError:
try:
key, value = item.split('=', 1)
except ValueError as ex:
raise InvalidArgumentValueError(
f'Usage error: {option_string} expects key=value pairs, a JSON object or array, or @file.'
) from ex
add_to_current(key, value)

if current:
Comment thread
v-elegacheva marked this conversation as resolved.
parsed_values.append(current)

allocations.extend(self._validate(allocation, option_string) for allocation in parsed_values)
pairs = [(item['providerId'].lower(), item['targetId'].lower()) for item in allocations]
if len(pairs) != len(set(pairs)):
raise InvalidArgumentValueError(f'Duplicate providerId/targetId pair specified for {option_string}.')

setattr(namespace, self.dest, allocations)

@classmethod
def _validate(cls, allocation, option_string):
if not isinstance(allocation, dict):
raise InvalidArgumentValueError(f'Each {option_string} allocation must be a JSON object.')

allocation = {cls._canonical_key(key): value for key, value in allocation.items()}

unknown_keys = set(allocation) - cls._allowed_keys
if unknown_keys:
raise InvalidArgumentValueError(
f'Unsupported key(s) for {option_string}: {", ".join(sorted(unknown_keys))}.'
)

for required_key in ('providerId', 'targetId'):
if not allocation.get(required_key):
raise InvalidArgumentValueError(f'{option_string} requires {required_key}.')

target_id = allocation['targetId']
if not isinstance(target_id, str) or not cls._TARGET_ID_PATTERN.fullmatch(target_id):
raise InvalidArgumentValueError(f'{option_string} targetId is not valid: {target_id}')

if not any(key in allocation for key in ('standardMinutesLifetime', 'highMinutesLifetime')):
raise InvalidArgumentValueError(
f'{option_string} requires standardMinutesLifetime and/or highMinutesLifetime.'
)

result = {
'providerId': str(allocation['providerId']),
'targetId': target_id
}
for key in ('standardMinutesLifetime', 'highMinutesLifetime'):
if key not in allocation:
continue
allocation_value = allocation[key]
if isinstance(allocation_value, (bool, float)):
raise InvalidArgumentValueError(f'{option_string} {key} must be an integer.')
try:
value = int(allocation_value)
except (TypeError, ValueError) as ex:
raise InvalidArgumentValueError(f'{option_string} {key} must be an integer.') from ex
if value < 0 or value > cls._MAX_MINUTES_LIFETIME:
raise InvalidArgumentValueError(
f'{option_string} {key} must be between 0 and {cls._MAX_MINUTES_LIFETIME}.'
)
result[key] = value
return result


def load_arguments(self, _): # pylint: disable=too-many-locals
workspace_name_type = CLIArgumentType(options_list=['--workspace-name', '-w'], help='Name of the Quantum Workspace. You can configure the default workspace using `az quantum workspace set`.', configured_default='workspace', id_part=None)
storage_account_name_type = CLIArgumentType(options_list=['--storage-account', '-a'], help='Name of the storage account to be used by a quantum workspace.')
Expand Down Expand Up @@ -60,6 +182,7 @@ def load_arguments(self, _): # pylint: disable=too-many-locals
entry_point_type = CLIArgumentType(help='The entry point for the QIR program or circuit. Required for some provider QIR jobs.')
skip_autoadd_type = CLIArgumentType(help='If specified, the plans that offer free credits will not automatically be added.')
workspace_kind_type = CLIArgumentType(options_list=['--workspace-kind'], help='The kind of the workspace to create.', choices=['V1', 'V2'])
quota_type = CLIArgumentType(options_list=['--quota'], help='Target quota allocation as provider-id, target-id, standard-minutes-lifetime, and optional high-minutes-lifetime key=value pairs, a JSON object or array, or `@{file}` with JSON content. standard-minutes-lifetime is required for a new allocation. camelCase keys (providerId, targetId, ...) are also accepted. Repeat --quota once per target.', action=QuotaAction, nargs='+')
key_type = CLIArgumentType(options_list=['--key-type'], help='The api keys to be regenerated, should be Primary and/or Secondary.')
enable_key_type = CLIArgumentType(options_list=['--enable-api-key'], help='Enable or disable API key authentication.')
job_type_type = CLIArgumentType(options_list=['--job-type'], help='Job type to be listed, example "QuantumComputing".')
Expand All @@ -85,6 +208,7 @@ def load_arguments(self, _): # pylint: disable=too-many-locals
c.argument('auto_accept', auto_accept_type)
c.argument('skip_autoadd', skip_autoadd_type)
c.argument('workspace_kind', workspace_kind_type)
c.argument('quota', quota_type)

with self.argument_context('quantum workspace user') as c:
c.argument('workspace_name', workspace_name_type)
Expand Down Expand Up @@ -181,3 +305,4 @@ def load_arguments(self, _): # pylint: disable=too-many-locals
with self.argument_context('quantum workspace update') as c:
c.argument('workspace_name', workspace_name_type)
c.argument('enable_key', enable_key_type)
c.argument('quota', quota_type)
2 changes: 1 addition & 1 deletion src/quantum/azext_quantum/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ def load_command_table(self, _):
w.command('quotas', 'quotas', validator=validate_workspace_info)
Comment thread
v-elegacheva marked this conversation as resolved.
w.command('keys list', 'list_keys')
w.command('keys regenerate', 'regenerate_keys')
w.command('update', 'enable_keys')
w.command('update', 'update')

with self.command_group('quantum workspace user', workspace_ops) as u:
u.command('create', 'add_user', validator=validate_workspace_info)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@
"resources": [
{
"type": "Microsoft.Quantum/workspaces",
"apiVersion": "2025-12-15-preview",
"apiVersion": "2026-06-15-preview",
"name": "[parameters('quantumWorkspaceName')]",
"location": "[parameters('location')]",
"tags": "[parameters('tags')]",
Expand Down Expand Up @@ -166,12 +166,12 @@
},
{
"apiVersion": "2020-04-01-preview",
"name": "[concat(parameters('storageAccountName'), '/Microsoft.Authorization/', guid(reference(concat('Microsoft.Quantum/Workspaces/', parameters('quantumWorkspaceName')), '2025-12-15-preview', 'Full').identity.principalId, variables('storageAccountContributorRoleId')))]",
"name": "[concat(parameters('storageAccountName'), '/Microsoft.Authorization/', guid(reference(concat('Microsoft.Quantum/Workspaces/', parameters('quantumWorkspaceName')), '2026-06-15-preview', 'Full').identity.principalId, variables('storageAccountContributorRoleId')))]",
"type": "Microsoft.Storage/storageAccounts/providers/roleAssignments",
"location": "[parameters('storageAccountLocation')]",
"properties": {
"roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', variables('storageAccountContributorRoleId'))]",
"principalId": "[reference(concat('Microsoft.Quantum/Workspaces/', parameters('quantumWorkspaceName')), '2025-12-15-preview', 'Full').identity.principalId]",
"principalId": "[reference(concat('Microsoft.Quantum/Workspaces/', parameters('quantumWorkspaceName')), '2026-06-15-preview', 'Full').identity.principalId]",
"principalType": "ServicePrincipal"
},
"dependsOn": [
Expand All @@ -180,12 +180,12 @@
},
{
"apiVersion": "2020-04-01-preview",
"name": "[concat(parameters('storageAccountName'), '/Microsoft.Authorization/', guid(reference(concat('Microsoft.Quantum/Workspaces/', parameters('quantumWorkspaceName')), '2025-12-15-preview', 'Full').identity.principalId, variables('storageBlobDataContributorRoleId')))]",
"name": "[concat(parameters('storageAccountName'), '/Microsoft.Authorization/', guid(reference(concat('Microsoft.Quantum/Workspaces/', parameters('quantumWorkspaceName')), '2026-06-15-preview', 'Full').identity.principalId, variables('storageBlobDataContributorRoleId')))]",
"type": "Microsoft.Storage/storageAccounts/providers/roleAssignments",
"location": "[parameters('storageAccountLocation')]",
"properties": {
"roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', variables('storageBlobDataContributorRoleId'))]",
"principalId": "[reference(concat('Microsoft.Quantum/Workspaces/', parameters('quantumWorkspaceName')), '2025-12-15-preview', 'Full').identity.principalId]",
"principalId": "[reference(concat('Microsoft.Quantum/Workspaces/', parameters('quantumWorkspaceName')), '2026-06-15-preview', 'Full').identity.principalId]",
"principalType": "ServicePrincipal"
},
"dependsOn": [
Expand Down
Loading
Loading