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
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
212 changes: 205 additions & 7 deletions src/AzureClient/AzureClient.cs
Original file line number Diff line number Diff line change
@@ -1,24 +1,222 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

#nullable enable

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Jupyter.Core;
using System.Linq;
using System.IO;
using System.Threading.Tasks;

using Microsoft.Azure.Quantum.Client;
using Microsoft.Identity.Client;
using Microsoft.Identity.Client.Extensions.Msal;
using System.Linq;
using System.IO;
using Microsoft.Quantum.Runtime;
using Microsoft.Jupyter.Core;
using Microsoft.Quantum.Simulation.Core;

namespace Microsoft.Quantum.IQSharp.AzureClient
{
/// <inheritdoc/>
public class AzureClient : IAzureClient
{
private string ConnectionString { get; set; } = string.Empty;
private string ActiveTargetName { get; set; } = string.Empty;
private AuthenticationResult? AuthenticationResult { get; set; }
private IQuantumClient? QuantumClient { get; set; }
private Azure.Quantum.Workspace? ActiveWorkspace { get; set; }

/// <inheritdoc/>
public async Task<ExecutionResult> ConnectAsync(
IChannel channel,
string subscriptionId,
string resourceGroupName,
string workspaceName,
string storageAccountConnectionString,
bool forceLoginPrompt = false)
{
ConnectionString = storageAccountConnectionString;

var clientId = "84ba0947-6c53-4dd2-9ca9-b3694761521b"; // Microsoft Quantum Development Kit
var authority = "https://login.microsoftonline.com/common";
var msalApp = PublicClientApplicationBuilder.Create(clientId).WithAuthority(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, clientId).Build();
var cacheHelper = await MsalCacheHelper.CreateAsync(storageCreationProperties);
cacheHelper.RegisterCache(msalApp.UserTokenCache);

var scopes = new List<string>() { "https://quantum.microsoft.com/Jobs.ReadWrite" };

bool shouldShowLoginPrompt = forceLoginPrompt;
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 AzureClientError.AuthenticationFailed.ToExecutionResult();
}

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

try
{
var jobsList = await QuantumClient.Jobs.ListAsync();
channel.Stdout($"Successfully connected to Azure Quantum workspace {workspaceName}.");
}
catch (Exception e)
{
channel.Stderr(e.ToString());
return AzureClientError.WorkspaceNotFound.ToExecutionResult();
}

return QuantumClient.ToJupyterTable().ToExecutionResult();
}

/// <inheritdoc/>
public async Task<ExecutionResult> PrintConnectionStatusAsync(IChannel channel) =>
QuantumClient == null
? AzureClientError.NotConnected.ToExecutionResult()
: QuantumClient.ToJupyterTable().ToExecutionResult();

/// <inheritdoc/>
public async Task<ExecutionResult> SubmitJobAsync(
IChannel channel,
IOperationResolver operationResolver,
string operationName)
{
if (ActiveWorkspace == null)
{
channel.Stderr("Please call %connect before submitting a job.");
return AzureClientError.NotConnected.ToExecutionResult();
}

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

if (string.IsNullOrEmpty(operationName))
{
channel.Stderr("Please pass a valid Q# operation name to %submit.");
return AzureClientError.NoOperationName.ToExecutionResult();
}

var operationInfo = operationResolver.Resolve(operationName);
var entryPointInfo = new EntryPointInfo<QVoid, Result>(operationInfo.RoslynType);
var entryPointInput = QVoid.Instance;
var machine = Azure.Quantum.QuantumMachineFactory.CreateMachine(ActiveWorkspace, ActiveTargetName, ConnectionString);
if (machine == null)
{
channel.Stderr($"Could not find an execution target for target {ActiveTargetName}.");
return AzureClientError.NoTarget.ToExecutionResult();
}

var job = await machine.SubmitAsync(entryPointInfo, entryPointInput);
return job.ToJupyterTable().ToExecutionResult();
}

/// <inheritdoc/>
public async Task<ExecutionResult> SetActiveTargetAsync(
IChannel channel,
string targetName)
{
// TODO: Validate that this target name is valid in the workspace.
ActiveTargetName = targetName;
return $"Active target is now {ActiveTargetName}".ToExecutionResult();
}

/// <inheritdoc/>
public async Task<ExecutionResult> PrintTargetListAsync(
IChannel channel)
{
if (QuantumClient == null)
{
channel.Stderr("Please call %connect before listing targets.");
return AzureClientError.NotConnected.ToExecutionResult();
}

var providersStatus = await QuantumClient.Providers.GetStatusAsync();
return providersStatus.ToJupyterTable().ToExecutionResult();
}

/// <inheritdoc/>
public async Task<ExecutionResult> PrintJobStatusAsync(
IChannel channel,
string jobId)
{
if (QuantumClient == null)
{
channel.Stderr("Please call %connect before getting job status.");
return AzureClientError.NotConnected.ToExecutionResult();
}

var jobDetails = await QuantumClient.Jobs.GetAsync(jobId);
if (jobDetails == null)
{
channel.Stderr($"Job ID {jobId} not found in current Azure Quantum workspace.");
return AzureClientError.JobNotFound.ToExecutionResult();
}

return jobDetails.ToJupyterTable().ToExecutionResult();
}

/// <inheritdoc/>
public async Task<ExecutionResult> PrintJobListAsync(
IChannel channel)
{
if (QuantumClient == null)
{
channel.Stderr("Please call %connect before listing jobs.");
return AzureClientError.NotConnected.ToExecutionResult();
}

var jobsList = await QuantumClient.Jobs.ListAsync();
if (jobsList == null || jobsList.Count() == 0)
{
channel.Stderr("No jobs found in current Azure Quantum workspace.");
return AzureClientError.JobNotFound.ToExecutionResult();
}

return jobsList.ToJupyterTable().ToExecutionResult();
}
}
}
7 changes: 4 additions & 3 deletions src/AzureClient/AzureClient.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
Expand All @@ -13,8 +13,9 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Identity.Client" Version="4.14.0" />
<PackageReference Include="Microsoft.Identity.Client.Extensions.Msal" Version="2.10.0-preview" />
<PackageReference Include="Microsoft.Azure.Quantum.Client" Version="0.11.2005.1420-beta" />
<PackageReference Include="Microsoft.Rest.ClientRuntime" Version="2.3.21" />
<PackageReference Include="Microsoft.Rest.ClientRuntime.Azure" Version="3.3.19" />
<PackageReference Include="System.Reactive" Version="4.3.2" />
</ItemGroup>

