Migrate VMs From One Datastore to Another – PowerCLI

If you need to move VMs off a datastore for things like; array maintenance or to do non-in place upgrades from older VMFS versions, then a script like this can help. For example, upgrading VMFS3 to VMFS5 is not the same thing. You can read about the differences here VMFS5 VMFS-3, What’s the Deal? If you need to just simply move all VMs from one datastore to another, then this script will get the job done. Even though VMware has come out with storage DRS and datastore maintenance mode, which can be used as well, I still find this script useful.

This assumes that all hosts can see both datastores or a VM move may fail. I set this up to prevent migrations from filling a datastore to over 80% used, and to prevent from the destination datastore being more than 1.5x over provisioned. If a migration will exceed that than every VM is checked before moving serially and any move that violates the constraints will be skipped. You will get a report at the end of the script execution. By default it will attempt to move 3 VMs at a time, but you can specify how many simultaneous moves to do at a time.

NOTE: Requires PowerCLI and a connection to a vCenter server. I would also recommend you use the a minimum of PowerShell v3 ($PSVersionTable) or you may experience unexpected results.

Function Code:

Move-VMsToDatastore

function Move-VMsToDatastore {
<#
.SYNOPSIS
Evacuate all VMs off a datastore by moving them to another datastore
.DESCRIPTION
Move-VMsToDatastore will attempt to migrate VMs from one datastore to another
If there is enough space on the target datastore to move all VMs it will run multiple migrations at once
The default number of concurrent migrations is 3 but can be specified with -ConcurrentMigrations.  If 
free space will be less than 80% or the destination datastore will be overprovisioned more than 1.5x
then the script will default to moving only 1 VM at a time and any VMs that violate those constraints will
be skipped
.EXAMPLE
C:\>Move-VMsToDatastore -SourceDataStore "CLARiiON_LUN1" -DestDataStore "CLARiiON_LUN2"

DESCRIPTION
-----------
This command will try to move all VMs from the datastore named CLARiiON_LUN1 to the datastore named CLARiiON_LUN2
.EXAMPLE
C:\>Move-VMsToDatastore -SourceDataStore "CLARiiON_LUN1" -DestDataStore "CLARiiON_LUN2" -ConcurrentMigrations 5

DESCRIPTION
-----------
This command will try to move all VMs from the datastore named CX_LUN001 to the datastore named CX_LUN002
If there is enough free space on the target datastore to move all VMs, it will run 5 moves simultaneously
#>
[CmdletBinding()]
    param (
        [parameter(Mandatory=$true)]
        [string]$SourceDataStore,
        
        [parameter(Mandatory=$true)]
        [string]$DestDataStore,
        
        [parameter(Mandatory=$false)]
        [int]$ConcurrentMigrations=3,
        
        [parameter(Mandatory=$false)]
        [switch]$SmallestFirst
    )
    PROCESS {
    
        $MAX_OVER_PROV = 1.5    #maximum over provision allowed is 1.5x
        $MIN_FREE = .2            #minimum free space allowed is 20%

        #get src and dest datastores
        try {
            $srcDS = Get-Datastore -Name $SourceDataStore -ErrorAction Stop
            $destDS = Get-Datastore -Name $DestDataStore -ErrorAction Stop
        }
        catch {
            Write-Host "`n$_`n" -ForegroundColor Magenta
            return
        }

        if($srcDS.Length -ne 1) {
            Write-Host "ERROR: Source or Destination name returned many or no datastores" -ForegroundColor Magenta
            return
        }
    
        #environment info
        $destView = $destDS | Get-View
        $srcView = $srcDS | Get-View
        $srcView.RefreshDatastoreStorageInfo()
        $destView.RefreshDatastoreStorageInfo()
        $destFreeGB = $destView.Summary.FreeSpace / 1024 / 1024 / 1024
        $srcUsedGB = ($srcView.Summary.Capacity - $srcView.Summary.FreeSpace) / 1024 / 1024 / 1024
        $minFreeSpaceAllowed = ($destView.Summary.Capacity / 1024 / 1024 / 1024) * $MIN_FREE
        $maxOverprovisioned = ($destView.Summary.Capacity / 1024 / 1024 / 1024) * $MAX_OVER_PROV
        $srcDsProvSpaceGB = ($srcView.Summary.Capacity - $srcView.Summary.FreeSpace + $srcView.Summary.Uncommitted) / 1024 / 1024 / 1024
        $destDsProvSpaceGB = ($destView.Summary.Capacity - $destView.Summary.FreeSpace + $destView.Summary.Uncommitted) / 1024 / 1024 / 1024
        $oneStream = $false
        
        #sorting with the smallest first will move smaller VMs first based on provisioned space
        if($SmallestFirst) {
            try {
                $vmsToMove = Get-VM -Datastore $srcDS | Sort-Object ProvisionedSpaceGB
            }
            catch {
                Write-Host "`n$_`n" -ForegroundColor Magenta
            }
        }
        else {
            try {
                $vmsToMove = Get-VM -Datastore $srcDS | Sort-Object ProvisionedSpaceGB -Descending
            }
            catch {
                Write-Host "`n$_`n" -ForegroundColor Magenta
            }
        }
        
        #no VMs are on source datastore
        if($vmsToMove -eq $null) {
            Write-Host "`nNo VMs found on source datastore " -ForegroundColor Yellow -NoNewline
            Write-Host "$SourceDataStore`n`n" -ForegroundColor Cyan 
            return
        }

        #user specified 1 migration at a time
        if( $ConcurrentMigrations -lt 2 ) {
            $oneStream = $true
        }
        
        #if the free space could be less than 80% after moves, or if the provisioned space will be over 1.5 times capacity, then move 1 at a time
        elseif( (($destFreeGB - $srcUsedGB) -lt $minFreeSpaceAllowed) -or (($srcDsProvSpaceGB + $destDsProvSpaceGB) -gt $maxOverprovisioned) ) {

            $oneStream = $true
            
            Write-Host "`n`n******************WARNING******************" -ForegroundColor DarkYellow
            Write-Host "Not all VMs will safely fit on $DestDataStore" -ForegroundColor DarkYellow
            Write-Host "Only 1 migration will run at a time" -ForegroundColor DarkYellow
            if($SmallestFirst) {
                Write-Host "VMs are scheduled to move SMALLEST->LARGEST" -ForegroundColor DarkYellow
            }
            else {
                Write-Host "VMs are scheduled to move LARGEST->SMALLEST" -ForegroundColor DarkYellow
            }
            Write-Host "and moves that could exceed 80% full or exceed" -ForegroundColor DarkYellow 
            Write-Host "1.5x overprovisioned on the target datastore" -ForegroundColor DarkYellow
            Write-Host "will be skipped" -ForegroundColor DarkYellow
            Write-Host "*******************************************`n`n" -ForegroundColor DarkYellow
        }
        #otherwise move the specified number of systems concurrently
        else {
            Write-Host "`n`nNumber of concurrent moves is set to: "  -ForegroundColor DarkGray -NoNewline
            write-host "$ConcurrentMigrations`n`n" -ForegroundColor Cyan
        }
        
        #move 1 VM at a time checking space after each move
        if($oneStream) {
        
            #start moving VMs
            foreach ($vm in $vmsToMove) {

                $vmProvSpaceGB = $vm.ProvisionedSpaceGB
                $vmUsedSpaceGB = $vm.UsedSpaceGB
                
                #grab new free space and provisioned space and calculate what the destination datastore values will be after the vm is migrated
                $destView.RefreshDatastoreStorageInfo()
                $destDsProvSpaceGB = ($destView.Summary.Capacity - $destView.Summary.FreeSpace + $destView.Summary.Uncommitted) / 1024 / 1024 / 1024
                $destFreeGB = $destView.Summary.FreeSpace / 1024 / 1024 / 1024
                $freeSpaceAfterMoveGB = [System.Math]::Round( ($destFreeGB - $vmUsedSpaceGB), 2)
                $provSpaceAfterMoveGB = [System.Math]::Round( ($destDsProvSpaceGB + $vmProvSpaceGB), 2)
                
                #don't migrate if vm move will bring free space on datastore below 20%
                if( $freeSpaceAfterMoveGB -lt $minFreeSpaceAllowed ) {
                    Write-Host $vm.Name -ForegroundColor Yellow -NoNewline
                    Write-Host " skipped " -ForegroundColor DarkGray -NoNewline
                    Write-Host "$freeSpaceAfterMoveGB GB" -ForegroundColor Yellow -NoNewline
                    Write-Host " sree space is below the allowed " -ForegroundColor DarkGray -NoNewline
                    Write-Host "$minFreeSpaceAllowed GB" -ForegroundColor Yellow
                }
                #don't migrate if vm move will overprovision datastore over 1.5 times the capacity
                elseif( $provSpaceAfterMoveGB -gt $maxOverprovisioned ) {
                    Write-Host $vm.Name -ForegroundColor Yellow -NoNewline
                    Write-Host " skipped " -ForegroundColor DarkGray -NoNewline
                    Write-Host "$provSpaceAfterMoveGB GB" -ForegroundColor Yellow -NoNewline
                    Write-Host " Provisioned is above the allowed " -ForegroundColor DarkGray -NoNewline
                    Write-Host "$maxOverprovisioned GB" -ForegroundColor Yellow
                }
                #ok to migrate vm
                else {
                    Write-Host "Moving " -ForegroundColor DarkGray -NoNewline
                    Write-Host $vm.Name -ForegroundColor Cyan -NoNewline
                    Write-Host " to " -ForegroundColor DarkGray -NoNewline
                    Write-Host $DestDataStore -ForegroundColor Cyan -NoNewline
                    Write-Host " from "  -ForegroundColor DarkGray -NoNewline
                    Write-Host $SourceDataStore -ForegroundColor Cyan
                    
                    try {
                        move-vm -datastore $destDS -VM $vm -ErrorAction Stop | Out-Null
                    }
                    catch {
                        Write-Host "`n$_`n" -ForegroundColor Magenta
                    }
                }
            }
        }
        #if there is enough space then run multiple moves at once (NO DISK SPACE CHECKS AFTER THIS POINT)
        else {
        
            $arrayOfMigrationTasks = @()
        
            foreach ($vm in $vmsToMove) {
                Write-Host "Moving " -ForegroundColor DarkGray -NoNewline
                Write-Host $vm.Name -ForegroundColor Cyan -NoNewline
                Write-Host " to " -ForegroundColor DarkGray -NoNewline
                Write-Host $DestDataStore -ForegroundColor Cyan -NoNewline
                Write-Host " from "  -ForegroundColor DarkGray -NoNewline
                Write-Host $SourceDataStore -ForegroundColor Cyan
                
                #migrate datastore
                try {
                    $arrayOfMigrationTasks += move-vm -datastore $destDS -VM $vm -RunAsync -ErrorAction Stop
                }
                catch {
                    Write-Host "`n$_`n" -ForegroundColor Magenta
                }
                
                #wait for the number of currently migrating systems to fall under the allowed simultaneous moves
                while ( $arrayOfMigrationTasks.Length -gt ($ConcurrentMigrations - 1) ) {
                    Start-Sleep -Seconds 10
                    
                    #pull tasks that are no longer running out of the list of running tasks
                    foreach ($migrationTask in $arrayOfMigrationTasks) {
                        if( (Get-Task -Id $migrationTask.Id).State -ne "Running" ) {
                            [array]$arrayOfMigrationTasks = $arrayOfMigrationTasks | ?{$_.Id -ne $migrationTask.Id}
                        }
                    }
                    Write-Host "Number of migrations still running = " -NoNewline -ForegroundColor DarkGray
                    Write-Host $arrayOfMigrationTasks.Length -ForegroundColor Cyan
                }
            }
        }
        
        #wait for remaining moves to finish before moving on
        while($arrayOfMigrationTasks.Length -gt 0) {
            
            Start-Sleep -Seconds 10
                    
            #pull tasks that are no longer running out of the list of running tasks
            foreach ($migrationTask in $arrayOfMigrationTasks) {
                if( (Get-Task -Id $migrationTask.Id).State -ne "Running" ) {
                    $arrayOfMigrationTasks = $arrayOfMigrationTasks | ?{$_.Id -ne $migrationTask.Id}
                }
            }
            Write-Host "Number of migrations still running = " -NoNewline -ForegroundColor DarkGray
            Write-Host $arrayOfMigrationTasks.Length -ForegroundColor Cyan
        }
        
        #check source datastore for VMs that didn't move
        $vmsRemaining = Get-VM -Datastore $srcDS
        
        if($vmsRemaining.Length -eq 0) {
            Write-Host ""
            Write-Host "ALL VMS HAVE BEEN SUCCESSFULLY EVACTUATED FROM: " -ForegroundColor Gray -NoNewline
            Write-Host $SourceDataStore -ForegroundColor Cyan
            Write-Host ""
        }
        else {
            Write-Host ""
            Write-Host "THE FOLLOWING VMS WERE NOT REMOVED FROM: " -ForegroundColor Gray -NoNewline 
            Write-Host $SourceDataStore -ForegroundColor Cyan
            $vmsRemaining | %{Write-Host $_.name -ForegroundColor Yellow}
            Write-Host ""
        }
        
        return
    }
}

