Windows Server

Building a Lab in Hyper-V with PowerShell, Part 3

Creating VMs with New-myVM.ps1 - Part 2

So, as I showed in the previous post, I've got my new VM built, but it's not really ready for use yet. For one thing, it needs a DVD attached and the boot order set, plus I want to add a second NIC, and change the number of processors assigned to it. First, setting up the memory, processors, static MAC address for the NIC and configuring the DVD if we're booting from DVD. (Which, I admit, I don't often do. Mostly I copy over a SysPrep'd VHDX file.)

To do this, I have a function, of course, called Set-myVMConfig, to do most of it, and a separate one that I use to configure the second NIC, Add-myNetAdapter

Function Set-myVMConfig {
   Write-Verbose "Setting Processor Count to 4 for $VMName"
   Set-VMProcessor      -VMName $VMName -Count 4
   Write-Verbose "Enabling Dynamic Memory"
   Set-VMMemory         -VMName $VMName -DynamicMemoryEnabled $True
   Write-Verbose "Assigning static MAC address of $MacAdd"
   Get-VMNetworkAdapter -VMName $VmName `
      | Set-VMNetworkAdapter -StaticMacAddress "$MacAdd"
   if ($DVD) {
      Write-Verbose "Building from DVD, so adding DVD drive, and configuring boot order"
      if (! $client) { 
         Add-VMDvdDrive -VMName $VmName
         Set-VMDvdDrive -VMName $VmName  -Path $ServerISO 
         $vmDVD     = Get-VMDvdDrive -VMName $VmName
         $vmDrive   = Get-VMHardDiskDrive -VMName $VmName 
         Set-VMFirmware -VMName $VmName  -FirstBootDevice $vmDVD 
         Set-VMFirmware -VMName $VmName -BootOrder $vmDVD,$vmDrive
      } else {
         Add-VMDvdDrive -VMName $VmName
         Set-VMDvdDrive -VMName $VmName -Path $ClientISO 
         $vmDVD     = Get-VMDvdDrive -VMName $VmName
         $vmDrive   = Get-VMHardDiskDrive -VMName $VmName 
         Set-VMFirmware -VMName $VmName   -FirstBootDevice $vmDVD 
         Set-VMFirmware -VMName $VmName   -BootOrder $vmDVD,$vmDrive
      }
   }
}

This sets the # of processors to 4, enables dynamic memory, sets a static MAC address on the first NIC, adds a DVD drive if appropriate, and sets the boot order to boot from the specified ISO file.

Almost done - now, all we need to do is add a second network adapter, and set it to a fixed MAC address as well.

Function Add-myNetAdapter {
   Write-Verbose "Adding second network adapter"
   Add-VmNetworkAdapter -VMName $VmName `
                        -SwitchName '199 Network' `
                        -StaticMacAddress "$199MacAdd" `
                        -Name '199 Ethernet'
}

Now, that we have all the functions, all we need to do is execute them, and that all happens with:

If (! ( Get-VM -Name $VmName -ErrorAction Continue 2>$NULL) ) {
   Test-SourcePath
   Test-Clean
   Copy-myVHD -wait
   Write-Verbose "VHD's copied if we were doing that, now creating the VM..."
   Create-myVM
   Write-Verbose "VM Created"
   $myVM = Get-VM -VMName $VMName
}
Set-myVMConfig
Add-myNetAdapter
$myVM | Format-List

And, since this whole thing has been broken up across a couple of posts, here's the whole script, including full Get-Help support.

New-myVM.ps1

<# 
.Synopsis
    Creates a new VM
.Description
    New-myVM and New-myClientVM make a new VM of Name $1 and MAC Address in the $MacBase 
    range of MAC addresses. If the command is run as New-myClientVM, then the -Client 
    parameter is assumed unless overridden at the command line. 
.Example 
   New-myVM -VMName Trey-DC-02 02
   Creates a new Server VM of name "Trey-DC-02" in the default MAC address range
   with the "02" as the final octet of MAC address. 
.Example 
   New-myVM -Name trey-client-22 -MacFinal 16 -DVD -Client $True
   Creates a new Client VM of name "trey-client-22" in the default MAC address range
   with 16 as the final octet of MAC address. The VM is installed from the default 
   Server 2016 DVD. 
.Example 
   New-myVM Trey-DC-02 02 -DVD
   Creates a new Server VM of name "Trey-DC-02" in the default MAC address range
   with the "02" as the final octet of MAC address. The VM is installed from the 
   default Server 2016 DVD. 
.Example
    New-myVM -VmName "Trey-WDS-11" -MacFinal "0B" -MacBase "00-15-5D-32-64-"
    Creates a new Server VM of name Trey-WDS-11 in a non-default MAC address range. 
.Example
   New-myClientVM -Name trey-client-01 
   Creates a new Windows 10 client VM, 'trey-client-01' using the default VHD, and 
   will prompt for the final 2 digits of the MAC address. 
.Example
   New-myVM -Name trey-client-01 -MACFinal 65 -Client $True -Source 'V:\Source' -Path 'Y:\'
   Creates a new Windows 10 client VM, 'trey-client-01' using the sysprep'd image at V:\Source, 
   and creating the VM at Y:\trey-client-01. The final two digits of the MAC address will 
   be 65. 
.Parameter VmName
   The name of the new VM
.Parameter MacFinal
   The last two digits in the MAC address of the VM
.Parameter MacBase
   The base MAC address for this VM. The default base is  "00-15-5D-32-10-"  
