PowerShell Script: Deploy VMs, and Configure the Guest OS in one Go on Hyper-V

I’ve been prepping for a lot of different speaking engagements coming up in the next few months and a very hot topic these days is the use of PowerShell and automation, when it comes to Hyper-V. With this in mind, I’ve prepped the below script for some of these upcoming discussions, and wanted to share it with the community so that it’ll be of help to some people.

The Story

Awhile back, I spoke at a VMware VMUG event where I talked about advanced VM provisioning using PowerCLI and templates. In that talk, I taught the group how to provision a virtual machine using the shell, and then reach into the VM and execute a number of commands against the guest OS to achieve even greater amounts of automation and followed up by posting the script HERE.

Well, I wanted to take the goal from that script and apply it to a Hyper-V environment using PowerShell Direct. For those that aren’t aware, PowerShell Direct is a new feature in the Windows Server 2016 feature set, that allows us to inject PowerShell commands into the guest OS of a running VM, without the network for networking. Without getting too into the specifics, it does this via the VMbus on the Hyper-V host, and it works VERY well.

NOTE: You must have either Windows Server 2016 TP5 or newer and/or Windows 10 running as BOTH the host and the guest in order for PowerShell Direct to work.

Let’s cover the workflow and the goal of this script first.

Technical Goals of the Script

  1. Must be FULLY automated from the moment you hit the enter key to start the script.
  2. Must provide verbose information for the administrators viewing if desired.
  3. Must Deploy 2 VMs from a pre-existing Sysprepped VHDX with an embedded unattend.xml answer file.
  4. Must be able to define static IP addresses.
  5. Must configure AD on one of the new VMs and provision a new AD Forest.
  6. Must create a new custom administrative account within the domain.
  7. 2nd VM must be automatically joined to the newly defined domain.
  8. File Services must be installed on the second VM
  9. A new SMB share must be defined on the file server VM, once the file services role is present.

This is certainly a lot to get done with a single press of the enter key, but it is doable. Also, this is likely just the beginnings of a new environment, but the script could very easily be modified to deploy more than just the two VMs.

Community Goals of the Script

Just like the PowerCLI/VMware script, My communities goals with THIS script remain the same. My goal from a teaching aspect with this script was to be the follow:

  1. Well Documented and Heavily Commented
  2. VERY sequential ordering of the commands, as to be easy to follow for beginners
  3. Best Practices definition of variables at the top of the script.
  4. Well segmented so that certain sections can be copied and duplicated if desired

The Secret Sauce

So what are the important bits that make this script work? With every script there are key points that really stand out, and I always like to point them out.

Pre-Prepared VHDX – While not strictly script related, this certainly makes the job much easier. The script assumes you have a VHDX file that already has Server 2016 installed and patched. It also assumes that you’ve sysprepped that image, and you’ve used the Windows ADK toolset to create an answer file. This takes care of the bulk of the OS customization upon deployment of the new VMs.

While Loop – If you look at the script, I have a small chunk of code in there that is from Ben Armstrong on the Hyper-V product group team. It essentially attempts to write a string of text to the console of the guest VM using PowerShell Direct, until it’s successful. What this allows you to do in your script execution is have it wait until PowerShell direct is functional before moving on. Very useful for sequential operations.

Invoke-Command –  While not a new ground breaking cmdlet, this script wouldn’t be possible without it. The invoke-command cmdlet is the one cmdlet in this script that is doing the bulk of the work. You’ll see it used with the -VMName  parameter extensively throughout the script. When used with the -VMName parameter, it lets PowerShell know that it’s running the mentioned script block with PowerShell direct and NOT standard PowerShell Remoting. See the script for examples.

The Script

So here is the part you’ve been waiting for. Below is the script. Take it! Use it. Learn from it! Hopefully it will be of some use!

Again the script is heavily self-documented and commented, so you should be able to read through it and set the needed variables as needed. Note, all of the user-definable variables are at the top of the script.

