/// <summary>
        /// Creates a Hyper-V virtual machine for a cluster node.
        /// </summary>
        /// <param name="node">The target node.</param>
        private void ProvisionVM(SshProxy <NodeDefinition> node)
        {
            // $todo(jeff.lill):
            //
            // This code currently assumes that the VM will use DHCP to obtain
            // its initial network configuration so the code can SSH into the
            // node to configure a static IP.
            //
            // It appears that it is possible to inject an IP address, but
            // I wasn't able to get this to work (perhaps Windows Server is
            // required).  Here's a link discussing this:
            //
            //  http://www.itprotoday.com/virtualization/modify-ip-configuration-vm-hyper-v-host
            //
            // An alternative technique might be to update [/etc/network/interfaces]
            // remotely via PowerShell as described here:
            //
            //  https://www.altaro.com/hyper-v/transfer-files-linux-hyper-v-guest/

            using (var hyperv = new HyperVClient())
            {
                var vmName = GetVmName(node.Metadata);

                // Copy the VHDX template file to the virtual machine's
                // virtual hard drive file.

                var driveTemplateInfoPath = driveTemplatePath + ".info";
                var driveTemplateInfo     = NeonHelper.JsonDeserialize <DriveTemplateInfo>(File.ReadAllText(driveTemplateInfoPath));
                var drivePath             = Path.Combine(vmDriveFolder, $"{vmName}.vhdx");

                node.Status = $"create: disk";

                // $hack(jeff.lill): Update console at 2 sec intervals to mitigate annoying flicker

                var updateInterval = TimeSpan.FromSeconds(2);
                var stopwatch      = new Stopwatch();

                stopwatch.Start();

                using (var input = new FileStream(driveTemplatePath, FileMode.Open, FileAccess.Read))
                {
                    if (driveTemplateInfo.Compressed)
                    {
                        using (var output = new FileStream(drivePath, FileMode.Create, FileAccess.ReadWrite))
                        {
                            using (var decompressor = new GZipStream(input, CompressionMode.Decompress))
                            {
                                var  buffer = new byte[64 * 1024];
                                long cbRead = 0;
                                int  cb;

                                while (true)
                                {
                                    cb = decompressor.Read(buffer, 0, buffer.Length);

                                    if (cb == 0)
                                    {
                                        break;
                                    }

                                    output.Write(buffer, 0, cb);

                                    cbRead += cb;

                                    var percentComplete = (int)(((double)output.Length / (double)cbRead) * 100.0);

                                    if (stopwatch.Elapsed >= updateInterval || percentComplete >= 100.0)
                                    {
                                        node.Status = $"create: disk [{percentComplete}%]";
                                        stopwatch.Restart();
                                    }
                                }
                            }
                        }
                    }
                    else
                    {
                        using (var output = new FileStream(drivePath, FileMode.Create, FileAccess.ReadWrite))
                        {
                            var buffer = new byte[64 * 1024];
                            int cb;

                            while (true)
                            {
                                cb = input.Read(buffer, 0, buffer.Length);

                                if (cb == 0)
                                {
                                    break;
                                }

                                output.Write(buffer, 0, cb);

                                var percentComplete = (int)(((double)output.Length / (double)input.Length) * 100.0);

                                if (stopwatch.Elapsed >= updateInterval || percentComplete >= 100.0)
                                {
                                    node.Status = $"create: disk [{percentComplete}%]";
                                    stopwatch.Restart();
                                }
                            }
                        }
                    }
                }

                // Stop and delete the virtual machine if one exists.

                if (hyperv.VMExists(vmName))
                {
                    hyperv.StopVM(vmName);
                    hyperv.RemoveVM(vmName);
                }

                // Create the virtual machine if it doesn't already exist.

                // We need to create a raw drive if the node hosts a Ceph OSD.

                var extraDrives = new List <VirtualDrive>();

                if (node.Metadata.Labels.CephOSD)
                {
                    extraDrives.Add(
                        new VirtualDrive()
                    {
                        IsDynamic = true,
                        Size      = node.Metadata.GetCephOSDDriveSize(cluster.Definition),
                        Path      = Path.Combine(vmDriveFolder, $"{vmName}-[1].vhdx")
                    });
                }

                var processors  = node.Metadata.GetVmProcessors(cluster.Definition);
                var memoryBytes = node.Metadata.GetVmMemory(cluster.Definition);
                var diskBytes   = node.Metadata.GetVmDisk(cluster.Definition);

                node.Status = $"create: virtual machine";
                hyperv.AddVM(
                    vmName,
                    processorCount: processors,
                    diskSize:       diskBytes.ToString(),
                    memorySize:     memoryBytes.ToString(),
                    drivePath:      drivePath,
                    switchName:     switchName,
                    extraDrives:    extraDrives);

                node.Status = $"start: virtual machine";

                hyperv.StartVM(vmName);

                // Retrieve the virtual machine's network adapters (there should only be one)
                // to obtain the IP address we'll use to SSH into the machine and configure
                // it's static IP.

                node.Status = $"discover: ip address";

                var adapters = hyperv.ListVMNetworkAdapters(vmName, waitForAddresses: true);
                var adapter  = adapters.FirstOrDefault();
                var address  = adapter.Addresses.First();

                if (adapter == null)
                {
                    throw new HyperVException($"Virtual machine [{vmName}] has no network adapters.");
                }

                // We're going to temporarily set the node to the current VM address
                // so we can connect via SSH.

                var nodePrivateAddress = node.PrivateAddress;

                try
                {
                    node.PrivateAddress = address;

                    using (var nodeProxy = cluster.GetNode(node.Name))
                    {
                        node.Status = $"connecting...";
                        nodeProxy.WaitForBoot();

                        // We need to ensure that the host folders exist.

                        nodeProxy.CreateHostFolders();

                        // Configure the node's network stack to the static IP address
                        // and upstream nameservers.

                        node.Status = $"config: network [IP={node.PrivateAddress}]";

                        var primaryInterface = node.GetNetworkInterface(address);

                        node.ConfigureNetwork(
                            networkInterface:   primaryInterface,
                            address:            nodePrivateAddress,
                            gateway:            IPAddress.Parse(cluster.Definition.Network.Gateway),
                            subnet:             NetworkCidr.Parse(cluster.Definition.Network.PremiseSubnet),
                            nameservers:        cluster.Definition.Network.Nameservers.Select(ns => IPAddress.Parse(ns)));

                        // Extend the primary partition and file system to fill
                        // the virtual the drive.

                        node.Status = $"resize: primary drive";

                        // $hack(jeff.lill):
                        //
                        // I've seen a transient error here but can't reproduce it.  I'm going
                        // to assume for now that the file system might not be quite ready for
                        // this operation directly after the VM has been rebooted, so we're going
                        // to delay for a few seconds before performing the operations.

                        Thread.Sleep(TimeSpan.FromSeconds(5));
                        nodeProxy.SudoCommand("growpart /dev/sda 2");
                        nodeProxy.SudoCommand("resize2fs /dev/sda2");

                        // Reboot to pick up the changes.

                        node.Status = $"restarting...";
                        nodeProxy.Reboot(wait: false);
                    }
                }
                finally
                {
                    // Restore the node's IP address.

                    node.PrivateAddress = nodePrivateAddress;
                }
            }
        }