Get Details About VM Task Reconfigure Events – PowerCLI

When troubleshooting an issue, many times I’ll see a “Reconfigure virtual machine” task in the VM Event log in vCenter. However, there is very little information as to what was actually done.

1

Luckily, you can get some of this information from PowerCLI by querying the event manager. Make sure you have PowerCLI loaded and are connected to vCenter to start

I’m going to use a test VM here which I’ve named “test”. I’m also going to specify that I want to look back 30 days into the event manager

$vmObj = Get-VM -Name "test"
$daysBack = 30

Now I just need to setup the filter to look only for events that have to do with the VM I’m specifying as well as the time frame I’m specifying. I’m using the VMware.Vim.EventFilterSpecByEntity to target the VM and the VMware.Vim.EventFilterSpecByTime to target my time frame.

$dateCurrent = Get-Date
$si = get-view ServiceInstance
$em = get-view $si.Content.EventManager
$EventFilterSpec = New-Object VMware.Vim.EventFilterSpec

#only target reconfiguration events
$EventFilterSpec.Type = "VmReconfiguredEvent"

#only target events on the specified VM
$EventFilterSpec.Entity = New-Object VMware.Vim.EventFilterSpecByEntity
$EventFilterSpec.Entity.Entity = ($vmObj | get-view).MoRef

#only query back to the specified number of days
$EventFilterSpec.Time = New-Object VMware.Vim.EventFilterSpecByTime
$EventFilterSpec.Time.BeginTime = $dateCurrent.adddays(-$daysBack)
$EventFilterSpec.Time.EndTime = $dateCurrent

Now to run the query

#run query
$evts = $em.QueryEvents($EventFilterSpec)

You can see that I’ve gotten back the 5 events seen in my screenshot above from vCenter

2

Since I’m troubleshooting, I don’t really care about every little event. Things like updating the notes or annotations on a VM will show up as events here. I’d rather just look at events where devices on the VM have been changed, which you can see is only 4 events

$deviceChangeEvts = $evts | ?{$_.ConfigSpec.DeviceChange}
$deviceChangeEvts.Length

4

Now I just want to look at a couple details about each change operation. So I’m going to select the device type, the change type, and the file operation in case it was a virtual disk event

$deviceChangeEvts | %{$_.ConfigSpec.DeviceChange} | select Operation,FileOperation,Device | ft -AutoSize

1

You may notice in the output above that there are 5 operations even though there are only 4 events I’m looking at. This is because a single event may have multiple operations. In this example, the first time I edited the VM settings I added a hard drive AND a vNIC at the same time, which you can see if I look only at that first event’s details

5

Now maybe I want some details on the hard drive that was added, like how big it is. You can drill into this info as well. Here I’m looping through each device changed and outputting additional information about the devices. You can see the capacity of that added drive was 10485760KB or 10GB

2

A couple things I’ve run into here. Changing the number of cvPU or the amount of vRAM on a VM doesn’t show up as a device change. So if you care about that and filter out anything that’s not a device change you’ll miss it.

Also, if you look at a change event I wasn’t able to find a way to see what the value was before the change. For example, I grew the size of a virtual hard disk from 40GB to 100GB. I can see the change event and the size of the drive as 100GB, but I don’t think it retains the old value of 40GB anywhere. So this isn’t going to help you get back to the original settings. You will need to do more investigation elsewhere for that ability

Scheduled Task – Audit AD Group Membership with PowerShell

There are a number of AD groups which I must provide membership reports on. Let’s say for the sake of this article that it has to be a weekly report. I can easily set this up using the PowerShell module for AD (provided in the RSAT for desktop OS) and my email function Email Array of Objects Using PowerShell. Once that’s working, I just need to add the full script as a scheduled task on a Windows server.

First off, I’m going to setup the information needed to send out the email

$dateSimple = get-date -UFormat "%m/%d/%Y"
$groupName = "Admin Group"
$to = "myemail@mydomain.com"
$from = "DoNotReply-AdminGroupReport@mydomain.net"
$subject = "Group Membership Report for $groupName on $dateSimple"
$smtp = "mySmtpRelay.mydomain.net"

Now let’s grab the group membership information we need and send the email using my function. Couple notes here. First, you may need to include a line to manually import the AD PS module (Import-Module ActiveDirectory) at the begining of the script. Newer versions of PS do this for you automatically. Also, you need to include the code for my email function in the script or import it as a module.

$groupMembers = Get-ADGroupMember $groupName -Recursive | select Name,SamAccountName,DistinguishedName | sort samaccountname
Send-EmailHTML -To $to -From $from -Subject $subject -SMTPServer $smtp -BodyAsArray $groupMembers