# DISCLAIMER: There are no warranties or support provided for this script. Use at you're own discretion. Andy Syrewicze and/or Altaro Software are not liable for any
# damage or problems that misuse of this script may cause.

# Script is Written by Andy Syrewicze - Tech. Evangelist with Altaro Software and is free to use as needed within your organization.

# This Script is a deployment script, it will first copy a pre-prepared VHDX, and create a new VM with it based on the settings below. Then the script deploys Active
# Directory to then VM and then provisions a new forest. Once complete, another new VM is created in the same manner and it is provisioned as a file server and a new
# Share is created. 

# Assumptions
# Your Hyper-V host is running Windows Server 2016 TP5, Windows 10 or newer
# Your Target Guest OS is running Windows Server 2016 TP5, Windows 10 or newer
# You have a pre-created "Golden Image" VHDX that has been syspreped with the /generalize /oobe and /mode:VM switches.
# You have created an answer file using the Windows SIM (System Image Manager) which is part of the Windows 10 ADK 
# https://developer.microsoft.com/en-us/windows/hardware/windows-assessment-deployment-kit#winADK 
# You have placed your answer file inside of the VHDX by mounting it in Windows 10 and then placing the unattend.xml file in the path
# C:\Windows\Panther

# First we set some targeting variables for the Host environment
# The $vSwitch Variable is the Name of the vSwitch you'd like the new VMs to use for network connectivity
# $VHDXPath is the root path of your VHDX storage 
$vSwitch = "Deployment Test"
$VHDXPath = "C:\users\Public\Documents\Hyper-V\Virtual hard disks"

# Global IP Settings Below
# NOTE: The cmdlets below reference the subnet mask bit for configuration of the IP Settings. Currently this script is configured for a standard
# Class C /24 subnet. 
$SubMaskBit = "24"

# Domain Controller Settings Below
# NOTE: As of the time of this writing, with 2016 TP5, the Domain Mode and Forest Mode are called "WinThreshold" I Expect this to change
# Once GA for 2016 is released. It will likely follow the previous naming scheme and be something like "win2016"
$DCVMName = "TEST-DC01"
$DCIP = "10.0.50.15"
$DomainMode = "WinThreshold";
$ForestMode = "WinThreshold";
$DomainName = "TestDomain.lcl";
$DSRMPWord = ConvertTo-SecureString -String "Password01" -AsPlainText -Force
$NewAdminUserName = "TestAdmin"
$NewAdminUserPWord = ConvertTo-SecureString -String "Password01" -AsPlainText -Force

# File Server Settings Below
$FSVMName = "TEST-FS01"
$FSIP = "10.0.50.30"
$SharePath = "C:\ShareTest"
$FolderName = "Public"
$ShareName = "Public"

# Then we setup some credentials to be called throughout the script
# NOTE: These are the credentials used within the guest VM.
# NOTE: If you only have a single image, you can likely get by with a single set of local credentials, instead of one for each
# workload.
$DCLocalUser = "$DCVMName\Administrator"
$DCLocalPWord = ConvertTo-SecureString -String "Password01" -AsPlainText -Force
$DCLocalCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $DCLocalUser, $DCLocalPWord

# Below Credentials are used by the File Server VM for first login to be able to add the machine to the new Domain.
$FSLocalUser = "$FSVMName\administrator"
$FSLocalPWord = ConvertTo-SecureString -String "Password01" -AsPlainText -Force
$FSLocalCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $FSLocalUser, $FSLocalPWord

# The below credentials are used by operations below once the domain controller virtual machine and the new domain are in place. These credentials should match the credentials
# used during the provisioning of the new domain. 
$DomainUser = "$DomainName\administrator"
$DomainPWord = ConvertTo-SecureString -String "Password01" -AsPlainText -Force
$DomainCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $DomainUser, $DomainPWord 

############################# Command Execution Starts Below ###################################


# The below section Provisions and Configures the Domain Controller with the Variables defined above

