Skip to content
This repository was archived by the owner on Jan 12, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
89282df
Updates to IQ# syntax highlighting
rmshaffer May 28, 2020
f53eb9c
Validate targets and load provider packages
rmshaffer May 28, 2020
75a8e35
Update Python interface for Azure commands
rmshaffer May 29, 2020
1787ea4
Merge branch 'feature/azure-client' into rmshaffer/azure-targets
rmshaffer Jun 1, 2020
52098e6
Simplify AzureExecutionTarget class
rmshaffer Jun 1, 2020
53fd8e9
Generate EntryPoint and compile into new assembly
rmshaffer Jun 1, 2020
a33a2c9
Changes for JobNotCompleted case
rmshaffer Jun 2, 2020
e47c9df
Merge branch 'rmshaffer/azure-targets' into rmshaffer/azure-entrypoint
rmshaffer Jun 2, 2020
4449040
Refactor entry point code into new classes
rmshaffer Jun 2, 2020
6f3a6fe
Use correct input and output types
rmshaffer Jun 2, 2020
d47fddd
Simplify property syntax
Jun 2, 2020
5215155
Add simple tests for AzureExecutionTarget class
rmshaffer Jun 2, 2020
eeb4b11
Merge branch 'rmshaffer/azure-targets' into rmshaffer/azure-entrypoint
rmshaffer Jun 2, 2020
5e62413
Recompile everything for specified execution target
rmshaffer Jun 3, 2020
26a79ea
Add tests and error handling
rmshaffer Jun 3, 2020
69a4fe6
Improve variable names
rmshaffer Jun 3, 2020
c63a006
Add status message while loading provider package
rmshaffer Jun 3, 2020
1c7d83f
Merge branch 'feature/azure-client' into rmshaffer/azure-targets
rmshaffer Jun 3, 2020
cbdd00e
Merge branch 'rmshaffer/azure-targets' into rmshaffer/azure-entrypoint
rmshaffer Jun 3, 2020
944cec0
Merge branch 'feature/azure-client' into rmshaffer/azure-targets
rmshaffer Jun 3, 2020
12c7e12
Merge branch 'rmshaffer/azure-targets' into rmshaffer/azure-entrypoint
rmshaffer Jun 3, 2020
b8e680b
Merge branch 'feature/azure-client' into rmshaffer/azure-targets
rmshaffer Jun 3, 2020
126f43b
Merge branch 'rmshaffer/azure-targets' into rmshaffer/azure-entrypoint
rmshaffer Jun 3, 2020
43c07ae
Add documentation to AzureExecutionTarget.GetProvider
rmshaffer Jun 5, 2020
2552632
Merge branch 'feature/azure-client' into rmshaffer/azure-targets
rmshaffer Jun 5, 2020
4bf71db
Merge branch 'rmshaffer/azure-targets' into rmshaffer/azure-entrypoint
rmshaffer Jun 5, 2020
52c481d
Extend tests to call EntryPoint.SubmitAsync
rmshaffer Jun 5, 2020
a470662
Wait for job completion on %azure.execute
rmshaffer Jun 5, 2020
354d826
Update comment
rmshaffer Jun 5, 2020
9041701
Add timeout for %azure.execute
rmshaffer Jun 5, 2020
0300ecd
Minor fixes in AzureClient
rmshaffer Jun 5, 2020
8ce9c63
Minor formatting and comment tweaks
rmshaffer Jun 5, 2020
e3e7bcc
Style improvements in test code
rmshaffer Jun 5, 2020
f807723
More consistent handling of job objects
rmshaffer Jun 5, 2020
1409213
Consistent error handling for IWorkspace calls
rmshaffer Jun 5, 2020
78196ff
Update to latest QDK released version
rmshaffer Jun 5, 2020
fce176e
Merge branch 'feature/azure-client' into rmshaffer/azure-entrypoint
rmshaffer Jun 8, 2020
0164134
Merge branch 'feature/azure-client' into rmshaffer/azure-entrypoint
rmshaffer Jun 8, 2020
edbe96e
Add encoders for CloudJob and TargetStatus
rmshaffer Jun 8, 2020
1b27c4b
Move extension methods into encoder files
rmshaffer Jun 8, 2020
3dabab4
Change signature of CloudJob.ToDictionary
rmshaffer Jun 8, 2020
7c69473
Update histogram deserialization and add encoders
rmshaffer Jun 8, 2020
a9bc3d3
Use JsonConverter classes directly
rmshaffer Jun 8, 2020
c9dec08
Small cleanup
rmshaffer Jun 8, 2020
b92db91
Register single JsonEncoder
rmshaffer Jun 8, 2020
bb82ac4
Add a simple HTML histogram display based on StateVectorToHtmlResultE…
rmshaffer Jun 9, 2020
78886f7
Various improvements from PR suggestions
rmshaffer Jun 9, 2020
274ebde
Use UTC for dates returned to Python
rmshaffer Jun 9, 2020
62a358e
Support jobName and shots parameters
rmshaffer Jun 9, 2020
204ed5a
Unify submission and execution code
rmshaffer Jun 9, 2020
7450585
Add some documentation to AzureSubmissionContext
rmshaffer Jun 9, 2020
bc90371
Documentation fix
rmshaffer Jun 9, 2020
f30a387
Add timeout and pollingInterval parameters
rmshaffer Jun 9, 2020
ce542ca
Refactor authentication and service calls
rmshaffer Jun 10, 2020
9b3b74b
Use switch syntax for entryPointInput
Jun 10, 2020
eb8de48
Remove 'All rights reserved.'
Jun 10, 2020
0429635
Addressing PR feedback and compiler warnings
rmshaffer Jun 11, 2020
5213b99
Use LINQ for decodedParameters
rmshaffer Jun 11, 2020
7e1146e
Merge branch 'rmshaffer/azure-entrypoint' into rmshaffer/azure-encoders
rmshaffer Jun 11, 2020
c0a7ce0
Merge branch 'rmshaffer/azure-encoders' into rmshaffer/azure-jobsettings
rmshaffer Jun 11, 2020
eb76029
Merge branch 'rmshaffer/azure-jobsettings' into rmshaffer/azure-clien…
rmshaffer Jun 11, 2020
2c39bfe
Merge branch 'feature/azure-client' into rmshaffer/azure-client-refac…
rmshaffer Jun 12, 2020
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
144 changes: 21 additions & 123 deletions src/AzureClient/AzureClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,15 @@

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using Microsoft.Azure.Quantum;
using Microsoft.Azure.Quantum.Client;
using Microsoft.Azure.Quantum.Client.Models;
using Microsoft.Extensions.Logging;
using Microsoft.Identity.Client;
using Microsoft.Identity.Client.Extensions.Msal;
using Microsoft.Jupyter.Core;
using Microsoft.Quantum.IQSharp.Common;
using Microsoft.Quantum.Simulation.Common;
using Microsoft.Rest.Azure;

