Wednesday 20 May 2015

Creating DHCP option 121 or 249 string via powershell

We have a number of networks that addresses were statically assigned.. we are now moving these to DHCP to aid with the DevOps principles and destroying machines and rebuilding from code. In order to facilitate this I needed to deliver the static routes via DHCP.. It turns out its a bit of a pain in the arse to do this.. From a stack overflow post:
You're seeing these because option 249 must be specified as as BINARY value, not as an IPADDRESS value. If you can't set this with the GUI then you'll have to convert your desired route into a hexadecimal string yourself. An example would be as follows: 10.1.1.0/24 accessible via 10.1.1.1 converts into "180a01010a010101". The first octet, "18", is the number of bits of subnet mask (0x18 = 24 decimal). The next octets are the network ID (0a = 10, 01 = 1, 01 = 1, for "10.1.1"), padded by zeros on the right if the subnet mask doesn't end on an even octet boundary. The last four octets are the IP address of the gateway. Set the value in the GUI and you'll be happier.
Well I had too many to do, so I couldn't do it with the GUI really and it was too prone to error with fat fingers.. so I knocked up a quick script to accomplish this.. a word of warning is that I have only done /20 /23 and /24 networks with this.. so test it and verify in the GUI!!

 Here's the powershell script with an example at the end of how its run for multiple routes...
function ConvertTo-MaskLength {
  <#
    .Synopsis
      Returns the length of a subnet mask.
    .Description
      ConvertTo-MaskLength accepts any IPv4 address as input, however the output value 
      only makes sense when using a subnet mask.
    .Parameter SubnetMask
      A subnet mask to convert into length
   
 Function pinched from http://www.indented.co.uk/2010/01/23/powershell-subnet-math/
  #>

  [CmdLetBinding()]
  param(
    [Parameter(Mandatory = $True, Position = 0, ValueFromPipeline = $True)]
    [Alias("Mask")]
    [Net.IPAddress]$SubnetMask
  )

  process {
    $Bits = "$( $SubnetMask.GetAddressBytes() | ForEach-Object { [Convert]::ToString($_, 2) } )" -replace '[\s0]'
    return $Bits.Length
  }
}

function GenerateHex {
  <#
    .Synopsis
      Returns the hex value for creating a static route (DHCP option 121 / 249) - RFC3442
    .Description
      GenerateHex creates the hex value for creating a static route.. untested with anything other than /23 /24
    .Parameter SubnetMask
     EG 255.255.255.0 or /24
    .Parameter NetworkAddress
      The Network address IE 192.168.1.0
    .Parameter Gateway Address
      The IP Address of the gateway to send the traffic to IE 192.168.1.254    
  #>

 [CmdLetBinding()]
 param(
     [Parameter(Mandatory = $True, Position = 0, ValueFromPipeline = $True)]
     [String]$NetworkAddress,
  
  [Parameter(Mandatory = $True, Position = 1, ValueFromPipeline = $True)]
     [String]$SubnetMask,
  
  [Parameter(Mandatory = $True, Position = 2, ValueFromPipeline = $True)]
     [String]$GatewayAddress
 )

 # Convert subnet mask to correct format
 if ($SubnetMask -like "/*") {
  $SubnetMask = $SubnetMask.Replace("/","")
 }elseif ($SubnetMask -like "*.*") {
  $SubnetMask = ConvertTo-MaskLength -SubnetMask $SubnetMask
 }

 # Split network into an object by octet
 $NetAddress = $NetworkAddress.split('.')

 # Step through the correct number of significant octets (mask / 8 then rounded up) and convert to hex
 foreach($i in 1..([math]::Ceiling($SubnetMask / 8))){
  $Network += "{0:X2}" -f [convert]::ToInt32($NetAddress[$i-1])
 }

 # Convert Gateway Address to Hex
 Foreach ($octet in $GatewayAddress.split('.')) {
  $Gateway += "{0:X2}" -f [convert]::ToInt32($octet)
 }

 # Convert Mask to Hex
 $maskHex = "{0:X2}" -f [convert]::ToInt32($SubnetMask)

 # return calculated output
 $value =  $maskHex + $Network + $Gateway
 return $value
}

$String = ""
$String += GenerateHex -NetworkAddress 192.168.1.0 -SubnetMask 255.255.255.0 -GatewayAddress 10.111.111.1
$String += GenerateHex -NetworkAddress 10.1.2.0 -SubnetMask 255.255.254.0 -GatewayAddress 10.111.111.1

# Display command rather than execute it
Write-Output "netsh dhcp server 192.168.1.1 scope 10.111.111.0 set optionvalue 121 binary $($string)"
I then needed to convert some existing boxes from having lots of local persistent routes and was planning on using something like this as the basis of starting a script..
Get-WmiObject Win32_IP4PersistedRouteTable | Select-Object Destination, Mask, Nexthop
however I quickly discovered the machine I was looking at doesn't have powershell installed.. Given I'm in a rush I resorted to VBS as it was my scripting language of choice prior to powershell.. so the following script was used to export the routes and generate the netsh command.. Crude I know, but not as crude as multiple static routes on a server, rather than using a Router!
' Export Routes.vbs
On Error Resume Next
Set objWMIService = GetObject("winmgmts:\\.\root\cimv2")
Set colItems = objWMIService.ExecQuery("Select * from Win32_IP4PersistedRouteTable",,48)