.Parameter DVD
   A switch that controls whether a DVD is added to the VM and used to mount an ISO for the 
   install. The default is to build the VM with no DVD drive. 
.Parameter Client
  A Boolean. When run as New-myVM, $Client defaults to False. If run as New-myClientVM, 
  the default is true. In either case, the command line parameter overrides the default. 
.Parameter Path
   The target path for the virtual machine. Default is to V:\. This is the base path, to 
   which the VMName is added to build the final path.
.Parameter Source
   The source path of the DVD or VHD used to build the virtual machine. Default is V:\Source. 
.Parameter vmSwitch
   The Hyper-V network switch to connect the VM to. New-myVM creates two network adapters. 
   One is connected to the 199 Network, and the second is controlled by the vmSwitch parameter. 
   The default is "Local-10", the internal lab switch. 
.Parameter 2012R2
   The 2012R2 switch specifies the use of the Server 2012 R2 image. 
.Inputs 
    [string]
    [string]
    [string]
    [switch]
    [Boolean]
    [string]
    [string]
    [string]
.Notes
    Author: Charlie Russel
 Copyright: 2017 by Charlie Russel
          : Permission to use is granted but attribution is appreciated
   ModHist: 1/1/2014 Initial
          : 1/31/2015 Mod for new parameter handling and comment header
          : 3/20/2015 Mod to use Sysprepped VHD and -10 MAC
          : 4/19/2015 Mod for verbose and running in a wrapper
          : 5/16/2016 Mod for new labhost 
          : 9/24/2016 Mod for New-myClientVM
          : 12/21/2016 Added additional parameters, updated help. (cpr)
          : 02/17/2017 Fixed problem with DVD and Gen2, updated help. (cpr)
#>

[CmdletBinding()]
Param ([Parameter(Mandatory = $True,Position = 0)][alias("Name")][string]$VmName,
       [Parameter(Mandatory = $True,Position = 1)][alias("Final")][string]$MacFinal,
       [Parameter(Mandatory = $False)][alias("Base")][string]$MacBase = "00-15-5D-32-0A-",
       [Parameter(Mandatory = $False)][string]$199MacBase = "00-15-5D-32-CE-",
       [Parameter(Mandatory=$False)][Switch]$DVD,
       [Parameter(Mandatory=$False)][Boolean]$Client=($myInvocation.myCommand.Name -match "Client"),
       [Parameter(Mandatory=$False)][alias("Target")][string]$Path = "V:",
       [Parameter(Mandatory=$False)][alias("VHDSource","DVDBase")][string]$Source = "V:\Source",
       [Parameter(Mandatory=$False)][alias("LocalSwitch","Network")][string]$vmSwitch = "Local-10",
       [Parameter(Mandatory=$False)][switch]$2012R2
       )

$MacAdd = $MacBase + $MacFinal
$199MacAdd = $199MacBase + $MacFinal
Write-Verbose "MacFinal is $MacFinal" 
Write-Verbose "MacAdd is $MacAdd on switch $vmSwitch" 
Write-Verbose "VMName is $VMName"
Write-Verbose "Client is $Client"
Write-Verbose "Path is $Path, Source is $Source, and 199 MAC address is $199MacBase + $MacFinal"
Write-Verbose "Sleeping for 5 seconds to give you a chance to exit..."
sleep 5

$VMBase     = "$Path\$VMName"
$VHDSource  = $Source
$DVDBase    = $Source
$VHDBase    = "$VMBase\Virtual Hard Disks"
$SysVHD     = "$VMBase\Virtual Hard Disks\$VmName-System.vhdx"
$MachineBase= "$VMBase\Virtual Machines"
$ServerISO  = "$DVDBase\en_windows_server_2016_x64_dvd_9718492.iso"
$ClientISO  = "$DVDBase\en_windows_10_enterprise_version_1607_updated_jan_2017_x64_dvd_9714415.iso"
$ClientVHD  = "$Source\Generalized-client.vhdx"
if ($2012R2) { 
   $ServerVHD = "$Source\Generalized-2012R2.vhdx"
} else {
   $ServerVHD  = "$Source\Generalized-System.vhdx"
}

Function Test-SourcePath () {
   if ($Client) {
      if ($dvd) {
         if (Test-Path $ClientISO) {
            Write-Verbose "Install DVD found at $ClientISO"
         } else {
            Throw "Client ISO not found at $ClientISO" 
         }
      } elseif (Test-Path $ClientVHD) { 
         Write-Verbose "Source VHD found at $ClientVHD"
      }
   } else {
      if ($dvd) {
         if (Test-Path $ServerISO) {
            Write-Verbose "Install DVD found at $ServerISO"
         } else {
            Throw "Server ISO not found at $ServerISO" 
         }
      } elseif (Test-Path $ServerVHD) { 
         Write-Verbose "Source VHD found at $ServerVHD"
      }
   }
}

if (! (Test-Path $VHDBase ) ) { 
   mkdir $VHDBase
}
if (! (Test-Path $MachineBase ) ) { 
   mkdir $MachineBase
}

Function Test-Clean () {
   If (Test-Path $VHDBase\*.vhdx ) {
      Throw "Found an existing VHD. Please clean up the target path and try again."
   }
}

function Copy-myVhd () {
      if ( $DVD ) {
         Write-Verbose "DVD specified. Not copying source VHD to $SysVHD"
      } else { 
         if ( $Client ) { 
            Write-Verbose "Creating VM from Sysprep'd VHD base $ClientVHD"
            cp $ClientVHD $SysVHD 
         } else { 
         Write-Verbose "Creating VM from Sysprep'd VHD base $ServerVHD"
            cp $ServerVHD $SysVHD
         } 
      }
}

