Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Powershell Cmdlet with 'dynamic' ConfirmImpact attribute setting

I'm writing a Powershell cmdlet that supports ShouldProcess. Instead of having a fixed ConfirmImpact value, I'd like a 'dynamic' value that depends upon the value of a parameter passed to the cmdlet. Let me illustrate with an example.

Let's pretend I'm a web hosting provider. I have many websites and each website belongs to one of the following categories, ordered by importance: Production, Test and Development. As part of my hosting management, I have a Remove-WebSite cmdlet for destroying websites. The following code illustrates this:

Class WebSite {
    [string] $Name
    [string] $Category # Can be one of: Production, Test, Development
}

Function Remove-WebSite {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true)]
        [WebSite] $WebSite
    )
    Write-Host "$($WebSite.Name) was destroyed"
}

At the moment websites are destroyed without confirmation. While this is convenient, too many interns have been destroying production sites by mistake so I'd like a bit more of a safety net on the Remove-WebSite cmdlet by taking advantage of the ShouldProcess feature of Powershell.

So I add the SupportsShouldProcess and ConfirmImpact values to the CmdletBinding attribute. My cmdlet definition becomes:

Function Remove-WebSite {
    [CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact='High')]
    Param(
        [Parameter(Mandatory=$true)]
        [WebSite] $WebSite
    )

    if ($PSCmdlet.ShouldProcess("$($WebSite.Category) site $($WebSite.Name)")) {
        Write-Host "$($WebSite.Name) was destroyed"
    }
}

With this definition, anyone calling the Remote-Website cmdlet is now asked to confirm that they really want to destroy the site. Hardly any production sites are being destroyed by mistake now, except the web developers are complaining that their automated scripts have stopped working.

What I'd really like is that the ConfirmImpact value for the cmdlet to vary at runtime depending on the importance of the category of the web site - High for production sites, Medium for test sites and Low for development sites. The following function definition illustrates this:

Function CategoryToImpact([string]$Category) {
    Switch ($Category) {
        'Production' {
            [System.Management.Automation.ConfirmImpact]::High
            break
        }
        'Test' {
            [System.Management.Automation.ConfirmImpact]::Medium
            break
        }
        'Development' {
            [System.Management.Automation.ConfirmImpact]::Low
            break
        }
        default {
            [System.Management.Automation.ConfirmImpact]::None
            break
        }
    }
}