Get Local Disk Space Report PowerShell

I have a functions that can be used to easily get you a report on disk space usage for any number of systems. I’ve used this in the past to email myself a list of the top offenders each night. I typically generate a list of systems using Get-ADComputer, but in this example I’ll just construct an array manually. The function Get-LocalDiskInfo uses WMI to query Win32_LogicalDisk and filters out anything that is not a local disk. It then returns a custom disk info object that has the percent free information in it.

The Get-LocalDiskReport function simply runs the Get-LocalDiskInfo for a list of systems. It also sorts the resulting array by PercentFree so the systems with low free space show up at the top of the list.

Here I’m manually creating an array of system names with 4 Windows Servers in it. After passing that to Get-LocalDiskReport, you can see what machines are the worst offenders

9

Function Code:

Get-LocalDiskReport

function Get-LocalDiskInfo {

    [CmdletBinding()] param (
        [parameter(Mandatory=$true)] [string]$ComputerName,
        [parameter(Mandatory=$false)] [Management.Automation.PSCredential] $Credential
    )
    PROCESS {

        $results = @()

        try {
            
            if($Credential) {
                $localDisks = Get-WmiObject -Class Win32_LogicalDisk -ComputerName $ComputerName -Credential $Credential | ?{$_.DriveType -eq 3}
            }
            else {
                $localDisks = Get-WmiObject -Class Win32_LogicalDisk -ComputerName $ComputerName | ?{$_.DriveType -eq 3}
            }
            
            foreach ($localDisk in $localDisks) {
                $custDiskObj = New-Object PSObject -Property @{
                    SystemName = $ComputerName
                    DriveLetter = $localDisk.DeviceID.TrimEnd(':')
                    VolumeName = $localDisk.VolumeName
                    CapacityGB = [System.Math]::Round($localDisk.Size/1024/1024/1024, 2)
                    FreeSpaceGB = [System.Math]::Round($localDisk.FreeSpace/1024/1024/1024, 2)
                    PercentFree = $null
                }
                if($localDisk.FreeSpace -ne 0) {
                    $custDiskObj.PercentFree = [System.Math]::Round(($localDisk.FreeSpace*100/$localDisk.Size), 0)
                }
                else {
                    $custDiskObj.PercentFree = 0
                }
                
                $results += $custDiskObj
            }
        }
        catch {
            Write-Host "`n$_`n" -ForegroundColor Magenta
            return
        }
        return $results | select SystemName,DriveLetter,VolumeName,CapacityGB,FreeSpaceGB,PercentFree | sort PercentFree
    }
}