# First we make a copy of the sysprepped "Gold Image" VHDX file. Also, note that a Unattend.XML file has been placed within the image as well.
Write-Verbose "Copying Master VHDX and Deploying new VM with name [$DCVMName]" -Verbose 
Copy-Item "$VHDXPath\MASTER.vhdx" "$VHDXPath\$DCVMNAME.vhdx"
Write-Verbose "VHDX Copied, Building VM...." -Verbose
New-VM -Name $DCVMName -MemoryStartupBytes 1GB -VHDPath "$VHDXPath\$DCVMName.vhdx" -Generation 2 -SwitchName $vSwitch
Write-Verbose "VM Creation Completed. Starting VM [$DCVMName]" -Verbose
Start-VM -Name $DCVMName

# After the inital provisioning, we wait until PowerShell Direct is functional and working within the guest VM before moving on.
# Big thanks to Ben Armstrong for the below useful Wait code 
Write-Verbose “Waiting for PowerShell Direct to start on VM [$DCVMName]” -Verbose
   while ((icm -VMName $DCVMName -Credential $DCLocalCredential {“Test”} -ea SilentlyContinue) -ne “Test”) {Sleep -Seconds 1}

Write-Verbose "PowerShell Direct responding on VM [$DCVMName]. Moving On...." -Verbose

# Next we configure the networking for the new DC VM. 
# NOTE: that the host variables are passed through by making use of the param command along with the -ArgumentList Paramater at the end of
#       the ScriptBlock.
# NOTE: The InterfaceAlias value may be different for your gold image, so adjust accordingly.
# NOTE: InterfaceAlias can be found by making use of the Get-NetIPAddress Cmdlet  
Invoke-Command -VMName $DCVMName -Credential $DCLocalCredential -ScriptBlock {
    param ($DCVMName, $DCIP, $SubMaskBit, $DFGW)
    New-NetIPAddress -IPAddress "$DCIP" -InterfaceAlias "Ethernet 2" -PrefixLength "$SubMaskBit" | Out-Null
    $DCEffectiveIP = Get-NetIPAddress -InterfaceAlias "Ethernet 2" | Select-Object IPAddress
    Write-Verbose "Assigned IPv4 and IPv6 IPs for VM [$DCVMName] are as follows" -Verbose 
    Write-Host $DCEffectiveIP | Format-List
    Write-Verbose "Updating Hostname for VM [$DCVMName]" -Verbose
    Rename-Computer -NewName "$DCVMName"
    } -ArgumentList $DCVMName, $DCIP, $SubMaskBit, $DFGW

Write-Verbose "Rebooting VM [$DCVMName] for hostname change to take effect" -Verbose
Stop-VM -Name $DCVMName
Start-VM -Name $DCVMName

Write-Verbose “Waiting for PowerShell Direct to start on VM [$DCVMName]” -Verbose
   while ((icm -VMName $DCVMName -Credential $DomainCredential {“Test”} -ea SilentlyContinue) -ne “Test”) {Sleep -Seconds 1}

Write-Verbose "PowerShell Direct responding on VM [$DCVMName]. Moving On...." -Verbose

# Next we'll proceed by installing the Active Directory Role and then configuring the machine as a new DC in a new AD Forest
Invoke-Command -VMName $DCVMName -Credential $DCLocalCredential -ScriptBlock {
    param ($DCVMName, $DomainMode, $ForestMode, $DomainName, $DSRMPWord) 
    Write-Verbose "Installing Active Directory Services on VM [$DCVMName]" -Verbose
    Install-WindowsFeature -Name "AD-Domain-Services" -IncludeManagementTools
    Write-Verbose "Configuring New Domain with Name [$DomainName] on VM [$DCVMName]" -Verbose
    Install-ADDSForest -ForestMode $ForestMode -DomainMode $DomainMode -DomainName $DomainName -InstallDns -NoDNSonNetwork -SafeModeAdministratorPassword $DSRMPWord -Force -NoRebootOnCompletion
    } -ArgumentList $DCVMName, $DomainMode, $ForestMode, $DomainName, $DSRMPWord