Expand Down
101 changes: 99 additions & 2 deletions src/AzureClient/Extensions.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

#nullable enable

using System;
using System.Collections.Generic;
using System.Text;
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Azure.Quantum.Client;
using Microsoft.Azure.Quantum.Client.Models;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Jupyter.Core;
using Microsoft.Quantum.Runtime;

namespace Microsoft.Quantum.IQSharp.AzureClient
{
Expand All @@ -22,5 +28,96 @@ public static void AddAzureClient(this IServiceCollection services)
{
services.AddSingleton<IAzureClient, AzureClient>();
}

/// <summary>
/// Encapsulates a given <see cref="AzureClientError"/> as the result of an execution.
/// </summary>
/// <param name="azureClientError">
/// The result of an IAzureClient API call.
/// </param>
public static ExecutionResult ToExecutionResult(this AzureClientError azureClientError) =>
new ExecutionResult
{
Status = ExecuteStatus.Error,
Output = azureClientError.ToDescription()
};

/// <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>
public 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>
/// <param name="task">
/// A task which will return the result of an IAzureClient API call.
/// </param>
public static async Task<ExecutionResult> ToExecutionResult(this Task<AzureClientError> task) =>
(await task).ToExecutionResult();

internal static Table<JobDetails> ToJupyterTable(this JobDetails jobDetails) =>
new List<JobDetails> { jobDetails }.ToJupyterTable();

internal static Table<JobDetails> ToJupyterTable(this IEnumerable<JobDetails> jobsList) =>
new Table<JobDetails>
{
Columns = new List<(string, Func<JobDetails, string>)>
{
("JobId", jobDetails => jobDetails.Id),
("JobName", jobDetails => jobDetails.Name),
("JobStatus", jobDetails => jobDetails.Status),
("Provider", jobDetails => jobDetails.ProviderId),
("Target", jobDetails => jobDetails.Target),
},
Rows = jobsList.ToList()
};

internal static Table<IQuantumMachineJob> ToJupyterTable(this IQuantumMachineJob job) =>
new Table<IQuantumMachineJob>
{
Columns = new List<(string, Func<IQuantumMachineJob, string>)>
{
("JobId", job => job.Id),
("JobStatus", job => job.Status),
("JobUri", job => job.Uri.ToString()),
},
Rows = new List<IQuantumMachineJob>() { job }
};

internal static Table<IQuantumClient> ToJupyterTable(this IQuantumClient quantumClient) =>
new Table<IQuantumClient>
{
Columns = new List<(string, Func<IQuantumClient, string>)>
{
("SubscriptionId", quantumClient => quantumClient.SubscriptionId),
("ResourceGroupName", quantumClient => quantumClient.ResourceGroupName),
("WorkspaceName", quantumClient => quantumClient.WorkspaceName),
},
Rows = new List<IQuantumClient>() { quantumClient }
};

internal static Table<TargetStatus> ToJupyterTable(this IEnumerable<ProviderStatus> providerStatusList) =>
new Table<TargetStatus>
{
Columns = new List<(string, Func<TargetStatus, string>)>
{
("TargetId", target => target.Id),
("CurrentAvailability", target => target.CurrentAvailability),
("AverageQueueTime", target => target.AverageQueueTime.ToString()),
("StatusPage", target => target.StatusPage),
},
Rows = providerStatusList.SelectMany(provider => provider.Targets).ToList()
};
}
}
Loading