/// <summary> /// Validates the options. /// </summary> /// <param name="clusterDefinition">The cluster definition.</param> /// <exception cref="ClusterDefinitionException">Thrown if the definition is not valid.</exception> internal void Validate(ClusterDefinition clusterDefinition) { Covenant.Requires <ArgumentNullException>(clusterDefinition != null, nameof(clusterDefinition)); var minioOptionsPrefix = $"{nameof(ClusterDefinition.Storage)}.{nameof(ClusterDefinition.Storage.Minio)}"; if (!clusterDefinition.Nodes.Any(n => n.Labels.Minio)) { if (clusterDefinition.Kubernetes.AllowPodsOnControlPlane.GetValueOrDefault() == true) { foreach (var node in clusterDefinition.Nodes) { node.Labels.MinioInternal = true; } } else { foreach (var node in clusterDefinition.Workers) { node.Labels.MinioInternal = true; } } } else { foreach (var node in clusterDefinition.Nodes.Where(n => n.Labels.Minio)) { node.Labels.MinioInternal = true; } } var serverCount = clusterDefinition.Nodes.Where(n => n.Labels.MinioInternal).Count(); if (serverCount * VolumesPerNode < 4) { throw new ClusterDefinitionException($"Minio requires at least [4] volumes within the cluster. Increase [{minioOptionsPrefix}.{nameof(MinioOptions.VolumesPerNode)}] so the number of nodes hosting Minio times [{VolumesPerNode}] is at least [4]."); } var minOsDiskAfterMinio = ByteUnits.Parse(KubeConst.MinimumOsDiskAfterMinio); foreach (var node in clusterDefinition.Nodes.Where(node => node.Labels.MinioInternal)) { var osDisk = ByteUnits.Parse(node.GetDataDiskSize(clusterDefinition)); var minioVolumes = ByteUnits.Parse(VolumeSize) * VolumesPerNode; if (osDisk - minioVolumes < minOsDiskAfterMinio) { throw new ClusterDefinitionException($"Node [{node.Name}] Operating System (boot) disk is too small. Increase this to at least [{ByteUnits.Humanize(minOsDiskAfterMinio + minioVolumes, powerOfTwo: true, spaceBeforeUnit: false)}]."); } } }
/// <summary> /// Returns the size in bytes of RAM to allocate to the MDS cache /// on this node integrated Ceph storage cluster is enabled and /// MDS is deployed to the node. /// </summary> /// <param name="clusterDefinition">The cluster definition.</param> /// <returns>The size in bytes or zero if Ceph is not enabled.</returns> public decimal GetCephMDSCacheSize(ClusterDefinition clusterDefinition) { if (!clusterDefinition.Ceph.Enabled) { return(0); } if (string.IsNullOrEmpty(Labels.CephMDSCacheSize)) { Labels.CephMDSCacheSize = clusterDefinition.Ceph.MDSCacheSize; } return(ByteUnits.Parse(Labels.CephMDSCacheSize)); }
/// <summary> /// Parses a dictionary of name/value labels by setting the appropriate /// properties of the parent node. /// </summary> /// <param name="labels">The label dictionary.</param> internal void Parse(Dictionary <string, string> labels) { // WARNING: // // This method will need to be updated whenever new standard labels are added or changed. foreach (var label in labels) { switch (label.Key) { case LabelAddress: Node.Address = label.Value; break; case LabelRole: Node.Role = label.Value; break; case LabelIngress: ParseCheck(label, () => { Node.Ingress = NeonHelper.ParseBool(label.Value); }); break; case LabelOpenEbs: ParseCheck(label, () => { Node.OpenEbsStorage = NeonHelper.ParseBool(label.Value); }); break; case LabelAzureVmSize: case LabelAzureStorageType: case LabelAzureDriveSize: if (Node.Azure == null) { Node.Azure = new AzureNodeOptions(); } switch (label.Key) { case LabelAzureVmSize: Node.Azure.VmSize = label.Value; break; case LabelAzureDriveSize: Node.Azure.DiskSize = label.Value; break; case LabelAzureStorageType: ParseCheck(label, () => { Node.Azure.StorageType = NeonHelper.ParseEnum <AzureStorageType>(label.Value); }); break; } break; case LabelStorageSize: ParseCheck(label, () => { Node.Labels.StorageSize = ByteUnits.Parse(label.Value).ToString(); }); break; case LabelStorageLocal: Node.Labels.StorageLocal = label.Value.Equals("true", StringComparison.OrdinalIgnoreCase); break; case LabelStorageHDD: Node.Labels.StorageHDD = label.Value.Equals("true", StringComparison.OrdinalIgnoreCase); break; case LabelStorageRedundant: Node.Labels.StorageRedundant = label.Value.Equals("true", StringComparison.OrdinalIgnoreCase); break; case LabelStorageEphemeral: Node.Labels.StorageEphemeral = label.Value.Equals("true", StringComparison.OrdinalIgnoreCase); break; case LabelComputeCores: ParseCheck(label, () => { Node.Labels.ComputeCores = int.Parse(label.Value); }); break; case LabelComputeRamMiB: ParseCheck(label, () => { Node.Labels.ComputeRam = int.Parse(label.Value); }); break; case LabelPhysicalMachine: Node.Labels.PhysicalMachine = label.Value; break; case LabelPhysicalLocation: Node.Labels.PhysicalLocation = label.Value; break; case LabelPhysicalAvailabilitytSet: Node.Labels.PhysicalAvailabilitySet = label.Value; break; case LabelPhysicalPower: Node.Labels.PhysicalPower = label.Value; break; case LabelDatacenter: case LabelEnvironment: // These labels don't currently map to node properties so we'll ignore them. break; default: // Must be a custom label. Node.Labels.Custom.Add(label.Key, label.Value); break; } } }
#pragma warning restore CA1416 /// <summary> /// Creates a virtual machine. /// </summary> /// <param name="machineName">The machine name.</param> /// <param name="memorySize"> /// A string specifying the memory size. This can be a long byte count or a /// byte count or a number with units like <b>512MiB</b>, <b>0.5GiB</b>, <b>2GiB</b>, /// or <b>1TiB</b>. This defaults to <b>2GiB</b>. /// </param> /// <param name="processorCount"> /// The number of virutal processors to assign to the machine. This defaults to <b>4</b>. /// </param> /// <param name="driveSize"> /// A string specifying the primary disk size. This can be a long byte count or a /// byte count or a number with units like <b>512MB</b>, <b>0.5GiB</b>, <b>2GiB</b>, /// or <b>1TiB</b>. Pass <c>null</c> to leave the disk alone. This defaults to <c>null</c>. /// </param> /// <param name="drivePath"> /// Optionally specifies the path where the virtual hard drive will be located. Pass /// <c>null</c> or empty to default to <b>MACHINE-NAME.vhdx</b> located in the default /// Hyper-V virtual machine drive folder. /// </param> /// <param name="checkpointDrives">Optionally enables drive checkpoints. This defaults to <c>false</c>.</param> /// <param name="templateDrivePath"> /// If this is specified and <paramref name="drivePath"/> is not <c>null</c> then /// the hard drive template at <paramref name="templateDrivePath"/> will be copied /// to <paramref name="drivePath"/> before creating the machine. /// </param> /// <param name="switchName">Optional name of the virtual switch.</param> /// <param name="extraDrives"> /// Optionally specifies any additional virtual drives to be created and /// then attached to the new virtual machine. /// </param> /// <remarks> /// <note> /// The <see cref="VirtualDrive.Path"/> property of <paramref name="extraDrives"/> may be /// passed as <c>null</c> or empty. In this case, the drive name will default to /// being located in the standard Hyper-V virtual drivers folder and will be named /// <b>MACHINE-NAME-#.vhdx</b>, where <b>#</b> is the one-based index of the drive /// in the enumeration. /// </note> /// </remarks> public void AddVm( string machineName, string memorySize = "2GiB", int processorCount = 4, string driveSize = null, string drivePath = null, bool checkpointDrives = false, string templateDrivePath = null, string switchName = null, IEnumerable <VirtualDrive> extraDrives = null) { Covenant.Requires <ArgumentNullException>(!string.IsNullOrEmpty(machineName), nameof(machineName)); CheckDisposed(); memorySize = ByteUnits.Parse(memorySize).ToString(); if (driveSize != null) { driveSize = ByteUnits.Parse(driveSize).ToString(); } var driveFolder = DefaultDriveFolder; if (string.IsNullOrEmpty(drivePath)) { drivePath = Path.Combine(driveFolder, $"{machineName}-[0].vhdx"); } else { driveFolder = Path.GetDirectoryName(Path.GetFullPath(drivePath)); } if (VmExists(machineName)) { throw new HyperVException($"Virtual machine [{machineName}] already exists."); } // Copy the template VHDX file. if (templateDrivePath != null) { File.Copy(templateDrivePath, drivePath); } // Resize the VHDX if requested. if (driveSize != null) { powershell.Execute($"{HyperVNamespace}Resize-VHD -Path '{drivePath}' -SizeBytes {driveSize}"); } // Create the virtual machine. var command = $"{HyperVNamespace}New-VM -Name '{machineName}' -MemoryStartupBytes {memorySize} -Generation 1"; if (!string.IsNullOrEmpty(drivePath)) { command += $" -VHDPath '{drivePath}'"; } if (!string.IsNullOrEmpty(switchName)) { command += $" -SwitchName '{switchName}'"; } try { powershell.Execute(command); } catch (Exception e) { throw new HyperVException(e.Message, e); } // We need to configure the VM's processor count and min/max memory settings. try { powershell.Execute($"{HyperVNamespace}Set-VM -Name '{machineName}' -ProcessorCount {processorCount} -StaticMemory -MemoryStartupBytes {memorySize}"); } catch (Exception e) { throw new HyperVException(e.Message, e); } // Create and attach any additional drives as required. if (extraDrives != null) { var diskNumber = 1; foreach (var drive in extraDrives) { if (string.IsNullOrEmpty(drive.Path)) { drive.Path = Path.Combine(driveFolder, $"{machineName}-[{diskNumber}].vhdx"); } if (drive.Size <= 0) { throw new ArgumentException("Virtual drive size must be greater than 0.", nameof(drive)); } NeonHelper.DeleteFile(drive.Path); var fixedOrDynamic = drive.IsDynamic ? "-Dynamic" : "-Fixed"; try { powershell.Execute($"{HyperVNamespace}New-VHD -Path '{drive.Path}' {fixedOrDynamic} -SizeBytes {drive.Size} -BlockSizeBytes 1MB"); powershell.Execute($"{HyperVNamespace}Add-VMHardDiskDrive -VMName '{machineName}' -Path \"{drive.Path}\""); } catch (Exception e) { throw new HyperVException(e.Message, e); } diskNumber++; } } // Windows 10 releases since the August 2017 Creators Update enable automatic // virtual drive checkpointing (which is annoying). We're going to disable this // by default. if (!checkpointDrives) { try { powershell.Execute($"{HyperVNamespace}Set-VM -CheckpointType Disabled -Name '{machineName}'"); } catch (Exception e) { throw new HyperVException(e.Message, e); } } // We need to do some extra configuration for nested virtual machines: // // https://docs.microsoft.com/en-us/virtualization/hyper-v-on-windows/user-guide/nested-virtualization if (IsNestedVirtualization) { // Enable nested virtualization for the VM. powershell.Execute($"{HyperVNamespace}Set-VMProcessor -VMName '{machineName}' -ExposeVirtualizationExtensions $true"); // Enable MAC address spoofing for the VMs network adapter. powershell.Execute($"{HyperVNamespace}Set-VMNetworkAdapter -VMName '{machineName}' -MacAddressSpoofing On"); } }
/// <summary> /// Verifies that the limit properties make sense. /// </summary> /// <returns><c>null</c> for valid properties, otherwise an error message.</returns> public string Validate() { decimal memory = -1; decimal memorySwap = -1; decimal memoryReservation = -1; decimal kernelMemory = -1; if (Memory != null) { try { memory = ByteUnits.Parse(Memory); if (memory < 0) { throw new Exception(); } } catch { return($"[{nameof(Memory)}={Memory}]: Value is not valid."); } } if (MemorySwap != null) { try { memorySwap = ByteUnits.Parse(MemorySwap); if (memorySwap < -1) { throw new Exception(); } } catch { return($"[{nameof(MemorySwap)}={MemorySwap}]: Value is not valid."); } if (memorySwap > 0 && memory == -1) { return($"[{nameof(Memory)}] must also be set when [{nameof(MemorySwap)}] is specified."); } if (memory != -1 && memorySwap <= memory) { return($"[{nameof(MemorySwap)}={MemorySwap}] must be greater than [{nameof(Memory)}={Memory}]."); } } if (MemorySwappiness != null) { if (MemorySwappiness.Value < 0 || MemorySwappiness.Value > 100) { return($"[{nameof(MemorySwappiness)}={MemorySwappiness}]: Value is not valid."); } } if (MemoryReservation != null) { try { memoryReservation = ByteUnits.Parse(MemoryReservation); if (memoryReservation < 0) { throw new Exception(); } } catch { return($"[{nameof(MemoryReservation)}={MemoryReservation}]: Value is not valid."); } } if (KernelMemory != null) { try { kernelMemory = ByteUnits.Parse(KernelMemory); if (memory < 4 * ByteUnits.MebiBytes) { return($"[{nameof(Memory)}={Memory}]: Value cannot be less than 4MiB."); } } catch { return($"[{nameof(KernelMemory)}={KernelMemory}]: Value is not valid."); } if (kernelMemory < 4 * ByteUnits.MebiBytes) { return($"[{nameof(KernelMemory)}={KernelMemory}]: Value cannot be less than 4MiB."); } } if (memory != -1 && memoryReservation != -1) { if (memoryReservation >= memory) { return($"[{nameof(MemoryReservation)}={MemoryReservation}] must be less than [{nameof(Memory)}={Memory}]."); } } if (OomKillDisable && memory == -1) { return($"[{nameof(OomKillDisable)}={OomKillDisable}] is not allowed when [{nameof(Memory)}] is not set."); } return(null); }