Getting Detailed Task Information With PowerCLI (Function)

If you are reading this there is a good chance that you have some experience with VMware PowerCLI. If that is the case, you are probably aware that PowerCLI is a solid, mature product. However, as any other software tool, it has some improvement opportunities. One of the most notable examples of its few shortcomings is, in my opinion, the Get-Task cmdlet. The Get-Task default output is quite limited compared to the information displayed by the vCenter or ESXi Task Panel.

In PowerShell, when you need to customize the output you can simply use the Select-Object and Format- cmdlets. But with Get-Task doing so does not make it much better. The output won't include the task's Target, Initiator (username) or Details. The following is the result of piping Get-Task to Get-Member. Take a look at the properties, some critical information is definitely missing. Also notice the ExtensionData property and stick a pin in it, we'll come back to it later.

Function Code

I thought it would be a good idea to invert the usual order for this post. Let's start by taking a look at the full function code and then go over it step by step.

Function Get-VTask {

<#
.SYNOPSIS
Retrieves detailed task information from a vCenter or ESXi server.

.DESCRIPTION
This function retrieves detailed task information from a vCenter or ESXi
server. By default the 'Get-Task' PowerCLI cmdlet does not display basic
information like username and entity / target.

.EXAMPLE
Get-VTask | Format-Table

Retrieves detailed information for all tasks, then formats the output as
table. The default output is formatted as list.

.EXAMPLE
Get-VTask -Username 'MyDomain\John.Doe' -Status Running

Retrieves detailed information for all running tasks started by user
'MyDomain\Jonh.Doe'.

.EXAMPLE
Get-VTask -Id Task-task-1511207

Retrieves detailed information for task with Id 'Task-task-1511207'.
#>
    
    #region Parameter Block
    
    <#
    PowerCLI original Get-Task cmdlet, has two parameter sets, one uses
    'Id' and another one uses 'Status'. Therefore, this function needs the
    same parameter sets. Parameters 'Entity' and 'Username' are added to
    both parameter sets as they are not mutually exclusive and they do not
    conflict neither with Id nor with Status.
    #>

    [CmdletBinding (DefaultParameterSetName = 'Status')]
    param (
        
        [Parameter (ParameterSetName = 'Status')]
        [ValidateSet ('Cancelled','Error','Queued','Running','Success','Unknown')]
        #Same value option from Get-Task
            [string]$Status,
        
        [Parameter (ParameterSetName = 'Id')]
            [string[]]$Id,
        
        [Parameter (ParameterSetName = 'Status')]
        [Parameter (ParameterSetName = 'Id')]
            [string]$Entity,
            <#Default Entity value set to * to ensure results include all
            entities when omitted in the command.#>
        
        [Parameter (ParameterSetName = 'Status')]
        [Parameter (ParameterSetName = 'Id')]
            [string]$Username
            <#Default Username value set to * to ensure results include
            all entities when omitted in the command.#>
    )

    #endregion Parameter Block

    BEGIN {

    <#
    The BEGIN block determines which task objects will be processed by
    filtering them based on Id, Username, Status, Entity or a valid
    combination of these parameters. These filtered objects will be the
    source of data for the new custom objects that will be generated in
    the PROCESS block.
    #>
        switch ($PSBoundParameters) {
            {$_.ContainsKey('Status')} {$Tasks = Get-Task -Status $Status}
            {$_.ContainsKey('Id')} {$Tasks = Get-Task -Id $Id}
            Default {$Tasks = Get-Task}
        } <#This switch statement determines whether Status or Id were
            specified in the command, only one is allowed by parameter
            sets. If not, all tasks are retrieved. Otherwise, Task
            objects are returned based on the value of either parameter,
            and stored in the $Tasks variable.#>


        If ($PSBoundParameters.ContainsKey('Username')) {
            $Tasks = $Tasks | Where-Object {$_.ExtensionData.Info.Reason.UserName -eq $Username}
        } <#If a value for Username was specified objects are filtered
            based on the value entered.#>

        If ($PSBoundParameters.ContainsKey('Entity')) {
            $Tasks = $Tasks | Where-Object {$_.ExtensionData.Info.EntityName -eq $Entity}
        } <#If a value for Entity was specified objects are filtered
            based on the value entered.#>

    }

    PROCESS {
        
        foreach ($Task in $Tasks) {
            <# Custom object is created with new parameters that expand
            the information provided by the original Get-Task cmdlet#>
            $TaskObject = [PSCustomObject] @{
                'Entity' = $Task.ExtensionData.Info.EntityName
                'Description' = $Task.Description
                'Status' = $Task.State
                'Progress' = $Task.PercentComplete
                'Username' = $Task.ExtensionData.Info.Reason.UserName
                'Message' = $Task.ExtensionData.Info.Description.Message
                'Id' = $Task.Id
                'StartTime' = $Task.ExtensionData.Info.StartTime
                'CompleteTime' = $Task.ExtensionData.Info.CompleteTime
                'IsCancellable' = $Task.ExtensionData.Info.Cancelable
            }
            
            #region set new object type
            #The following lines set the object's new type to 'System.Custom.VTask'
            $TaskObject.PSObject.TypeNames.Clear() 
            $TaskObject.PSObject.TypeNames.Add('System.Custom.VTask')
            #endregion set new object type

            Write-Output $TaskObject #Displays the new objects onscreen
        }

    }

    END {}
}