Write-Verbose "Rebooting VM [$DCVMName] to complete installation of new AD Forest" -Verbose
Stop-VM -Name $DCVMName
Start-VM -Name $DCVMName

Write-Verbose “Waiting for PowerShell Direct to start on VM [$DCVMName]” -Verbose
   while ((icm -VMName $DCVMName -Credential $DomainCredential {“Test”} -ea SilentlyContinue) -ne “Test”) {Sleep -Seconds 1}

Write-Verbose "PowerShell Direct responding on VM [$DCVMName]. Moving On...." -Verbose

Write-Verbose "DC Provisioning Complete!!!!" -Verbose

# We're going to setup an AD Administrative user based on the variables above that will have access to the file share we create below
# Note that we do this in a loop as it will take some time for AD to be ready inside of the new DC VM. As such this command will execute
# Until it is successful, and as a result, we know that AD is ready for the rest of the script. 

Write-Verbose "Creating new Administrative User within Domain [$DomainName] That will have access to Share [$ShareName] on VM [$FSVMName]" -Verbose

Invoke-Command -VMName $DCVMName -Credential $DomainCredential -ScriptBlock {
    param ($NewAdminUserName, $NewAdminUserPWord)
    Write-Verbose "Waiting for AD Web Services to be in a running state" -Verbose
    $ADWebSvc = Get-Service ADWS | Select-Object *
    while($ADWebSvc.Status -ne 'Running')
            {
            Start-Sleep -Seconds 1
            }
    Do {
    Start-Sleep -Seconds 30
    Write-Verbose "Waiting for AD to be Ready for User Creation" -Verbose
    New-ADUser -Name "$NewAdminUserName" -AccountPassword $NewAdminUserPWord
    Enable-ADAccount -Identity "$NewAdminUserName"
    $ADReadyCheck = Get-ADUser -Identity $NewAdminUserName
    }
    Until ($ADReadyCheck.Enabled -eq "True")
    Add-ADGroupMember -Identity "Domain Admins" -Members "$NewAdminUserName"
    } -ArgumentList $NewAdminUserName, $NewAdminUserPWord

Write-Verbose "User [$NewAdminUserName] Created." -Verbose




# The below section is used to Provision a new file server VM, add it to the new domain, and configure a basic share.

# First we make a copy of the sysprepped "Gold Image" VHDX file. Also, note that a Unattend.XML file has been placed within the image as well.
Write-Verbose "Copying Master VHDX and Deploying new VM with name [$FSVMName]" -Verbose 
Copy-Item "$VHDXPath\MASTER.vhdx" "$VHDXPath\$FSVMNAME.vhdx"
Write-Verbose "VHDX Copied, Building VM...." -Verbose
New-VM -Name $FSVMName -MemoryStartupBytes 1GB -VHDPath "$VHDXPath\$FSVMName.vhdx" -Generation 2 -SwitchName $vSwitch
Write-Verbose "VM Creation Completed. Starting VM [$FSVMName]" -Verbose
Start-VM -Name $FSVMName
 
# After the inital provisioning, we wait until the PowerShell Direct is functional and working within the guest VM before moving on.
# Big thanks to Ben Armstrong for the below useful Wait code 
Write-Verbose “Waiting for PowerShell Direct to start on VM [$FSVMName]” -Verbose
   while ((icm -VMName $FSVMName -Credential $FSLocalCredential {“Test”} -ea SilentlyContinue) -ne “Test”) {Sleep -Seconds 1}

Write-Verbose "PowerShell Direct responding on VM [$FSVMName]. Moving On...." -Verbose


