Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update Azure CLI quantum extension to multi-region #17

Merged
merged 8 commits into from
Dec 8, 2020
14 changes: 13 additions & 1 deletion src/quantum/HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.*
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.

22 changes: 13 additions & 9 deletions src/quantum/azext_quantum/_client_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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, *_):
Expand All @@ -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
6 changes: 3 additions & 3 deletions src/quantum/azext_quantum/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 \\
Expand All @@ -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
Expand All @@ -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
Expand Down
21 changes: 21 additions & 0 deletions src/quantum/azext_quantum/_location_helper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# --------------------------------------------------------------------------------------------
# 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):
ricardo-espinoza marked this conversation as resolved.
Show resolved Hide resolved
if (not raw_location):
ricardo-espinoza marked this conversation as resolved.
Show resolved Hide resolved
return DEFAULT_WORKSPACE_LOCATION
location = re.sub("[^A-Za-z0-9]","",raw_location).lower()
if (location == ""):
ricardo-espinoza marked this conversation as resolved.
Show resolved Hide resolved
return DEFAULT_WORKSPACE_LOCATION
return location

23 changes: 19 additions & 4 deletions src/quantum/azext_quantum/_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,36 @@
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")
if not ws.resource_group:
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 including location.
ricardo-espinoza marked this conversation as resolved.
Show resolved Hide resolved
"""
validate_workspace_internal(cmd, namespace, False)


def validate_target_info(cmd, namespace):
Expand Down
6 changes: 3 additions & 3 deletions src/quantum/azext_quantum/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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')

Expand Down
30 changes: 15 additions & 15 deletions src/quantum/azext_quantum/operations/job.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand Down Expand Up @@ -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.
"""
Expand All @@ -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":
Expand All @@ -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.
"""
Expand All @@ -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
Expand All @@ -222,16 +222,16 @@ 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.
"""
job = submit(cmd, program_args, resource_group_name, workspace_name, target_id, project, job_name, shots, storage, no_build)
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":
Expand Down
6 changes: 3 additions & 3 deletions src/quantum/azext_quantum/operations/target.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()


Expand Down
11 changes: 7 additions & 4 deletions src/quantum/azext_quantum/operations/workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -33,24 +33,27 @@ 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

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"
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion src/quantum/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down