It's about time I automated the creation of VM's in Hyper-V to mount the ISO's and start the VM's if necessary. I'm building a bunch and tearing them down and clicking is annoying.

I keep building up new VM's and the most annoying part, I keep forgetting to set the VLAN Id. So, I'm automating it. I should have done this a while ago. I'm going to use PowerShell.

It's actually really easy to do. Lets first get some things out of the way, like disk space, memory, switch name, the VLAN Id and the name. I only have one external switch, which is the one I want to use, so I use PowerShell to get that name, so I don't have to put it in manually.

$type = "Linux"
$gigs = (1024*1024*1024)
$memory = $gigs*4
$computerName = "."
$switches = $switches = Get-VMSwitch -SwitchType External -ComputerName $ComputerName
$switch = $switches[0].Name
$vlan = 128
$vhdSize = $gigs*127
$vmname = "Test " + [System.DateTime]::Now.ToString("yyyyMMddHHmmss")

I wanted to keep it simple and use the location configured as default in Hyper-V. My location is D:\Virtual Machines\Hard Drives. But whatever, it doesn't matter because we pull it through PowerShell.

$vmhost = Get-VMHost -ComputerName $ComputerName
$vhdPath = [System.IO.Path]::Combine($vmhost.VirtualHardDiskPath, "${vmname}.vhdx")

Easy, now we have the path to the virtual hard disk file that will be created. Next, create the virtual machine with the values from above.