# Next we configure the networking for the new FS VM. 
# NOTE: that the host variables are passed through by makinguse of the param command along with the -ArgumentList Paramater at the end of
#       the ScriptBlock.
# NOTE: The InterfaceAlias value may be different for your gold image, so adjust accordingly.  
Invoke-Command -VMName $FSVMName -Credential $FSLocalCredential -ScriptBlock {
    param ($FSVMName, $FSIP, $SubMaskBit, $DFGW, $DCVMName, $DCIP)
    New-NetIPAddress -IPAddress "$FSIP" -InterfaceAlias "Ethernet 2" -PrefixLength "$SubMaskBit" | Out-Null
    $FSEffectiveIP = Get-NetIPAddress -InterfaceAlias "Ethernet 2" | Select-Object IPAddress
    Write-Verbose "Assigned IPv4 and IPv6 IPs for VM [$FSVMName] are as follows" -Verbose 
    Write-Host $FSEffectiveIP | Format-List
    Write-Verbose "Setting DNS Source to [$DCVMName] with IP [$DCIP]" -Verbose
    Set-DnsClientServerAddress -InterfaceAlias "Ethernet 2" -ServerAddresses "$DCIP"
    Write-Verbose "Updating Hostname for VM [$FSVMName]" -Verbose
    Rename-Computer -NewName "$FSVMName"
    } -ArgumentList $FSVMName, $FSIP, $SubMaskBit, $DFGW, $DCVMName, $DCIP

Write-Verbose "Rebooting VM [$FSVMName] for hostname change to take effect" -Verbose
Stop-VM -Name $FSVMName
Start-VM -Name $FSVMName

Write-Verbose “Waiting for PowerShell Direct to start on VM [$FSVMName]” -Verbose
   while ((icm -VMName $FSVMName -Credential $FSLocalCredential {“Test”} -ea SilentlyContinue) -ne “Test”) {Sleep -Seconds 1}

Write-Verbose "PowerShell Direct responding on VM [$FSVMName]. Moving On...." -Verbose

# The below Adds the File Server VM to the newly Created Domain. 

Write-Verbose "Adding VM [$FSVMName] to domain [$DomainName]" -Verbose

Invoke-Command -VMName $FSVMName -Credential $FSLocalCredential -ScriptBlock {
    param ($DomainName, $DomainCredential)
    Add-Computer -DomainName $DomainName -Credential $DomainCredential
    } -ArgumentList $DomainName, $DomainCredential

Write-Verbose "Initiating Reboot of VM [$FSVMName] to complete domain join to domain [$DomainName]" -Verbose
Stop-VM -Name $FSVMName
Start-VM -Name $FSVMName

Write-Verbose “Waiting for PowerShell Direct to start on VM [$FSVMName]” -Verbose
   while ((icm -VMName $FSVMName -Credential $DomainCredential {“Test”} -ea SilentlyContinue) -ne “Test”) {Sleep -Seconds 1}

Write-Verbose "PowerShell Direct responding on VM [$FSVMName]. Moving On...." -Verbose

# Now we install the File Server Role and Create the Share

Write-Verbose "Installing File-Server Role on VM [$FSVMName]." -Verbose

Invoke-Command -VMName $FSVMName -Credential $DomainCredential -ScriptBlock {
    param ($SharePath, $FolderName, $ShareName, $DomainName, $NewAdminUserName)
    Install-WindowsFeature -Name "FS-FileServer" -IncludeManagementTools
    Write-Verbose "Creating File Share [$ShareName] at path [$SharePath\$Foldername]." -Verbose
    New-Item -Path $SharePath -Name $FolderName -ItemType "Directory";
    New-SmbShare -Name "$ShareName" -Path "$SharePath\$FolderName" -FullAccess "$DomainName\$NewAdminUserName"
    } -ArgumentList $SharePath, $FolderName, $ShareName, $DomainName, $NewAdminUserName



Write-Verbose "Environment Setup Complete. End of Script" -Verbose

# END OF SCRIPT

Summary

There you have it! It is my hope that this will be of some use to you, whether that be helping you get a task done at your job, or helping you learn how to use PowerShell and automate.