For Each objItem in colItems   
    wscript.echo "$String += GenerateHex -NetworkAddress " & objItem.Destination & " -SubnetMask " & objItem.Mask & " -GatewayAddress " & objItem.NextHop
Next

Tuesday 12 May 2015

Naming VMWare Guest (Windows) NIC's

One of the things that annoy's me a little bit (A lot!) is when the adapters on a server have no meaningful name and you are only left with the network address to try and work out what the hell the network is, on some systems where you may have multiple adapters then this is only harder.

We talked over at work how best to identify what network the machine was on and work from there, we use Cisco Nexus 1000v switches on some of our vmware estate, so I initially thought of using CDP to determine the VLAN ID or name.. however I dismissed it as soon as I thought of it. The key was in the word 'some', so this potentially means this process wouldn't work for test and dev environments where an enterprise plus licence wasn't / isn't currently present.

So the option we came up with is using the MAC address to identify the correct adapter and then set the name based on the vmware port group.

To achieve the rename in the guest I am using the
Invoke-VMScript
command, which allows you to execute a command in the guest providing you have the correct credentials, vmware tools running and tcp port 902 open in the correct places (or is it 901? - 902 I think, but I've had a beer now!)

In order to do the rename, I currently use
Get-Netadapter
and
Rename-NetAdapter
commands which are only available in 2012 onwards, I will over time amend this to work with 2008 (prob using wmi and netsh), but this is only an initial draft, so the usual caveat's apply!

The script builds a scriptblock object to pass through to command, which took a little bit of fiddling to ensure that some variables weren't expanded such as $_.MacAddress in the where clause, but then the other variables were expanded, I'm sure when I revisit this I will change my mind of how to do it, but for now it works, so function over form.

FWIW, I have made it fail back to the administrator username if the supplied username fails auth.. but modify this to suit your own environment.

Script:

# Name VM Guest Adapters
# David Wallis 12/05/2015 blog.wallis2000.co.uk

# Variables
$viServer = "vmware.blah.local"
$ServerName = "Server001"
$Username = "Admin123"
$Password = "Password"

# Add Snapin if needed
If (-not (Get-PSSnapin VMware.VimAutomation.Core -ErrorAction SilentlyContinue)) {
    Add-PSSnapin VMware.VimAutomation.Core | Out-Null 
}

# Connect to Vsphere
$null = Connect-VIServer $viServer 

# Hide Progress Bars as System Center Orchestrator might not like these 
$ProgressPreference='SilentlyContinue'

# get VM Object
$vm = Get-VM -Name $ServerName -ErrorAction stop
 
function RenameNic
{
    [CmdletBinding()]
    param (
     [Parameter(Mandatory=$true)]
     [String] $User,
  
  [Parameter(Mandatory=$true)]
     [String] $GuestPw,
  
  [Parameter(Mandatory=$true)]
     [String] $MacAddress,
  
  [Parameter(Mandatory=$true)]
     [String] $NewInterfaceName
 )

  # Replace colons with dashes in the MAC address so that we can use it to match with the get-NetAdapter cmdlet.
  $macAddress = $macAddress.Replace(":","-")

  # Construct Scriptblock to execute the command
  $command = 'Get-Netadapter | where {$_.MacAddress -eq ' +  [char]34 + $macAddress + [char]34 + "} | Rename-NetAdapter -NewName " + [char]34 + $NewInterfaceName + [char]34
  Write-Verbose "Attempting to execute command within the VM ($($VM)): $($command)"
  $ScriptBlock = [scriptblock]::Create($command)
  
  # Try and invoke the command within VM
 try
 {  
  $output = Invoke-VMScript -vm $vm -guestuser $Username -guestpassword $Password -scripttype powershell -scripttext $ScriptBlock -ErrorAction stop
  # Nothing should be returned.. but the output would be in the below
  # $output.ScriptOutput
 }
 catch [VMware.VimAutomation.ViCore.Types.V1.ErrorHandling.InvalidGuestLogin]
 {
  # Invalid Guest Login
  Write-Verbose 'Invalid credentials trying the username "Administrator"'
  $output = Invoke-VMScript -vm $vm -guestuser "Administrator" -guestpassword $Password -scripttype powershell -scripttext $ScriptBlock -ErrorAction stop
  write-output $output.ScriptOutput
  
 }
 catch
 {
  throw
 }
 
}

# Get all the Adapters assigned to the VM and rename the nic to the vm portgroup name using the MAC
# to identify the correct adapters - this will only work on 2012 onwards currently
$Adapters = $vm | Get-NetworkAdapter | Select NetworkName, MacAddress
foreach ($adapter in $Adapters){
 write-verbose "Renaming Adapter with MAC Address ($($adapter.MacAddress))"
 RenameNic -User $Username -GuestPw $Password -MacAddress $($adapter.MacAddress) -NewInterfaceName $($adapter.NetworkName) -verbose
}