function Get-LocalDiskReport {
    [CmdletBinding()] param (
        [parameter(Mandatory=$true)] [array]$ComputerNameList,
        [parameter(Mandatory=$false)] [Management.Automation.PSCredential] $Credential
    )
    PROCESS {
    
        $results = @()
    
        $ComputerNameList | %{
            if($Credential) {
                $results += Get-LocalDiskInfo -ComputerName $_ -Credential $Credential
            }
            else {
                $results += Get-LocalDiskInfo -ComputerName $_
            }
        }
        
        return $results | sort PercentFree
    }
}

Email Array of Objects Using PowerShell

I use a couple different email functions to send different reports. This can be very useful to get script results back when you setup PowerShell based scheduled tasks.  This particular function adds an HTML header that formats the table a little nicer than the ConvertTo-HTML cmdlet does on its' own. It then sends out an HTML formatted email which. You just need to pass an array of objects to the function along with the normal email type information. (NOTE: using Select-Object to specify exactly what you want will help with the clarity of the email) Here is an example where I'm emailing myself the list of processes currently running on a system

Example Usage:

Untitled - PowerGUI Script Editor
$to = "example@mydomain.com"
$from = "PowerShellScript@mydomain.net"
$subject = "List of Running Processes on " + $env:COMPUTERNAME
$smtp = "mySmtpRelay.mydomain.net"
$content = get-process | select processname,id
Send-EmailHTML -To $to -From $from -Subject $subject -SMTPServer $smtp -BodyAsArray $content

Resulting Email:

1

Function Code:

Send-EmailHTML