If you have questions, feel free to use the comment form below, and I’m looking forward to seeing if this script was useful to you.

Enjoy!

 

Threat Monitor
Share this post

Not a DOJO Member yet?

Join thousands of other IT pros and receive a weekly roundup email with the latest content & updates!

46 thoughts on "PowerShell Script: Deploy VMs, and Configure the Guest OS in one Go on Hyper-V"

  • Gustavo Maia says:

    Hi!

    Great article! Could you provide the answer file used on this deployment?

    Thanks!

    • Glad you liked it Gustavo!

      My answer file for this was pretty vanilla. It really only contained licensing, hostname customization, regional, and data/time related stuff. While it is a bit dated, reading the following will give you a fairly good idea of the process to making your own. Once complete you can mount you master VHDX and place the unattend.xml into the C:WindowsPanther folder in your master image.

      Hope this helps!

  • Mark Allison says:

    Nice script! How do you slipstream in the answer file? I don’t have SIM or the ADK, is there a way to do it without those?

    • I did indeed use SIM, which is contained within the ADK. They’re free tools, you just have to download them. Once I had the answer file prepped, including the licensing and randomized hostname settings, mount the prepared MASTER.vhdx in Windows, and then drop the unattend.xml file in C:WindowsPanther. Then you should be all set!

  • Trempest Humphries says:

    Hi Andy. This is really nice and impressive. I didn’t know powershell could automate so much! Yes I am new. In fact fresh to the workforce.
    I am hoping you could help me.
    Here’s what I would like to automate at my office.
    The goal is to create VMs on HyperV using a template created by my senior, sysprep it then add to domain.

    1. Prompt for VMname
    2. Create folder with VMname in D:Hyper-VVirtual Machines
    3. Copy template file from FileServer1template.vhdx to D:Hyper-VVirtual Hard Disks
    4. Rename copied template.vhdx to same name as folder’s
    5. Create new VM with 4096Mb memory, 2 CPU, attach virtual switch “vSwitch”, attach copied and renamed .vhdx file
    6. Notify that VM has been created and start VM
    7. Sysprep new VM
    8. Rename VM’s computer hostname to VMname
    9. Set local Administrator password to “Password101″
    10. Add to Windows Domain and reboot VM
    11. Move all VM files to D:Hyper-VVirtual Machines”VMname” (Please don’t ask me why. My senior wants it that way and he can’t tell me why either 🙁
    12. Display VMname and IP address when VM has started
    13. Prompt for user input to update Name section in HyperV VM’s settings

    • Hi Trempest!

      Glad you like the script, and indeed PowerShell can do a TON of automation. It just takes a little work up front to get it there!.

      What you propose should be easily doable. Many of the steps you’ve outlined I’ve done in the script. So as you look through the script I do many of the things you mention like numbers 3,4,5,6,8,10,12.

      Number 1 can easily by done by using the Read-Host Cmdlet. For example…
      $Hostname = Read-Host "Please Enter a name for the new VM"
      Then to verify the output
      Write-Verbose "The name of the VM is $Hostname"

      2 and 11 are simply file movement operations and shouldn’t be too difficult. The only thing you’d have to watch out for is if you changed any of the paths for the VM’s core files. In that case the Set-VM cmdlet should be able to help you out. If it were me, I would place them in the needed location to start with as to simplify things.

      number 7 and 9 shouldn’t be necessary as part of the script. You should be starting with a pre-sysprepped image that already contains your base settings as well as the local administrator password.

      number 13 shouldn’t be needed as the VM can be created by using the New-VM cmdlet and calling the $hostname variable that gets set in step 1. From there the VM’s name should already be correct upon deployment.

      Hope this helps!

Leave a comment or ask a question

Your email address will not be published. Required fields are marked *

Your email address will not be published. Required fields are marked *

Notify me of follow-up replies via email

Yes, I would like to receive new blog posts by email

What is the color of grass?

Please note: If you’re not already a member on the Dojo Forums you will create a new account and receive an activation email.