diff --git a/src/quantum/HISTORY.rst b/src/quantum/HISTORY.rst index 711c940ac30..a3a1865efaf 100644 --- a/src/quantum/HISTORY.rst +++ b/src/quantum/HISTORY.rst @@ -6,4 +6,16 @@ Release History 0.11.2906.2 ++++++ * Initial release. Version intended to work with Azure Quantum Private Preview - and with QDK version 0.11.2906.* \ No newline at end of file + and with QDK version 0.11.2906.* + +0.13.2011.1901 +++++++ +* Adding methods for creating and deleting workspaces. +* Note: Setting providers from the CLI is not supported. +* Aligned to QDK 0.13.20111004. + +0.14.2012.701 +++++++ +* Updating multi-region in data plane REST API +* Aligned to QDK 0.14.2011120240. + diff --git a/src/quantum/azext_quantum/_client_factory.py b/src/quantum/azext_quantum/_client_factory.py index 6ea7b65e8fc..1f112784470 100644 --- a/src/quantum/azext_quantum/_client_factory.py +++ b/src/quantum/azext_quantum/_client_factory.py @@ -6,18 +6,22 @@ # pylint: disable=line-too-long import os +from ._location_helper import normalize_location def is_env(name): return 'AZURE_QUANTUM_ENV' in os.environ and os.environ['AZURE_QUANTUM_ENV'] == name -def base_url(): +def base_url(location): if 'AZURE_QUANTUM_BASEURL' in os.environ: return os.environ['AZURE_QUANTUM_BASEURL'] if is_env('canary'): - return "https://app-jobs-canarysouthcentralus.azurewebsites.net/" - return "https://app-jobscheduler-prod.azurewebsites.net/" + return "https://eastus2euap.quantum.azure.com/" + normalized_location = normalize_location(location) + if is_env('dogfood'): + return f"https://{normalized_location}.quantum-test.azure.com/" + return f"https://{normalized_location}.quantum.azure.com/" def _get_data_credentials(cli_ctx, subscription_id=None): @@ -27,10 +31,10 @@ def _get_data_credentials(cli_ctx, subscription_id=None): return creds -def cf_quantum(cli_ctx, subscription_id=None, resource_group_name=None, workspace_name=None): +def cf_quantum(cli_ctx, subscription_id=None, resource_group_name=None, workspace_name=None, location=None): from .vendored_sdks.azure_quantum import QuantumClient creds = _get_data_credentials(cli_ctx, subscription_id) - return QuantumClient(creds, subscription_id, resource_group_name, workspace_name, base_url=base_url()) + return QuantumClient(creds, subscription_id, resource_group_name, workspace_name, base_url=base_url(location)) def cf_quantum_mgmt(cli_ctx, *_): @@ -43,9 +47,9 @@ def cf_workspaces(cli_ctx, *_): return cf_quantum_mgmt(cli_ctx).workspaces -def cf_providers(cli_ctx, subscription_id=None, resource_group_name=None, workspace_name=None): - return cf_quantum(cli_ctx, subscription_id, resource_group_name, workspace_name).providers +def cf_providers(cli_ctx, subscription_id=None, resource_group_name=None, workspace_name=None, location=None): + return cf_quantum(cli_ctx, subscription_id, resource_group_name, workspace_name, location).providers -def cf_jobs(cli_ctx, subscription_id=None, resource_group_name=None, workspace_name=None): - return cf_quantum(cli_ctx, subscription_id, resource_group_name, workspace_name).jobs +def cf_jobs(cli_ctx, subscription_id=None, resource_group_name=None, workspace_name=None, location=None): + return cf_quantum(cli_ctx, subscription_id, resource_group_name, workspace_name, location).jobs diff --git a/src/quantum/azext_quantum/_help.py b/src/quantum/azext_quantum/_help.py index d2f01030ffa..1a91e8486fd 100644 --- a/src/quantum/azext_quantum/_help.py +++ b/src/quantum/azext_quantum/_help.py @@ -21,7 +21,7 @@ - name: Submit the Q# program from the current folder text: |- az quantum job submit -g MyResourceGroup -w MyWorkspace \\ - --job-name MyJob + -l MyLocation --job-name MyJob - name: Get the status of an Azure Quantum job text: |- az quantum job show -g MyResourceGroup -w MyWorkspace \\ @@ -38,7 +38,7 @@ examples: - name: Get the list of targets available in a Azure Quantum workspaces text: |- - az quantum target list -g MyResourceGroup -w MyWorkspace + az quantum target list -g MyResourceGroup -w MyWorkspace -l MyLocation - name: Select a default when submitting jobs to Azure Quantum text: |- az quantum target set -t target-id @@ -62,7 +62,7 @@ az quantum workspace delete -g MyResourceGroup -w MyWorkspace - name: Select a default Azure Quantum workspace for future commands text: |- - az quantum workspace set -g MyResourceGroup -w MyWorkspace + az quantum workspace set -g MyResourceGroup -w MyWorkspace -l MyLocation - name: Show the currently selected default Azure Quantum workspace text: |- az quantum workspace show diff --git a/src/quantum/azext_quantum/_location_helper.py b/src/quantum/azext_quantum/_location_helper.py new file mode 100644 index 00000000000..20b7caae2e6 --- /dev/null +++ b/src/quantum/azext_quantum/_location_helper.py @@ -0,0 +1,20 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +import re + +DEFAULT_WORKSPACE_LOCATION = 'westus' + +# Currently, we're only checking that the provided location doesn't contain unsafe characters +# but there is no guarantee that the returned value exists as an Azure region. +# If an invalid region is specified, then the error will happen when the corresponding API +# endpoint isn't found. +def normalize_location(raw_location): + if not raw_location: + return DEFAULT_WORKSPACE_LOCATION + location = re.sub("[^A-Za-z0-9]","",raw_location).lower() + if not location: + return DEFAULT_WORKSPACE_LOCATION + return location diff --git a/src/quantum/azext_quantum/_validators.py b/src/quantum/azext_quantum/_validators.py index a5078fd9eb8..766f14663e0 100644 --- a/src/quantum/azext_quantum/_validators.py +++ b/src/quantum/azext_quantum/_validators.py @@ -8,14 +8,14 @@ from .operations.workspace import WorkspaceInfo from .operations.target import TargetInfo - -def validate_workspace_info(cmd, namespace): +def validate_workspace_internal(cmd, namespace, require_location): """ - Makes sure all parameters for a workspace are available. + Internal implementation to validate workspace info parameters with an optional location """ group = getattr(namespace, 'resource_group_name', None) name = getattr(namespace, 'workspace_name', None) - ws = WorkspaceInfo(cmd, group, name) + location = getattr(namespace, 'location', None) + ws = WorkspaceInfo(cmd, group, name, location) if not ws.subscription: raise ValueError("Missing subscription argument") @@ -23,6 +23,21 @@ def validate_workspace_info(cmd, namespace): raise ValueError("Missing resource-group argument") if not ws.name: raise ValueError("Missing workspace-name argument") + if require_location and not ws.location: + raise ValueError("Missing location argument") + +def validate_workspace_info(cmd, namespace): + """ + Makes sure all parameters for a workspace are available including location. + """ + validate_workspace_internal(cmd, namespace, True) + + +def validate_workspace_info_no_location(cmd, namespace): + """ + Makes sure all parameters for a workspace are available, not including location. + """ + validate_workspace_internal(cmd, namespace, False) def validate_target_info(cmd, namespace): diff --git a/src/quantum/azext_quantum/commands.py b/src/quantum/azext_quantum/commands.py index a5481ca273d..874b19c0835 100644 --- a/src/quantum/azext_quantum/commands.py +++ b/src/quantum/azext_quantum/commands.py @@ -7,7 +7,7 @@ from collections import OrderedDict from azure.cli.core.commands import CliCommandType -from ._validators import validate_workspace_info, validate_target_info, validate_workspace_and_target_info +from ._validators import validate_workspace_info, validate_target_info, validate_workspace_and_target_info, validate_workspace_info_no_location def transform_targets(providers): @@ -83,9 +83,9 @@ def load_command_table(self, _): with self.command_group('quantum workspace', workspace_ops) as w: w.command('create', 'create') - w.command('delete', 'delete', validator=validate_workspace_info) + w.command('delete', 'delete', validator=validate_workspace_info_no_location) w.command('list', 'list') - w.command('show', 'show', validator=validate_workspace_info) + w.command('show', 'show', validator=validate_workspace_info_no_location) w.command('set', 'set', validator=validate_workspace_info) w.command('clear', 'clear') diff --git a/src/quantum/azext_quantum/operations/job.py b/src/quantum/azext_quantum/operations/job.py index 238cdb0e81d..6a4aedc7ae8 100644 --- a/src/quantum/azext_quantum/operations/job.py +++ b/src/quantum/azext_quantum/operations/job.py @@ -16,21 +16,21 @@ logger = logging.getLogger(__name__) -def list(cmd, resource_group_name=None, workspace_name=None): +def list(cmd, resource_group_name=None, workspace_name=None, location=None): """ Get the list of jobs in a Quantum Workspace. """ - info = WorkspaceInfo(cmd, resource_group_name, workspace_name) - client = cf_jobs(cmd.cli_ctx, info.subscription, info.resource_group, info.name) + info = WorkspaceInfo(cmd, resource_group_name, workspace_name, location) + client = cf_jobs(cmd.cli_ctx, info.subscription, info.resource_group, info.name, info.location) return client.list() -def show(cmd, job_id, resource_group_name=None, workspace_name=None): +def show(cmd, job_id, resource_group_name=None, workspace_name=None, location=None): """ Get the job's status and details. """ - info = WorkspaceInfo(cmd, resource_group_name, workspace_name) - client = cf_jobs(cmd.cli_ctx, info.subscription, info.resource_group, info.name) + info = WorkspaceInfo(cmd, resource_group_name, workspace_name, location) + client = cf_jobs(cmd.cli_ctx, info.subscription, info.resource_group, info.name, info.location) return client.get(job_id) @@ -160,7 +160,7 @@ def _parse_blob_url(url): } -def output(cmd, job_id, resource_group_name=None, workspace_name=None): +def output(cmd, job_id, resource_group_name=None, workspace_name=None, location=None): """ Get the results of a Q# execution. """ @@ -176,8 +176,8 @@ def output(cmd, job_id, resource_group_name=None, workspace_name=None): else: logger.debug("Downloading job results blob into %s", path) - info = WorkspaceInfo(cmd, resource_group_name, workspace_name) - client = cf_jobs(cmd.cli_ctx, info.subscription, info.resource_group, info.name) + info = WorkspaceInfo(cmd, resource_group_name, workspace_name, location) + client = cf_jobs(cmd.cli_ctx, info.subscription, info.resource_group, info.name, info.location) job = client.get(job_id) if job.status != "Succeeded": @@ -192,7 +192,7 @@ def output(cmd, job_id, resource_group_name=None, workspace_name=None): return data -def wait(cmd, job_id, resource_group_name=None, workspace_name=None, max_poll_wait_secs=5): +def wait(cmd, job_id, resource_group_name=None, workspace_name=None, location=None, max_poll_wait_secs=5): """ Place the CLI in a waiting state until the job finishes execution. """ @@ -201,8 +201,8 @@ def wait(cmd, job_id, resource_group_name=None, workspace_name=None, max_poll_wa def has_completed(job): return job.status in ("Succeeded", "Failed", "Cancelled") - info = WorkspaceInfo(cmd, resource_group_name, workspace_name) - client = cf_jobs(cmd.cli_ctx, info.subscription, info.resource_group, info.name) + info = WorkspaceInfo(cmd, resource_group_name, workspace_name, location) + client = cf_jobs(cmd.cli_ctx, info.subscription, info.resource_group, info.name, info.location) # TODO: LROPoller... w = False @@ -222,8 +222,8 @@ def has_completed(job): return job -def execute(cmd, program_args, resource_group_name=None, workspace_name=None, target_id=None, project=None, - job_name=None, shots=None, storage=None, no_build=False): +def execute(cmd, program_args, resource_group_name=None, workspace_name=None, location=None, target_id=None, + project=None, job_name=None, shots=None, storage=None, no_build=False): """ Submit a job for quantum execution on Azure Quantum, and waits for the result. """ @@ -231,7 +231,7 @@ def execute(cmd, program_args, resource_group_name=None, workspace_name=None, ta logger.warning("Job id: %s", job.id) logger.debug(job) - job = wait(cmd, job.id, resource_group_name, workspace_name) + job = wait(cmd, job.id, resource_group_name, workspace_name, location) logger.debug(job) if not job.status == "Succeeded": diff --git a/src/quantum/azext_quantum/operations/target.py b/src/quantum/azext_quantum/operations/target.py index 03dd62b1176..78dff608b7e 100644 --- a/src/quantum/azext_quantum/operations/target.py +++ b/src/quantum/azext_quantum/operations/target.py @@ -51,12 +51,12 @@ def set(cmd, target_id=None): return info -def list(cmd, resource_group_name=None, workspace_name=None): +def list(cmd, resource_group_name=None, workspace_name=None, location=None): """ Get the list of providers and their targets in an Azure Quantum workspace. """ - info = WorkspaceInfo(cmd, resource_group_name, workspace_name) - client = cf_providers(cmd.cli_ctx, info.subscription, info.resource_group, info.name) + info = WorkspaceInfo(cmd, resource_group_name, workspace_name, location) + client = cf_providers(cmd.cli_ctx, info.subscription, info.resource_group, info.name, info.location) return client.get_status() diff --git a/src/quantum/azext_quantum/operations/workspace.py b/src/quantum/azext_quantum/operations/workspace.py index c00561f34b3..f7c43a9d5bf 100644 --- a/src/quantum/azext_quantum/operations/workspace.py +++ b/src/quantum/azext_quantum/operations/workspace.py @@ -14,7 +14,7 @@ class WorkspaceInfo(object): - def __init__(self, cmd, resource_group_name=None, workspace_name=None): + def __init__(self, cmd, resource_group_name=None, workspace_name=None, location=None): from azure.cli.core.commands.client_factory import get_subscription_id # Hierarchically selects the value for the given key. @@ -33,11 +33,13 @@ def select_value(key, value): self.subscription = get_subscription_id(cmd.cli_ctx) self.resource_group = select_value('group', resource_group_name) self.name = select_value('workspace', workspace_name) + self.location = select_value('location', location) def clear(self): self.subscription = '' self.resource_group = '' self.name = '' + self.location = '' def save(self, cmd): from azure.cli.core.util import ConfiguredDefaultSetter @@ -45,12 +47,13 @@ def save(self, cmd): with ConfiguredDefaultSetter(cmd.cli_ctx.config, False): cmd.cli_ctx.config.set_value('quantum', 'group', self.resource_group) cmd.cli_ctx.config.set_value('quantum', 'workspace', self.name) + cmd.cli_ctx.config.set_value('quantum', 'location', self.location) + def get_basic_quantum_workspace(location, info, storage_account): qw = QuantumWorkspace() # Use a default provider - # Replace this with user specified providers as part of task: - # https://ms-quantum.visualstudio.com/Quantum%20Program/_workitems/edit/16184 + # Replace this with user specified providers as part of task 16184. prov = Provider() prov.provider_id = "Microsoft" prov.provider_sku = "Basic" @@ -110,7 +113,7 @@ def show(cmd, resource_group_name=None, workspace_name=None): Get the details of the given (or current) Azure Quantum workspace. """ client = cf_workspaces(cmd.cli_ctx) - info = WorkspaceInfo(cmd, resource_group_name, workspace_name) + info = WorkspaceInfo(cmd, resource_group_name, workspace_name, None) if (not info.resource_group) or (not info.name): raise CLIError("Please run 'az quantum workspace set' first to select a default Quantum Workspace.") ws = client.get(info.resource_group, info.name) diff --git a/src/quantum/setup.py b/src/quantum/setup.py index 4cf24b9ad77..d17b385bda0 100644 --- a/src/quantum/setup.py +++ b/src/quantum/setup.py @@ -16,7 +16,7 @@ # TODO: Confirm this is the right version number you want and it matches your # HISTORY.rst entry. -VERSION = '0.12.2010.1901' +VERSION = '0.14.2012.701' # The full list of classifiers is available at # https://pypi.python.org/pypi?%3Aaction=list_classifiers