The comments will hopefully make the code easier to understand. You will notice the use of multiline comments and code regions, last week we covered them in this post.

The name of the function is 'Get-VTask' in order to avoid confusions and conflicts with the original Get-Task function. There must be better names, so feel free to change it to anything you like.

The first comment block is the comment-based help which we will not discuss today but should be pretty self explanatory, for more information read this. Also you can run 'help Get-VTask' on the command line to see it in action.

ExtensionData

As mentioned before, ExtensionData is a property of the Task objects, in fact, is a property of most PowerCLI objects. ExtensionData allows access to all vSphere API properties and methods. Try running this: '(Get-Task | Select-Object -First 1).ExtensionData'. This command will return the ExtensionData property values for the first Task object. In the output, if you see anything that starts with 'VMware.Vim' it can probably be expanded -like Info and Client in our example. To expand Info, run '(Get-Task | Select-Object -First 1).ExtensionData.Info'. The output now includes way more information, including EntityName. Finally, run '(Get-Task | Select-Object -First 1).ExtensionData.Info.Reason', now we have found a Username property. Below is this sequence of commands and outputs.

PowerCLI_ExtensionData

This function uses ExtensionData to filter Task objects and retrieve their properties.

Parameter Block

In the parameter block region we build two parameter sets, Status and Id, Status is set as default. Look at the syntax in the help of the Get-Task cmdlet (help Get-Task). You will notice there are two parameter sets, one for Status and one for Id. Since we are building a wrapper of Get-Task, we will align our function to these sets.

The following parameters are specified in the parameter block:

  • Status: values are limited to 'Cancelled', 'Error', 'Queued', 'Running', 'Success', 'Unknown' like in Get-Task. Added to the Status parameter set.
  • Id: It accepts multiple values, notice the ' [string[]]' data type. Added to the Id parameter set.
  • Entity: Added to both parameter sets.
  • Username: Added to both parameter sets.
[CmdletBinding (DefaultParameterSetName = 'Status')]
param (
        
    [Parameter (ParameterSetName = 'Status')]
    [ValidateSet ('Cancelled','Error','Queued','Running','Success','Unknown')]
    #Same value option from Get-Task
        [string]$Status,
        
    [Parameter (ParameterSetName = 'Id')]
        [string[]]$Id,
        
    [Parameter (ParameterSetName = 'Status')]
    [Parameter (ParameterSetName = 'Id')]
        [string]$Entity,
        <#Default Entity value set to * to ensure results include all
        entities when omitted in the command.#>
        
    [Parameter (ParameterSetName = 'Status')]
    [Parameter (ParameterSetName = 'Id')]
        [string]$Username
        <#Default Username value set to * to ensure results include
        all entities when omitted in the command.#>
)

BEGIN Block

In the BEGIN block, a switch statement determines the original task objects that will be processed by the PROCESS block. The '$PSBoundParameters' variable is generated automatically by Powershell and contains the parameters specified in the command. The ContainsKey method checks whether the specified value is included in $PSBoundParameters. So, if Status was specified we run 'Get-Task -Status $Status' inside the function to get the task objects, and the same applies for Id. If neither is specified in the command the default action is simply 'Get-Task'. The resulting objects are stored in the $Tasks variable.

switch ($PSBoundParameters) {
    {$_.ContainsKey('Status')} {$Tasks = Get-Task -Status $Status}
    {$_.ContainsKey('Id')} {$Tasks = Get-Task -Id $Id}
    Default {$Tasks = Get-Task}
}

Once the Task objects are filtered based on Status and Id, we use Where-Object to further filter the results if required, based on Username and/or Entity, depending on how the command was structured. This updates the $Tasks variable with the filtered objects.