namespace Microsoft.Quantum.IQSharp.AzureClient
{
Expand All @@ -30,19 +25,15 @@ public class AzureClient : IAzureClient
private IEntryPointGenerator EntryPointGenerator { get; }
private string ConnectionString { get; set; } = string.Empty;
private AzureExecutionTarget? ActiveTarget { get; set; }
private AuthenticationResult? AuthenticationResult { get; set; }
private IQuantumClient? QuantumClient { get; set; }
private Azure.Quantum.IWorkspace? ActiveWorkspace { get; set; }
private IAzureWorkspace? ActiveWorkspace { get; set; }
private string MostRecentJobId { get; set; } = string.Empty;
private IPage<ProviderStatus>? AvailableProviders { get; set; }
private IEnumerable<TargetStatus>? AvailableTargets { get => AvailableProviders?.SelectMany(provider => provider.Targets); }
private IEnumerable<TargetStatus>? ValidExecutionTargets { get => AvailableTargets?.Where(target => AzureExecutionTarget.IsValid(target.Id)); }
private string ValidExecutionTargetsDisplayText
{
get => ValidExecutionTargets == null
? "(no execution targets available)"
: string.Join(", ", ValidExecutionTargets.Select(target => target.Id));
}
private IEnumerable<ProviderStatus>? AvailableProviders { get; set; }
private IEnumerable<TargetStatus>? AvailableTargets => AvailableProviders?.SelectMany(provider => provider.Targets);
private IEnumerable<TargetStatus>? ValidExecutionTargets => AvailableTargets?.Where(target => AzureExecutionTarget.IsValid(target.Id));
private string ValidExecutionTargetsDisplayText =>
ValidExecutionTargets == null
? "(no execution targets available)"
: string.Join(", ", ValidExecutionTargets.Select(target => target.Id));

public AzureClient(
IExecutionEngine engine,
Expand Down Expand Up @@ -77,98 +68,33 @@ public async Task<ExecutionResult> ConnectAsync(IChannel channel,
{
ConnectionString = storageAccountConnectionString;

var azureEnvironmentEnvVarName = "AZURE_QUANTUM_ENV";
var azureEnvironmentName = System.Environment.GetEnvironmentVariable(azureEnvironmentEnvVarName);
var azureEnvironment = AzureEnvironment.Create(azureEnvironmentName, subscriptionId);

var msalApp = PublicClientApplicationBuilder
.Create(azureEnvironment.ClientId)
.WithAuthority(azureEnvironment.Authority)
.Build();

// Register the token cache for serialization
var cacheFileName = "aad.bin";
var cacheDirectoryEnvVarName = "AZURE_QUANTUM_TOKEN_CACHE";
var cacheDirectory = System.Environment.GetEnvironmentVariable(cacheDirectoryEnvVarName);
if (string.IsNullOrEmpty(cacheDirectory))
{
cacheDirectory = Path.Join(System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile), ".azure-quantum");
}

var storageCreationProperties = new StorageCreationPropertiesBuilder(cacheFileName, cacheDirectory, azureEnvironment.ClientId).Build();
var cacheHelper = await MsalCacheHelper.CreateAsync(storageCreationProperties);
cacheHelper.RegisterCache(msalApp.UserTokenCache);

bool shouldShowLoginPrompt = refreshCredentials;
if (!shouldShowLoginPrompt)
{
try
{
var accounts = await msalApp.GetAccountsAsync();
AuthenticationResult = await msalApp.AcquireTokenSilent(
azureEnvironment.Scopes, accounts.FirstOrDefault()).WithAuthority(msalApp.Authority).ExecuteAsync();
}
catch (MsalUiRequiredException)
{
shouldShowLoginPrompt = true;
}
}

if (shouldShowLoginPrompt)
{
AuthenticationResult = await msalApp.AcquireTokenWithDeviceCode(
azureEnvironment.Scopes,
deviceCodeResult =>
{
channel.Stdout(deviceCodeResult.Message);
return Task.FromResult(0);
}).WithAuthority(msalApp.Authority).ExecuteAsync();
}

if (AuthenticationResult == null)
var azureEnvironment = AzureEnvironment.Create(subscriptionId);
ActiveWorkspace = await azureEnvironment.GetAuthenticatedWorkspaceAsync(channel, resourceGroupName, workspaceName, refreshCredentials);
if (ActiveWorkspace == null)
{
return AzureClientError.AuthenticationFailed.ToExecutionResult();
}

var credentials = new Rest.TokenCredentials(AuthenticationResult.AccessToken);
QuantumClient = new QuantumClient(credentials)
{
SubscriptionId = subscriptionId,
ResourceGroupName = resourceGroupName,
WorkspaceName = workspaceName,
BaseUri = azureEnvironment.BaseUri,
};
ActiveWorkspace = new Azure.Quantum.Workspace(
QuantumClient.SubscriptionId,
QuantumClient.ResourceGroupName,
QuantumClient.WorkspaceName,
AuthenticationResult?.AccessToken,
azureEnvironment.BaseUri);

try
{
AvailableProviders = await QuantumClient.Providers.GetStatusAsync();
}
catch (Exception e)
AvailableProviders = await ActiveWorkspace.GetProvidersAsync();
if (AvailableProviders == null)
{
Logger?.LogError(e, $"Failed to download providers list from Azure Quantum workspace: {e.Message}");
return AzureClientError.WorkspaceNotFound.ToExecutionResult();
}
Copy link
Member

Choose a reason for hiding this comment

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

This should be a different error, as there is a workspace...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, we have created the "workspace" object on the client, but downloading the providers list is the first time we actually attempt to communicate with Azure Quantum. For example, if the user has mistyped the workspace name, this is the error they will see. The friendly string displayed is: "No Azure Quantum workspace was found that matches the specified criteria."


channel.Stdout($"Connected to Azure Quantum workspace {QuantumClient.WorkspaceName}.");
channel.Stdout($"Connected to Azure Quantum workspace {ActiveWorkspace.Name}.");

return ValidExecutionTargets.ToExecutionResult();
}

/// <inheritdoc/>
public async Task<ExecutionResult> GetConnectionStatusAsync(IChannel channel)
{
if (QuantumClient == null || AvailableProviders == null)
if (ActiveWorkspace == null || AvailableProviders == null)
{
Copy link
Member

Choose a reason for hiding this comment

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

I wonder if it's the right message that you can't connect to a workspace unless it has Q# providers.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The behavior in this case is that the connection to the workspace will succeed, but there will simply be no valid targets to choose from. But it's a good point, and I'll add an output message to make this clear. It would still be possible, for example, to query the jobs list in this case.

return AzureClientError.NotConnected.ToExecutionResult();
}

channel.Stdout($"Connected to Azure Quantum workspace {QuantumClient.WorkspaceName}.");
channel.Stdout($"Connected to Azure Quantum workspace {ActiveWorkspace.Name}.");

return ValidExecutionTargets.ToExecutionResult();
}
Expand All @@ -194,7 +120,7 @@ private async Task<ExecutionResult> SubmitOrExecuteJobAsync(IChannel channel, Az
return AzureClientError.NoOperationName.ToExecutionResult();
}

var machine = QuantumMachineFactory.CreateMachine(ActiveWorkspace, ActiveTarget.TargetId, ConnectionString);
var machine = ActiveWorkspace.CreateQuantumMachine(ActiveTarget.TargetId, ConnectionString);
if (machine == null)
{
// We should never get here, since ActiveTarget should have already been validated at the time it was set.
Expand Down Expand Up @@ -258,7 +184,7 @@ private async Task<ExecutionResult> SubmitOrExecuteJobAsync(IChannel channel, Az
// handle Jupyter kernel interrupt here and break out of this loop
await Task.Delay(TimeSpan.FromSeconds(submissionContext.ExecutionPollingInterval));
if (cts.IsCancellationRequested) break;
cloudJob = await GetCloudJob(MostRecentJobId);
cloudJob = await ActiveWorkspace.GetJobAsync(MostRecentJobId);
channel.Stdout($"[{DateTime.Now.ToLongTimeString()}] Current job status: {cloudJob?.Status ?? "Unknown"}");
}
while (cloudJob == null || cloudJob.InProgress);
Expand Down Expand Up @@ -351,7 +277,7 @@ public async Task<ExecutionResult> GetJobResultAsync(IChannel channel, string jo
jobId = MostRecentJobId;
}

var job = await GetCloudJob(jobId);
var job = await ActiveWorkspace.GetJobAsync(jobId);
if (job == null)
{
channel.Stderr($"Job ID {jobId} not found in current Azure Quantum workspace.");
Expand Down Expand Up @@ -398,7 +324,7 @@ public async Task<ExecutionResult> GetJobStatusAsync(IChannel channel, string jo
jobId = MostRecentJobId;
}

var job = await GetCloudJob(jobId);
var job = await ActiveWorkspace.GetJobAsync(jobId);
if (job == null)
{
channel.Stderr($"Job ID {jobId} not found in current Azure Quantum workspace.");
Expand All @@ -417,7 +343,7 @@ public async Task<ExecutionResult> GetJobListAsync(IChannel channel)
return AzureClientError.NotConnected.ToExecutionResult();
}

var jobs = await GetCloudJobs();
var jobs = await ActiveWorkspace.ListJobsAsync();
if (jobs == null || jobs.Count() == 0)
{
channel.Stderr("No jobs found in current Azure Quantum workspace.");
Expand All @@ -426,33 +352,5 @@ public async Task<ExecutionResult> GetJobListAsync(IChannel channel)

return jobs.ToExecutionResult();
}

private async Task<CloudJob?> GetCloudJob(string jobId)
{
try
{
return await ActiveWorkspace.GetJobAsync(jobId);
}
catch (Exception e)
{
Logger?.LogError(e, $"Failed to retrieve the specified Azure Quantum job: {e.Message}");
}

return null;
}

private async Task<IEnumerable<CloudJob>?> GetCloudJobs()
{
try
{
return await ActiveWorkspace.ListJobsAsync();
}
catch (Exception e)
{
Logger?.LogError(e, $"Failed to retrieve the list of jobs from the Azure Quantum workspace: {e.Message}");
}

return null;
}
}
}
111 changes: 98 additions & 13 deletions src/AzureClient/AzureEnvironment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,66 +5,151 @@

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Azure.Quantum.Client;
using Microsoft.Identity.Client;
using Microsoft.Identity.Client.Extensions.Msal;
using Microsoft.Jupyter.Core;
Copy link
Member

Choose a reason for hiding this comment

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

This code looks great.
Wondering if we should move it to be part of the Azure.Quantum.Client package itself. Pretty much the same should be done wherever we try to call Azure Quantum.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed, I had the same thought while I was writing it. There are a few Jupyter dependencies sprinkled in, and I want some of these abstractions in IQ# anyway in order to easily mock them, but a lot of this can and probably should be moved into Azure.Quantum.Client. I'll create a task for this.


namespace Microsoft.Quantum.IQSharp.AzureClient
{
internal enum AzureEnvironmentType { Production, Canary, Dogfood };

internal class AzureEnvironment
{
public string ClientId { get; private set; } = string.Empty;
public string Authority { get; private set; } = string.Empty;
public List<string> Scopes { get; private set; } = new List<string>();
public Uri? BaseUri { get; private set; }
public AzureEnvironmentType Type { get; private set; }

private string SubscriptionId { get; set; } = string.Empty;
private string ClientId { get; set; } = string.Empty;
private string Authority { get; set; } = string.Empty;
private List<string> Scopes { get; set; } = new List<string>();
private Uri? BaseUri { get; set; }

private AzureEnvironment()
{
}

public static AzureEnvironment Create(string environment, string subscriptionId)
public static AzureEnvironment Create(string subscriptionId)
{
if (Enum.TryParse(environment, true, out AzureEnvironmentType environmentType))
var azureEnvironmentEnvVarName = "AZURE_QUANTUM_ENV";
var azureEnvironmentName = System.Environment.GetEnvironmentVariable(azureEnvironmentEnvVarName);

if (Enum.TryParse(azureEnvironmentName, true, out AzureEnvironmentType environmentType))
{
switch (environmentType)
{
case AzureEnvironmentType.Production:
return Production();
return Production(subscriptionId);
case AzureEnvironmentType.Canary:
return Canary();
return Canary(subscriptionId);
case AzureEnvironmentType.Dogfood:
return Dogfood(subscriptionId);
default:
throw new InvalidOperationException("Unexpected EnvironmentType value.");
}
}

return Production();
return Production(subscriptionId);
}

public async Task<IAzureWorkspace?> GetAuthenticatedWorkspaceAsync(IChannel channel, string resourceGroupName, string workspaceName, bool refreshCredentials)
{
// Find the token cache folder
var cacheDirectoryEnvVarName = "AZURE_QUANTUM_TOKEN_CACHE";
var cacheDirectory = System.Environment.GetEnvironmentVariable(cacheDirectoryEnvVarName);
if (string.IsNullOrEmpty(cacheDirectory))
{
cacheDirectory = Path.Join(System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile), ".azure-quantum");
}

// Register the token cache for serialization
var cacheFileName = "aad.bin";
var storageCreationProperties = new StorageCreationPropertiesBuilder(cacheFileName, cacheDirectory, ClientId).Build();
var cacheHelper = await MsalCacheHelper.CreateAsync(storageCreationProperties);
var msalApp = PublicClientApplicationBuilder.Create(ClientId).WithAuthority(Authority).Build();
cacheHelper.RegisterCache(msalApp.UserTokenCache);

// Perform the authentication
bool shouldShowLoginPrompt = refreshCredentials;
AuthenticationResult? authenticationResult = null;
if (!shouldShowLoginPrompt)
{
try
{
var accounts = await msalApp.GetAccountsAsync();
authenticationResult = await msalApp.AcquireTokenSilent(
Scopes, accounts.FirstOrDefault()).WithAuthority(msalApp.Authority).ExecuteAsync();
}
catch (MsalUiRequiredException)
{
shouldShowLoginPrompt = true;
}
}

if (shouldShowLoginPrompt)
{
authenticationResult = await msalApp.AcquireTokenWithDeviceCode(
Scopes,
deviceCodeResult =>
{
channel.Stdout(deviceCodeResult.Message);
return Task.FromResult(0);
}).WithAuthority(msalApp.Authority).ExecuteAsync();
}

if (authenticationResult == null)
{
return null;
}

// Construct and return the AzureWorkspace object
var credentials = new Rest.TokenCredentials(authenticationResult.AccessToken);
var azureQuantumClient = new QuantumClient(credentials)
{
SubscriptionId = SubscriptionId,
ResourceGroupName = resourceGroupName,
WorkspaceName = workspaceName,
BaseUri = BaseUri,
};
var azureQuantumWorkspace = new Azure.Quantum.Workspace(
azureQuantumClient.SubscriptionId,
azureQuantumClient.ResourceGroupName,
azureQuantumClient.WorkspaceName,
authenticationResult?.AccessToken,
BaseUri);

return new AzureWorkspace(azureQuantumClient, azureQuantumWorkspace);
}

private static AzureEnvironment Production() =>
private static AzureEnvironment Production(string subscriptionId) =>
new AzureEnvironment()
{
Type = AzureEnvironmentType.Production,
ClientId = "84ba0947-6c53-4dd2-9ca9-b3694761521b", // QDK client ID
Authority = "https://login.microsoftonline.com/common",
Scopes = new List<string>() { "https://quantum.microsoft.com/Jobs.ReadWrite" },
BaseUri = new Uri("https://app-jobscheduler-prod.azurewebsites.net/"),
SubscriptionId = subscriptionId,
};

private static AzureEnvironment Dogfood(string subscriptionId) =>
new AzureEnvironment()
{
Type = AzureEnvironmentType.Dogfood,
ClientId = "46a998aa-43d0-4281-9cbb-5709a507ac36", // QDK dogfood client ID
Authority = GetDogfoodAuthority(subscriptionId),
Scopes = new List<string>() { "api://dogfood.azure-quantum/Jobs.ReadWrite" },
BaseUri = new Uri("https://app-jobscheduler-test.azurewebsites.net/"),
SubscriptionId = subscriptionId,
};

private static AzureEnvironment Canary()
private static AzureEnvironment Canary(string subscriptionId)
{
var canary = Production();
var canary = Production(subscriptionId);
canary.Type = AzureEnvironmentType.Canary;
canary.BaseUri = new Uri("https://app-jobs-canarysouthcentralus.azurewebsites.net/");
return canary;
}
Expand Down
Loading