function Create-myVM () { 
if ($DVD ) { 
  Write-Verbose "Creating $vmname from DVD with the following command:"
  Write-Verbose "New-VM -Name $VmName -MemoryStartupBytes 1024MB -BootDevice VHD -Generation 2 -SwitchName $vmSwitch -NewVHDPath $SysVHD -NewVHDSize 200GB -Path $MachineBase "
  Sleep 3
  New-VM -Name $VmName `
       -MemoryStartupBytes 1024MB `
       -BootDevice VHD `
       -Generation 2 `
       -SwitchName $vmSwitch `
       -NewVHDPath $SysVHD `
       -NewVHDSize 200GB `
       -Path $MachineBase
} else { 
  New-VM -Name $VmName `
       -MemoryStartupBytes 1024MB `
       -BootDevice VHD `
       -Generation 2 `
       -SwitchName $vmSwitch `
       -VHDPath $SysVHD `
       -Path $MachineBase
  }
}

Function Set-myVMConfig {
   Write-Verbose "Setting Processor Count to 4 for $VMName"
   Set-VMProcessor      -VMName $VMName -Count 4
   Write-Verbose "Enabling Dynamic Memory"
   Set-VMMemory         -VMName $VMName -DynamicMemoryEnabled $True
   Write-Verbose "Assigning static MAC address of $MacAdd"
   Get-VMNetworkAdapter -VMName $VmName `
      | Set-VMNetworkAdapter -StaticMacAddress "$MacAdd"
   if ($DVD) {
      Write-Verbose "Building from DVD, so adding DVD drive, and configuring boot order"
      if (! $client) { 
         Add-VMDvdDrive -VMName $VmName
         Set-VMDvdDrive -VMName $VmName  -Path $ServerISO 
         $vmDVD     = Get-VMDvdDrive -VMName $VmName
         $vmDrive   = Get-VMHardDiskDrive -VMName $VmName 
         Set-VMFirmware -VMName $VmName  -FirstBootDevice $vmDVD 
         Set-VMFirmware -VMName $VmName -BootOrder $vmDVD,$vmDrive
      } else {
         Add-VMDvdDrive -VMName $VmName
         Set-VMDvdDrive -VMName $VmName -Path $ClientISO 
         $vmDVD     = Get-VMDvdDrive -VMName $VmName
         $vmDrive   = Get-VMHardDiskDrive -VMName $VmName 
         Set-VMFirmware -VMName $VmName   -FirstBootDevice $vmDVD 
         Set-VMFirmware -VMName $VmName   -BootOrder $vmDVD,$vmDrive
      }
   }
}

Function Add-myNetAdapter {
   Write-Verbose "Adding second network adapter"
   Add-VmNetworkAdapter -VMName $VmName `
                        -SwitchName "199 Network" `
                        -StaticMacAddress "$199MacAdd" `
                        -Name "199 Ethernet"
}

If (! ( Get-VM -Name $VmName -ErrorAction Continue 2>$NULL) ) {
   Test-SourcePath
   Test-Clean
   Copy-myVHD -wait
   Write-Verbose "VHD's copied if we were doing that, now creating the VM..."
   Create-myVM
   Write-Verbose "VM Created"
   $myVM = Get-VM -VMName $VMName
}
Set-myVMConfig $myVM
Add-myNetAdapter $myVM
$myVM | Format-List

I hope you find this script useful, and I'd love to hear comments, suggestions for improvements, or bug reports as appropriate. As always, if you use this script as the basis for your own work, please respect my copyright and provide appropriate attribution.

 

Next up in the Building a Lab with PowerShell series will how to configure your DHCP server with PowerShell. This will take advantage of the fixed MAC addresses I create for all my lab machines and use these to populate DHCP Reservations.

Building a Lab in Hyper-V with PowerShell, Part 2

Creating VMs with New-myVM.ps1 - Part 1

As we saw in Part 1 of this series, I build my labs almost entirely with Windows PowerShell scripts. In that first post, I showed how to set the MAC address range on a Hyper-V host. I use this MAC address range to explicitly set my lab VMs to a specific MAC address for their NICs. This allows me to pre-configure all the IP addresses in DHCP by using reservations. (I showed you how to install and preconfigure the DHCP Server role in my post on Configuring Windows Server 2016 core as a DHCP Server with PowerShell. And I'll show you how to create all those reservations automatically in a later post in this series.)

For this post, and the couple, I want to share my "New-myVM.ps1" script. I use this script to create client and server VMs, from either DVD or sysprep'd VHDX files.

First, the parameters that New-myVM accepts:

[CmdletBinding()]
Param ([Parameter(Mandatory = $True,Position = 0)][alias("Name")][string]$VmName,
       [Parameter(Mandatory = $True,Position = 1)][alias("Final")][string]$MacFinal,
       [Parameter(Mandatory = $False)][alias("Base")][string]$MacBase = "00-15-5D-C8-0A-",
       [Parameter(Mandatory = $False)][string]$199MacBase = "00-15-5D-C8-CE-",
       [Parameter(Mandatory=$False)][alias("ISO")][Switch]$DVD,
       [Parameter(Mandatory=$False)][Boolean]$Client=($myInvocation.myCommand.Name -match "Client"),
       [Parameter(Mandatory=$False)][alias("Target")][string]$Path = "V:\",
       [Parameter(Mandatory=$False)][alias("VHDSource","DVDBase")][string]$Source = "V:\Source",
       [Parameter(Mandatory=$False)][alias("LocalSwitch","Network")][string]$vmSwitch = "10 Network",
       [Parameter(Mandatory=$False)][switch]$2012R2
       )

I have only two required parameters, the VMName and final two digits of the MAC addresses that the VM will use. Everything else defaults to some reasonable value for my labs. You'll notice that by default, I don't install from DVD, and I'm not installing Server 2012R2, but Server 2016. And, finally, a bit you might not have seen before, my default to determine if this is a client build or a server build.

[Parameter(Mandatory=$False)][Boolean]$Client=($myInvocation.myCommand.Name -match "Client")

The $Client parameter is a Boolean. I can specify it on the command line with -Client $True/$False, or I can allow it to accept the default value. The interesting thing is that the default value is dynamic. I use an NTFS hard link for New-myVM.ps1 to give it a second name, New-myClientVM.ps1.


Sidebar: NTFS Hard Links 

NTFS filesystem hard links take a single file and give it multiple names. The file is stored only once on the filesystem, but it has multiple names that can access the file. Each name for the file is completely equal. Even if you delete the original filename, all the linked versions are still present and completely unaffected by the deletion. You create a hard link in Server 2016 or Windows 10 with New-Item or in earlier versions of Windows with the built-in CMD command mklink. So, for example:

New-Item -Type HardLink `
         -Path 'C:\Build\New-myClientVM.ps1','C:\Build\New-myServerVM.ps1' `
         -Value 'C:\Build\New-myVM.ps1'

#Older OS Version:
cmd /c mklink /h C:\Build\New-myClientVM.ps1 C:\Build\New-myVM.ps1
cmd /c mklink /h C:\Build\New-myServerVM.ps1 C:\Build\New-myVM.ps1

I then use ($myInvocation.myCommand.Name -match "Client") to see if the filename I started the script with was New-myClientVM.ps1 , or one of the other names I have for this script.  ($myInvocation.myCommand.Name -match "Client") returns $True for New-myClientVM.ps1, but $False for filenames that don't include 'Client' in the filename.

 

Next, I set some basic variables used throughout the script. These are periodically tweaked as new builds become my default. Currently, they're set at:

$VMBase     = "$Path\$VMName"
$VHDSource  = $Source
$DVDBase    = $Source
$VHDBase    = "$VMBase\Virtual Hard Disks"
$SysVHD     = "$VMBase\Virtual Hard Disks\$VmName-System.vhdx"
$MachineBase= "$VMBase\Virtual Machines"
$ServerISO  = "$DVDBase\en_windows_server_2016_x64_dvd_9718492.iso"
$ClientISO  = "$DVDBase\en_windows_10_enterprise_version_1607_updated_jan_2017_x64_dvd_9714415.iso"
$ClientVHD  = "$Source\Generalized-client.vhdx"
if ($2012R2) { 
   $ServerVHD = "$Source\Generalized-2012R2.vhdx"
} else {
   $ServerVHD  = "$Source\Generalized-System.vhdx"
}

Nothing special there. Next, three functions to verify paths, etc. These are pretty basic Test-Path statements. If I need to create directories, I do. But if I don't find my source files where I expect them, or I find an already existing .vhdx where I'm not expecting one, then I use simple Throw statements to get me out and report the problem, since either of these failures will cause the script to fail, usually in an ugly way. ;)

Function Test-SourcePath () {
   if ($Client) {
      if ($dvd) {
         if (Test-Path $ClientISO) {
            Write-Verbose "Install ISO found at $ClientISO"
         } else {
            Throw "Client ISO not found at $ClientISO" 
         }
      } elseif (Test-Path $ClientVHD) { 
         Write-Verbose "Source VHD found at $ClientVHD"
      }
   } else {
      if ($dvd) {
         if (Test-Path $ServerISO) {
            Write-Verbose "Install ISO found at $ServerISO"
         } else {
            Throw "Server ISO not found at $ServerISO" 
         }
      } elseif (Test-Path $ServerVHD) { 
         Write-Verbose "Source VHD found at $ServerVHD"
      }
   }
}

if (! (Test-Path $VHDBase ) ) { 
   mkdir $VHDBase
}
if (! (Test-Path $MachineBase ) ) { 
   mkdir $MachineBase
}

Function Test-Clean () {
   If (Test-Path $VHDBase\*.vhdx ) {
      Throw "Found an existing VHD. Please clean up the target path and try again."
   }
}

You'll notice with these functions, and the ones that follow, everything builds on those original variables created at the top of the script, or as part of the script parameters. Yes, I need to keep those up to date. But there's only one place to make the changes.

Now, assuming I'm most likely going to be building from a sysprep'd VHD, I  copy that .vhdx file into my "Virtual Hard Disks" folder for this VM, changing the name as I do to reflect the new VM's name.

function Copy-myVhd () {
      if ( $DVD ) {
         Write-Verbose "DVD specified. Not copying source VHD to $SysVHD"
      } else { 
         if ( $Client ) { 
            Write-Verbose "Creating VM from Sysprep'd VHD base $ClientVHD"
            cp $ClientVHD $SysVHD 
         } else { 
         Write-Verbose "Creating VM from Sysprep'd VHD base $ServerVHD"
            cp $ServerVHD $SysVHD
         } 
      }
}

Now that we have all that setup work done, let's actually create the VM. We have to do this in two separate steps because Hyper-V doesn't give us a way to set the number of CPUs or configure some other stuff as part of the initial creation of the VM. We have to modify the VM after we first create it. Silly, but not all that hard to deal with in a script, though a real nuisance at the interactive command line.

Create-myVM {
if ($DVD ) { 
  New-VM -Name $VmName `
       -MemoryStartupBytes 1024MB `
       -BootDevice VHD `
       -Generation 2 `
       -SwitchName $vmSwitch `
       -NewVHDPath $SysVHD `
       -NewVHDSize 200GB `
       -Path $MachineBase
} else { 
  New-VM -Name $VmName `
       -MemoryStartupBytes 1024MB `
       -BootDevice VHD `
       -Generation 2 `
       -SwitchName $vmSwitch `
       -VHDPath $SysVHD `
       -Path $MachineBase
  }
}