If ($PSBoundParameters.ContainsKey('Username')) {
    $Tasks = $Tasks | Where-Object {$_.ExtensionData.Info.Reason.UserName -eq $Username}
} <#If a value for Username was specified objects are filtered
    based on the value entered.#>

If ($PSBoundParameters.ContainsKey('Entity')) {
    $Tasks = $Tasks | Where-Object {$_.ExtensionData.Info.EntityName -eq $Entity}
} <#If a value for Entity was specified objects are filtered
    based on the value entered.#>

PROCESS Block

The PROCESS block processes each object stored in the $Tasks variables which was set in the BEGIN block. A foreach construct loops through the Task objects and uses the PSCustomObject type accelerator to create new objects with the following properties: Entity, Description, Status, Progress, Username, Message, Id, StartTime, CompleteTime and IsCancellable. We use ExtensionData to get most of these properties.

Note: The New-Object cmdlet is an alternative to the PSCustomObject type accelerator. However, I personally prefer the latter because it gives me control over how the properties are sorted. New-Object doesn't necessarily keep the object's properties in the order in which they are specified.

<# Custom object is created with new parameters that expand
the information provided by the original Get-Task cmdlet#>
$TaskObject = [PSCustomObject] @{
    'Entity' = $Task.ExtensionData.Info.EntityName
    'Description' = $Task.Description
    'Status' = $Task.State
    'Progress' = $Task.PercentComplete
    'Username' = $Task.ExtensionData.Info.Reason.UserName
    'Message' = $Task.ExtensionData.Info.Description.Message
    'Id' = $Task.Id
    'StartTime' = $Task.ExtensionData.Info.StartTime
    'CompleteTime' = $Task.ExtensionData.Info.CompleteTime
    'IsCancellable' = $Task.ExtensionData.Info.Cancelable
}

Then we change the object's type to 'System.Custom.Task' by editing the 'TypeNames' property, using the Clear and Add methods. First we remove all types from the objects, and then we add the 'System.Custom.VTask' type. Pipe Get-VTask to Get-Member and look at the first line of the output: 'TypeName: System.Custom.VTask'. This is the output object type.

Finally Write-Output displays the new objects onscreen.

#region set new object type
#The following lines set the object's new type to 'System.Custom.VTask'
$TaskObject.PSObject.TypeNames.Clear() 
$TaskObject.PSObject.TypeNames.Add('System.Custom.VTask')
#endregion set new object type

Write-Output $TaskObject #Displays the new objects onscreen

Output

PowerShell's formatting system lists all of the properties by default, unless there is a view for the output object type ('System.Custom.VTask' in this case). We can create views using Format.ps1xml files, but this is out of scope for now. The Get-VTask function will display the output in list format due to the amount of properties the objects have.

That said, we can use the Format cmdlets (Format-Table, Format-List, Format-Wide, etc.) as well as Sort-Object to modify the output format. For example:

Get-VTask -Username 'MyCompany\JDoe' | Select-Object Entity, Progress, Status, Message | Format-Table -AutoSize

Get-VTask_Output

Limitations

As stated earlier, the Get-VTask function is a wrapper of Get-Task, it leverages filters and ExtensionData to produce a more informative output by creating new objects. As a result, there are some limitations to consider:

  1. We cannot pipe the output of Get-VTask to Stop-Task. Stop-Task does not accept pipeline input ByPropertyName. It does accept pipeline input ByValue but the output object of Get-VTask is clearly not the type Stop-Task expects.
  2. It takes longer to run. Applying filters and pulling information with ExtensionData is less efficient than running Get-Task directly.  The amount of time required to generate the output is proportional to the amount of objects that must be processed.
  3. You cannot use wildcards in parameter values. The filters inside the function look for exact matches only.
  4. Get-Task exhibits the typical cmdlet streaming behavior. This means that you see the objects on the screen as PowerShell processes them one by one. With Get-VTask the Task objects are first stored in the $Tasks variable, then a foreach loop creates new objects from the original objects. This process causes a delay in the output when the Username and/or Entity parameters are specified because each one puts the whole set of objects through a filter. The results are then displayed all at once, in a single block at the very end of the function execution (when the foreach loop runs).

Alternative Function

If you prefer to see the results as they become available, here is an alternative function that addresses the behavior described in point 4. This function uses the ForEach-Object cmdlet instead of a foreach loop. The ForEach-Object cmdlet uses the pipeline whereas the foreach loop doesn't. Then the pipeline processes objects one by one, this is the key difference. This applies to filters and output which is why we see the results right away and not at the end of the commands execution. In addition, this function is structured to evaluate all possible combinations of the Entity and Username parameters so that only one filter is applied to the Task objects in every scenario.