Here is an example of the full code: AuditGroupMembership

The resultant email will look something like this:

1

Now I just need to schedule a task to run this script every week on a Windows server that has PowerShell and the AD PS module installed. I’ll copy the full script out to C:\AuditScript.ps1 on the server. Now I just need to setup the task

I’m going to create a new task on one of my Windows Server 2008 R2 boxes. I’m going to name the task and select the option to run it whether the user is logged in or not. You may want to specify a different user ID here as well. Ideally you would use an account where the password doesn’t change. Otherwise you will have to update the stored password periodically.

2

Going to set my trigger here for weekly on Mondays at 5AM

3

Last, I need to set up the action to run my script with PowerShell. It’s probably best to use the full path to the PowerShell executable, but for simplicity I’m not going to do that here. Then I’m going to pass the full path to my script in as an argument

4

When I go to save the task it will ask me to enter the password for the user ID that will run the script and I’m done. Now I don’t have to manually gather this information for the report, nor do I have to email it off to someone manually. PowerShell and the task scheduler will do all that for me each week!

Cleanup Stale Files on VMWare Datastores – PowerCLI

In large VMWare environments, it’s quite common to have random files leftover on VMFS datastores. Sometimes this is explainable: maybe an admin removes a VM from inventory instead of deleting it from disk and then forgets about it. Other times I’ve seen sVmotion leave entire VMDK files behind after a migration or if a migration fails midway through. Regardless of how it happens, it can be impractical to manually clean this up in a large environment. In order to better identify these leftover disk hogs, I use a PowerCLI function to look for unused directories which I run each month and have the results emailed to myself for review. The function scans the top level directories on a datastore and will consider a directory stale if non of the files within have been modified in the past 30 days (default value)

Since this can take a very long time to run, it outputs what datastore/directory it is currently looking at so you know it isn’t hung up. The function will return an array of custom PS objects that contain the Directory Name with the name of the Datastore that directory lives on for each stale directory identified. It will NOT attempt to delete anything, it is simply for reporting. It works by creating a VimDatastore PSDrive to each datastore and then using the basic Get-ChildItem cmdlet to browse around the files/directories.

You can run it with no parameters and it will use 30 days as the default for StaleAfterDays and run against all datastores returned by Get-Datastore. Or you can ignore local disks by passing SkipNonSharedDatastores and if you are connected to a vCenter instance you can target a specific data center by passing the TargetDatacenter argument.

Get-StaleDatastoreDirs

function Get-StaleDatastoreDirs {
<#
.SYNOPSIS
Get a list of datastore directories where all files are inactive for more than $StaleAfterDays days 
.DESCRIPTION
Get-StaleDatastoreDirs traverses top level datastore directories looking for any directory where all
the files within haven't been written to for over $StaleAfterDays days.  The default is >30 days
.EXAMPLE
Get-StaleDatastoreDirs

DESCRIPTION
-----------
This command will run against all datastores and use a default value of 30 for $StaleAfterDays

.EXAMPLE
Get-StaleDatastoreDirs -StaleAfterDays 60 -TargetDatacenter "My Datacenter" -SkipNonSharedDatastores

DESCRIPTION
-----------
This command will run against datastores in the specified datacenter and return any that have over 60
days of inactivity.  It will also skip any non-shared datastores
#>

    [CmdletBinding()] param (
        [Parameter(Mandatory=$false)] [Int]$StaleAfterDays = 30,
        [Parameter(Mandatory=$false)] [Switch]$SkipNonSharedDatastores,
        [Parameter(Mandatory=$false)] [String]$TargetDatacenter
    )

    process {

        $current = Get-Date
        $listOfStaleDirs = @()
        
        try {
            if( $SkipNonSharedDatastores ) {
                if( $TargetDatacenter ) {
                    $dsList = Get-Datastore -Location (Get-Datacenter $TargetDatacenter -ErrorAction Stop) -ErrorAction Stop | ?{$_.ExtensionData.Summary.MultipleHostAccess}
                }
                else {
                    $dsList = Get-Datastore -ErrorAction Stop | ?{$_.ExtensionData.Summary.MultipleHostAccess}
                }
            }
            else {
                if( $TargetDatacenter ) {
                    $dsList = Get-Datastore -Location (Get-Datacenter $TargetDatacenter -ErrorAction Stop) -ErrorAction Stop
                }
                else {
                    $dsList = Get-Datastore -ErrorAction Stop
                }
            }
            $dsList = $dsList | sort Name
        }
        catch {
            Write-Host $_ -ForegroundColor Magenta
            return
        }
        
        foreach ($datastore in $dsList) {

            Write-Host "Scanning datastore:" $datastore.Name
            New-PSDrive -Location $datastore -Name ds -PSProvider VimDatastore -Root '\' | Out-Null
            
            [array]$dirs = Get-ChildItem -Path 'ds:\' | ?{$_.Name -ne ".vSphere-HA" -and $_.ItemType -eq "Folder"} | sort Name
            
            foreach ($dir in $dirs) {
                Write-Host "`tScanning directory:" $dir.Name
                $numOfStaleFiles = 0
                [array]$subfiles = Get-ChildItem -Path $dir.FullName -Recurse | ?{$_.ItemType -ne "Folder"}
                
                #check to see if current file has been written to in $staleAfterDays num of days
                foreach ($file in $subfiles) {
                    if ( ($current.Subtract($file.LastWriteTime)).TotalDays -gt $StaleAfterDays ) {
                        $numOfStaleFiles++
                    }
                }
                
                #if all files in directory haven't been written to in $staleAfterDays num of days
                if($subfiles.length -eq $numOfStaleFiles) {
                    Write-Host "`tFound directory stale for over $StaleAfterDays days:" $dir.Name -ForegroundColor Cyan
                    $custDirObj = New-Object PSObject -Property @{
                        DatastoreName = $datastore.Name
                        DirectoryName = $dir.Name
                    }
                    $listOfStaleDirs += $custDirObj
                }
            }
            Remove-PSDrive ds
        }

        return $listOfStaleDirs
    }
}

