Skip to content

Commit

Permalink
Improve (de)activation of capabilities (#304)
Browse files Browse the repository at this point in the history
* Improve (de)activation of capabilities
* Deactivate dynamic memory and secure boot by default
* Add unit tests

Closes #289
  • Loading branch information
ChristopherMann authored Jan 21, 2025
1 parent 0111226 commit 140ab15
Show file tree
Hide file tree
Showing 13 changed files with 587 additions and 249 deletions.
55 changes: 49 additions & 6 deletions src/core/src/Eryph.Core/CatletCapabilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<CatletCapabilityConfig> 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);
}
FindCapability(configs, EryphConstants.Capabilities.DynamicMemory)
.Map(c => !IsExplicitlyDisabled(c))
.IfNone(false);

public static bool IsDynamicMemoryExplicitlyDisabled(
Seq<CatletCapabilityConfig> configs) =>
FindCapability(configs, EryphConstants.Capabilities.DynamicMemory)
.Map(IsExplicitlyDisabled)
.IfNone(false);

public static bool IsNestedVirtualizationEnabled(
Seq<CatletCapabilityConfig> configs) =>
FindCapability(configs, EryphConstants.Capabilities.NestedVirtualization)
.Map(c => !IsExplicitlyDisabled(c))
.IfNone(false);

public static bool IsSecureBootEnabled(
Seq<CatletCapabilityConfig> configs) =>
FindCapability(configs, EryphConstants.Capabilities.SecureBoot)
.Map(c => !IsExplicitlyDisabled(c))
.IfNone(false);

public static bool IsTpmEnabled(
Seq<CatletCapabilityConfig> configs) =>
FindCapability(configs, EryphConstants.Capabilities.Tpm)
.Map(c => !IsExplicitlyDisabled(c))
.IfNone(false);

public static Option<string> FindSecureBootTemplate(
Seq<CatletCapabilityConfig> 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<CatletCapabilityConfig> FindCapability(
Seq<CatletCapabilityConfig> 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));
}
4 changes: 2 additions & 2 deletions src/core/src/Eryph.VmManagement/Converging/ConvergeCPU.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ public override async Task<Either<Error, TypedPsObject<VirtualMachineInfo>>> 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);

Expand Down
22 changes: 11 additions & 11 deletions src/core/src/Eryph.VmManagement/Converging/ConvergeMemory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,12 @@ private static EitherAsync<Error, TypedPsObject<VirtualMachineInfo>> Converge(
Func<string, Task> 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
Expand Down Expand Up @@ -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<Error, Unit>(unit)
from reloadedVmInfo in vmInfo.RecreateOrReload(powershellEngine)
select reloadedVmInfo;

private static Either<
Error,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Either<Error, TypedPsObject<VirtualMachineInfo>>> Converge(
TypedPsObject<VirtualMachineInfo> 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<bool>(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();


}
}
public override Task<Either<Error, TypedPsObject<VirtualMachineInfo>>> Converge(
TypedPsObject<VirtualMachineInfo> vmInfo) =>
ConvergeNestedVirtualizationState(vmInfo).ToEither();

private EitherAsync<Error, TypedPsObject<VirtualMachineInfo>> ConvergeNestedVirtualizationState(
TypedPsObject<VirtualMachineInfo> vmInfo) =>
from _ in RightAsync<Error, Unit>(unit)
let expectedNestedVirtualization = CatletCapabilities.IsNestedVirtualizationEnabled(
Context.Config.Capabilities.ToSeq())
from vmProcessorInfo in GetVmProcessorInfo(vmInfo)
let actualNestedVirtualization = vmProcessorInfo.ExposeVirtualizationExtensions
from __ in expectedNestedVirtualization == actualNestedVirtualization
? RightAsync<Error, Unit>(unit)
: ConfigureNestedVirtualization(vmInfo, expectedNestedVirtualization)
from updatedVmInfo in vmInfo.RecreateOrReload(Context.Engine)
select updatedVmInfo;

private EitherAsync<Error, Unit> ConfigureNestedVirtualization(
TypedPsObject<VirtualMachineInfo> 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<Error, VMProcessorInfo> GetVmProcessorInfo(
TypedPsObject<VirtualMachineInfo> vmInfo) =>
from _ in RightAsync<Error, Unit>(unit)
let command = PsCommandBuilder.Create()
.AddCommand("Get-VMProcessor")
.AddParameter("VM", vmInfo.PsObject)
from vmSecurityInfos in Context.Engine.GetObjectValuesAsync<VMProcessorInfo>(command)
.ToError()
from vmSecurityInfo in vmSecurityInfos.HeadOrNone()
.ToEitherAsync(Error.New($"Failed to fetch processor information for the VM {vmInfo.Value.Id}."))
select vmSecurityInfo;
}
107 changes: 57 additions & 50 deletions src/core/src/Eryph.VmManagement/Converging/ConvergeSecureBoot.cs
Original file line number Diff line number Diff line change
@@ -1,65 +1,72 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Eryph.ConfigModel;
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 ConvergeSecureBoot : ConvergeTaskBase
public class ConvergeSecureBoot(
ConvergeContext context)
: ConvergeTaskBase(context)
{
public ConvergeSecureBoot(ConvergeContext context) : base(context)
{
}

public override async Task<Either<Error, TypedPsObject<VirtualMachineInfo>>> Converge(
TypedPsObject<VirtualMachineInfo> 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<VMFirmwareInfo>(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<Either<Error, TypedPsObject<VirtualMachineInfo>>> Converge(
TypedPsObject<VirtualMachineInfo> vmInfo) =>
ConvergeSecureBootState(vmInfo).ToEither();

private EitherAsync<Error, TypedPsObject<VirtualMachineInfo>> ConvergeSecureBootState(
TypedPsObject<VirtualMachineInfo> vmInfo) =>
from _ in RightAsync<Error, Unit>(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<Error, Unit>(unit)
: ConfigureSecureBoot(vmInfo, expectedSecureBootState, expectedSecureBootTemplate)
from updatedVmInfo in vmInfo.RecreateOrReload(Context.Engine)
select updatedVmInfo;

private EitherAsync<Error, Unit> ConfigureSecureBoot(
TypedPsObject<VirtualMachineInfo> 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;

}
}
private EitherAsync<Error, VMFirmwareInfo> GetFirmwareInfo(
TypedPsObject<VirtualMachineInfo> vmInfo) =>
from _ in RightAsync<Error, Unit>(unit)
let command = PsCommandBuilder.Create()
.AddCommand("Get-VMFirmware")
.AddParameter("VM", vmInfo.PsObject)
from vmSecurityInfos in Context.Engine.GetObjectValuesAsync<VMFirmwareInfo>(command)
.ToError()
from vMSecurityInfo in vmSecurityInfos.HeadOrNone()
.ToEitherAsync(Error.New($"Failed to fetch firmware information for the VM {vmInfo.Value.Id}."))
select vMSecurityInfo;
}
7 changes: 6 additions & 1 deletion src/core/src/Eryph.VmManagement/Converging/ConvergeTpm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -63,7 +64,11 @@ from vMSecurityInfo in vmSecurityInfos.HeadOrNone()
private EitherAsync<Error, Unit> ConfigureTpm(
TypedPsObject<VirtualMachineInfo> 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<Error, Unit> EnableTpm(
TypedPsObject<VirtualMachineInfo> vmInfo) =>
Expand Down
Loading

0 comments on commit 140ab15

Please sign in to comment.