Function Remove-WebSite {
    [CmdletBinding(SupportsShouldProcess=$true<#,ConfirmImpact="Depends!"#>)]
    Param(
        [Parameter(Mandatory=$true)]
        [WebSite] $WebSite
    )

    # This doesn't work but I hope it illustrates what I'd *like* to do
    #$PSCmdLet.ConfirmImpact = CategoryToImpact($WebSite.Category)

    if ($PSCmdlet.ShouldProcess("$($WebSite.Category) site $($WebSite.Name)")) {
        Write-Host "$($WebSite.Name) was destroyed"
    }
}

Assuming it's possible, how can this be done?

Here's a paste a the complete script plus some test code: http://pastebin.com/kuk6HNm6

like image 305
Dan Stevens Avatar asked Oct 16 '25 14:10

Dan Stevens


2 Answers

This isn't exactly what you're asking for (which I think is strictly speaking, impossible), but it might be a better approach.

Leave ConfirmImpact alone and instead prompt the user with $PSCmdlet.ShouldContinue().

According to the guidance given in Requesting Confirmation from Cmdlets (emphasis mine):

For most cmdlets, you do not have to explicitly specify ConfirmImpact. Instead, use the default setting of the parameter, which is Medium. If you set ConfirmImpact to High, the operation will be confirmed by default. Reserve this setting for highly disruptive actions, such as reformatting a hard-disk volume.

Further:

Most cmdlets request confirmation using only the ShouldProcess method. However, some cases might require additional confirmation. For these cases, supplement the ShouldProcess call with a call to the ShouldContinue method.

...

If a cmdlet calls the ShouldContinue method, the cmdlet must also provide a Force switch parameter. If the user specifies Force when the user invokes the cmdlet, the cmdlet should still call ShouldProcess, but it should bypass the call to ShouldContinue.

Given this guidance, I propose the following changes:

Function Remove-WebSite {
    [CmdletBinding(SupportsShouldProcess=$true)]
    Param(
        [Parameter(Mandatory=$true)]
        [WebSite] $WebSite ,
        [Switch] $Force
    )

    if ($PSCmdlet.ShouldProcess("$($WebSite.Category) site $($WebSite.Name)")) {
        $destroy =
            $Force -or
            $WebSite.Category -ne 'Production' -or
            $PSCmdlet.ShouldContinue("Are you sure you want to destroy $($WebSite.Name)?", "Really destroy this?")
        if ($destroy) {
            Write-Host "$($WebSite.Name) was destroyed"
        }
    }
}
like image 108
briantist Avatar answered Oct 18 '25 04:10

briantist


The simplest solution is to remove the $PSCmdlet.ShouldProcess method call and conditionally call the $PSCmdlet.ShouldContinue method according to our own criteria. The problem with this is that we loose -WhatIf functionality. As briantist points out, $PSCmdlet.ShouldContinue should be used along side $PSCmdlet.ShouldProcess, except this can result in superfluous confirm prompts i.e. the user is prompt twice when once would have sufficed.

With experimentation, I've found that by setting ConfirmImpact='None' in the CmdletBinding attribute declaration, ShouldProcess no longer displays a prompt, but still returns $false if -WhatIf is specified. As a result ShouldProcess and ShouldContinue can be both called and still only have a single prompt displayed to the user. I can then use my own logic to determine whether to call ShouldContinue or not.

Here's a complete solution:

# Represents a website
Class WebSite {
    # The name of the web site
    [string] $Name

    # The category of the website, which can be one of: Production, Test, Development
    [string] $Category # Can be one of

    <#
        Gets the ConfirmImpact level based on Category, as follows:

            Category     ConfirmImpact
            -----------  -------------
            Production   High
            Test         Medium
            Development  Low
            Default      None
    #>
    [System.Management.Automation.ConfirmImpact] GetImpact() {
        Switch ($this.Category) {
            'Production' {
                return [System.Management.Automation.ConfirmImpact]::High
            }
            'Test' {
                return [System.Management.Automation.ConfirmImpact]::Medium
            }
            'Development' {
                return [System.Management.Automation.ConfirmImpact]::Low
            }
        }
        return [System.Management.Automation.ConfirmImpact]::None
    }

    # String representation of WebSite
    [string] ToString() {
        return "$($this.Category) site $($this.Name)"
    }
}

<#
.SYNOPSIS
Destroys a WebSite

.DESCRIPTION
The Remove-WebSite cmdlet permanently destroys a website so use with care.
To avoid accidental deletion, the caller will be prompted to confirm the
invocation of the command if the value of $ConfirmPreference is less than
or equal to the Impact level of the WebSite. The Impact level is based
upon the category, as follows:

    Category     ConfirmImpact
    -----------  -------------
    Production   High
    Test         Medium
    Development  Low
    Default      None

.PARAMETER Website
The WebSite to destroy.

.PARAMETER Force
Destroys website without prompt

.PARAMETER Confirm
Require confirmation prompt always regardless of $ConfirmPreference

.PARAMETER WhatIf
Show what would happen if the cmdlet was run. The cmdlet is not run.

#>
Function Remove-WebSite {
    # Set ConfirmImpact to 'None' so that ShouldProcess automatically returns
    # true without asking for confirmation, regardless of the value of
    # $ConfirmPreference. 
    [CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact='None')]
    Param(
        [Parameter(Mandatory=$true)]
        [WebSite] $WebSite,
        [Switch] $Force
    )

    # Returns true without prompt unless -WhatIf is specified when, in which case
    # false is returned without prompt
    if ($PSCmdlet.ShouldProcess($WebSite)) {

        # Determine whether to continue with the command. Only destroy website if...
        $continue = 
            # ...forced to by Force parameter...
            $Force -or

            #...or the Impact level of the Website is less than $ConfirmPreference...
            $WebSite.GetImpact() -lt $ConfirmPreference -or

            #...or the user clicked 'Yes' in ShouldContinue prompt
            $PSCmdlet.ShouldContinue("Are you sure you want to destroy $($WebSite)?", $null)

        if ($continue) {
            Write-Host "$($WebSite.Name) was destroyed"
        }
    }
}
like image 24
Dan Stevens Avatar answered Oct 18 '25 04:10

Dan Stevens



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!