Get VMFS Datastores That Were Upgraded From VMFS3 – PowerCLI

I found that I wasn’t taking full advantage of VAAI because some of my VMFS-5 datastores had different block sizes. This was causing some VM deployments to sporadically run long. After some digging I found a few articles on the differences between upgrading a VMFS datastore and building on new. One good article is VMFS5 VMFS-3, What’s the Deal?

Ok, now that I know that upgrading a VMFS datastore has some drawbacks, I want to identify them all so I can rebuild them all new. I could just look for any VMFS-5 datastores with block sizes that aren’t set to 1MB, but that wouldn’t grab everything since VMFS-3 could use 1MB as well. That’s when I stumbled on another great blog about detecting the upgraded datastores by start sector using ESXCLI How to detect VMFS3 and VMFS3 Upgraded Datastores with PowerCLI; Now with more sugar!.

Great post and super simple way to get the NAA ID’s, but it still left me having to do some manual work. I’d have multiple entries for the same datastore and I’d have to coorelate the NAA ID back to a datastore name for my other scripts. I also wanted just VMFS-5 upgraded datastores, so I have a example usage at the bottom which filters out anything not VMFS-5

I did struggle with how to best do this. I couldn’t find an easy way to pull the datastore name from the NAA ID, so I’m just scanning all the datastores on a host looking for a match. That also required me to build a custom object to store the ESXi host name with each NAA ID in order to do this scan later. Not ideal, but it gets me what I wanted.

Function Code:

Get-DatastoresWithStartSector

function Get-DatastoresWithStartSector ($StartSector) {
    
    $customResults = @()
    $datastores = @()
    
    #search through all hosts for datastores with a start sector value of $StartSector
    Get-VMHost | % {
        
        try {
            $esxcli = Get-EsxCli -VMHost $_ -ErrorAction Stop
        
            $foundDevices = $esxcli.storage.core.device.partition.list() | ?{ $_.StartSector -eq $StartSector }
            
            foreach ($device in $foundDevices) {
                $customResults += New-Object PSObject -Property @{
                    NAA = $device.Device
                    VMHost = $_
                }
            }
        }
        catch {
            Write-Host "Get-EsxCli error on host" -ForegroundColor Magenta
            Write-Host "$_" -ForegroundColor Magenta
        }
    }
    
    #remove duplicates from the list gathered above and match the datastore name to device number
    foreach ( $custom in ($customResults | Sort NAA -Unique) ) {
        $datastores += Get-Datastore -VMHost $custom.VMHost | ?{$_.ExtensionData.Info.Vmfs.Extent.diskname -eq $custom.NAA}
    }
    
    return $datastores | Sort Name
}

#EXAMPLE: get all datastores with old start sector that are at VMFS version 5
#Get-DatastoresWithStartSector 128 | ?{$_.ExtensionData.Info.vmfs.MajorVersion -ne 5}

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)) )
        
    }
}