/// <summary> /// Performs any required Hyper-V initialization before host nodes can be provisioned. /// </summary> private void PrepareHyperV() { // Determine where we're going to place the VM hard drive files and // ensure that the directory exists. if (!string.IsNullOrEmpty(hive.Definition.Hosting.VmDriveFolder)) { vmDriveFolder = hive.Definition.Hosting.VmDriveFolder; } else { vmDriveFolder = HyperVClient.DefaultDriveFolder; } Directory.CreateDirectory(vmDriveFolder); // Download the zipped VHDX template if it's not already present or has // changed. Note that we're going to name the file the same as the file // name from the URI and we're also going to persist the ETAG and file // length in file with the same name with a [.info] extension. // // Note that I'm not actually going check for ETAG changes to update // the download file. The reason for this is that I want to avoid the // situation where the user has provisioned some nodes with one version // of the template and then goes on later to provision new nodes with // an updated template. The [neon hive setup --remove-templates] // option is provided to delete any cached templates. // // This should only be an issue for people using the default "latest" // drive template. Production hives should reference a specific // drive template. var driveTemplateUri = new Uri(hive.Definition.Hosting.LocalHyperV.HostVhdxUri); var driveTemplateName = driveTemplateUri.Segments.Last(); driveTemplatePath = Path.Combine(HiveHelper.GetVmTemplatesFolder(), driveTemplateName); var driveTemplateInfoPath = Path.Combine(HiveHelper.GetVmTemplatesFolder(), driveTemplateName + ".info"); var driveTemplateIsCurrent = true; var driveTemplateInfo = (DriveTemplateInfo)null; if (!File.Exists(driveTemplatePath) || !File.Exists(driveTemplateInfoPath)) { driveTemplateIsCurrent = false; } else { try { driveTemplateInfo = NeonHelper.JsonDeserialize <DriveTemplateInfo>(File.ReadAllText(driveTemplateInfoPath)); if (new FileInfo(driveTemplatePath).Length != driveTemplateInfo.Length) { driveTemplateIsCurrent = false; } } catch { // The [*.info] file must be corrupt. driveTemplateIsCurrent = false; } } if (!driveTemplateIsCurrent) { controller.SetOperationStatus($"Download Template VHDX: [{hive.Definition.Hosting.LocalHyperV.HostVhdxUri}]"); Task.Run( async() => { using (var client = new HttpClient()) { // Download the file. var response = await client.GetAsync(hive.Definition.Hosting.LocalHyperV.HostVhdxUri, HttpCompletionOption.ResponseHeadersRead); response.EnsureSuccessStatusCode(); var contentLength = response.Content.Headers.ContentLength; try { using (var fileStream = new FileStream(driveTemplatePath, FileMode.Create, FileAccess.ReadWrite)) { using (var downloadStream = await response.Content.ReadAsStreamAsync()) { var buffer = new byte[64 * 1024]; int cb; while (true) { cb = await downloadStream.ReadAsync(buffer, 0, buffer.Length); if (cb == 0) { break; } await fileStream.WriteAsync(buffer, 0, cb); if (contentLength.HasValue) { var percentComplete = (int)(((double)fileStream.Length / (double)contentLength) * 100.0); controller.SetOperationStatus($"Downloading VHDX: [{percentComplete}%] [{hive.Definition.Hosting.LocalHyperV.HostVhdxUri}]"); } else { controller.SetOperationStatus($"Downloading VHDX: [{fileStream.Length} bytes] [{hive.Definition.Hosting.LocalHyperV.HostVhdxUri}]"); } } } } } catch { // Ensure that the template and info files are deleted if there were any // errors, to help avoid using a corrupted template. if (File.Exists(driveTemplatePath)) { File.Decrypt(driveTemplatePath); } if (File.Exists(driveTemplateInfoPath)) { File.Decrypt(driveTemplateInfoPath); } throw; } // Generate the [*.info] file. var templateInfo = new DriveTemplateInfo(); templateInfo.Length = new FileInfo(driveTemplatePath).Length; if (response.Headers.TryGetValues("ETag", out var etags)) { // Note that ETags look like they're surrounded by double // quotes. We're going to strip these out if present. templateInfo.ETag = etags.SingleOrDefault().Replace("\"", string.Empty); } File.WriteAllText(driveTemplateInfoPath, NeonHelper.JsonSerialize(templateInfo, Formatting.Indented)); } }).Wait(); controller.SetOperationStatus(); } // Handle any necessary Hyper-V initialization. using (var hyperv = new HyperVClient()) { // We're going to create the [neonHIVE] external switch if there // isn't already an external switch. controller.SetOperationStatus("Scanning network adapters"); var switches = hyperv.ListVMSwitches(); var externalSwitch = switches.FirstOrDefault(s => s.Type == VirtualSwitchType.External); if (externalSwitch == null) { hyperv.NewVMExternalSwitch(switchName = defaultSwitchName, IPAddress.Parse(hive.Definition.Network.Gateway)); } else { switchName = externalSwitch.Name; } // Ensure that the hive virtual machines exist and are stopped, // taking care to issue a warning if any machines already exist // and we're not doing [force] mode. controller.SetOperationStatus("Scanning virtual machines"); var existingMachines = hyperv.ListVMs(); var conflicts = string.Empty; controller.SetOperationStatus("Stopping virtual machines"); foreach (var machine in existingMachines) { var nodeName = ExtractNodeName(machine.Name); var drivePath = Path.Combine(vmDriveFolder, $"{machine.Name}.vhdx"); var isClusterVM = hive.FindNode(nodeName) != null; if (isClusterVM) { if (forceVmOverwrite) { if (machine.State != VirtualMachineState.Off) { hive.GetNode(nodeName).Status = "stop virtual machine"; hyperv.StopVM(machine.Name); hive.GetNode(nodeName).Status = string.Empty; } // The named machine already exists. For force mode, we're going to stop and // reuse the machine but replace the hard drive file as long as the file name // matches what we're expecting for the machine. We'll delete the VM if // the names don't match and recreate it below. // // The reason for doing this is to avoid generating new MAC addresses // every time we reprovision a VM. This could help prevent the router/DHCP // server from running out of IP addresses for the subnet. var drives = hyperv.GetVMDrives(machine.Name); if (drives.Count != 1 || !drives.First().Equals(drivePath, StringComparison.InvariantCultureIgnoreCase)) { // Remove the machine and recreate it below. hive.GetNode(nodeName).Status = "delete virtual machine"; hyperv.RemoveVM(machine.Name); hive.GetNode(nodeName).Status = string.Empty; } else { continue; } } else { // We're going to report errors when one or more machines already exist and // [--force] was not specified. if (conflicts.Length > 0) { conflicts += ", "; } conflicts += nodeName; } } } if (!string.IsNullOrEmpty(conflicts)) { throw new HyperVException($"[{conflicts}] virtual machine(s) already exist and cannot be automatically replaced unless you specify [--force]."); } controller.SetOperationStatus(); } }