New-VM -Name $vmname `
       -MemoryStartupBytes $memory `
       -BootDevice VHD `
       -SwitchName $switch `
       -NewVHDSizeBytes $vhdSize `
       -Generation 2 `
       -NewVHDPath $vhdPath `
       -ComputerName $ComputerName

Now we set the VLAN if needed. If you don't need to set the VLAN, then you can skip this part and move on.

if ($vlan -ne 0) {
    Write-Information "Setting vlan"
    $nics = Get-VMNetworkAdapter -ComputerName $ComputerName -VMName $VMName
    foreach ($nic in $nics) {
        Set-VMNetworkAdapterVlan -VlanId $VLan -Access -ComputerName $ComputerName -VMNetworkAdapterName ($nic.Name) -VMName $VMName
    }
}

We need to make sure we have a DVD drive so we can mount an ISO to install an OS. If you're planning on installing over a network, you can skip this.

$dvdDrives = Get-VMDvdDrive -ComputerName $ComputerName -VMName $VMName

if ($null -eq $dvdDrive -or
    $dvdDrive.Length -eq 0) {
    Write-Information "Adding a dvd drive for installation"
    Add-VMDvdDrive -ComputerName $ComputerName -VMName $VMName
    $dvdDrives = Get-VMDvdDrive -ComputerName $ComputerName -VMName $VMName
}
else {
    Write-Information "Dvd drive already existed"
}

$dvdDrive = $dvdDrives[0]

Next I add the DVD to the boot order, and since I hate how slow the network boot is in Hyper-V, I removed it.

Write-Information "Setting boot order to hard drive, dvd and removing the NIC from the boot order"
$hardDrives = Get-VMHardDiskDrive -ComputerName $ComputerName -VMName $VMName
$primaryDrive = $hardDrives[0]

Set-VMFirmware -BootOrder @($primaryDrive, $dvdDrive) -ComputerName $ComputerName -VMName $VMName

Linux doesn't work very well with secure boot enabled, you'll notice in the top section of this post I have a $type = "Linux". That is used in this if statement to determine whether I need to turn off the secure boot.

$startVm = $false
if ($type -like "Debian") {
    Write-Information "Setting up for Debian 10.0.0"
    Set-VMFirmware -EnableSecureBoot Off -ComputerName $ComputerName -VMName $VMName
    # attach the correct iso
    Set-VMDvdDrive -Path "C:\ISOs\debian-10.0.0-amd64-netinst.iso" `
                   -ComputerName $ComputerName `
                   -VMName $VMName `
                   -ControllerNumber $dvdDrive.ControllerNumber `
                   -ControllerLocation $dvdDrive.ControllerLocation
    $startVm = $true
}
elseif ($type -like "Linux") {
    Write-Information "Setting up for unknown linux distribution"
    Set-VMFirmware -EnableSecureBoot Off -ComputerName $ComputerName -VMName $VMName
}
elseif ($type -like "Server2019") {
    Write-Information "Setting up for Server 2019"
    Set-VMDvdDrive -Path "C:\ISOs\Server2019.iso" `
                   -ComputerName $ComputerName `
                   -VMName $VMName `
                   -ControllerNumber $dvdDrive.ControllerNumber `
                   -ControllerLocation $dvdDrive.ControllerLocation
    $startVm = $true        
}
elseif ($type -like "Server2016") {
    Write-Information "Setting up for Server 2016"
    Set-VMDvdDrive -Path "C:\ISOs\Server2016.iso" `
                   -ComputerName $ComputerName `
                   -VMName $VMName `
                   -ControllerNumber $dvdDrive.ControllerNumber `
                   -ControllerLocation $dvdDrive.ControllerLocation
    $startVm = $true        
}
elseif ($type -like "Windows10") {
    Write-Information "Setting up for Windows 10"
    Set-VMDvdDrive -Path "C:\ISOs\Windows10.iso" `
                   -ComputerName $ComputerName `
                   -VMName $VMName `
                   -ControllerNumber $dvdDrive.ControllerNumber `
                   -ControllerLocation $dvdDrive.ControllerLocation
    $startVm = $true        
}
else {
    Write-Information "Unexpected OS Type, not doing anything"
}

And then to start the VM if I set the path to the ISO.

if ($startVm -eq $true) {
    Start-VM -ComputerName $ComputerName -VMName $VMName
}

Now I want it to be a command I can just type in, something like new-supercoolvm -computer xyz -blah blah blah. So, I turned it into a function and slapped it in my PowerShell profile script.

Here's my new function.

function New-SuperCoolVM {
  param(
    [Parameter(Mandatory)]
    [ValidateSet("Debian", "Linux", "Server2019", "Server2016", "Windows10", "Unknown")]
    [string] $Type,

    [Parameter(Mandatory)]
    [string] $VMName,

    [string] $ComputerName = ".",
    [int] $MemoryGB = 4,
    [int] $DiskGB = 127,
    [int] $VLan = 128
  )

  # make it so we can see the output
  $oldip = $InformationPreference
  $InformationPreference = "Continue"

  $gigs = (1024*1024*1024)
  $memory = $gigs*$memoryGb
  $vhdSize = $gigs*127

  # get the vm switch
  $switches = Get-VMSwitch -SwitchType External -ComputerName $ComputerName
  $switch = $switches[0].Name 

  # get the vhd path
  $vmhost = Get-VMHost -ComputerName $ComputerName
  $vhdPath = [System.IO.Path]::Combine($vmhost.VirtualHardDiskPath, "${vmname}.vhdx")

  Write-Information "Creating Virtual Machine ""${vmname}"" with ${memory} bytes of memory and ${vhdsize} bytes of drive space."

  New-VM -Name $vmname `
         -MemoryStartupBytes $memory `
         -BootDevice VHD `
         -SwitchName $switch `
         -NewVHDSizeBytes $vhdSize `
         -Generation 2 `
         -NewVHDPath $vhdPath `
         -ComputerName $ComputerName

  if ($vlan -ne 0) {
    Write-Information "Setting vlan"
    $nics = Get-VMNetworkAdapter -ComputerName $ComputerName -VMName $VMName
    foreach ($nic in $nics) {
      Set-VMNetworkAdapterVlan -VlanId $VLan -Access -ComputerName $ComputerName -VMNetworkAdapterName ($nic.Name) -VMName $VMName
    }
  }

  $dvdDrive = Get-VMDvdDrive -ComputerName $ComputerName -VMName $VMName

  if ($null -eq $dvdDrive -or
    $dvdDrive.Length -eq 0) {
    Write-Information "Adding a dvd drive for installation"
    Add-VMDvdDrive -ComputerName $ComputerName -VMName $VMName
    $dvdDrive = Get-VMDvdDrive -ComputerName $ComputerName -VMName $VMName
  }
  else {
    Write-Information "Dvd drive already existed"
  }

  $dvdDrive = $dvdDrive[0]

  Write-Information "Setting boot order to hard drive, dvd and removing the NIC from the boot order"
  $hardDrives = Get-VMHardDiskDrive -ComputerName $ComputerName -VMName $VMName
  $primaryDrive = $hardDrives[0]

  Set-VMFirmware -BootOrder @($primaryDrive, $dvdDrive) -ComputerName $ComputerName -VMName $VMName

  $startVm = $false
  if ($type -like "Debian") {
    Write-Information "Setting up for Debian 10.0.0"
    Set-VMFirmware -EnableSecureBoot Off -ComputerName $ComputerName -VMName $VMName
    # attach the correct iso
    Set-VMDvdDrive -Path "C:\ISOs\debian-10.0.0-amd64-netinst.iso" `
                   -ComputerName $ComputerName `
                   -VMName $VMName `
                   -ControllerNumber $dvdDrive.ControllerNumber `
                   -ControllerLocation $dvdDrive.ControllerLocation
    $startVm = $true
  }
  elseif ($type -like "Linux") {
    Write-Information "Setting up for unknown linux distribution"
    Set-VMFirmware -EnableSecureBoot Off -ComputerName $ComputerName -VMName $VMName
  }
  elseif ($type -like "Server2019") {
    Write-Information "Setting up for Server 2019"
    Set-VMDvdDrive -Path "C:\ISOs\Server2019.iso" `
                   -ComputerName $ComputerName `
                   -VMName $VMName `
                   -ControllerNumber $dvdDrive.ControllerNumber `
                   -ControllerLocation $dvdDrive.ControllerLocation
    $startVm = $true        
  }
  elseif ($type -like "Server2016") {
    Write-Information "Setting up for Server 2016"
    Set-VMDvdDrive -Path "C:\ISOs\Server2016.iso" `
                   -ComputerName $ComputerName `
                   -VMName $VMName `
                   -ControllerNumber $dvdDrive.ControllerNumber `
                   -ControllerLocation $dvdDrive.ControllerLocation
    $startVm = $true        
  }
  elseif ($type -like "Windows10") {
    Write-Information "Setting up for Windows 10"
    Set-VMDvdDrive -Path "C:\ISOs\Windows10.iso" `
                   -ComputerName $ComputerName `
                   -VMName $VMName `
                   -ControllerNumber $dvdDrive.ControllerNumber `
                   -ControllerLocation $dvdDrive.ControllerLocation
    $startVm = $true        
  }
  else {
    Write-Information "Unexpected OS Type, not doing anything"
  }

  if ($startVm -eq $true) {
      Start-VM -ComputerName $ComputerName -VMName $VMName
  }

  $InformationPreference = $oldip
}

I opened a new PowerShell (as Admin because I'm testing on my local system), typed in New-SuperCoolVM -Type Debian -VMName "test1" and poof, it created a new VM, mounted the Debian ISO, set the VLAN, and started the VM. To target my Hyper-V server, I used New-SuperCoolVM -Computer MYSERVERNAME -Type Debian -VMName "test1" and it created a new VM on my server, just like it did on my local system. Nice! This will be so much nicer going forward!