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

[Hyper-V Extension] Add ability for the Hyper-V extension to create VMs from the VM gallery #2438

Merged
merged 4 commits into from
Mar 26, 2024
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using HyperVExtension.Common;

namespace HyperVExtension.Exceptions;

internal sealed class DownloadOperationCancelledException : Exception
{
public DownloadOperationCancelledException(IStringResource stringResource)
: base(stringResource.GetLocalized("DownloadOperationCancelled"))
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using HyperVExtension.Common;

namespace HyperVExtension.Exceptions;

internal sealed class DownloadOperationFailedException : Exception
{
public DownloadOperationFailedException(IStringResource stringResource)
: base(stringResource.GetLocalized("DownloadOperationFailed"))
{
}

public DownloadOperationFailedException(string message)
: base(message)
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using HyperVExtension.Common;

namespace HyperVExtension.Exceptions;

internal sealed class NoVMImagesAvailableException : Exception
{
public NoVMImagesAvailableException(IStringResource stringResource)
: base(stringResource.GetLocalized("NoImagesFoundError"))
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using HyperVExtension.Common;

namespace HyperVExtension.Exceptions;

internal sealed class OperationInProgressException : Exception
{
public OperationInProgressException(IStringResource stringResource)
: base(stringResource.GetLocalized("OperationInProgressError"))
{
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

using HyperVExtension.Models;
using HyperVExtension.Models.VirtualMachineCreation;
using HyperVExtension.Providers;
using HyperVExtension.Services;
using Microsoft.Extensions.DependencyInjection;
Expand All @@ -18,15 +19,20 @@ public static IServiceCollection AddHyperVExtensionServices(this IServiceCollect
services.AddTransient<IWindowsServiceController, WindowsServiceController>();

// Services
services.AddHttpClient();
services.AddSingleton<IComputeSystemProvider, HyperVProvider>();
services.AddSingleton<HyperVExtension>();
services.AddSingleton<IHyperVManager, HyperVManager>();
services.AddSingleton<IWindowsIdentityService, WindowsIdentityService>();
services.AddSingleton<IVMGalleryService, VMGalleryService>();
services.AddSingleton<IArchiveProviderFactory, ArchiveProviderFactory>();
services.AddSingleton<IDownloaderService, DownloaderService>();

// Pattern to allow multiple non-service registered interfaces to be used with registered interfaces during construction.
services.AddSingleton<IPowerShellService>(psService =>
ActivatorUtilities.CreateInstance<PowerShellService>(psService, new PowerShellSession()));
services.AddSingleton<HyperVVirtualMachineFactory>(sp => psObject => ActivatorUtilities.CreateInstance<HyperVVirtualMachine>(sp, psObject));
services.AddSingleton<HyperVVirtualMachineFactory>(serviceProvider => psObject => ActivatorUtilities.CreateInstance<HyperVVirtualMachine>(serviceProvider, psObject));
services.AddSingleton<VmGalleryCreationOperationFactory>(serviceProvider => parameters => ActivatorUtilities.CreateInstance<VMGalleryVMCreationOperation>(serviceProvider, parameters));

return services;
}
Expand Down
44 changes: 44 additions & 0 deletions HyperVExtension/src/HyperVExtension/Extensions/StreamExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace HyperVExtension.Extensions;

public static class StreamExtensions
{
/// <summary>
/// Copies the contents of a source stream to a destination stream and reports the progress of the operation.
/// </summary>
/// <param name="source"> The source stream that data will be copied from </param>
/// <param name="destination">The destination stream the data will be copied into </param>
/// <param name="progressProvider">The object that progress will be reported to</param>
/// <param name="bufferSize">The size of the buffer which is used to read data from the source stream and write it to the destination stream</param>
/// <param name="cancellationToken">A cancellation token that will allow the caller to cancel the operation</param>
public static async Task CopyToAsync(this Stream source, Stream destination, IProgress<long> progressProvider, int bufferSize, CancellationToken cancellationToken)
{
var buffer = new byte[bufferSize];
long totalRead = 0;

while (true)
bbonaby marked this conversation as resolved.
Show resolved Hide resolved
{
cancellationToken.ThrowIfCancellationRequested();
var bytesRead = await source.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken);

if (bytesRead == 0)
{
break;
}

await destination.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken);
totalRead += bytesRead;

// Report the progress of the operation.
progressProvider.Report(totalRead);
}
}
}
48 changes: 46 additions & 2 deletions HyperVExtension/src/HyperVExtension/Helpers/BytesHelper.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Globalization;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.UI.Shell;

namespace HyperVExtension.Helpers;

public static class BytesHelper
{
private const int ByteToStringBufferSize = 16;
bbonaby marked this conversation as resolved.
Show resolved Hide resolved
bbonaby marked this conversation as resolved.
Show resolved Hide resolved

public const decimal OneKbInBytes = 1ul << 10;

public const decimal OneMbInBytes = 1ul << 20;
Expand All @@ -18,7 +24,6 @@ public static class BytesHelper
/// <summary>
/// Converts bytes represented by a long value to its human readable string
/// equivalent in either megabytes, gigabytes or terabytes.
/// Note: this is only for internal use and is not localized.
/// </summary>
public static string ConvertFromBytes(ulong size)
{
Expand All @@ -30,7 +35,46 @@ public static string ConvertFromBytes(ulong size)
{
return $"{(size / OneGbInBytes).ToString("F", CultureInfo.InvariantCulture)} GB";
}
else if (size >= OneMbInBytes)
{
return $"{(size / OneMbInBytes).ToString("F", CultureInfo.InvariantCulture)} MB";
}

return $"{(size / OneKbInBytes).ToString("F", CultureInfo.InvariantCulture)} KB";
}

return $"{(size / OneMbInBytes).ToString("F", CultureInfo.InvariantCulture)} MB";
/// <summary>
/// Converts from a ulong amount of bytes to a localized string representation of that byte size in gigabytes.
/// Relies on StrFormatByteSizeEx to convert to localized.
/// </summary>
/// <returns>
/// If succeeds internally return localized size in gigabytes, otherwise falls back to community toolkit
/// implementation which is in English.
/// </returns>
public static string ConvertBytesToString(ulong sizeInBytes)
{
unsafe
{
// 15 characters + null terminator.
var buffer = new string(' ', ByteToStringBufferSize);
fixed (char* tempPath = buffer)
{
var result =
PInvoke.StrFormatByteSizeEx(
sizeInBytes,
SFBS_FLAGS.SFBS_FLAGS_TRUNCATE_UNDISPLAYED_DECIMAL_DIGITS,
tempPath,
ByteToStringBufferSize);
if (result != HRESULT.S_OK)
{
// Fallback to using non localized version which is in english.
return ConvertFromBytes(sizeInBytes);
}
else
{
return buffer.Trim().Trim('\0');
}
}
}
}
}
21 changes: 21 additions & 0 deletions HyperVExtension/src/HyperVExtension/Helpers/HyperVStrings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ public static class HyperVStrings
public const string VMSnapshotId = "VMSnapshotId";
public const string VMSnapshotName = "VMSnapshotName";
public const string Size = "Size";
public const string VirtualMachinePath = "VirtualMachinePath";
public const string VirtualHardDiskPath = "VirtualHardDiskPath";
public const string LogicalProcessorCount = "LogicalProcessorCount";

// Hyper-V PowerShell commands strings
public const string GetModule = "Get-Module";
Expand All @@ -59,6 +62,10 @@ public static class HyperVStrings
public const string RemoveVMSnapshot = "Remove-VMSnapshot";
public const string CreateVMCheckpoint = "Checkpoint-VM";
public const string RestartVM = "Restart-VM";
public const string GetVMHost = "Get-VMHost";
public const string NewVM = "New-VM";
public const string SetVM = "Set-VM";
public const string SetVMFirmware = "Set-VMFirmware";

// Hyper-V PowerShell command parameter strings
public const string ListAvailable = "ListAvailable";
Expand All @@ -68,6 +75,20 @@ public static class HyperVStrings
public const string Save = "Save";
public const string TurnOff = "TurnOff";
public const string PassThru = "PassThru";
public const string NewVHDPath = "NewVHDPath";
public const string NewVHDSizeBytes = "NewVHDSizeBytes";
public const string EnableSecureBoot = "EnableSecureBoot";
public const string EnhancedSessionTransportType = "EnhancedSessionTransportType";
public const string Generation = "Generation";
public const string VHDPath = "VHDPath";
public const string MemoryStartupBytes = "MemoryStartupBytes";
public const string VM = "VM";
public const string SwitchName = "SwitchName";
public const string DefaultSwitchName = "Default Switch";
public const string ParameterOnState = "On";
public const string ParameterOffState = "Off";
public const string ParameterHvSocket = "HvSocket";
public const string ParameterVmBus = "VMBus";

// Hyper-V psObject property values
public const string CanStopService = "CanStop";
Expand Down
1 change: 1 addition & 0 deletions HyperVExtension/src/HyperVExtension/HyperVExtension.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="System.Management.Automation" Version="7.4.0" />
<PackageReference Include="System.Security.Principal.Windows" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Management.Automation;
using System.Text;
using System.Threading.Tasks;
using HyperVExtension.Helpers;

namespace HyperVExtension.Models;

/// <summary>
/// Represents the Hyper-V virtual machine host. For the Hyper-V Extension this is the local computer.
/// </summary>
public class HyperVVirtualMachineHost
{
private readonly PsObjectHelper _psObjectHelper;

public uint LogicalProcessorCount => _psObjectHelper.MemberNameToValue<uint>(HyperVStrings.LogicalProcessorCount);

public string VirtualHardDiskPath => _psObjectHelper.MemberNameToValue<string>(HyperVStrings.VirtualHardDiskPath) ?? string.Empty;

public string VirtualMachinePath => _psObjectHelper.MemberNameToValue<string>(HyperVStrings.VirtualMachinePath) ?? string.Empty;

public HyperVVirtualMachineHost(PSObject psObject)
{
_psObjectHelper = new PsObjectHelper(psObject);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Text.Json.Serialization;
using HyperVExtension.Models.VirtualMachineCreation;

namespace HyperVExtension.Models.VMGalleryJsonToClasses;

/// <summary>
/// Used to generate the source code for the classes when we deserialize the Json recieved from the VM gallery
/// and any associated json.
/// .Net 8 requires a JsonSerializerContext to be used with the JsonSerializerOptions when
/// deserializing JSON into objects.
/// See : https://learn.microsoft.com/dotnet/standard/serialization/system-text-json/source-generation?pivots=dotnet-8-0
/// for more information
/// </summary>
[JsonSerializable(typeof(VMGalleryItemWithHashBase))]
[JsonSerializable(typeof(VMGalleryConfig))]
[JsonSerializable(typeof(VMGalleryDetail))]
[JsonSerializable(typeof(VMGalleryDisk))]
[JsonSerializable(typeof(VMGalleryImage))]
[JsonSerializable(typeof(VMGalleryImageList))]
[JsonSerializable(typeof(VMGalleryLogo))]
[JsonSerializable(typeof(VMGalleryRequirements))]
[JsonSerializable(typeof(VMGallerySymbol))]
[JsonSerializable(typeof(VMGalleryThumbnail))]
[JsonSerializable(typeof(VMGalleryCreationUserInput))]
public sealed partial class JsonSourceGenerationContext : JsonSerializerContext
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace HyperVExtension.Models.VMGalleryJsonToClasses;

/// <summary>
/// Represents the 'config' json object of an image in the VM Gallery. See Gallery Json "https://go.microsoft.com/fwlink/?linkid=851584"
/// </summary>
public sealed class VMGalleryConfig
{
public string SecureBoot { get; set; } = string.Empty;

public string EnhancedSessionTransportType { get; set; } = string.Empty;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace HyperVExtension.Models.VMGalleryJsonToClasses;

/// <summary>
/// Represents the 'detail' json object of an image in the VM Gallery. See Gallery Json "https://go.microsoft.com/fwlink/?linkid=851584"
/// </summary>
public sealed class VMGalleryDetail
{
public string Name { get; set; } = string.Empty;

public string Value { get; set; } = string.Empty;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace HyperVExtension.Models.VMGalleryJsonToClasses;

/// <summary>
/// Represents the 'disk' json object of an image in the VM Gallery. See Gallery Json "https://go.microsoft.com/fwlink/?linkid=851584"
/// </summary>
public sealed class VMGalleryDisk : VMGalleryItemWithHashBase
{
public string ArchiveRelativePath { get; set; } = string.Empty;

public long SizeInBytes { get; set; }
}
Loading
Loading