And, we now have a VM. It's not quite what we want and need, yet, but we have a VM. One problem, I can't set the boot device to DVD, regardless of what the PowerShell help pages say, because I'm building Generation 2 virtual machines, and they don't allow you to specify 'CD' as the boot device during initial setup. So, we'll have to configure that, along with the other tweaks we need, as part of the next stage of the whole process. And for that, you'll have to wait until tomorrow, when I do Part 2 of New-myVM. :)

Promoting a new domain controller

I’ve been working with Windows Server 2016 CTP5 recently, and because I installed it without the Desktop Experience (what we used to call a Server Core installation), I’m having to do everything in Windows PowerShell. No complaints, I enjoy it, but it does force me to think about things a bit sometimes.

One of the tasks I needed to do was promote a new server to be a secondary domain controller. The PowerShell command for this is: Install-ADDSDomainController. But before you start promoting a new DC, it’s a really good idea to test that the promotion will succeed by using the Test-ADDSDomainControllerInstallation cmdlet. So, I combined the two steps into a simple script that allows you to run the test, and if the output looks clean, finish the installation and initiate a reboot.

The script is smart enough to realize you haven’t installed the ActiveDirectory feature yet, and goes ahead and installs it for you.

<# 
.Synopsis 
Tests a candidate domain controller, and then promotes it to DC.

