Skip to content

Commit

Permalink
Hyper-V extension: Check guest OS version before apply configuration. (
Browse files Browse the repository at this point in the history
  • Loading branch information
sshilov7 authored Sep 12, 2024
1 parent 1d5d5aa commit ca1ca04
Show file tree
Hide file tree
Showing 9 changed files with 185 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ public void SendMessage(IRequestMessage requestMessage, uint communicationIdCoun

// Try to remove all parts of the message first just in case if previous communication session was
// abruptly terminates and we got old messages not removed.
for (var i = 0; i < numberOfParts; i++)
// The key format is DevSetup{<number>}-<index>-<total> where index starts from 1.
// Best effort. Log error if failed, but don't throw.
for (var i = 1; i <= numberOfParts; i++)
{
RemoveKvpItem($"{kvpNameStart}{i}{kvpNameEnd}");
}
Expand Down Expand Up @@ -237,17 +239,17 @@ private List<IResponseMessage> TryReadResponseMessages(string communicationId, b

private Dictionary<string, string> ReadGuestKvps()
{
return MessageHelper.MergeMessageParts(ReadRawGuestKvps());
return MessageHelper.MergeMessageParts(ReadGuestExchangeItems());
}

private Dictionary<string, string> ReadRawGuestKvps()
private Dictionary<string, string> ReadRawGuestKvps(string itemsType)
{
Dictionary<string, string> guestKvps = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);

using var collection = _vmWmi.GetRelated("Msvm_KvpExchangeComponent");
foreach (ManagementObject kvpExchangeComponent in collection)
{
foreach (var exchangeDataItem in (string[])kvpExchangeComponent["GuestExchangeItems"])
foreach (var exchangeDataItem in (string[])kvpExchangeComponent[itemsType])
{
XPathDocument xpathDoc = new XPathDocument(XmlReader.Create(new StringReader(exchangeDataItem)));
XPathNavigator navigator = xpathDoc.CreateNavigator();
Expand All @@ -269,6 +271,16 @@ private Dictionary<string, string> ReadRawGuestKvps()
return guestKvps;
}

private Dictionary<string, string> ReadGuestExchangeItems()
{
return ReadRawGuestKvps("GuestExchangeItems");
}

public Dictionary<string, string> ReadGuestProperties()
{
return ReadRawGuestKvps("GuestIntrinsicExchangeItems");
}

public void CleanUp()
{
lock (_kvpNamesToCleanup)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ public List<IGuestResponse> WaitForResponse(uint communicationIdCounter, string
return result;
}

public Dictionary<string, string> ReadGuestProperties()
{
return _channel.ReadGuestProperties();
}

public void Dispose()
{
Dispose(true);
Expand Down
6 changes: 6 additions & 0 deletions extensions/HyperVExtension/src/HyperVExtension/Constants.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Windows.Win32.System.Diagnostics.Debug;

namespace HyperVExtension;

internal sealed class Constants
Expand Down Expand Up @@ -28,4 +30,8 @@ internal sealed class Constants
public const string HyperVTemplatesSubPath = @"HyperVExtension\Templates";

public static string SystemRootPath { get; } = Path.GetPathRoot(Environment.SystemDirectory)!;

public static int GuestPlatformIdWindows { get; } = (int)VER_PLATFORM.VER_PLATFORM_WIN32_NT;

public static Version MinWindowsVersionForApplyConfiguration { get; } = new Version(10, 0, 19041, 0);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using HyperVExtension.Common;
using Windows.Win32.Foundation;

namespace HyperVExtension.Exceptions;

internal sealed class GuestOsOperationNotSupportedException : GuestOsVersionException
{
public GuestOsOperationNotSupportedException(IStringResource stringResource, Dictionary<string, string>? guestOsProperties)
: base(stringResource.GetLocalized("GuestOsOperationNotSupported", $"Windows {Constants.MinWindowsVersionForApplyConfiguration}"), guestOsProperties)
{
HResult = HRESULT.E_NOTSUPPORTED;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Globalization;
using System.Management.Automation;
using System.Text;
using HyperVExtension.Common;
using HyperVExtension.Helpers;

namespace HyperVExtension.Exceptions;

internal class GuestOsVersionException : Exception
{
public GuestOsVersionException(IStringResource stringResource, Exception innerException, Dictionary<string, string>? guestOsProperties)
: base(stringResource.GetLocalized("FailedToDetermineGuestOsVersion", $"Windows {Constants.MinWindowsVersionForApplyConfiguration}"), innerException)
{
HResult = innerException.HResult;
GuestOsProperties = guestOsProperties;
}

protected GuestOsVersionException(string message, Dictionary<string, string>? guestOsProperties)
: base(message)
{
GuestOsProperties = guestOsProperties;
}

public Dictionary<string, string>? GuestOsProperties { get; }

public override string ToString()
{
StringBuilder message = new();
message.Append(CultureInfo.InvariantCulture, $"{Message}.");
if (InnerException != null)
{
message.Append(CultureInfo.InvariantCulture, $" {InnerException}.");
}

if (GuestOsProperties != null)
{
GuestOsProperties.TryGetValue(HyperVStrings.OSPlatformId, out var osPlatformId);
GuestOsProperties.TryGetValue(HyperVStrings.OSVersion, out var osVersion);
GuestOsProperties.TryGetValue(HyperVStrings.OSName, out var osName);
if ((osPlatformId != null) ||
(osVersion != null) ||
(osName != null))
{
message.Append(" (");
if (osPlatformId != null)
{
message.Append(CultureInfo.InvariantCulture, $"{HyperVStrings.OSPlatformId} = {osPlatformId}");
}

if (osVersion != null)
{
message.Append(CultureInfo.InvariantCulture, $", {HyperVStrings.OSVersion} = {osVersion}");
}

if (osName != null)
{
message.Append(CultureInfo.InvariantCulture, $", {HyperVStrings.OSName} = {osName}");
}

message.Append(')');
}
}

if (InnerException != null)
{
message.Append(CultureInfo.InvariantCulture, $"{Environment.NewLine}{InnerException}.");
}

return message.ToString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,9 @@ public static class HyperVStrings
public const string StartingState = "Starting";
public const string StoppingState = "Stopping";
public const string ResumingState = "Resuming";

// Hyper-V properties readable from GuestIntrinsicExchangeItems
public const string OSPlatformId = "OSPlatformId";
public const string OSVersion = "OSVersion";
public const string OSName = "OSName";
}
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,10 @@ public SDK.ApplyConfigurationResult ApplyConfiguration(ApplyConfigurationOperati

using var guestSession = new GuestKvpSession(Guid.Parse(Id));

// Verify that the VM OS supports configuration operation.
// Currently only Windows is supported. The minimum requirement is Windows 20H1 (19041)
ThrowIfApplyConfigurationIsNotSupported(guestSession);

// Query VM by sending a request to DevSetupAgent.
var getStateRequest = new GetStateRequest();
var communicationId = guestSession.SendRequest(getStateRequest, CancellationToken.None);
Expand Down Expand Up @@ -806,6 +810,49 @@ public SDK.ApplyConfigurationResult ApplyConfiguration(ApplyConfigurationOperati
}
}

private void ThrowIfApplyConfigurationIsNotSupported(GuestKvpSession guestSession)
{
Dictionary<string, string>? osVersionInfo = null;
var isApplyConfigurationSupported = false;

try
{
// The dictionary returned from GetOsVersionInfo is case-insensitive, so the keys are not case-sensitive.
osVersionInfo = GetOsVersionInfo(guestSession);
var osPlatformId = int.Parse(osVersionInfo[HyperVStrings.OSPlatformId], CultureInfo.InvariantCulture);
if (osPlatformId == Constants.GuestPlatformIdWindows)
{
var osVersion = Version.Parse(osVersionInfo[HyperVStrings.OSVersion]);
if (osVersion >= Constants.MinWindowsVersionForApplyConfiguration)
{
isApplyConfigurationSupported = true;
}
}
}
catch (Exception ex)
{
throw new GuestOsVersionException(_stringResource, ex, osVersionInfo);
}

if (!isApplyConfigurationSupported)
{
throw new GuestOsOperationNotSupportedException(_stringResource, osVersionInfo);
}
}

private Dictionary<string, string> GetOsVersionInfo(GuestKvpSession guestSession)
{
var guestOsProperties = guestSession.ReadGuestProperties();
if (guestOsProperties == null ||
!guestOsProperties.ContainsKey(HyperVStrings.OSPlatformId) ||
!guestOsProperties.ContainsKey(HyperVStrings.OSVersion))
{
throw new HResultException(HRESULT.E_UNEXPECTED, _stringResource.GetLocalized("FailedToReadGuestOsProperties"));
}

return guestOsProperties;
}

private bool DeployDevSetupAgent(ApplyConfigurationOperation operation, int attemptNumber)
{
var powerShell = _host.GetService<IPowerShellService>();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
GetCurrentPackageFullName
WIN32_ERROR
E_UNEXPECTED
E_NOTSUPPORTED
E_FAIL
E_ABORT
S_OK
MAX_PATH
SFBS_FLAGS
StrFormatByteSizeEx
SetForegroundWindow
GetDiskFreeSpaceEx
GetDiskFreeSpaceEx
VER_PLATFORM
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,18 @@
<value>Unable to start the operation because it is already in progress</value>
<comment>Error text to tell the user that the process to create the virtual machine is already in progress.</comment>
</data>
<data name="FailedToReadGuestOsProperties" xml:space="preserve">
<value>Failed to read guest OS properties</value>
<comment>Error text to tell the user that an error happened while trying to read virtual machine's OS properties.</comment>
</data>
<data name="FailedToDetermineGuestOsVersion" xml:space="preserve">
<value>Failed to determine guest OS version. The operation requires {0}</value>
<comment>Error text to tell the user that an error happened while trying to determine virtual machine's OS version. The {0} parameter will indicate the required OS version.</comment>
</data>
<data name="GuestOsOperationNotSupported" xml:space="preserve">
<value>This operation is not supported for the guest OS. The operation requires {0}</value>
<comment>Error text to tell the user that an operation is not supported for the virtual machine's OS version. The {0} parameter will indicate the required OS version.</comment>
</data>
<data name="VmCredentialRequest.CancelText" xml:space="preserve">
<value>Cancel</value>
<comment>Cancel button text in the dialog to enter Hyper-V VM admin credential.</comment>
Expand Down

0 comments on commit ca1ca04

Please sign in to comment.