The downside of this approach is that the code is longer, and it may also take a little longer to run since the foreach loop runs faster than the ForEach-Object cmdlet.

Here is the function code:

Function Get-ViTask {
 
<#
.SYNOPSIS
Retrieves detailed task information from a vCenter or ESXi server.
 
.DESCRIPTION
This function retrieves detailed task information from a vCenter or ESXi
server. By default the 'Get-Task' PowerCLI cmdlet does not display basic
information like username and entity / target.
 
.EXAMPLE
Get-VTask | Format-Table
 
Retrieves detailed information for all tasks, then formats the output as
table. The default output is formatted as list.
 
.EXAMPLE
Get-VTask -Username 'MyDomain\John.Doe' -Status Running
 
Retrieves detailed information for all running tasks started by user
'MyDomain\Jonh.Doe'.
 
.EXAMPLE
Get-VTask -Id Task-task-1511207
 
Retrieves detailed information for task with Id 'Task-task-1511207'.
#>
     
    #region Parameter Block
     
    <#
    PowerCLI original Get-Task cmdlet, has two parameter sets, one uses
    'Id' and another one uses 'Status'. Therefore, this function needs
    the same parameter sets. Parameters 'Entity' and 'Username' are added
    to both parameter sets as they are not mutually exclusive and they do
    not conflict neither with Id nor with Status.
    #>
 
    [CmdletBinding (DefaultParameterSetName = 'Status')]
    param (
         
        [Parameter (ParameterSetName = 'Status')]
        [ValidateSet ('Cancelled','Error','Queued','Running','Success','Unknown')]
        #Same options from Get-Task
            [string]$Status,
         
        [Parameter (ParameterSetName = 'Id')]
            [string[]]$Id,
         
        [Parameter (ParameterSetName = 'Status')]
        [Parameter (ParameterSetName = 'Id')]
            [string]$Entity,
            <#Default Entity value set to * to ensure results include
              all entities when omitted in the command.#>
         
        [Parameter (ParameterSetName = 'Status')]
        [Parameter (ParameterSetName = 'Id')]
            [string]$Username
            <#Default Username value set to * to ensure results include
              all entities when omitted in the command.#>
    )
 
    #endregion Parameter Block
 
    BEGIN {
 
    <#
    The BEGIN block determines which task objects will be processed by
    filtering them based on Id, Username, Status, Entity or a valid
    combination of these parameters. These filtered objects will be the
    source of data for the new custom objects that will be generated in
    the PROCESS block.
    #>
        switch ($PSBoundParameters) {
            {$_.ContainsKey("Status")} {$Tasks = Get-Task -Status $Status}
            {$_.ContainsKey("Id")} {$Tasks = Get-Task -Id $Id}
            Default {$Tasks = Get-Task}
    } <#This switch statement determines whether Status or Id were
        specified in the command, only one is allowed by parameter sets.
        If not, all tasks are retrieved. Otherwise, Task objects are
        returned based on the value of either parameter, and stored in
        the $Tasks variable.#>
 
    } #BEGIN
 
    PROCESS {
         
        If ($PSBoundParameters.ContainsKey("Username") -and $PSBoundParameters.ContainsKey("Entity")) {
            $Tasks | Where-Object {$_.ExtensionData.Info.Reason.UserName -eq $Username -and $_.ExtensionData.Info.EntityName -eq $Entity} |
            ForEach-Object -Process {
                <# Custom object is created with new parameters that
                   extend the information provided by the original Get-
                   Task cmdlet#>
                    $TaskObject = [PSCustomObject] @{
                        "Entity" = $_.ExtensionData.Info.EntityName
                        "Description" = $_.Description
                        "Status" = $_.State
                        "Progress" = $_.PercentComplete
                        "Username" = $_.ExtensionData.Info.Reason.UserName
                        "Message" = $_.ExtensionData.Info.Description.Message
                        "Id" = $_.Id
                        "StartTime" = $_.ExtensionData.Info.StartTime
                        "CompleteTime" = $_.ExtensionData.Info.CompleteTime
                        "IsCancellable" = $_.ExtensionData.Info.Cancelable
                    }
 
                #region set new object type
                <#The following lines set the object's new type to
                  "System.Custom.VTask"#>
                $TaskObject.PSObject.TypeNames.Clear() 
                $TaskObject.PSObject.TypeNames.Add("System.Custom.VTask")
                #endregion set new object type
 
                $TaskObject | Write-Output
                #Displays the new objects onscreen
            }
        } <#If values for Username and Entity were specified objects are
            filtered based on the values entered.#>
                 
        ElseIf ($PSBoundParameters.ContainsKey("Username")) {
            $Tasks | Where-Object {$_.ExtensionData.Info.Reason.UserName -eq $Username} |
            ForEach-Object -Process {
                <# Custom object is created with new parameters that
                   extend the information provided by the original Get-
                   Task cmdlet#>
                    $TaskObject = [PSCustomObject] @{
                        "Entity" = $_.ExtensionData.Info.EntityName
                        "Description" = $_.Description
                        "Status" = $_.State
                        "Progress" = $_.PercentComplete
                        "Username" = $_.ExtensionData.Info.Reason.UserName
                        "Message" = $_.ExtensionData.Info.Description.Message
                        "Id" = $_.Id
                        "StartTime" = $_.ExtensionData.Info.StartTime
                        "CompleteTime" = $_.ExtensionData.Info.CompleteTime
                        "IsCancellable" = $_.ExtensionData.Info.Cancelable
                    }
 
                #region set new object type
                <#The following lines set the object's new type to
                  "System.Custom.VTask"#>
                $TaskObject.PSObject.TypeNames.Clear() 
                $TaskObject.PSObject.TypeNames.Add("System.Custom.VTask")
                #endregion set new object type
 
                $TaskObject | Write-Output
                #Displays the new objects onscreen
            }
        } <#If a value for Username was specified objects are filtered
           based on the value entered.#>
             
        ElseIf ($PSBoundParameters.ContainsKey("Entity")) {
            $Tasks | Where-Object {$_.ExtensionData.Info.EntityName -eq $Entity} |
            ForEach-Object -Process {
                <#Custom object is created with new parameters that
                  extend the information provided by the original Get-
                  Task cmdlet#>
                    $TaskObject = [PSCustomObject] @{
                        "Entity" = $_.ExtensionData.Info.EntityName
                        "Description" = $_.Description
                        "Status" = $_.State
                        "Progress" = $_.PercentComplete
                        "Username" = $_.ExtensionData.Info.Reason.UserName
                        "Message" = $_.ExtensionData.Info.Description.Message
                        "Id" = $_.Id
                        "StartTime" = $_.ExtensionData.Info.StartTime
                        "CompleteTime" = $_.ExtensionData.Info.CompleteTime
                        "IsCancellable" = $_.ExtensionData.Info.Cancelable
                    }
 
                #region set new object type
                <#The following lines set the object's new type to
                  "System.Custom.VTask"#>
                $TaskObject.PSObject.TypeNames.Clear() 
                $TaskObject.PSObject.TypeNames.Add("System.Custom.VTask")
                #endregion set new object type
 
                $TaskObject | Write-Output
                #Displays the new objects onscreen
            }
        } <#If a value for Entity was specified objects are filtered
            based on the value entered.#>
 
 
        Else {
            $Tasks | ForEach-Object -Process {
                <#Custom object is created with new parameters that
                  extend the information provided by the original Get-
                  Task cmdlet#>
                    $TaskObject = [PSCustomObject] @{
                        "Entity" = $_.ExtensionData.Info.EntityName
                        "Description" = $_.Description
                        "Status" = $_.State
                        "Progress" = $_.PercentComplete
                        "Username" = $_.ExtensionData.Info.Reason.UserName
                        "Message" = $_.ExtensionData.Info.Description.Message
                        "Id" = $_.Id
                        "StartTime" = $_.ExtensionData.Info.StartTime
                        "CompleteTime" = $_.ExtensionData.Info.CompleteTime
                        "IsCancellable" = $_.ExtensionData.Info.Cancelable
                    }
 
                #region set new object type
                <#The following lines set the object's new type to
                  "System.Custom.VTask"#>
                $TaskObject.PSObject.TypeNames.Clear() 
                $TaskObject.PSObject.TypeNames.Add("System.Custom.VTask")
                #endregion set new object type
 
                $TaskObject | Write-Output
                #Displays the new objects onscreen
            }
        } <#If no values were specified objects are not filtered and all
            tasks are returned.#>
  
    } #PROCESS
 
    END {}
}

I will not get into too many details but you can play with this function as you see fit. Either function generates the same output.

Feel free to leave comments and questions below.

The source code for this post is available in my GitHub repository.