.Description 
Promote-myDC first tests if a domain controller can be successfully promoted, and, 
if the user confirms that the test was successful, completes the promotion and 
restarts the new domain controller. 

.Example 
Promote-myDC -Domain TreyResearch.net

Tests if the local server can be promoted to domain controller for the 
domain TreyResearch.net. The user is prompted after the test completes 
and must press the Y key to continue the promotion. 

.Parameter Domain 
The domain to which the server will be promoted to domain controller. 

.Inputs 
[string] 

.Notes 
    Author: Charlie Russel 
 Copyright: 2016 by Charlie Russel 
          : Permission to use is granted but attribution is appreciated 
   Initial: 05/14/2016 (cpr) 
   ModHist: 
          : 
#> 
[CmdletBinding()] 
Param( 
     [Parameter(Mandatory=$True,Position=0)] 
     [string] 
     $Domain 
     )

Write-Verbose "Testing if ADDSDeployment module is available" 
If ( ! (Get-Module ADDSDeployment )) { 
   Write-Verbose "Installing the ActiveDirectory Windows Feature, since you seem to have forgotten that." 
   Install-WindowsFeature -Name ActiveDirectory -IncludeManagementTools 
   Write-Host "" 
}

If ( ! (Get-Module ADDSDeployment )) { 
   throw "Failed to install the ActiveDirectory Windows Feature." 
}

