Командлет Powershell с «динамической» настройкой атрибута ConfirmImpact

Я пишу командлет Powershell, который поддерживает ShouldProcess. Вместо фиксированного значения ConfirmImpact мне нужно «динамическое» значение, которое зависит от значения параметра, переданного командлету. Позвольте мне проиллюстрировать это примером.

Давайте представим, что я провайдер веб-хостинга. У меня много веб-сайтов, и каждый веб-сайт принадлежит к одной из следующих категорий, упорядоченных по важности: Production, Test и Development. В рамках управления хостингом у меня есть командлет Remove-WebSite для уничтожения веб-сайтов. Следующий код иллюстрирует это:

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

На данный момент сайты уничтожаются без подтверждения. Хотя это удобно, слишком много стажеров по ошибке уничтожали производственные сайты, поэтому я хотел бы немного больше подстраховки в командлете Remove-WebSite, воспользовавшись преимуществами ShouldProcess функции Powershell.

Поэтому я добавляю значения SupportsShouldProcess и ConfirmImpact к атрибуту CmdletBinding. Мое определение командлета становится следующим:

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

С этим определением любого, кто вызывает командлет Remote-Website, теперь просят подтвердить, что он действительно хочет уничтожить сайт. Вряд ли какие-либо производственные сайты сейчас уничтожаются по ошибке, за исключением того, что веб-разработчики жалуются на то, что их автоматизированные скрипты перестали работать.

Мне бы очень хотелось, чтобы значение ConfirmImpact для командлета менялось во время выполнения в зависимости от важности категории веб-сайта — High для рабочих сайтов, Medium для тестовых сайтов и Low для сайтов разработки. Это иллюстрирует следующее определение функции:

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

Предполагая, что это возможно, как это можно сделать?

Вот вставка полного скрипта и тестового кода: http://pastebin.com/kuk6HNm6


person Dan Stevens    schedule 27.05.2016    source источник
comment
Вы можете поставить BEGIN{Switch($WebSite){{$_.Catagory -eq "High" -and $PSCmdlet.ShouldProcess("$($WebSite.Category) site $($WebSite.Name)"}{"$($WebSite.Name) was destroyed"};default {"$($WebSite.Name) was destroyed"}} вместо вашего If утверждения, поэтому, если влияние невелико, оно не будет запрашивать   -  person TheMadTechnician    schedule 27.05.2016


Ответы (2)


Это не совсем то, о чем вы просите (что, строго говоря, невозможно), но это может быть лучший подход.

Оставьте ConfirmImpact в покое и вместо этого подскажите пользователю с помощью $PSCmdlet.ShouldContinue().

В соответствии с инструкциями, приведенными в разделе Запрос подтверждения от командлетов (выделено мной):

Для большинства командлетов не требуется явно указывать ConfirmImpact. Вместо этого используйте значение параметра по умолчанию, которое равно Medium. Если для параметра ConfirmImpact установлено значение High, операция будет подтверждена по умолчанию. Зарезервируйте этот параметр для крайне разрушительных действий, таких как переформатирование тома жесткого диска.

Дальше:

Большинство командлетов запрашивают подтверждение, используя только метод ShouldProcess. Однако в некоторых случаях может потребоваться дополнительное подтверждение. В этих случаях дополните вызов ShouldProcess вызовом метода ShouldContinue.

...

Если командлет вызывает метод ShouldContinue, он также должен предоставить параметр переключения Force. Если пользователь указывает Force при вызове командлета, командлет все равно должен вызывать ShouldProcess, но должен пропускать вызов ShouldContinue.

Учитывая это руководство, я предлагаю следующие изменения:

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"
        }
    }
}
person briantist    schedule 27.05.2016
comment
Спасибо за предложение метода ShouldContinue. Похоже, что это более прямой метод запроса подтверждения от пользователя, чем ShouldProcess, где сообщение подтверждения зависит от переменной $ConfirmPreference и значения атрибута ConfirmImpact. Я должен иметь возможность использовать это для реализации моей собственной политики подтверждения для этой команды. Еще один связанный с этим вопрос: похоже, что использование ShouldContinue работает без SupportsShouldProcess, установленного на $true. Как вы думаете, мне все равно следует установить для этого параметра значение true, даже если я не использую метод ShouldProcess? - person Dan Stevens; 31.05.2016
comment
@DanStevens страница, на которую я ссылаюсь, предлагает использовать ShouldContinue в дополнение к ShouldProcess. Независимо от того, работает это технически или нет, я думаю, вам все равно следует использовать ShouldProcess, чтобы вы могли должным образом поддерживать параметр -WhatIf, который полезен для всех, кто использует вашу функцию. Если вы не используете ShouldProcess (или вы никаким образом не поддерживаете -WhatIf), вам определенно не следует использовать SupportsShouldProcess, потому что вы будете ложно сообщать вызывающему абоненту, что они могут безопасно вызывать его с помощью -WhatIf без каких-либо последствий. - person briantist; 31.05.2016
comment
Проблема с использованием ShouldProcess и ShouldContinue в том виде, в котором они используются, заключается в том, что я получаю 2 отдельных подтверждения, когда мне нужно только 1. Я думаю, что могу обойти это, установив ConfirmImpact='Low' в атрибуте CmdletBinding. - person Dan Stevens; 31.05.2016
comment
@DanStevens, как я использую его в своем примере, должен когда-либо запрашивать пользователя только один раз, пока ConfirmImpact остается в покое (наследует Medium). - person briantist; 31.05.2016
comment
Если для $ConfirmPreference установлено значение по умолчанию High, он будет запрашивать только один раз, но если $ConfirmPreference равно Medium или Low, пользователь будет спрошен дважды. - person Dan Stevens; 31.05.2016
comment
@DanStevens странно .. это кажется обратным, и по умолчанию должно быть Medium, а не High. - person briantist; 31.05.2016
comment
Давайте продолжим обсуждение в чате. - person Dan Stevens; 31.05.2016

Самое простое решение — удалить вызов метода $PSCmdlet.ShouldProcess и условно вызвать метод $PSCmdlet.ShouldContinue в соответствии с нашими собственными критериями. Проблема в том, что мы теряем -WhatIf функциональность. Как указывает briantist, $PSCmdlet.ShouldContinue следует использовать вместе с $PSCmdlet.ShouldProcess, за исключением того, что это может привести к излишним запросам на подтверждение, т.е. пользователю будет предложено дважды, хотя одного раза было бы достаточно.

Экспериментируя, я обнаружил, что при установке ConfirmImpact='None' в объявлении атрибута CmdletBinding ShouldProcess больше не отображает подсказку, но по-прежнему возвращает $false, если указано -WhatIf. В результате ShouldProcess и ShouldContinue могут быть вызваны одновременно, но пользователю будет отображаться только одно приглашение. Затем я могу использовать свою собственную логику, чтобы определить, следует ли вызывать ShouldContinue или нет.

Вот полное решение:

# 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"
        }
    }
}
person Dan Stevens    schedule 31.05.2016
comment
Хороший самостоятельный ответ! :) - person briantist; 01.06.2016