diff --git a/src/core/src/Eryph.Core/CatletCapabilities.cs b/src/core/src/Eryph.Core/CatletCapabilities.cs index 721c812c..2b6f5aa8 100644 --- a/src/core/src/Eryph.Core/CatletCapabilities.cs +++ b/src/core/src/Eryph.Core/CatletCapabilities.cs @@ -6,15 +6,58 @@ using Eryph.ConfigModel.Catlets; using LanguageExt; +using static LanguageExt.Prelude; + namespace Eryph.Core; public static class CatletCapabilities { public static bool IsDynamicMemoryEnabled( Seq configs) => - configs.Find(c => string.Equals( - c.Name, EryphConstants.Capabilities.DynamicMemory, StringComparison.OrdinalIgnoreCase)) - .Map(c => !c.Details.ToSeq().Exists(d => string.Equals( - d, EryphConstants.CapabilityDetails.Disabled, StringComparison.OrdinalIgnoreCase))) - .IfNone(true); -} \ No newline at end of file + FindCapability(configs, EryphConstants.Capabilities.DynamicMemory) + .Map(c => !IsExplicitlyDisabled(c)) + .IfNone(false); + + public static bool IsDynamicMemoryExplicitlyDisabled( + Seq configs) => + FindCapability(configs, EryphConstants.Capabilities.DynamicMemory) + .Map(IsExplicitlyDisabled) + .IfNone(false); + + public static bool IsNestedVirtualizationEnabled( + Seq configs) => + FindCapability(configs, EryphConstants.Capabilities.NestedVirtualization) + .Map(c => !IsExplicitlyDisabled(c)) + .IfNone(false); + + public static bool IsSecureBootEnabled( + Seq configs) => + FindCapability(configs, EryphConstants.Capabilities.SecureBoot) + .Map(c => !IsExplicitlyDisabled(c)) + .IfNone(false); + + public static bool IsTpmEnabled( + Seq configs) => + FindCapability(configs, EryphConstants.Capabilities.Tpm) + .Map(c => !IsExplicitlyDisabled(c)) + .IfNone(false); + + public static Option FindSecureBootTemplate( + Seq configs) => + FindCapability(configs, EryphConstants.Capabilities.SecureBoot) + .ToSeq() + .Bind(c => c.Details.ToSeq()) + .Filter(notEmpty) + .Find(d => d.StartsWith("template:", StringComparison.OrdinalIgnoreCase)) + .Bind(d => d.Split(':').ToSeq().At(1)); + + public static Option FindCapability( + Seq configs, + string name) => + configs.Find(c => string.Equals(c.Name, name, StringComparison.OrdinalIgnoreCase)); + + public static bool IsExplicitlyDisabled( + CatletCapabilityConfig capabilityConfig) => + capabilityConfig.Details.ToSeq() + .Any(d => string.Equals(d, EryphConstants.CapabilityDetails.Disabled, StringComparison.OrdinalIgnoreCase)); +} diff --git a/src/core/src/Eryph.VmManagement/Converging/ConvergeCPU.cs b/src/core/src/Eryph.VmManagement/Converging/ConvergeCPU.cs index ee48e6fc..58d68802 100644 --- a/src/core/src/Eryph.VmManagement/Converging/ConvergeCPU.cs +++ b/src/core/src/Eryph.VmManagement/Converging/ConvergeCPU.cs @@ -18,8 +18,8 @@ public override async Task>> Con var configCount = Context.Config.Cpu?.Count.GetValueOrDefault(1) ?? 1; if (vmInfo.Value.ProcessorCount == configCount) return vmInfo; - if (vmInfo.Value.State == VirtualMachineState.Running) - return Error.New("Cannot change CPU count of a running catlet."); + if (vmInfo.Value.State is not (VirtualMachineState.Off or VirtualMachineState.OffCritical)) + return Error.New("Cannot change CPU count if the catlet is not stopped. Stop the catlet and retry."); await Context.ReportProgress($"Configure Catlet CPU count: {configCount}").ConfigureAwait(false); diff --git a/src/core/src/Eryph.VmManagement/Converging/ConvergeMemory.cs b/src/core/src/Eryph.VmManagement/Converging/ConvergeMemory.cs index 64733569..54fead48 100644 --- a/src/core/src/Eryph.VmManagement/Converging/ConvergeMemory.cs +++ b/src/core/src/Eryph.VmManagement/Converging/ConvergeMemory.cs @@ -24,8 +24,12 @@ private static EitherAsync> Converge( Func reportProgress) => from configuredMemory in ValidateMemoryConfig(config.Memory) .ToAsync() - let useDynamicMemory = CatletCapabilities.IsDynamicMemoryEnabled( - config.Capabilities.ToSeq()) + let capabilities = config.Capabilities.ToSeq() + let isDynamicMemoryEnabled = CatletCapabilities.IsDynamicMemoryEnabled(capabilities) + let isDynamicMemoryExplicitlyDisabled = CatletCapabilities + .IsDynamicMemoryExplicitlyDisabled(capabilities) + let useDynamicMemory = !isDynamicMemoryExplicitlyDisabled + && (isDynamicMemoryEnabled || configuredMemory.Minimum.IsSome || configuredMemory.Maximum.IsSome) // When startup, minimum and maximum are not all configured, we ensure // that the missing value are consistent with the configured ones. let minMemory = configuredMemory.Minimum @@ -77,16 +81,12 @@ from result in changedUseDynamicMemory.IsSome || changedStartupMemory.IsSome || changedMinMemory.IsSome || changedMaxMemory.IsSome - ? from _ in TryAsync(async () => - { - await reportProgress(message); - return unit; - }).ToEither() + ? from _ in TryAsync(() => reportProgress(message).ToUnit()).ToEither() from __ in powershellEngine.RunAsync(command5).ToError().ToAsync() - from reloadedVmInfo in vmInfo.RecreateOrReload(powershellEngine) - select reloadedVmInfo - : vmInfo - select vmInfo; + select unit + : RightAsync(unit) + from reloadedVmInfo in vmInfo.RecreateOrReload(powershellEngine) + select reloadedVmInfo; private static Either< Error, diff --git a/src/core/src/Eryph.VmManagement/Converging/ConvergeNestedVirtualization.cs b/src/core/src/Eryph.VmManagement/Converging/ConvergeNestedVirtualization.cs index 27cd69ca..c66ab65b 100644 --- a/src/core/src/Eryph.VmManagement/Converging/ConvergeNestedVirtualization.cs +++ b/src/core/src/Eryph.VmManagement/Converging/ConvergeNestedVirtualization.cs @@ -3,60 +3,61 @@ using System.Threading.Tasks; using Eryph.Core; using Eryph.VmManagement.Data; +using Eryph.VmManagement.Data.Core; using Eryph.VmManagement.Data.Full; using LanguageExt; using LanguageExt.Common; +using static LanguageExt.Prelude; + namespace Eryph.VmManagement.Converging; -public class ConvergeNestedVirtualization : ConvergeTaskBase +public class ConvergeNestedVirtualization( + ConvergeContext context) + : ConvergeTaskBase(context) { - public ConvergeNestedVirtualization(ConvergeContext context) : base(context) - { - } - - public override async Task>> Converge( - TypedPsObject vmInfo) - { - var capability = Context.Config.Capabilities?.FirstOrDefault(x => x.Name == - EryphConstants.Capabilities.NestedVirtualization); - - if (capability == null) - return vmInfo; - - var onOffState = (capability.Details?.Any(x => - string.Equals(x, EryphConstants.CapabilityDetails.Disabled, - StringComparison.OrdinalIgnoreCase))).GetValueOrDefault() ? OnOffState.Off : OnOffState.On; - - return await (from exposedExtensions in Context.Engine.GetObjectValuesAsync(new PsCommandBuilder() - .AddCommand("Get-VMProcessor") - .AddParameter("VM",vmInfo.PsObject) - .AddCommand("Select-Object") - .AddParameter("ExpandProperty", "ExposeVirtualizationExtensions") - ).ToError().Bind( - r => r.HeadOrLeft(Error.New("Failed to read processor details.")).ToAsync()) - let currentOnOffState = exposedExtensions ? OnOffState.On : OnOffState.Off - from uNestedVirtualization in currentOnOffState == onOffState - ? Unit.Default - : Unit.Default.Apply(async _ => - { - if (vmInfo.Value.State == VirtualMachineState.Running) - return Error.New("Cannot change nested virtualization settings of a running catlet."); - - if(onOffState == OnOffState.On) - await Context.ReportProgress("Enabling nested virtualization.").ConfigureAwait(false); - else - await Context.ReportProgress("Disabling nested virtualization.").ConfigureAwait(false); - - return await Context.Engine.RunAsync(PsCommandBuilder.Create() - .AddCommand("Set-VMProcessor") - .AddParameter("VM", vmInfo.PsObject) - .AddParameter("ExposeVirtualizationExtensions", onOffState == OnOffState.On) - ).ToError(); - }).ToAsync() - from newVmInfo in vmInfo.RecreateOrReload(Context.Engine) - select newVmInfo).ToEither(); - - - } -} \ No newline at end of file + public override Task>> Converge( + TypedPsObject vmInfo) => + ConvergeNestedVirtualizationState(vmInfo).ToEither(); + + private EitherAsync> ConvergeNestedVirtualizationState( + TypedPsObject vmInfo) => + from _ in RightAsync(unit) + let expectedNestedVirtualization = CatletCapabilities.IsNestedVirtualizationEnabled( + Context.Config.Capabilities.ToSeq()) + from vmProcessorInfo in GetVmProcessorInfo(vmInfo) + let actualNestedVirtualization = vmProcessorInfo.ExposeVirtualizationExtensions + from __ in expectedNestedVirtualization == actualNestedVirtualization + ? RightAsync(unit) + : ConfigureNestedVirtualization(vmInfo, expectedNestedVirtualization) + from updatedVmInfo in vmInfo.RecreateOrReload(Context.Engine) + select updatedVmInfo; + + private EitherAsync ConfigureNestedVirtualization( + TypedPsObject vmInfo, + bool exposeVirtualizationExtensions) => + from _1 in guard(vmInfo.Value.State is VirtualMachineState.Off or VirtualMachineState.OffCritical, + Error.New("Cannot change virtualization settings if the catlet is not stopped. Stop the catlet and retry.")) + .ToEitherAsync() + from _2 in Context.ReportProgressAsync(exposeVirtualizationExtensions + ? "Enabling nested virtualization." + : "Disabling nested virtualization.") + let command = PsCommandBuilder.Create() + .AddCommand("Set-VMProcessor") + .AddParameter("VM", vmInfo.PsObject) + .AddParameter("ExposeVirtualizationExtensions", exposeVirtualizationExtensions) + from _3 in Context.Engine.RunAsync(command).ToError().ToAsync() + select unit; + + private EitherAsync GetVmProcessorInfo( + TypedPsObject vmInfo) => + from _ in RightAsync(unit) + let command = PsCommandBuilder.Create() + .AddCommand("Get-VMProcessor") + .AddParameter("VM", vmInfo.PsObject) + from vmSecurityInfos in Context.Engine.GetObjectValuesAsync(command) + .ToError() + from vmSecurityInfo in vmSecurityInfos.HeadOrNone() + .ToEitherAsync(Error.New($"Failed to fetch processor information for the VM {vmInfo.Value.Id}.")) + select vmSecurityInfo; +} diff --git a/src/core/src/Eryph.VmManagement/Converging/ConvergeSecureBoot.cs b/src/core/src/Eryph.VmManagement/Converging/ConvergeSecureBoot.cs index aa7b81d9..8e7a26e2 100644 --- a/src/core/src/Eryph.VmManagement/Converging/ConvergeSecureBoot.cs +++ b/src/core/src/Eryph.VmManagement/Converging/ConvergeSecureBoot.cs @@ -1,7 +1,6 @@ using System; using System.Linq; using System.Threading.Tasks; -using Eryph.ConfigModel; using Eryph.Core; using Eryph.VmManagement.Data; using Eryph.VmManagement.Data.Core; @@ -9,57 +8,65 @@ using LanguageExt; using LanguageExt.Common; +using static LanguageExt.Prelude; + namespace Eryph.VmManagement.Converging; -public class ConvergeSecureBoot : ConvergeTaskBase +public class ConvergeSecureBoot( + ConvergeContext context) + : ConvergeTaskBase(context) { - public ConvergeSecureBoot(ConvergeContext context) : base(context) - { - } - - public override async Task>> Converge( - TypedPsObject vmInfo) - { - var secureBootCapability = Context.Config.Capabilities?.FirstOrDefault(x => x.Name == - EryphConstants.Capabilities.SecureBoot); - - if (secureBootCapability == null) - return vmInfo; - - var templateName = - secureBootCapability.Details?.FirstOrDefault(x => x.StartsWith("template:", - StringComparison.OrdinalIgnoreCase))?.Split(':')[1] - ?? "MicrosoftWindows"; - - var onOffState = (secureBootCapability.Details?.Any(x => - string.Equals(x, EryphConstants.CapabilityDetails.Disabled, StringComparison.OrdinalIgnoreCase))).GetValueOrDefault() ? OnOffState.Off : OnOffState.On; - - return await (from currentFirmware in Context.Engine.GetObjectsAsync(new PsCommandBuilder() - .AddCommand("get-VMFirmware") - .AddArgument(vmInfo.PsObject)).ToError().ToAsync().Bind( - r => r.HeadOrLeft(Error.New("VM firmware not found")).ToAsync()) - from uSecureBoot in currentFirmware.Value.SecureBootTemplate == templateName && currentFirmware.Value.SecureBoot == onOffState - ? Unit.Default - : Unit.Default.Apply(async _ => - { - if (vmInfo.Value.State == VirtualMachineState.Running) - return Error.New("Cannot change secure boot settings of a running catlet."); - - if(onOffState == OnOffState.On) - await Context.ReportProgress($"Configuring secure boot settings (Template: {templateName})").ConfigureAwait(false); - else - await Context.ReportProgress($"Configuring secure boot settings (Secure Boot: Off)").ConfigureAwait(false); - - return await Context.Engine.RunAsync(PsCommandBuilder.Create() - .AddCommand("Set-VMFirmware") - .AddParameter("VM", vmInfo.PsObject) - .AddParameter("EnableSecureBoot", onOffState) - .AddParameter("SecureBootTemplate", templateName)).ToError(); - - }).ToAsync() - from newVmInfo in vmInfo.RecreateOrReload(Context.Engine) - select newVmInfo).ToEither(); + public override Task>> Converge( + TypedPsObject vmInfo) => + ConvergeSecureBootState(vmInfo).ToEither(); + + private EitherAsync> ConvergeSecureBootState( + TypedPsObject vmInfo) => + from _ in RightAsync(unit) + let expectedSecureBootState = CatletCapabilities.IsSecureBootEnabled( + Context.Config.Capabilities.ToSeq()) + let expectedSecureBootTemplate = CatletCapabilities.FindSecureBootTemplate( + Context.Config.Capabilities.ToSeq()) + .IfNone("MicrosoftWindows") + from vmFirmwareInfo in GetFirmwareInfo(vmInfo) + let currentSecureBootState = vmFirmwareInfo.SecureBoot == OnOffState.On + let currentSecureBootTemplate = vmFirmwareInfo.SecureBootTemplate + from __ in expectedSecureBootState == currentSecureBootState + && expectedSecureBootTemplate == currentSecureBootTemplate + ? RightAsync(unit) + : ConfigureSecureBoot(vmInfo, expectedSecureBootState, expectedSecureBootTemplate) + from updatedVmInfo in vmInfo.RecreateOrReload(Context.Engine) + select updatedVmInfo; + private EitherAsync ConfigureSecureBoot( + TypedPsObject vmInfo, + bool enableSecureBoot, + string secureBootTemplate) => + from _1 in guard(vmInfo.Value.State is VirtualMachineState.Off or VirtualMachineState.OffCritical, + Error.New("Cannot change secure boot settings if the catlet is not stopped. Stop the catlet and retry.")) + .ToEitherAsync() + from _2 in Context.ReportProgressAsync(enableSecureBoot + ? $"Configuring secure boot settings (Template: {secureBootTemplate})" + : "Configuring secure boot settings (Secure Boot: Off)") + // Hyper-V allows us to set the SecureBootTemplate even if SecureBoot is disabled. + // Hence, this works as expected. + let command = PsCommandBuilder.Create() + .AddCommand("Set-VMFirmware") + .AddParameter("VM", vmInfo.PsObject) + .AddParameter("EnableSecureBoot", enableSecureBoot ? OnOffState.On : OnOffState.Off) + .AddParameter("SecureBootTemplate", secureBootTemplate) + from _3 in Context.Engine.RunAsync(command).ToError().ToAsync() + select unit; - } -} \ No newline at end of file + private EitherAsync GetFirmwareInfo( + TypedPsObject vmInfo) => + from _ in RightAsync(unit) + let command = PsCommandBuilder.Create() + .AddCommand("Get-VMFirmware") + .AddParameter("VM", vmInfo.PsObject) + from vmSecurityInfos in Context.Engine.GetObjectValuesAsync(command) + .ToError() + from vMSecurityInfo in vmSecurityInfos.HeadOrNone() + .ToEitherAsync(Error.New($"Failed to fetch firmware information for the VM {vmInfo.Value.Id}.")) + select vMSecurityInfo; +} diff --git a/src/core/src/Eryph.VmManagement/Converging/ConvergeTpm.cs b/src/core/src/Eryph.VmManagement/Converging/ConvergeTpm.cs index c7b74dfb..89980b16 100644 --- a/src/core/src/Eryph.VmManagement/Converging/ConvergeTpm.cs +++ b/src/core/src/Eryph.VmManagement/Converging/ConvergeTpm.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using Eryph.ConfigModel.Catlets; using Eryph.Core; +using Eryph.VmManagement.Data; using Eryph.VmManagement.Data.Core; using Eryph.VmManagement.Data.Full; using LanguageExt; @@ -63,7 +64,11 @@ from vMSecurityInfo in vmSecurityInfos.HeadOrNone() private EitherAsync ConfigureTpm( TypedPsObject vmInfo, bool enableTpm) => - enableTpm ? EnableTpm(vmInfo) : DisableTpm(vmInfo); + from _1 in guard(vmInfo.Value.State is VirtualMachineState.Off or VirtualMachineState.OffCritical, + Error.New("Cannot change TPM settings if the catlet is not stopped. Stop the catlet and retry.")) + .ToEitherAsync() + from _2 in enableTpm ? EnableTpm(vmInfo) : DisableTpm(vmInfo) + select unit; private EitherAsync EnableTpm( TypedPsObject vmInfo) => diff --git a/src/core/src/Eryph.VmManagement/Data/Core/VMFirmwareInfo.cs b/src/core/src/Eryph.VmManagement/Data/Core/VMFirmwareInfo.cs index a1696f8e..208f9827 100644 --- a/src/core/src/Eryph.VmManagement/Data/Core/VMFirmwareInfo.cs +++ b/src/core/src/Eryph.VmManagement/Data/Core/VMFirmwareInfo.cs @@ -1,22 +1,18 @@ using System; -namespace Eryph.VmManagement.Data.Core -{ - public class VMFirmwareInfo - { - //public VMBootSourceInfo[] BootOrder { get; private set; } - - public IPProtocolPreference PreferredNetworkBootProtocol { get; private set; } +namespace Eryph.VmManagement.Data.Core; - public OnOffState SecureBoot { get; private set; } +public class VMFirmwareInfo +{ + public IPProtocolPreference PreferredNetworkBootProtocol { get; init; } - public string SecureBootTemplate { get; private set; } - public Guid? SecureBootTemplateId { get; private set; } + public OnOffState SecureBoot { get; init; } - public ConsoleModeType ConsoleMode { get; private set; } + public string SecureBootTemplate { get; init; } + public Guid? SecureBootTemplateId { get; init; } - public OnOffState PauseAfterBootFailure { get; private set; } - } + public ConsoleModeType ConsoleMode { get; init; } -} \ No newline at end of file + public OnOffState PauseAfterBootFailure { get; init; } +} diff --git a/src/core/src/Eryph.VmManagement/Data/Core/VMProcessorInfo.cs b/src/core/src/Eryph.VmManagement/Data/Core/VMProcessorInfo.cs new file mode 100644 index 00000000..7c2c1187 --- /dev/null +++ b/src/core/src/Eryph.VmManagement/Data/Core/VMProcessorInfo.cs @@ -0,0 +1,63 @@ +using Microsoft.Management.Infrastructure; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.NetworkInformation; +using System.Text; +using System.Threading.Tasks; +using System.Xml.Linq; + +namespace Eryph.VmManagement.Data.Core; + +/// +/// Contains information about the processor settings of a Hyper-V VM +/// as returned by Get-VMProcessor. +/// +public class VMProcessorInfo +{ + public string ResourcePoolName { get; init; } + + public long Count { get; init; } + + public bool CompatibilityForMigrationEnabled { get; init; } + + public bool CompatibilityForOlderOperatingSystemsEnabled { get; init; } + + public long HwThreadCountPerCore { get; init; } + + public bool ExposeVirtualizationExtensions { get; init; } + + public bool EnablePerfmonPmu { get; init; } + + public bool EnablePerfmonArchPmu { get; init; } + + public bool EnablePerfmonLbr { get; init; } + + public bool EnablePerfmonPebs { get; init; } + + public bool EnablePerfmonIpt { get; init; } + + public bool EnableLegacyApicMode { get; init; } + + public bool AllowACountMCount { get; init; } + + public string CpuBrandString { get; init; } + + public int PerfCpuFreqCapMhz { get; init; } + + public int L3CacheWays { get; init; } + + public long Maximum { get; init; } + + public long Reserve { get; init; } + + public int RelativeWeight { get; init; } + + public long MaximumCountPerNumaNode { get; init; } + + public long MaximumCountPerNumaSocket { get; init; } + + public bool EnableHostResourceProtection { get; init; } + + public string Id { get; init; } +} diff --git a/src/core/test/Eryph.VmManagement.Test/ConvergeCPUTests.cs b/src/core/test/Eryph.VmManagement.Test/ConvergeCPUTests.cs index 8d9f618a..f792071d 100644 --- a/src/core/test/Eryph.VmManagement.Test/ConvergeCPUTests.cs +++ b/src/core/test/Eryph.VmManagement.Test/ConvergeCPUTests.cs @@ -1,48 +1,56 @@ using Eryph.ConfigModel.Catlets; using Eryph.VmManagement.Converging; +using Eryph.VmManagement.Data; +using Eryph.VmManagement.Data.Full; +using FluentAssertions; using LanguageExt; -using LanguageExt.Common; -using Microsoft.Extensions.Logging.Abstractions; using Xunit; -namespace Eryph.VmManagement.Test +using static LanguageExt.Prelude; + +namespace Eryph.VmManagement.Test; + +public class ConvergeCpuTests { - public class ConvergeCpuTests : IClassFixture - { - private readonly ConvergeFixture _fixture; + private readonly ConvergeFixture _fixture = new(); + private readonly ConvergeCPU _convergeTask; + private AssertCommand? _executedCommand; - public ConvergeCpuTests(ConvergeFixture fixture) + public ConvergeCpuTests() + { + _convergeTask = new(_fixture.Context); + _fixture.Engine.RunCallback = cmd => { - _fixture = fixture; - } + _executedCommand = cmd; + return unit; + }; + } - [Theory] - [InlineData(null, 1L)] - [InlineData(1, 1L)] - [InlineData(2, 1L)] - public async Task Converges_Cpu_if_necessary(int? configCpu, long vmCpu) + [Theory] + [InlineData(null, 1L)] + [InlineData(null, 2L)] + [InlineData(1, 1L)] + [InlineData(2, 1L)] + public async Task Converges_Cpu_if_necessary(int? configCpu, long vmCpu) + { + _fixture.Config.Cpu = new CatletCpuConfig { Count = configCpu }; + var vmData = _fixture.Engine.ToPsObject(new VirtualMachineInfo { + State = VirtualMachineState.Off, + ProcessorCount = vmCpu, + }); + + await _convergeTask.Converge(vmData); - _fixture.Config.Cpu = new CatletCpuConfig { Count = configCpu }; - var vmData = _fixture.Engine.ToPsObject(new Data.Full.VirtualMachineInfo { ProcessorCount = vmCpu }); - var called = false; - - _fixture.Engine.RunCallback = command => - { - called = true; - return Unit.Default; - }; - - var convergeTask = new ConvergeCPU(_fixture.Context); - await convergeTask.Converge(vmData); - - if(configCpu.GetValueOrDefault(1) == vmCpu) - Assert.False(called); - else - Assert.True(called); - - + if (configCpu.GetValueOrDefault(1) == vmCpu) + { + _executedCommand.Should().BeNull(); + return; } + _executedCommand.Should().NotBeNull(); + _executedCommand!.ShouldBeCommand("Set-VMProcessor") + .ShouldBeParam("VM", vmData.PsObject) + .ShouldBeParam("Count", configCpu.GetValueOrDefault(1)); } } diff --git a/src/core/test/Eryph.VmManagement.Test/ConvergeMemoryTests.cs b/src/core/test/Eryph.VmManagement.Test/ConvergeMemoryTests.cs index 6afc34b1..e94c8974 100644 --- a/src/core/test/Eryph.VmManagement.Test/ConvergeMemoryTests.cs +++ b/src/core/test/Eryph.VmManagement.Test/ConvergeMemoryTests.cs @@ -18,10 +18,12 @@ namespace Eryph.VmManagement.Test; public class ConvergeMemoryTests { private readonly ConvergeFixture _fixture = new(); + private readonly ConvergeMemory _convergeTask; private AssertCommand? _executedCommand; public ConvergeMemoryTests() { + _convergeTask = new(_fixture.Context); _fixture.Engine.RunCallback = cmd => { _executedCommand = cmd; @@ -30,7 +32,7 @@ public ConvergeMemoryTests() } [Fact] - public async Task Converge_NoMemoryConfiguration_CommandIsNotExecuted() + public async Task Converge_NoMemoryConfiguration_DisablesDynamicMemory() { _fixture.Config.Memory = null; var vmInfo = _fixture.Engine.ToPsObject(new VirtualMachineInfo @@ -41,15 +43,72 @@ public async Task Converge_NoMemoryConfiguration_CommandIsNotExecuted() MemoryMaximum = 42 * 1024L * 1024, }); - var convergeTask = new ConvergeMemory(_fixture.Context); - var result = await convergeTask.Converge(vmInfo); + var result = await _convergeTask.Converge(vmInfo); result.Should().BeRight(); - _executedCommand.Should().BeNull(); + _executedCommand.Should().NotBeNull(); + _executedCommand!.ShouldBeCommand("Set-VMMemory") + .ShouldBeParam("VM", vmInfo.PsObject) + .ShouldBeParam("DynamicMemoryEnabled", false) + .ShouldBeComplete(); + } + + [Fact] + public async Task Converge_MinimumMemoryIsConfigured_EnablesDynamicMemory() + { + _fixture.Config.Memory = new CatletMemoryConfig() + { + Minimum = 1024, + Startup = 2048, + }; + var vmInfo = _fixture.Engine.ToPsObject(new VirtualMachineInfo + { + DynamicMemoryEnabled = false, + MemoryStartup = 2048 * 1024L * 1024, + MemoryMinimum = 512 * 1024L * 1024, + MemoryMaximum = 2048 * 1024L * 1024, + }); + + var result = await _convergeTask.Converge(vmInfo); + result.Should().BeRight(); + + _executedCommand.Should().NotBeNull(); + _executedCommand!.ShouldBeCommand("Set-VMMemory") + .ShouldBeParam("VM", vmInfo.PsObject) + .ShouldBeParam("DynamicMemoryEnabled", true) + .ShouldBeParam("MinimumBytes", 1024 * 1024L * 1024) + .ShouldBeComplete(); + } + + [Fact] + public async Task Converge_MaximumMemoryIsConfigured_EnablesDynamicMemory() + { + _fixture.Config.Memory = new CatletMemoryConfig() + { + Maximum = 4096, + Startup = 2048, + }; + var vmInfo = _fixture.Engine.ToPsObject(new VirtualMachineInfo + { + DynamicMemoryEnabled = false, + MemoryStartup = 2048 * 1024L * 1024, + MemoryMinimum = 2048 * 1024L * 1024, + MemoryMaximum = 3072 * 1024L * 1024, + }); + + var result = await _convergeTask.Converge(vmInfo); + result.Should().BeRight(); + + _executedCommand.Should().NotBeNull(); + _executedCommand!.ShouldBeCommand("Set-VMMemory") + .ShouldBeParam("VM", vmInfo.PsObject) + .ShouldBeParam("DynamicMemoryEnabled", true) + .ShouldBeParam("MaximumBytes", 4096 * 1024L * 1024) + .ShouldBeComplete(); } [Fact] - public async Task Converge_DynamicMemoryIsDisabled_DisablesDynamicMemory() + public async Task Converge_DynamicMemoryIsExplicitlyDisabled_DisablesDynamicMemory() { _fixture.Config.Capabilities = [ @@ -59,20 +118,26 @@ public async Task Converge_DynamicMemoryIsDisabled_DisablesDynamicMemory() Details = [EryphConstants.CapabilityDetails.Disabled] }, ]; - _fixture.Config.Memory = null; + _fixture.Config.Memory = new CatletMemoryConfig() + { + Startup = 2048, + Minimum = 512, + Maximum = 4096, + }; var vmInfo = _fixture.Engine.ToPsObject(new VirtualMachineInfo { DynamicMemoryEnabled = true, - MemoryStartup = 42 * 1024L * 1024, - MemoryMinimum = 42 * 1024L * 1024, - MemoryMaximum = 42 * 1024L * 1024, + MemoryStartup = 2048 * 1024L * 1024, + MemoryMinimum = 1024 * 1024L * 1024, + MemoryMaximum = 3072 * 1024L * 1024, }); - var convergeTask = new ConvergeMemory(_fixture.Context); - var result = await convergeTask.Converge(vmInfo); + var result = await _convergeTask.Converge(vmInfo); result.Should().BeRight(); _executedCommand.Should().NotBeNull(); + // MinimumBytes and MaximumBytes must not be set as Hyper-V + // rejects them when DynamicMemoryEnabled is false. _executedCommand!.ShouldBeCommand("Set-VMMemory") .ShouldBeParam("VM", vmInfo.PsObject) .ShouldBeParam("DynamicMemoryEnabled", false) @@ -96,8 +161,7 @@ public async Task Converge_MemoryIsFullyConfigured_UpdatesAllSizes() MemoryMaximum = 42 * 1024L * 1024, }); - var convergeTask = new ConvergeMemory(_fixture.Context); - var result = await convergeTask.Converge(vmInfo); + var result = await _convergeTask.Converge(vmInfo); result.Should().BeRight(); _executedCommand.Should().NotBeNull(); @@ -113,6 +177,13 @@ public async Task Converge_MemoryIsFullyConfigured_UpdatesAllSizes() [Fact] public async Task Converge_MemorySizesMismatched_UpdateSizesWhichAreNotExplicitlyConfigured() { + _fixture.Config.Capabilities = + [ + new CatletCapabilityConfig + { + Name = EryphConstants.Capabilities.DynamicMemory, + }, + ]; _fixture.Config.Memory = new CatletMemoryConfig { Startup = 2048, @@ -125,8 +196,7 @@ public async Task Converge_MemorySizesMismatched_UpdateSizesWhichAreNotExplicitl MemoryMaximum = 1024 * 1024L * 1024, }); - var convergeTask = new ConvergeMemory(_fixture.Context); - var result = await convergeTask.Converge(vmInfo); + var result = await _convergeTask.Converge(vmInfo); result.Should().BeRight(); _executedCommand.Should().NotBeNull(); @@ -139,7 +209,7 @@ public async Task Converge_MemorySizesMismatched_UpdateSizesWhichAreNotExplicitl } [Fact] - public async Task Converge_NoChanges_CommandIsNotExecuted() + public async Task Converge_NoChangesWithDynamicMemory_CommandIsNotExecuted() { _fixture.Config.Memory = new CatletMemoryConfig { @@ -155,8 +225,28 @@ public async Task Converge_NoChanges_CommandIsNotExecuted() MemoryMaximum = 4096 * 1024L * 1024, }); - var convergeTask = new ConvergeMemory(_fixture.Context); - var result = await convergeTask.Converge(vmInfo); + var result = await _convergeTask.Converge(vmInfo); + result.Should().BeRight(); + + _executedCommand.Should().BeNull(); + } + + [Fact] + public async Task Converge_NoChangesWithoutDynamicMemory_CommandIsNotExecuted() + { + _fixture.Config.Memory = new CatletMemoryConfig + { + Startup = 2048, + }; + var vmInfo = _fixture.Engine.ToPsObject(new VirtualMachineInfo + { + DynamicMemoryEnabled = false, + MemoryStartup = 2048 * 1024L * 1024, + MemoryMinimum = 512 * 1024L * 1024, + MemoryMaximum = 1 * 1024L * 1024 *1024 * 1024, + }); + + var result = await _convergeTask.Converge(vmInfo); result.Should().BeRight(); _executedCommand.Should().BeNull(); @@ -178,8 +268,7 @@ public async Task Converge_InvalidMinimum_ReturnError() MemoryMaximum = 42 * 1024L * 1024, }); - var convergeTask = new ConvergeMemory(_fixture.Context); - var result = await convergeTask.Converge(vmInfo); + var result = await _convergeTask.Converge(vmInfo); result.Should().BeLeft().Which.Message.Should().Be( "Startup memory (1024 MiB) cannot be less than minimum memory (2048 MiB)."); @@ -202,8 +291,7 @@ public async Task Converge_InvalidMaximum_ReturnError() MemoryMaximum = 42 * 1024L * 1024, }); - var convergeTask = new ConvergeMemory(_fixture.Context); - var result = await convergeTask.Converge(vmInfo); + var result = await _convergeTask.Converge(vmInfo); result.Should().BeLeft().Which.Message.Should().Be( "Startup memory (4096 MiB) cannot be more than maximum memory (2048 MiB)."); diff --git a/src/core/test/Eryph.VmManagement.Test/ConvergeNestedVirtualizationTests.cs b/src/core/test/Eryph.VmManagement.Test/ConvergeNestedVirtualizationTests.cs index 5fb7c581..077c3837 100644 --- a/src/core/test/Eryph.VmManagement.Test/ConvergeNestedVirtualizationTests.cs +++ b/src/core/test/Eryph.VmManagement.Test/ConvergeNestedVirtualizationTests.cs @@ -1,78 +1,78 @@ using Eryph.ConfigModel.Catlets; using Eryph.Core; using Eryph.VmManagement.Converging; -using LanguageExt; -using LanguageExt.Common; +using Eryph.VmManagement.Data; +using Eryph.VmManagement.Data.Core; +using Eryph.VmManagement.Data.Full; +using FluentAssertions; using Xunit; -namespace Eryph.VmManagement.Test -{ - public class ConvergeNestedVirtualizationTests - { - private readonly ConvergeFixture _fixture = new(); +using static LanguageExt.Prelude; - [Theory] - [InlineData(false, false)] - [InlineData(false, true)] - [InlineData(true, false)] - [InlineData(true, true)] - [InlineData(true, null)] - [InlineData(false, null)] - - public async Task Converges_NestedVirtualization_if_necessary(bool exposed, bool? shouldExpose) - { - var vmData = _fixture.Engine.ToPsObject( - new Data.Full.VirtualMachineInfo()); - var called = false; +namespace Eryph.VmManagement.Test; +public class ConvergeNestedVirtualizationTests +{ + private readonly ConvergeFixture _fixture = new(); + private readonly ConvergeNestedVirtualization _convergeTask; + private readonly TypedPsObject _vmInfo; + private AssertCommand? _executedCommand; - if(shouldExpose.HasValue) - _fixture.Config.Capabilities = new[] - { - new CatletCapabilityConfig - { - Name = EryphConstants.Capabilities.NestedVirtualization, - Details = new []{shouldExpose.GetValueOrDefault() - ? EryphConstants.CapabilityDetails.Enabled - : EryphConstants.CapabilityDetails.Disabled} - } - }; + public ConvergeNestedVirtualizationTests() + { + _convergeTask = new(_fixture.Context); + _vmInfo = _fixture.Engine.ToPsObject(new VirtualMachineInfo() + { + State = VirtualMachineState.Off, + }); + _fixture.Engine.RunCallback = cmd => + { + _executedCommand = cmd; + return unit; + }; + } - AssertCommand? runCommand = null; - _fixture.Engine.RunCallback = command => - { - called = true; - runCommand = command; - return Unit.Default; - }; - _fixture.Engine.GetValuesCallback = (_, command) => - { - if (command.ToString().StartsWith("Get-VMProcessor")) + [Theory, CombinatorialData] + public async Task Converge_EnablesNestedVirtualizationWhenNecessary(bool exposed, bool? shouldExpose) + { + _fixture.Config.Capabilities = shouldExpose.HasValue switch + { + true => + [ + new CatletCapabilityConfig { - return new object []{exposed}.ToSeq(); + Name = EryphConstants.Capabilities.NestedVirtualization, + Details = shouldExpose.GetValueOrDefault() + ? [EryphConstants.CapabilityDetails.Enabled] + : [EryphConstants.CapabilityDetails.Disabled], } + ], + false => null, + }; - return new PowershellFailure{ Message = $"Unexpected command {command}"}; - }; - - - var convergeTask = new ConvergeNestedVirtualization(_fixture.Context); - await convergeTask.Converge(vmData); + _fixture.Engine.GetValuesCallback = (_, command) => + { + command.ShouldBeCommand("Get-VMProcessor") + .ShouldBeParam("VM", _vmInfo.PsObject) + .ShouldBeComplete(); - if (exposed == shouldExpose || shouldExpose == null) + return Seq1(new VMProcessorInfo { - Assert.False(called); - return; - } - - Assert.True(called); - Assert.NotNull(runCommand); - runCommand.ShouldBeCommand("Set-VMProcessor") - .ShouldBeParam("VM") - .ShouldBeParam("ExposeVirtualizationExtensions", shouldExpose); + ExposeVirtualizationExtensions = exposed, + }); + }; + await _convergeTask.Converge(_vmInfo); + if (exposed == shouldExpose.GetValueOrDefault()) + { + _executedCommand.Should().BeNull(); + return; } - + + _executedCommand.Should().NotBeNull(); + _executedCommand!.ShouldBeCommand("Set-VMProcessor") + .ShouldBeParam("VM") + .ShouldBeParam("ExposeVirtualizationExtensions", shouldExpose.GetValueOrDefault()); } } diff --git a/src/core/test/Eryph.VmManagement.Test/ConvergeSecureBootTests.cs b/src/core/test/Eryph.VmManagement.Test/ConvergeSecureBootTests.cs new file mode 100644 index 00000000..430019d7 --- /dev/null +++ b/src/core/test/Eryph.VmManagement.Test/ConvergeSecureBootTests.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Eryph.ConfigModel.Catlets; +using Eryph.Core; +using Eryph.VmManagement.Converging; +using Eryph.VmManagement.Data; +using Eryph.VmManagement.Data.Core; +using Eryph.VmManagement.Data.Full; +using FluentAssertions; +using LanguageExt; +using Xunit; + +using static LanguageExt.Prelude; + +namespace Eryph.VmManagement.Test; + +public class ConvergeSecureBootTests +{ + private readonly ConvergeFixture _fixture = new(); + private readonly ConvergeSecureBoot _convergeTask; + private readonly TypedPsObject _vmInfo; + private AssertCommand? _executedCommand; + + public ConvergeSecureBootTests() + { + _convergeTask = new(_fixture.Context); + _vmInfo = _fixture.Engine.ToPsObject(new VirtualMachineInfo() + { + State = VirtualMachineState.Off, + }); + _fixture.Engine.RunCallback = cmd => + { + _executedCommand = cmd; + return unit; + }; + } + + [Theory, CombinatorialData] + public async Task Converge_EnablesSecureBootWhenNecessary( + bool secureBoot, + bool? shouldSecureBoot) + { + _fixture.Config.Capabilities = shouldSecureBoot.HasValue switch + { + true => + [ + new CatletCapabilityConfig + { + Name = EryphConstants.Capabilities.SecureBoot, + Details = shouldSecureBoot.Value + ? [EryphConstants.CapabilityDetails.Enabled] + : [EryphConstants.CapabilityDetails.Disabled], + } + ], + false => null, + }; + + _fixture.Engine.GetValuesCallback = (_, command) => + { + command.ShouldBeCommand("Get-VMFirmware") + .ShouldBeParam("VM", _vmInfo.PsObject) + .ShouldBeComplete(); + + return Seq1(new VMFirmwareInfo + { + SecureBoot = secureBoot ? OnOffState.On : OnOffState.Off, + SecureBootTemplate = "MicrosoftWindows", + }); + }; + + await _convergeTask.Converge(_vmInfo); + + if (secureBoot == shouldSecureBoot.GetValueOrDefault()) + { + _executedCommand.Should().BeNull(); + return; + } + + _executedCommand.Should().NotBeNull(); + _executedCommand!.ShouldBeCommand("Set-VMFirmware") + .ShouldBeParam("VM") + .ShouldBeParam( + "EnableSecureBoot", + shouldSecureBoot.GetValueOrDefault() ? OnOffState.On : OnOffState.Off); + } + + [Fact] + public async Task Converge_DifferentSecureBootTemplate_ChangesSecureBootTemplate() + { + _fixture.Config.Capabilities = + [ + new CatletCapabilityConfig + { + Name = EryphConstants.Capabilities.SecureBoot, + Details = ["template:TestTemplate"], + } + ]; + + _fixture.Engine.GetValuesCallback = (_, command) => + { + command.ShouldBeCommand("Get-VMFirmware") + .ShouldBeParam("VM", _vmInfo.PsObject) + .ShouldBeComplete(); + + return Seq1(new VMFirmwareInfo + { + SecureBoot = OnOffState.On, + SecureBootTemplate = "MicrosoftWindows", + }); + }; + + await _convergeTask.Converge(_vmInfo); + + _executedCommand.Should().NotBeNull(); + _executedCommand!.ShouldBeCommand("Set-VMFirmware") + .ShouldBeParam("VM") + .ShouldBeParam("EnableSecureBoot", OnOffState.On) + .ShouldBeParam("SecureBootTemplate", "TestTemplate"); + } +} diff --git a/src/core/test/Eryph.VmManagement.Test/ConvergeTpmTests.cs b/src/core/test/Eryph.VmManagement.Test/ConvergeTpmTests.cs index c3311931..63e529c7 100644 --- a/src/core/test/Eryph.VmManagement.Test/ConvergeTpmTests.cs +++ b/src/core/test/Eryph.VmManagement.Test/ConvergeTpmTests.cs @@ -1,6 +1,7 @@ using Eryph.ConfigModel.Catlets; using Eryph.Core; using Eryph.VmManagement.Converging; +using Eryph.VmManagement.Data; using Eryph.VmManagement.Data.Core; using Eryph.VmManagement.Data.Full; using FluentAssertions; @@ -20,7 +21,10 @@ public class ConvergeTpmTests public ConvergeTpmTests() { _convergeTask = new(_fixture.Context); - _vmInfo = _fixture.Engine.ToPsObject(new VirtualMachineInfo()); + _vmInfo = _fixture.Engine.ToPsObject(new VirtualMachineInfo() + { + State = VirtualMachineState.Off, + }); } [Fact]