Write-Verbose "Testing if server $env:computername can be promoted to DC in the $Domain domain" 
Write-Host "" 
Test-ADDSDomainControllerInstallation ` 
      -NoGlobalCatalog:$false ` 
      -CreateDnsDelegation:$false ` 
      -CriticalReplicationOnly:$false ` 
      -DatabasePath "C:\Windows\NTDS" ` 
      -DomainName $Domain ` 
      -LogPath "C:\Windows\NTDS" ` 
      -NoRebootOnCompletion:$false ` 
      -SiteName "Default-First-Site-Name" ` 
      -SysvolPath "C:\Windows\SYSVOL" ` 
      -InstallDns:$true ` 
      -Force 
Write-Host "" 
Write-Host "" 
Write-Host ""

Write-Host -NoNewLine "If the above looks correct, press Y to continue...  " 
$Key = [console]::ReadKey($true) 
$sKey = $key.key

Write-Verbose "The $sKey key was pressed." 
Write-Host "" 
Write-Host "" 
If ( $sKey -eq "Y" ) { 
   Write-Host "The $sKey key was pressed, so proceeding with promotion of $env:computername to domain controller." 
   Write-Host "" 
   sleep 5 
   Install-ADDSDomainController ` 
      -SkipPreChecks ` 
      -NoGlobalCatalog:$false ` 
      -CreateDnsDelegation:$false ` 
      -CriticalReplicationOnly:$false ` 
      -DatabasePath "C:\Windows\NTDS" ` 
      -DomainName $Domain ` 
      -InstallDns:$true ` 
      -LogPath "C:\Windows\NTDS" ` 
      -NoRebootOnCompletion:$false ` 
      -SiteName "Default-First-Site-Name" ` 
      -SysvolPath "C:\Windows\SYSVOL" ` 
      -Force:$true 
} else { 
   Write-Host "The $sKey key was pressed, exiting to allow you to fix the problem." 
   Write-Host "" 
   Write-Host "" 
}

	                    
	                

Finally, the death of CMD!

Well, OK, it’s still there in Windows 10 But finally, completely, I no longer need it for ANYTHING! The final piece that I still used is now available in PowerShell v5. The latest publicly available Preview Build of Windows 10 Technical Preview (build 9926), includes updates to New-Item to support Junctions and Hard Links. Finally!

 

There’s no direct help available for it yet (I checked and downloaded the very latest), but a little poking around produced:

PS> New-Item -Type HardLink -Path 2ndTest.txt -Value Test.txt

I can edit 2ndTest.txt and the changes appear in Test.txt, and I can delete Test.txt and 2ndTest.txt is still there and editable. Interestingly, I do NOT need to run this as an administrator, assuming I have permissions in the folder where I’m creating the second (or third or 15th file). And, as expected, this can’t cross drive boundaries.

 

This build also includes support for directory Junctions, (-Type Junction) so between these two, I think we finally have all we need to kiss goodbye all further use of cmd.exe. (I really hate having to use cmd c/ from my PowerShell to get something done. Really. It’s so DOS.)

 

My thanks to the PowerShell team who took my challenge and mantra of Death To CMD to heart and have finally made Windows PowerShell the tool I can use for everything.

(Note: Currently the most current build available for download separately is the November build which doesn’t have these two New-Item features, so the only way to get them right now is in the latest Windows 10 build. But that’s likely to change soon.)

 

ETA: The February WMF 5.0 build is available from Windows Download Centre. This includes support for Windows Server 2012, Windows Server 2012 R2, and Windows 8.1 Pro and Enterprise. And yes, the changes to New-Item are in there. :)

 

Oh, and while we’re at it – there’s all sorts of other goodies in PowerShell v5, including OneGet, a great way to get new modules for your PowerShell. Check it out.

Getting the Free Disk Space of Remote Computers

This started out as a simple script to try to get the free space on one of my servers, but I quickly discovered that using WMI’s Win32_LogicalDisk could only give me part of the solution. The catch is that Win32_LogicalDisk doesn’t return the information about volumes that aren’t assigned drive letters. Which is a problem if what you really need to know is how close to out of space your backup disk is! Because Windows Server Backup takes total control of the backup target disk, and doesn’t mount it as a drive letter, you need to use a different WMI Class to get the information you need. After asking some friends (PowerShell MVPs ROCK!), I was pointed to Win32_Volume, which returns all sorts of information about disk volumes whether they are assigned drive letters or not.

The next issue was how to get the actual information I wanted, and then to format it so that it actually made some sense. For example:

PSH> (Get-WmiObject –ComputerName Server1 –Class Win32_Volume).FreeSpace
21654667264
103541030912
75879378944
142417367040
5500928
565375053824
PSH>

This doesn’t really cut it. OK, let’s try at least getting it into a table:

PSH> Get-WmiObject –ComputerName Server1 –Class Win32_Volume | ft –auto DriveLetter,Label,FreeSpace
DriveLetter Label                               FreeSpace 
----------- -----                               --------- 
C:                                            21655351296 
D:          DATA                             103541030912 
E:          EXCHANGE                          75879378944 
F:          FILES                            142417367040 
Y:          New Volume                            5500928 
            Server1 2014_10_15 10:57 DISK_03 565375053824

Well, that’s a bit more useful, but frankly, that number for the backup volume seems big, but is it 500 GB, or 50 GB? At first glance, I have no idea. And if it’s 50 GB, I’m in trouble, but if it’s 500 GB, we’re fine. So, we need to do a bit of manipulation to the output from Format-Table, and the tool for this is to create an Expression that allows you to calculate and format a result in a way that makes more sense. For this, we use an expression as the property to display. So, for example, to display that “565375053824” as Gigabytes, we use:

PSH>
Get-WmiObject –ComputerName Server1 –Class Win32_Volume `
       | ft –auto DriveLetter,`
                  Label,`
                  @{Label=”Free(GB)”;Expression={'{0:N0}’ –F ($_.FreeSpace/1GB)}}
DriveLetter Label                            Free(GB) 
----------- -----                            -------- 
C:                                           20 
D:          DATA                             96 
E:          EXCHANGE                         71 
F:          FILES                            133 
Y:          New Volume                       0 
            Server1 2014_10_15 10:57 DISK_03 527 
PSH>

Now we’re getting somewhere. But what did we do? We use the @{} to tell Format-Table that we were going to use an expression to define a column of data. The Label=”Free(GB)” creates a new column header, and the Expression={“{0:N0}” –F  means we’re going to have a numeric value (including thousands separators) with no decimal values. The calculated value for the column is ($_.FreeSpace/1GB).

So we now have a useful listing of free space on the remote server. Of course, it might be even more useful to know the percentage free. No problem, for that we use the formatting expression “{0:P0}” to express the column as a percentage, and use the calculation of ($_.FreeSpace/$_.Capacity), letting PowerShell do the work of converting that to a percentage. So:

PSH>
Get-WmiObject –ComputerName Server1 –Class Win32_Volume `
       | ft –auto DriveLetter,`
                  Label,`
                  @{Label=”Free(GB)”;Expression={“{0:N0}” –F ($_.FreeSpace/1GB)}},`
                  @{Label=”%Free”;Expression={“{0:P0}” –F ($_.FreeSpace/$_.Capacity)}}
DriveLetter Label                         Free(GB)  %Free 
----------- -----                         --------  ----- 
C:                                            20      17 % 
D:          DATA                              96      48 % 
E:          EXCHANGE                          71      71 % 
F:          FILES                             133     18 % 
Y:          New Volume                        0       58 % 
            Server1 12014_10_15 10:57 DISK_03 527     51 % 
PSH>

Now we almost have it. Next, it would probably be useful to get the total capacity of the disk while we’re at it, and since I have more than one server, we should probably plan on passing this whole thing an array of computer names. So, the final script, at least for this first pass:

# ********************************************* 
# ScriptName: Get-myFreeSpace.ps1 
# 
# Description: Script to get the free disk space
#            : on a remote computer and display it usefully
# 
# ModHist: 26/11/2014 - Initial, Charlie
#        : 
# 
# 
# *********************************************
[CmdletBinding()]
Param ([Parameter(Mandatory=$False,Position=0)]
         [String[]]$ComputerName = "Server1")
Write-Host ""
ForEach ( $Name in $ComputerName ) {
   Write-Host "Disk Utilization for server $Name is: "
   Get-WmiObject  -ComputerName $Name -Class Win32_Volume `
      | Format-Table  -auto `
         @{Label="Drive";`
            Expression={$_.DriveLetter};`
            Align="Right"},`
         @{Label="Free(GB)";`
            Expression={"{0:N0}" -f ($_.FreeSpace/1GB)};`
            Align="Right"},`
         @{Label="% Free";`
            Expression={"{0:P0}" -f ($_.FreeSpace / $_.Capacity)};`
            Align="Right"},`
         @{Label="Size(GB)";`
            Expression={"{0:N0}" -f ($_.Capacity / 1GB)};`
            Align="Right"},`
         @{Label="Volume Label";`
            Expression={$_.Label};`
            Width=25} 
}

You’ll see I tweaked the formatting to right align the calculated expressions, and gave my volume label column some extra space to improve readability. The result is:

PSH> Get-myFreeSpace.ps1 –ComputerName “Server1”,”Server2”
Disk Utilization for server Server1 is:
 Drive Free(GB) % Free Size(GB) Volume Label 
----- -------- ------ -------- ------------ 
   C:       20   17 %      120 
   D:       96   48 %      200 DATA 
   E:       71   71 %      100 EXCHANGE 
   F:      133   18 %      750 FILES 
   Y:        0   58 %        0 New Volume 
           527   51 %    1,024 Server1 2014_10_15 10:57 DISK_03 
   
Disk Utilization for server Server2 is:
Drive Free(GB) % Free Size(GB) Volume Label 
----- -------- ------ -------- ------------ 
             0   25 %        0 
   D:    1,697   53 %    3,214 Data 
   C:      484   95 %      512 
PSH>

Windows MultiPoint Server Disconnects

There is a known issue with Windows Server 2008 R2 (and Windows 7) that causes the network interface to change “location” from a Domain network to a Public network under some conditions. This is described in MS KnowledgeBase article: 2524478. The situation that causes this is made more likely to happen on a WMS server because of the Loop Back Adapters. I’ve been running this particular hotfix here for more than a week, with no ill effects. And a whole lot fewer WMS disconnects from my RDP clients! If you are using WMS, and using RDP clients, I strongly recommend that you download a copy of this hot fix and apply it if you’re seeing any unexplained disconnects from these clients.

Charlie.

Update1: There’s a new blog post up on the Official SBS Blog that talks about this. If you’re running WMS, you really want to get this hotfix.

Update2: If you’re running through a Remote Desktop Gateway (RD Gateway) to connect to your WMS server, you should install this hotfix on the RDGateway box as well. For example, on my SBS 2011 Standard network, my laptops use applications on the WMS server using RemoteApps. But I have them set to go through the RD Gateway on my SBS server, even when I’m in the office. This allows my connections to stay the same regardless of whether I’m in the office or on the road. I was still getting some disconnects even after installing this hotfix on the WMS server. But when I added them to the SBS server (my RD Gateway, remember), the disconnects went away. I haven’t had one all day. WOOT!

Protecting SBS Remote Web Access

I’ve been a firm believer in Two Factor Authentication (TFA) for a long time, and we use ScorpionSoft’s AuthAnvil here on all our servers and laptops. When we upgraded to Windows Small Business Server 2011 Standard early this year on our production network, one of the features that I was missing, and wasn’t happy about, was the lack of that TFA on the new Remote Web Access login page. When we contacted ScorpionSoft, they assured us it was coming soon, and asked us if we’d like to be on the TAP to get an early look at the product. Of course we said yes, and today I’m allowed to talk about it, and show a picture. So, first the picture - RemoteHomePage_01

See, looks just like regular RWA, except that it has an extra field for my AuthAnvil credential. That AuthAnvil credential is a combination of a PIN, and an 8 character one-time password. So, before any one can log on to my RWA site, they need to have three identification factors that assure me that they are who they say they are:

  • They must have an Active Directory account name and password that have permission to use RWA
  • They must know the PIN for that account
  • They must have the correct one-time password for that account

That one-time password (OTP) is generated at the time the user wants to log in from either their smart-phone, or from a hardware dongle. I’ve got both, but I have to say I end up using the generator on my iPhone 99% of the time.

The thing I like the most about AuthAnvil and RWWGuard is that it is completely transparent to my users. I don’t have to train them, make sure they’ve got some special card reader, or give them a different login page or anything. When I rolled out RWWGuard on my production server this morning, it just worked. And my users immediately recognized the new field and logged right in.

Now there are several vendors of two-factor authentication solutions, but the only one that has a product that integrates directly with SBS and with RWA is ScorpionSoft’s AuthAnvil and RWWGuard. And, frankly, their entire way of doing business recognizes that small businesses have just as compelling a need for secure authentication solutions as large businesses, and they’ve designed their product suite and business practices to scale from the large enterprise down to my small business. I like that, and it’s not an easy thing to do.

UPDATE: RWWGuard 2011 is officially available. And it's free to all AuthAnvil customers! Love it.

Uninstalling Windows Server 2008 R2 SP1 Beta and RC

Before you can install Windows Server 2008 R2 SP1 (or Windows 7 SP1), you need to uninstall the beta or RC version that might be installed. On a GUI install of Windows Server, no problem, run AppWiz.cpl and click on Updates. But on Hyper-V Server 2008 R2 (or Server Core) there is no obvious way to uninstall the beta or RC versions of the Service Pack. You need to use the command line version of the Windows Update Installer: wusa.exe.

wusa.exe /uninstall /kb:976932

It’ll take a couple of reboots before it’s completely uninstalled, and then you can install the SP1 you just downloaded.

Update: Note, if it wasn't clear. This command line works equally well from GUI installs, including Windows 7.

PowerShell, Hyper-V and WMI

I’ve started an article over on the PowerShell Survival Guide Wiki to drop in quick hits how to do “stuff” with Hyper-V, using PowerShell and the native WMI interface of Hyper-V. The WMI namespace for Hyper-V is  “root\virtualization”. Turns out managing Hyper-V isn’t as hard as I thought, at least in no small part because working with WMI in PowerShell is actually pretty straightforward. I’m still learning and poking around, but this stuff is all over the net if your bingle skills are good. Today I added the simple steps needed to create a VHD, either dynamic or fixed, to the Wiki page. Any one who wants to join in is more than welcome – that’s what a Wiki is all about after all.

Charlie.

Windows Server Backup with PowerShell

Windows Server 2008 R2 added some useful enhancements to the PowerShell snapin that was in Windows Server 2008. But it’s still a pain to use, and a bit hard to get your head around. Here’s a basic PowerShell script to do a backup. There are enough comments in there that it should help you get started.

# -----------------------------------------------------------------
# Script to set a Windows Server Backup policy for a server
#
# Assumes: Volumes C: and D: to backup.
#        : Target - \storageserverserverbackup
#        : Exclusions - D:temp, D:Users*.MP3, D:Users*.WMA
#        : VSS Mode - Full Backup
#        : System State - True
#        : Bare Metal Recovery - True
#        : Schedule - 12:30 PM, 11:00 PM
# Requires: Elevated PowerShell
#         :   (Must be run As Administrator)
#
# Planned Improvements: [minor] Check for elevated priv, error politely if not
#                     : [minor] Check for Server Backup PSSnapin before trying to load
#                     : [major] Accept cmdline parameters
#
# ModHist: 1/31/11 - initial (Charlie Russel)
#        :
#
# With profound thanks to Richard Siddaway, Windows PowerShell MVP
# -----------------------------------------------------------------
# By default, the PSSnapin isn''t loaded automatically, so first load it.
Add-PSSnapin Windows.ServerBackup # will error if already loaded, but continue

# First, create a new empty policy
# Alternately, you can open the existing policy in Edit mode with
#   $BackupPolicy = Get-WBPolicy -edit
$BackupPolicy = New-WBPolicy

# Now,define the parts of it.
# First,let''s do the "source" volumes that will be part of the backup.
# This requires us to first get a list of them, and then parse that list
# to add the ones we want (C: and D:)
# We don''t actually need C:, since we''ll get that as part of Bare Metal Restore,
# but we include it anyway for completeness

$volC = Get-WBVolume -AllVolumes | Where {$_.MountPath -eq "C:"}
$volD = Get-WBVolume -AllVolumes | Where {$_.MountPath -eq "D:"}
$Volumes = $volC,$volD

# Now, add those volumes to the blank policy.
Add-WBVolume -policy $BackupPolicy -volume $Volumes

# If you want to do the entire volume, you don''t need to define any exclusions,
# but if you want to exclude any files or folders, you need to define that exclusion
# and add that to the backup policy you''re building. First, define it. 
$excTEMP = New-WBFileSpec -Filespec D:Temp -exclude
$excWMA = New-WBFileSpec -Filespec "D:Users*.wma" -exclude  # Recursive unless
$excMP3 = New-WBFileSpec -Filespec "D:Users*.mp3" -exclude  # -NonRecursive specified
$FileExclusions = $excTEMP,$excWMA,$excMP3

# and then add that to the policy we''re building
Add-WBFileSpec -policy $BackupPolicy -filespec $FileExclusions

# Define the backup target, this time as a network share.
# For backup to share, you need to create a credential to connect to the remote share.
# This wouldn''t be required for specifying a removable disk.
# You can specify the username here (DOMAINUser) but will be prompted for password
$Cred = Get-Credential exampleuser

# Now, define the target
$Target = New-WBBackupTarget -NetworkPath \StorageServerServerBackup -Credential $Cred

# Add the target to the policy
Add-WBBackupTarget -policy $BackupPolicy -target $Target

# Define the schedule
$sch1 = [datetime]"01/31/2011 12:30:00"
$sch2 = [datetime]"01/31/2011 21:00:00"
Set-WBSchedule -policy $BackupPolicy -schedule $sch1,$sch2

# Set for system state and for bare metal recovery
Add-WBSystemState -policy $BackupPolicy
Add-WBBareMetalRecovery -policy $BackupPolicy

# Finally, set for full VSS Backup
Set-WBVssBackupOptions -policy $BackupPolicy -VssFullBackup

# Finally, we need to SET the policy before it actually takes control
Set-WBPolicy -force -policy $BackupPolicy

# This completes the configuration of the SBS server backup policy
$Server = (hostname).tolower()
" The server $Server now has the following backup configuration: "
""
Get-WBPolicy