Untitled - PowerGUI Script Editor
function Send-EmailHTML {
<#
.SYNOPSIS
Sends HTML Formatted Email
.DESCRIPTION
Send-EmailHTML will take an array of objects and emails a table in HTML 
.EXAMPLE
$to = "example@mydomain.com"
$from = "PowerShellScript@mydomain.net"
$subject = "List of Running Processes on " + $env:COMPUTERNAME
$smtp = "mySmtpRelay.mydomain.net"
$content = get-process | select processname,id
Send-EmailHTML -To $to -From $from -Subject $subject -SMTPServer $smtp -BodyAsArray $content

DESCRIPTION
-----------
This will email a table of all running processes on the system returned from the Get-Process 
cmdlet.  Using a select statement returns the information with clarity
#>
    [CmdletBinding()] param (
        [parameter(Mandatory=$true)] [string]$To,
        [parameter(Mandatory=$true)] [string]$From,
        [parameter(Mandatory=$true)] [string]$Subject,
        [parameter(Mandatory=$true)] [string]$SMTPServer,
        [parameter(Mandatory=$true)] [array]$BodyAsArray,
        [parameter(Mandatory=$false)] [string]$CC,
        [Parameter(Mandatory=$false)] [string]$BCC
    )
    PROCESS {

        #html style definition
        $htmlHeader = @'
            <style>
                body { background-color:#FFFFFF; } 
                body,table,td,th { font-family:Tahoma; color:Black; Font-Size:10pt } 
                th { font-weight:bold; background-color:#1F497D; color:White } 
                td { background-color:#DDDDDD; }
            </style>
'@
        try {
            #convert array into HTML fragment string
            $htmlContent = $BodyAsArray | ConvertTo-Html -Fragment | Out-String
            
            #initialize mail object
            $mail = new-object System.Net.Mail.MailMessage
            $mail.from = $From
            $mail.to.add($To)
            if ($CC) { 
                $mail.cc.add($CC) 
            }
            if ($BCC) { 
                $mail.bcc.add($BCC) 
            }
            $mail.subject = $Subject
            
            #add html to mail body
            $html = [System.Net.Mail.AlternateView]::CreateAlternateViewFromString( ($htmlHeader + $htmlContent), $null, "text/html" )
            $mail.AlternateViews.Add($html)
            $mail.IsBodyHtml = 1
            
            #send mail
            $smtpClient = new-object System.Net.Mail.SmtpClient
            $smtpClient.Host = $SMTPServer
            $smtpClient.Send($mail)
        }
        catch {
            Write-Host "`n" $_.Exception.Message "`n" -ForegroundColor Magenta
            return
        }
    }    
}

Update HP Onboard Administrator Firmware PowerShell

I’ve been in situations where every 3-6 months we have to update firmware on 30+ HP Blade Chassis Onboard Administrators (OAs) at a time. I figured I’d just use my Invoke-SSHCommand function to automate this using PowerShell. This requires that you have the firmware bin file on an accessible FTP server.  You will need to either load or include the code for Invoke-SSHCommand in order for these functions to work.

If you have redundant OAs, you only need to target one of them and both will get the firmware update.  Because this may not be the active OA at the time you run this, you can pass the param -MakeActiveIfStandby to fail over the OAs before applying the update.

If FTP doesn’t allow anonymous gets, you need to specify the logon credentials in the FTPConnectionString value when you call the function. This is the standard ftp format:  ”ftp://<USERNAME>:<PASSWORD>@<FTP URL>/<FILENAME>”

NOTE: I’m using the bin file name to determine what version is being pushed out, so renaming the bin file is not supported

All I now need to do is maintain a list of all my OA IPs/Hostnames and run something like this to push updates out

$user = "Administrator"
$ftp = "ftp://myftpserver/hpoa370.bin"
$pw = Read-Host "Enter Password for $user" -AsSecureString

Get-Content "C:\OA-Hostname-List.txt" | %{
    Update-HPOAFirmware -OAHostName $_ -FTPConnectionString $ftp -UserName $user -SecurePassword $pw -MakeActiveIfStandby
}

Function Code:

Update-HPOAFirmware

function Invoke-SSHCommand {
<#
.SYNOPSIS
Invoke SSH Command on a Remote Computer
.DESCRIPTION
Invoke-SSHCommand uses PuTTY's plink.exe to execute a SSH command on a remote system
.EXAMPLE
Invoke-SSHCommand -Command "uname -a" -ComputerName "myComputer"

DESCRIPTION
-----------
Run uname against the remote computer

.EXAMPLE
$user = "testuser"
$userpw = Read-Host -Prompt "Enter Password" -AsSecureString
Invoke-SSHCommand -Command "touch /tmp/PowerShellHasBeenHere" -ComputerName "myComputer" -UserName $user -SecurePassword $userpw

DESCRIPTION
-----------
Creates a blank file in /tmp on the remote computer.  This time we are passing the user name and password
as parameters
#>

    [CmdletBinding()] param (
        [Parameter(Mandatory=$true)] [String]$ComputerName,
        [Parameter(Mandatory=$true)] [String]$Command,
        [Parameter(Mandatory=$false)] [String]$PathToPlink="C:\Program Files (x86)\PuTTY\plink.exe",
        [Parameter(Mandatory=$false)] [String]$UserName,
        [Parameter(Mandatory=$false)] [Security.SecureString]$SecurePassword,
        [Parameter(Mandatory=$false)] [Switch]$AutoAcceptKeys
    )

    process {
    
        #check for plink.exe file
        Write-Host ""
        if(! (Test-Path $PathToPlink) ) {
            Write-Host "ERROR: $PathToPlink not found" -ForegroundColor Magenta
            return
        }
        #get username if not passed
        if(!$UserName) {
            $UserName = Read-Host -Prompt "Enter User Name"
        }
        #get password if not passed
        if(!$SecurePassword) {
            $SecurePassword = Read-Host -Prompt "Enter Password" -AsSecureString
        }
        Write-Host ""
        
        #convert from secure pw for command execution
        $pw = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecurePassword))
        
        #formulate plink command
        if($AutoAcceptKeys) {
            $plinkCommand = "`"echo yes | `"$PathToPlink`" -ssh -l $UserName -pw $pw $ComputerName `"$Command`"`""
        }
        else {
            $plinkCommand = "`"`"$PathToPlink`" -ssh -l $UserName -pw $pw $ComputerName `"$Command`"`""
        }
        
        #run plink command
        $result = cmd /c $plinkCommand
        
        return $result
    }
}

function Get-HPOAFirmwareVersion {
<#
.SYNOPSIS
Get OA Firmware Version
.DESCRIPTION
Get-HPOAFirmwareVersion will return the firmware version on the OA specified
.EXAMPLE
Get-HPOAFirmwareVersion -OAHostName "HPOA1"

DESCRIPTION
-----------
This command will return the version of firmware installed on HPOA1
.EXAMPLE
$SecurePassword = Read-Host "Enter Password" -AsSecureString
Get-HPOAFirmwareVersion  -OAHostName "HPOA1" -UserName "Administrator" -SecurePassword $SecurePassword

DESCRIPTION
-----------
This command specifys the login credentials for the OA so you don't get prompted
#>
    [CmdletBinding()] param (
        [parameter(Mandatory=$true)] [string]$OAHostName,
        [parameter(Mandatory=$false)] [string]$UserName,
        [parameter(Mandatory=$false)] [System.Security.SecureString]$SecurePassword,
        [Parameter(Mandatory=$false)] [Switch]$AutoAcceptKeys
    )
    PROCESS {
    
        #get missing params
        if( ! $UserName ) {
            $UserName = Read-Host "Enter UserName"
        }
        if( ! $SecurePassword ) {
            $SecurePassword = Read-Host "Enter Password for $UserName" -AsSecureString
        }
        
        if($AutoAcceptKeys) {
            $oaInfo = Invoke-SSHCommand -ComputerName $OAHostName -Command "show oa info" -UserName $UserName -SecurePassword $SecurePassword -AutoAcceptKeys | ?{$_ -match "Firmware Ver."}
        }
        else {
            $oaInfo = Invoke-SSHCommand -ComputerName $OAHostName -Command "show oa info" -UserName $UserName -SecurePassword $SecurePassword | ?{$_ -match "Firmware Ver."}
        }
        
        return $oaInfo
    }
}

function Update-HPOAFirmware {
<#
.SYNOPSIS
Update OA Firmware from FTP site
.DESCRIPTION
Update-HPOAFirmware will attempt to upgrade OA firmware from a bin file hosted on an FTP server
.EXAMPLE
Update-HPOAFirmware -OAHostName "HPOA1" -FTPConnectionString "ftp://iss:tools4AL@ftp.usa.hp.com/OAfw/hpoa370.bin"

DESCRIPTION
-----------
This command will update HPOA1 to firmware 3.70 using the user iss and password tools4AL from HP's OA firmware FTP repo
.EXAMPLE
$SecurePassword = Read-Host "Enter Password" -AsSecureString
Update-HPOAFirmware -OAHostName "HPOA1" -FTPConnectionString "ftp://myftpserver/hpoa370.bin" -UserName "Administrator" -SecurePassword $SecurePassword

DESCRIPTION
-----------
This command will update HPOA1 from myftpserver to 3.70 using anonymous ftp.  The username and password for the OA
are specified here to prevent being prompted later
#>
    [CmdletBinding()] param (
        [parameter(Mandatory=$true)] [string]$OAHostName,
        [parameter(Mandatory=$true)] [string]$FTPConnectionString,
        [parameter(Mandatory=$false)] [switch]$MakeActiveIfStandby,
        [parameter(Mandatory=$false)] [string]$UserName,
        [parameter(Mandatory=$false)] [System.Security.SecureString]$SecurePassword,
        [Parameter(Mandatory=$false)] [Switch]$AutoAcceptKeys
    )
    PROCESS {
        
        #get missing params
        if( ! $UserName ) {
            $UserName = Read-Host "Enter UserName"
        }
        if( ! $SecurePassword ) {
            $SecurePassword = Read-Host "Enter Password for $UserName" -AsSecureString
        }
        
        try {
        
            #get current and new firmware version to check if update is required
            ###if user changes filename or HP changes it's bin filename format, this method of version check will no longer work and exception will hopefully be caught###
            $oaCurrentFirm = Get-HPOAFirmwareVersion -OAHostName $OAHostName -UserName $UserName -SecurePassword $SecurePassword
            $currentVersion = [int]($oaCurrentFirm.Substring($oaCurrentFirm.IndexOf(':')+2, 4)).Replace(".","")
            $newBinFileName = $FTPConnectionString.Substring($FTPConnectionString.LastIndexOf('/')+1,$FTPConnectionString.Length-$FTPConnectionString.LastIndexOf('/')-1)
            $newVersion = [int]$newBinFileName.Substring(4,3)
            
            #update needed
            if($newVersion -gt $currentVersion) { 
            
                $oaRole = Invoke-SSHCommand -ComputerName $OAHostName -Command "show oa status" -UserName $UserName -SecurePassword $SecurePassword | ?{$_ -match "Role:"}
                
                if($oaRole -eq $null -or $oaRole -eq "") {
                    Write-Host "`nERROR: Invoking command on $OAHostName`n" -ForegroundColor Magenta
                    return
                }
                
                #if OA is standby either try to make active or exit
                if($oaRole -match "Standby") {
                    
                    if(! $MakeActiveIfStandby) {
                        Write-Host "`nOA $OAHostName is the Standby OA"
                        $answer = Read-Host "Would you like to attempt to fail over to Active [y/n] "
                        
                        if($answer -ne "y") {
                            return
                        }
                    }
                    
                    $standbyToActive = Invoke-SSHCommand -ComputerName $OAHostName -Command "force takeover" -UserName $UserName -SecurePassword $SecurePassword
                    
                    #wait for OA to become the active
                    while ($oaRole -notmatch "Active") {
                        Start-Sleep -Seconds 60
                        $oaRole = Invoke-SSHCommand -ComputerName $OAHostName -Command "show oa status" -UserName $UserName -SecurePassword $SecurePassword | ?{$_ -match "Role:"}
                    }
                }
                
                #perform update if OA is active
                if($oaRole -match "Active") {
                    $oaUpdate = Invoke-SSHCommand -ComputerName $OAHostName -Command "UPDATE IMAGE $FTPConnectionString" -UserName $UserName -SecurePassword $SecurePassword
                }
                #otherwise OA is not active so exit
                else {
                    Write-Host "`nERROR: $OAHostName couldn't be made active`n" -ForegroundColor Magenta
                    return
                }
            }
            else {
                $oaUpdate = "No Update Required for $OAHostName"
            }
            return $oaUpdate
            
        }
        catch {
            Write-Host "`nERROR: Check FTP location and make sure you don't rename the bin file" -ForegroundColor Magenta
            Write-Host "`n$_`n" -ForegroundColor Magenta
        }
    }
}

Audit Local Admin Group Membership PowerShell

Auditing system access can be made much easier with PowerShell.  This function returns the members of the local admin group on a Windows system by using WMI.  If there are domain groups nested in the local admin group, you can use the parameter -EnumerateDomainGroups and the script will attempt to pull domain group membership information from AD.  This functionality uses PowerShell’s AD module which is a requirement if you want to enumerate these groups.  This will result in a list of all user accounts, local and domain, that have access to the system.

Here I’m just asking for the members of Administrators with no domain enumeration.

3

And here I’m asking the function to go out to AD and find out what user accounts are members of the domain groups. Even though I had to blank out most of the results, you can see that it pulled user objects from the “domain admins” group

4

Function Code:

Get-LocalAdminGroupMembers

function Get-LocalAdminGroupMembers {
<#
.SYNOPSIS
Get a list of accounts in the local admin group of a system
.DESCRIPTION
Get-LocalAdminGroupMembers uses wmi to gather a list of the members of the administrators group
.EXAMPLE
Get-LocalAdminGroupMembers

DESCRIPTION
-----------
This command will return the members of the administrators group for the local system
.EXAMPLE
Get-LocalAdminGroupMembers -ComputerName "SERVER1"

DESCRIPTION
-----------
This command will return the members of the administrators group for the system named SERVER1
.EXAMPLE
Get-LocalAdminGroupMembers -ComputerName "SERVER1" -EnumerateDomainGroups

DESCRIPTION
-----------
This is the same as the previous example with 1 differce; It will attempt to identify/enumerate 
the members nested into any identified domain groups.  This requires that the AD PowerShell module
#>
    [CmdletBinding()] param (
        [parameter(Mandatory=$false)] [string]$ComputerName=$env:COMPUTERNAME,
        [parameter(Mandatory=$false)] [switch]$EnumerateDomainGroups,
        [parameter(Mandatory=$false)] [Management.Automation.PSCredential] $Credential
    )
    PROCESS {
    
        $resultArray = @()
    
        try {
        
            #get wmi group member info for local admin group
            if($Credential) {
                $adminObjects = Get-WmiObject win32_groupuser -ComputerName $ComputerName -ErrorAction Stop -Credential $Credential | ? {$_.groupcomponent -like '*Name="Administrators"'}
            }
            else {
                $adminObjects = Get-WmiObject win32_groupuser -ComputerName $ComputerName -ErrorAction Stop | ? {$_.groupcomponent -like '*Name="Administrators"'}
            }
            
            foreach ($adminObject in $adminObjects) {
                $partComponent = $adminObject.partcomponent
                $itemName = $partComponent.Substring($partComponent.LastIndexOf('=')+2,$partComponent.length-$partComponent.LastIndexOf('=')-3)
                $domainName = $partComponent.Substring(0,$partComponent.LastIndexOf('=')-6)
                $domainName = $domainName.Substring($domainName.LastIndexOf('"')+1,$domainName.length-$domainName.lastindexof('"')-1)
                
                #if FQDN, pull out computer name
                if($ComputerName.IndexOf('.') -ne -1) {
                    $ComputerName = $ComputerName.Substring(0,$ComputerName.IndexOf('.')-1)
                }
                
                #determine account type
                if($partComponent -match "Win32_UserAccount.Domain" -and $domainName -eq $ComputerName) {
                    $acctType = "Local User"
                }
                elseif($partComponent -match "Win32_UserAccount.Domain") {
                    $acctType = "Domain User"
                }
                elseif ($partComponent -match "Win32_Group.Domain") {
                    $acctType = "Domain Group"
                }
                elseif ($partComponent -match "Win32_SystemAccount.Domain") {
                    $acctType = "System"
                }
                else {
                    $acctType = "UNKNOWN"
                }
                
                #try to get domain group members
                if($EnumerateDomainGroups -and $acctType -eq "Domain Group") {
                    try {
                        Get-ADGroupMember -Recursive -Identity $itemName -ErrorAction Stop | %{
                            #create custom member object
                            $custMemberObj = New-Object PSObject -Property @{
                                COMPUTER_NAME = $ComputerName
                                ACCOUNT_TYPE = "Domain Nested User"
                                ACCOUNT_NAME = $_.Name
                                DOMAIN_NAME = $domainName
                                PARENT_GROUP = $itemName
                            }
                            $resultArray += $custMemberObj
                        }
                    }
                    catch {
                        Write-Host "`nWARNING: Enumerating Domain Group $itemName failed`n" -ForegroundColor Yellow
                        #create custom member object
                        $custMemberObj = New-Object PSObject -Property @{
                            COMPUTER_NAME = $ComputerName
                            ACCOUNT_TYPE = $acctType
                            ACCOUNT_NAME = $itemName
                            DOMAIN_NAME = $domainName
                            PARENT_GROUP = ""
                        }
                    }
                }
                else {
                    #create custom member object
                    $custMemberObj = New-Object PSObject -Property @{
                        COMPUTER_NAME = $ComputerName
                        ACCOUNT_TYPE = $acctType
                        ACCOUNT_NAME = $itemName
                        DOMAIN_NAME = $domainName
                        PARENT_GROUP = ""
                    }
                }
                
                $resultArray += $custMemberObj
            }
        }
        catch {
            Write-Host "`n$_`n" -ForegroundColor Magenta
            return
        }
        
        return ($resultArray | sort ACCOUNT_TYPE)
    }
}

Get Uptime Using WMI and PowerShell

Grabbing a system’s uptime can be very useful information when troubleshooting.  This function will use WMI to calculate a system’s uptime.  It works on local and remote systems and you can specify credentials for the WMI call if you need to. It will return a System.TimeSpan value.

Here is an example:

2

Function Code:

Get-Uptime

function Get-Uptime {
<#
.SYNOPSIS
Get Uptime of Windows Computer
.DESCRIPTION
Get-Uptime will pull WMI uptime information from a Windows computer
.EXAMPLE
Get-Uptime

DESCRIPTION
-----------
This command will return the uptime for the local system
.EXAMPLE
Get-Uptime -computerName "SERVER1"

DESCRIPTION
-----------
This command will return the uptime for the system named SERVER1
#>
    [CmdletBinding()] param (
        [parameter(Mandatory=$false)] [string]$ComputerName=$env:COMPUTERNAME,
        [parameter(Mandatory=$false)] [Management.Automation.PSCredential] $Credential
    )
    PROCESS {
        
        #get wmi OS information
        try {
            if($Credential) {
                $wmic = gwmi Win32_OperatingSystem -computername $ComputerName -ErrorAction Stop -Credential $Credential
            }
            else {
                $wmic = gwmi Win32_OperatingSystem -computername $ComputerName -ErrorAction Stop    
            }
        }
        catch {
            Write-Host "`n$_`n" -ForegroundColor Magenta
            return
        }
        if ($wmic.lastbootuptime -eq $null) {
            Write-Host "`nWARNING: No Boot Time Data Found`n" -ForegroundColor Yellow
            return
        }
        
        #return timespan between now and last boot time
        return ( (Get-Date) - ($wmic.ConvertToDateTime($wmic.lastbootuptime)) )
        
    }
}

Invoke DOS Command in PowerShell

Unfortunately, there are still legacy dos applications that still provide value for Windows administrators.  Also, PowerShell isn’t yet installed on all our Windows servers and can’t be soley relied on:(  So being able to invoke a remote DOS command from PowerShell can be helpful at times.  I’ve written a script to do just that.  It works by creating a process on the remote system using WMI and writing the results to a temporary text file.  The function will then grab the results from the temp file on the remote system and return them.

Here I’m pulling netstat data from a remote system

5

Or maybe I don’t want to get anything back, but I want to force Windows Update to check for new updates

6

Function Code:

Invoke-DOSCommand

function Invoke-DOSCommand {
<#
.SYNOPSIS
Run a DOS command on a remote host and return results
.DESCRIPTION
Invoke-DOSCommand will run a dos command on a remote system and will return the results.
It does this using WMI to create a process and by creating a temporary file on the remote
to store the results if any
.EXAMPLE
Invoke-DOSCommand -Command "netstat -ano"

DESCRIPTION
-----------
This command will return the results from netstat on the local system
.EXAMPLE
Invoke-DOSCommand -Command "netstat -ano" -ComputerName "SERVER1"

DESCRIPTION
-----------
This command will return the results from netstat on the remote system SERVER1
.EXAMPLE
Invoke-DOSCommand -Command "netstat -ano" -ComputerName "SERVER1" | ?{$_ -match "LISTENING"}

DESCRIPTION
-----------
This command is the same as the previous example, but here we are returning only interfaces
that are in the listening state
#>
    [CmdletBinding()] param(                
        [Parameter(Mandatory=$true)] $Command,
        [Parameter(Mandatory=$false)] $ComputerName,
        [Parameter(Mandatory=$false)] [string]$TempLocalDirPath="C:\",
        [Parameter(Mandatory=$false)] [switch]$Force
    )
    
    process{
        
        #if computername is specified assume remote system
        if($ComputerName){
        
            $tempFileName = (get-date -UFormat "%Y%m%d%H%M%S") + "DOStemp.txt"
            
            #generate local file path
            if( $TempLocalDirPath.EndsWith('\') ) {
                $localFilePath = $TempLocalDirPath + $tempFileName
            }
            else {
                $localFilePath = $TempLocalDirPath + '\' + $tempFileName
            }
            
            #generate remote file path from local path using hidden admin share
            try {
                $remoteFolderPath = "\\$ComputerName\" + (Split-Path $TempLocalDirPath -Qualifier -ErrorAction Stop).TrimEnd(":") + "$" + (Split-Path $TempLocalDirPath -NoQualifier -ErrorAction Stop)
            }
            catch {
                Write-Host "`nERROR: Bad TempLocalDirPath value" -ForegroundColor Magenta
                Write-Host "`n" $_.Exception.Message "`n" -ForegroundColor Magenta
            }
            
            #test that folder exists and is reachable
            if( !(Test-Path $remoteFolderPath) ) {
                Write-Host "`nERROR: Remote path $remoteFolderPath desn't exist or is inaccessible`n" -ForegroundColor Magenta
                return
            }
            
            #add filename to path
            if( $remoteFolderPath.EndsWith('\') ) {
                $remoteFilePath = $remoteFolderPath + $tempFileName    
            }
            else {
                $remoteFilePath = $remoteFolderPath + '\' + $tempFileName
            }
            Write-Host "`nTemp file will be located at $remoteFilePath" -ForegroundColor Cyan

            #if there is already a file with this name -Force required
            if( (Test-Path $remoteFilePath) -and !($Force) ) {
                Write-Host "`nWARNING: File $remoteFilePath Already Exists" -ForegroundColor Yellow
                Write-Host "You can use the -Force parameter to overwrite or you can" -ForegroundColor Yellow
                Write-Host "specify a different temp path using -TempLocalDirPath" -ForegroundColor Yellow
                Write-Host "the default path is C:\`n" -ForegroundColor Yellow
                return
            }

            #create remote dos command
            $cmd = "cmd /c $Command > $localFilePath"
        
            #run command as process using WMI invocation
            try {
                $processID = (Invoke-WmiMethod -Class win32_process -Name create -ArgumentList $cmd -ComputerName $ComputerName -ErrorAction Stop).ProcessID
            }
            catch {
                Write-Host "`nERROR: creating process" -ForegroundColor Magenta
                Write-Host "`n$_`n" -ForegroundColor Magenta
                return
            }
            
            #wait for process to complete
            Write-Host "`nWaiting for process $processID to complete..." -ForegroundColor Cyan -NoNewline
            while (Get-Process -Id $processID -ComputerName $ComputerName -ErrorAction SilentlyContinue) {
               Start-Sleep -Seconds 2
               Write-Host "." -ForegroundColor Cyan -NoNewline
            }
            Write-Host ""
        
            #get results from temp file
            Start-Sleep -Seconds 4
            try {
                $result = Get-Content $remoteFilePath -ErrorAction Stop
            }
            catch {
                Write-Host "`nERROR: Couldn't get file $remoteFilePath" -ForegroundColor Magenta
                Write-Host "`n" $_.Exception.Message "`n" -ForegroundColor Magenta
                return
            }
        
            #cleanup temp file
            Remove-Item $remoteFilePath -Force -ErrorAction SilentlyContinue
            if( Test-Path $remoteFilePath ) {
                Write-Host "`nWARNING: Unable to remove remote file" -ForegroundColor Yellow
            }
        }
        #otherwise run command on local system
        else {
            $result = cmd /c $Command
        }
        
        #return command results
        if($result -eq $null) {
            Write-Host "`nNothing was returned by remote command`n" -ForegroundColor Cyan
            return
        }
        else {
            return $result
        }
    }
}

Invoke Remote SSH Command From PowerShell

I find that running scripts against both Windows and Linux systems together can be more efficient than running separate scripts.  This can be accomplished quite easily with the plink.exe binary from the PuTTY installation package.

The following script is designed to work with the default installation path of plink.exe (“C:\Program Files (x86)\PuTTY\plink.exe”), but you can specify an alternate path for the executable if you wish.  You need to specify a “ComputerName” as well as the remote SSH “Command” you wish to run.  If you are going to bulk run against many systems, you can pass the “UserName”, “SecurePassword”, and even “AutoAcceptKeys” to ignore the SSH key warning.

Here I’m grabbing some uname information from a remote Linux system

7

Here I’m getting the RedHat release info. This time I’m passing the “UserName” and “SecurePassword” so I don’t get prompted

8

Function Code:

Invoke-SSHCommand

function Invoke-SSHCommand {
<#
.SYNOPSIS
Invoke SSH Command on a Remote Computer
.DESCRIPTION
Invoke-SSHCommand uses PuTTY's plink.exe to execute a SSH command on a remote system
.EXAMPLE
Invoke-SSHCommand -Command "uname -a" -ComputerName "myComputer"

DESCRIPTION
-----------
Run uname against the remote computer

.EXAMPLE
$user = "testuser"
$userpw = Read-Host -Prompt "Enter Password" -AsSecureString
Invoke-SSHCommand -Command "touch /tmp/PowerShellHasBeenHere" -ComputerName "myComputer" -UserName $user -SecurePassword $userpw

DESCRIPTION
-----------
Creates a blank file in /tmp on the remote computer.  This time we are passing the user name and password
as parameters
#>

    [CmdletBinding()] param (
        [Parameter(Mandatory=$true)] [String]$ComputerName,
        [Parameter(Mandatory=$true)] [String]$Command,
        [Parameter(Mandatory=$false)] [String]$PathToPlink="C:\Program Files (x86)\PuTTY\plink.exe",
        [Parameter(Mandatory=$false)] [String]$UserName,
        [Parameter(Mandatory=$false)] [Security.SecureString]$SecurePassword,
        [Parameter(Mandatory=$false)] [Switch]$AutoAcceptKeys
    )

    process {
    
        #check for plink.exe file
        Write-Host ""
        if(! (Test-Path $PathToPlink) ) {
            Write-Host "ERROR: $PathToPlink not found" -ForegroundColor Magenta
            return
        }
        #get username if not passed
        if(!$UserName) {
            $UserName = Read-Host -Prompt "Enter User Name"
        }
        #get password if not passed
        if(!$SecurePassword) {
            $SecurePassword = Read-Host -Prompt "Enter Password" -AsSecureString
        }
        Write-Host ""
        
        #convert from secure pw for command execution
        $pw = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecurePassword))
        
        #formulate plink command
        if($AutoAcceptKeys) {
            $plinkCommand = "`"echo yes | `"$PathToPlink`" -ssh -l $UserName -pw $pw $ComputerName `"$Command`"`""
        }
        else {
            $plinkCommand = "`"`"$PathToPlink`" -ssh -l $UserName -pw $pw $ComputerName `"$Command`"`""
        }
        
        #run plink command
        $result = cmd /c $plinkCommand
        
        return $result
    }
}