Skip to content
This repository was archived by the owner on Jan 12, 2024. It is now read-only.
53 changes: 34 additions & 19 deletions src/AzureClient/AzureClient.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

#nullable enable
Expand All @@ -20,12 +20,14 @@ namespace Microsoft.Quantum.IQSharp.AzureClient
/// <inheritdoc/>
public class AzureClient : IAzureClient
{
internal IAzureWorkspace? ActiveWorkspace { get; private set; }
private ILogger<AzureClient> Logger { get; }
private IReferences References { get; }
private IEntryPointGenerator EntryPointGenerator { get; }
private IMetadataController MetadataController { get; }
private bool IsPythonUserAgent => MetadataController?.UserAgent?.StartsWith("qsharp.py") ?? false;
private string ConnectionString { get; set; } = string.Empty;
private AzureExecutionTarget? ActiveTarget { get; set; }
private IAzureWorkspace? ActiveWorkspace { get; set; }
private string MostRecentJobId { get; set; } = string.Empty;
private IEnumerable<ProviderStatus>? AvailableProviders { get; set; }
private IEnumerable<TargetStatus>? AvailableTargets => AvailableProviders?.SelectMany(provider => provider.Targets);
Expand All @@ -39,11 +41,13 @@ public AzureClient(
IExecutionEngine engine,
IReferences references,
IEntryPointGenerator entryPointGenerator,
IMetadataController metadataController,
ILogger<AzureClient> logger,
IEventService eventService)
{
References = references;
EntryPointGenerator = entryPointGenerator;
MetadataController = metadataController;
Logger = logger;
eventService?.TriggerServiceInitialized<IAzureClient>(this);

Expand All @@ -55,6 +59,8 @@ public AzureClient(
baseEngine.RegisterDisplayEncoder(new TargetStatusToTextEncoder());
baseEngine.RegisterDisplayEncoder(new HistogramToHtmlEncoder());
baseEngine.RegisterDisplayEncoder(new HistogramToTextEncoder());
baseEngine.RegisterDisplayEncoder(new AzureClientErrorToHtmlEncoder());
baseEngine.RegisterDisplayEncoder(new AzureClientErrorToTextEncoder());
}
}

Expand Down Expand Up @@ -83,6 +89,11 @@ public async Task<ExecutionResult> ConnectAsync(IChannel channel,

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

if (ValidExecutionTargets.Count() == 0)
{
channel.Stderr($"No valid Q# execution targets found in Azure Quantum workspace {ActiveWorkspace.Name}.");
}

return ValidExecutionTargets.ToExecutionResult();
}

Expand All @@ -103,20 +114,19 @@ private async Task<ExecutionResult> SubmitOrExecuteJobAsync(IChannel channel, Az
{
if (ActiveWorkspace == null)
{
channel.Stderr("Please call %azure.connect before submitting a job.");
channel.Stderr($"Please call {GetCommandDisplayName("connect")} before submitting a job.");
return AzureClientError.NotConnected.ToExecutionResult();
}

if (ActiveTarget == null)
{
channel.Stderr("Please call %azure.target before submitting a job.");
channel.Stderr($"Please call {GetCommandDisplayName("target")} before submitting a job.");
return AzureClientError.NoTarget.ToExecutionResult();
}

if (string.IsNullOrEmpty(submissionContext.OperationName))
{
var commandName = execute ? "%azure.execute" : "%azure.submit";
channel.Stderr($"Please pass a valid Q# operation name to {commandName}.");
channel.Stderr($"Please pass a valid Q# operation name to {GetCommandDisplayName(execute ? "execute" : "submit")}.");
return AzureClientError.NoOperationName.ToExecutionResult();
}

Expand Down Expand Up @@ -206,28 +216,29 @@ public async Task<ExecutionResult> GetActiveTargetAsync(IChannel channel)
{
if (AvailableProviders == null)
{
channel.Stderr("Please call %azure.connect before getting the execution target.");
channel.Stderr($"Please call {GetCommandDisplayName("connect")} before getting the execution target.");
return AzureClientError.NotConnected.ToExecutionResult();
}

if (ActiveTarget == null)
{
channel.Stderr("No execution target has been specified. To specify one, run:\n%azure.target <target ID>");
channel.Stderr($"No execution target has been specified. To specify one, call {GetCommandDisplayName("target")} with the target ID.");
channel.Stdout($"Available execution targets: {ValidExecutionTargetsDisplayText}");
return AzureClientError.NoTarget.ToExecutionResult();
}

channel.Stdout($"Current execution target: {ActiveTarget.TargetId}");
channel.Stdout($"Available execution targets: {ValidExecutionTargetsDisplayText}");
return ActiveTarget.TargetId.ToExecutionResult();

return AvailableTargets.First(target => target.Id == ActiveTarget.TargetId).ToExecutionResult();
}

/// <inheritdoc/>
public async Task<ExecutionResult> SetActiveTargetAsync(IChannel channel, string targetId)
{
if (AvailableProviders == null)
{
channel.Stderr("Please call %azure.connect before setting an execution target.");
channel.Stderr($"Please call {GetCommandDisplayName("connect")} before setting an execution target.");
return AzureClientError.NotConnected.ToExecutionResult();
}

Expand All @@ -254,15 +265,17 @@ public async Task<ExecutionResult> SetActiveTargetAsync(IChannel channel, string
channel.Stdout($"Loading package {ActiveTarget.PackageName} and dependencies...");
await References.AddPackage(ActiveTarget.PackageName);

return $"Active target is now {ActiveTarget.TargetId}".ToExecutionResult();
channel.Stdout($"Active target is now {ActiveTarget.TargetId}");

return AvailableTargets.First(target => target.Id == ActiveTarget.TargetId).ToExecutionResult();
}

/// <inheritdoc/>
public async Task<ExecutionResult> GetJobResultAsync(IChannel channel, string jobId)
{
if (ActiveWorkspace == null)
{
channel.Stderr("Please call %azure.connect before getting job results.");
channel.Stderr($"Please call {GetCommandDisplayName("connect")} before getting job results.");
return AzureClientError.NotConnected.ToExecutionResult();
}

Expand All @@ -286,7 +299,7 @@ public async Task<ExecutionResult> GetJobResultAsync(IChannel channel, string jo

if (!job.Succeeded || string.IsNullOrEmpty(job.Details.OutputDataUri))
{
channel.Stderr($"Job ID {jobId} has not completed. To check the status, use:\n %azure.status {jobId}");
channel.Stderr($"Job ID {jobId} has not completed. To check the status, call {GetCommandDisplayName("status")} with the job ID.");
return AzureClientError.JobNotCompleted.ToExecutionResult();
}

Expand All @@ -309,7 +322,7 @@ public async Task<ExecutionResult> GetJobStatusAsync(IChannel channel, string jo
{
if (ActiveWorkspace == null)
{
channel.Stderr("Please call %azure.connect before getting job status.");
channel.Stderr($"Please call {GetCommandDisplayName("connect")} before getting job status.");
return AzureClientError.NotConnected.ToExecutionResult();
}

Expand Down Expand Up @@ -339,18 +352,20 @@ public async Task<ExecutionResult> GetJobListAsync(IChannel channel)
{
if (ActiveWorkspace == null)
{
channel.Stderr("Please call %azure.connect before listing jobs.");
channel.Stderr($"Please call {GetCommandDisplayName("connect")} before listing jobs.");
return AzureClientError.NotConnected.ToExecutionResult();
}

var jobs = await ActiveWorkspace.ListJobsAsync();
if (jobs == null || jobs.Count() == 0)
var jobs = await ActiveWorkspace.ListJobsAsync() ?? new List<CloudJob>();
if (jobs.Count() == 0)
{
channel.Stderr("No jobs found in current Azure Quantum workspace.");
return AzureClientError.JobNotFound.ToExecutionResult();
}

return jobs.ToExecutionResult();
}

private string GetCommandDisplayName(string commandName) =>
IsPythonUserAgent ? $"qsharp.azure.{commandName}()" : $"%azure.{commandName}";
}
}
93 changes: 93 additions & 0 deletions src/AzureClient/AzureClientError.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

#nullable enable

using System.ComponentModel;

namespace Microsoft.Quantum.IQSharp.AzureClient
{
/// <summary>
/// Describes possible error results from <see cref="IAzureClient"/> methods.
/// </summary>
public enum AzureClientError
{
/// <summary>
/// Method completed with an unknown error.
/// </summary>
[Description(Resources.AzureClientErrorUnknownError)]
UnknownError = 1000,

/// <summary>
/// No connection has been made to any Azure Quantum workspace.
/// </summary>
[Description(Resources.AzureClientErrorNotConnected)]
NotConnected,

/// <summary>
/// A target has not yet been configured for job submission.
/// </summary>
[Description(Resources.AzureClientErrorNoTarget)]
NoTarget,

/// <summary>
/// The specified target is not valid for job submission.
/// </summary>
[Description(Resources.AzureClientErrorInvalidTarget)]
InvalidTarget,

/// <summary>
/// A job meeting the specified criteria was not found.
/// </summary>
[Description(Resources.AzureClientErrorJobNotFound)]
JobNotFound,

/// <summary>
/// The result of a job was requested, but the job has not yet completed.
/// </summary>
[Description(Resources.AzureClientErrorJobNotCompleted)]
JobNotCompleted,

/// <summary>
/// The job output failed to be downloaded from the Azure storage location.
/// </summary>
[Description(Resources.AzureClientErrorJobOutputDownloadFailed)]
JobOutputDownloadFailed,

/// <summary>
/// No Q# operation name was provided where one was required.
/// </summary>
[Description(Resources.AzureClientErrorNoOperationName)]
NoOperationName,

/// <summary>
/// The specified Q# operation name is not recognized.
/// </summary>
[Description(Resources.AzureClientErrorUnrecognizedOperationName)]
UnrecognizedOperationName,

/// <summary>
/// The specified Q# operation cannot be used as an entry point.
/// </summary>
[Description(Resources.AzureClientErrorInvalidEntryPoint)]
InvalidEntryPoint,

/// <summary>
/// The Azure Quantum job submission failed.
/// </summary>
[Description(Resources.AzureClientErrorJobSubmissionFailed)]
JobSubmissionFailed,

/// <summary>
/// Authentication with the Azure service failed.
/// </summary>
[Description(Resources.AzureClientErrorAuthenticationFailed)]
AuthenticationFailed,

/// <summary>
/// A workspace meeting the specified criteria was not found.
/// </summary>
[Description(Resources.AzureClientErrorWorkspaceNotFound)]
WorkspaceNotFound,
}
}
19 changes: 15 additions & 4 deletions src/AzureClient/AzureEnvironment.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

#nullable enable
Expand All @@ -16,10 +16,11 @@

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

internal class AzureEnvironment
{
public static string EnvironmentVariableName => "AZURE_QUANTUM_ENV";
public AzureEnvironmentType Type { get; private set; }

private string SubscriptionId { get; set; } = string.Empty;
Expand All @@ -34,8 +35,7 @@ private AzureEnvironment()

public static AzureEnvironment Create(string subscriptionId)
{
var azureEnvironmentEnvVarName = "AZURE_QUANTUM_ENV";
var azureEnvironmentName = System.Environment.GetEnvironmentVariable(azureEnvironmentEnvVarName);
var azureEnvironmentName = System.Environment.GetEnvironmentVariable(EnvironmentVariableName);

if (Enum.TryParse(azureEnvironmentName, true, out AzureEnvironmentType environmentType))
{
Expand All @@ -47,6 +47,8 @@ public static AzureEnvironment Create(string subscriptionId)
return Canary(subscriptionId);
case AzureEnvironmentType.Dogfood:
return Dogfood(subscriptionId);
case AzureEnvironmentType.Mock:
return Mock();
default:
throw new InvalidOperationException("Unexpected EnvironmentType value.");
}
Expand All @@ -57,6 +59,12 @@ public static AzureEnvironment Create(string subscriptionId)

public async Task<IAzureWorkspace?> GetAuthenticatedWorkspaceAsync(IChannel channel, string resourceGroupName, string workspaceName, bool refreshCredentials)
{
if (Type == AzureEnvironmentType.Mock)
{
channel.Stdout("AZURE_QUANTUM_ENV set to Mock. Using mock Azure workspace rather than connecting to the real service.");
return new MockAzureWorkspace(workspaceName);
}

// Find the token cache folder
var cacheDirectoryEnvVarName = "AZURE_QUANTUM_TOKEN_CACHE";
var cacheDirectory = System.Environment.GetEnvironmentVariable(cacheDirectoryEnvVarName);
Expand Down Expand Up @@ -154,6 +162,9 @@ private static AzureEnvironment Canary(string subscriptionId)
return canary;
}

private static AzureEnvironment Mock() =>
new AzureEnvironment() { Type = AzureEnvironmentType.Mock };

private static string GetDogfoodAuthority(string subscriptionId)
{
try
Expand Down
4 changes: 2 additions & 2 deletions src/AzureClient/AzureExecutionTarget.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

#nullable enable
Expand All @@ -11,7 +11,7 @@ internal enum AzureProvider { IonQ, Honeywell, QCI }

internal class AzureExecutionTarget
{
public string TargetId { get; private set; }
public string TargetId { get; private set; } = string.Empty;
public string PackageName => $"Microsoft.Quantum.Providers.{GetProvider(TargetId)}";

public static bool IsValid(string targetId) => GetProvider(targetId) != null;
Expand Down
2 changes: 1 addition & 1 deletion src/AzureClient/AzureSubmissionContext.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

#nullable enable
Expand Down
2 changes: 1 addition & 1 deletion src/AzureClient/AzureWorkspace.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

#nullable enable
Expand Down
19 changes: 2 additions & 17 deletions src/AzureClient/Extensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

#nullable enable
Expand Down Expand Up @@ -37,24 +37,9 @@ internal static ExecutionResult ToExecutionResult(this AzureClientError azureCli
new ExecutionResult
{
Status = ExecuteStatus.Error,
Output = azureClientError.ToDescription()
Output = azureClientError,
};

/// <summary>
/// Returns the string value of the <see cref="DescriptionAttribute"/> for the given
/// <see cref="AzureClientError"/> enumeration value.
/// </summary>
/// <param name="azureClientError"></param>
/// <returns></returns>
internal static string ToDescription(this AzureClientError azureClientError)
{
var attributes = azureClientError
.GetType()
.GetField(azureClientError.ToString())
.GetCustomAttributes(typeof(DescriptionAttribute), false) as DescriptionAttribute[];
return attributes?.Length > 0 ? attributes[0].Description : string.Empty;
}

/// <summary>
/// Encapsulates a given <see cref="AzureClientError"/> as the result of an execution.
/// </summary>
Expand Down
Loading