Public/Start-WindowsActivation.ps1

#Requires -Version 5

<#
.Synopsis
Installs product keys or activates Windows
.DESCRIPTION
A drop in replacement for slmgr script. By default attempts activation using the product
key already installed on the machine. Use -UseKmsClientKey to also install the
KMS client setup key (GVLK) for the detected OS edition before activating. This is a
material licensing change and is therefore opt-in. Use -ProductKey to install an
explicit product key before activating it in the same operation.
.INPUTS
string[]. You can pass the computer names
.OUTPUTS
slmgr-ps.LicensingOperationResult. One result is emitted for each attempted computer.
Throws one aggregate error after processing the requested computer batch when any
computer fails.
.EXAMPLE
Start-WindowsActivation -Verbose
.EXAMPLE
Start-WindowsActivation -UseKmsClientKey -Verbose
.EXAMPLE
Start-WindowsActivation -ProductKey XXXXX-XXXXX-XXXXX-XXXXX-XXXXX
.EXAMPLE
Start-WindowsActivation -ActivationId aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee
.EXAMPLE
Start-WindowsActivation -Computer WS01 -Credentials (Get-Credential)
.EXAMPLE
Start-WindowsActivation -Computer WS01, WS02 -CacheDisabled
.EXAMPLE
Start-WindowsActivation -Computer WS01 -KMSServerFQDN server.domain.net -KMSServerPort 2500
.EXAMPLE
Start-WindowsActivation -ReArm
.EXAMPLE
Start-WindowsActivation -ReArm -ApplicationId 11111111-2222-3333-4444-555555555555
.EXAMPLE
Start-WindowsActivation -ReArm -ActivationId aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee
.EXAMPLE
Start-WindowsActivation -Offline -ConfirmationID 123456-123456-123456-123456-123456-123456-123456-123456-123456
.LINK
https://github.com/zbalkan/slmgr-ps
#>

function Start-WindowsActivation
{
    [OutputType([PSCustomObject])]
    [CmdletBinding(SupportsShouldProcess = $true,
        PositionalBinding = $false,
        ConfirmImpact = 'High',
        DefaultParameterSetName = 'ActivateWithKMS')]
    Param
    (
        [Parameter(Mandatory = $false,
            Position = 0,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            ValueFromRemainingArguments = $false)]
        [Parameter(ParameterSetName = 'ActivateWithKMS')]
        [Parameter(ParameterSetName = 'Rearm')]
        [Parameter(ParameterSetName = 'Offline')]
        [AllowNull()]
        [string[]]
        $Computer = @('localhost'),

        [Parameter(Mandatory = $false,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false,
            ValueFromRemainingArguments = $false)]
        [Parameter(ParameterSetName = 'ActivateWithKMS')]
        [Parameter(ParameterSetName = 'Rearm')]
        [Parameter(ParameterSetName = 'Offline')]
        [AllowNull()]
        [PSCredential]
        $Credentials,

        [Parameter(Mandatory = $false,
            Position = 1,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false,
            ValueFromRemainingArguments = $false,
            ParameterSetName = 'ActivateWithKMS')]
        [Alias('KmsServer')]
        [ValidateNotNullOrEmpty()]
        [string]
        $KMSServerFQDN,

        [Parameter(Mandatory = $false,
            Position = 2,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false,
            ValueFromRemainingArguments = $false,
            ParameterSetName = 'ActivateWithKMS')]
        [ValidateRange(1, 65535)]
        [int]
        $KMSServerPort = 1688,

        [Parameter(Mandatory = $false,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false,
            ValueFromRemainingArguments = $false,
            ParameterSetName = 'Rearm')]
        [switch]
        $Rearm,

        [Parameter(Mandatory = $false,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false,
            ValueFromRemainingArguments = $false,
            ParameterSetName = 'Rearm')]
        [Guid]
        $ApplicationId,

        [Parameter(Mandatory = $false,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false,
            ValueFromRemainingArguments = $false,
            ParameterSetName = 'ActivateWithKMS')]
        [switch]
        $CacheDisabled,

        [Parameter(Mandatory = $false,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false,
            ValueFromRemainingArguments = $false,
            ParameterSetName = 'ActivateWithKMS')]
        [switch]
        $UseKmsClientKey,

        [Parameter(Mandatory = $false,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false,
            ValueFromRemainingArguments = $false,
            ParameterSetName = 'ActivateWithKMS')]
        [string]
        $ProductKey,

        [Parameter(Mandatory = $false,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false,
            ValueFromRemainingArguments = $false,
            ParameterSetName = 'ActivateWithKMS')]
        [Parameter(ParameterSetName = 'Offline')]
        [Parameter(ParameterSetName = 'Rearm')]
        [Guid]
        $ActivationId,

        [Parameter(Mandatory = $false,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false,
            ValueFromRemainingArguments = $false,
            ParameterSetName = 'Offline')]
        [switch]$Offline,

        [Parameter(Mandatory = $true,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false,
            ValueFromRemainingArguments = $false,
            ParameterSetName = 'Offline')]
        [ValidateScript(
            {
                $stripped = $_ -replace '[\s\-]', ''
                if ($stripped -match '^\d{54}$')
                {
                    $true
                }
                else
                {
                    throw "$_ is not a valid Confirmation ID. Expected 54 digits (9 groups of 6), optionally separated by dashes or spaces."
                }
            })]
        [ValidateNotNullOrEmpty()]
        [string]
        $ConfirmationId
    )
    Begin
    {
        $hasProductKey = $PSBoundParameters.ContainsKey('ProductKey')
        $hasActivationId = $PSBoundParameters.ContainsKey('ActivationId')
        $hasApplicationId = $PSBoundParameters.ContainsKey('ApplicationId')
        if ($PSCmdlet.ParameterSetName -eq 'Rearm' -and -not $Rearm.IsPresent)
        {
            throw 'ApplicationId and ActivationId require the Rearm switch in the rearm parameter set.'
        }
        if ($hasApplicationId -and $hasActivationId)
        {
            throw 'ApplicationId and ActivationId cannot be used together for rearm.'
        }
        if ($UseKmsClientKey.IsPresent -and $hasProductKey)
        {
            throw 'UseKmsClientKey and ProductKey cannot be used together.'
        }
        if ($hasActivationId -and ($UseKmsClientKey.IsPresent -or $hasProductKey))
        {
            throw 'ActivationId cannot be combined with product-key installation because InstallProductKey is service-scoped.'
        }
        $hasInvalidProductKey = $ProductKey -notmatch '^[A-Za-z0-9]{5}(?:-[A-Za-z0-9]{5}){4}$'
        if ($hasProductKey -and $hasInvalidProductKey)
        {
            throw 'ProductKey must contain five groups of five alphanumeric characters separated by dashes.'
        }
        if ($PSBoundParameters.ContainsKey('KMSServerFQDN'))
        {
            $endpointParameters = @{ Endpoint = $KMSServerFQDN }
            if ($PSBoundParameters.ContainsKey('KMSServerPort'))
            {
                $endpointParameters['Port'] = $KMSServerPort
            }
            $resolvedKmsEndpoint = Resolve-KmsEndpoint @endpointParameters
        }

        $activationFailures = [System.Collections.Generic.List[System.Management.Automation.ErrorRecord]]::new()
        $requestedActivationId = if ($hasActivationId) { $ActivationId } else { $null }
        $operation = switch ($PSCmdlet.ParameterSetName)
        {
            'Offline' { 'OfflineActivation' }
            'Rearm'
            {
                if ($hasApplicationId) { 'RearmApplication' }
                elseif ($hasActivationId) { 'RearmSku' }
                else { 'RearmWindows' }
            }
            default { 'Activate' }
        }
    }
    Process
    {
        Write-Verbose "Enumerating computers: $($Computer.Count) computer(s)."
        foreach ($c in $Computer)
        {
            $action = switch ($PSCmdlet.ParameterSetName)
            {
                'Offline' { 'Apply an offline Windows confirmation ID' }
                'Rearm'
                {
                    if ($hasApplicationId) { "Rearm application $ApplicationId" }
                    elseif ($hasActivationId) { "Rearm licensing product $ActivationId" }
                    else { 'Rearm Windows' }
                }
                default { 'Activate Windows' }
            }
            if (-not $pscmdlet.ShouldProcess($c, $action))
            {
                continue
            }

            Write-Verbose "Creating new CimSession for computer $c"
            $session = $null
            $outcome = $null
            try
            {
                $session = Get-Session -Computer $c -Credentials $Credentials -ErrorAction Stop

                Write-Verbose 'Connecting to SoftwareLicensingService...'
                $service = Get-CimInstance -CimSession $session -ClassName SoftwareLicensingService -ErrorAction Stop

                switch ($PSCmdlet.ParameterSetName)
                {
                    'Offline'
                    {
                        Write-Verbose 'Initiating offline activation operation'
                        $offlineParams = @{
                            CimSession     = $session
                            Service        = $service
                            ConfirmationId = $ConfirmationId
                        }
                        if ($hasActivationId) { $offlineParams['ActivationId'] = $ActivationId }
                        $outcome = Invoke-OfflineActivation @offlineParams
                    }

                    'Rearm'
                    {
                        Write-Verbose 'Initiating ReArm operation'
                        $rearmParams = @{ CimSession = $session; Service = $service }
                        if ($hasApplicationId) { $rearmParams['ApplicationId'] = $ApplicationId }
                        if ($hasActivationId) { $rearmParams['ActivationId'] = $ActivationId }
                        $outcome = Invoke-Rearm @rearmParams
                    }

                    'ActivateWithKMS'
                    {
                        if ($CacheDisabled.IsPresent)
                        {
                            Write-Verbose 'Disabling KMS host caching'
                            $service | Invoke-SppCimMethod `
                                -MethodName DisableKeyManagementServiceHostCaching `
                                -Arguments @{ DisableCaching = $true }
                        }

                        Write-Verbose 'Initiating Windows activation operation'
                        $kmsParams = @{ CimSession = $session; Service = $service }
                        if ($PSBoundParameters.ContainsKey('KMSServerFQDN'))
                        {
                            $kmsParams['KMSServerFQDN'] = $resolvedKmsEndpoint.Host
                            $kmsParams['KMSServerPort'] = $resolvedKmsEndpoint.Port
                        }
                        elseif ($PSBoundParameters.ContainsKey('KMSServerPort'))
                        {
                            $kmsParams['KMSServerPort'] = $KMSServerPort
                        }
                        if ($UseKmsClientKey.IsPresent) { $kmsParams['InstallKmsClientKey'] = $true }
                        if ($hasProductKey) { $kmsParams['ProductKey'] = $ProductKey }
                        if ($hasActivationId) { $kmsParams['ActivationId'] = $ActivationId }
                        $outcome = Invoke-KMSActivation @kmsParams
                    }

                    default
                    {
                        throw 'Unknown parameter combination'
                    }
                }

                $resultActivationId = if ($null -ne $outcome -and $null -ne $outcome.ActivationId)
                {
                    $outcome.ActivationId
                }
                else
                {
                    $requestedActivationId
                }
                $productName = if ($null -ne $outcome) { $outcome.ProductName } else { $null }
                $verificationState = if ($null -ne $outcome -and -not [string]::IsNullOrEmpty($outcome.VerificationState))
                {
                    $outcome.VerificationState
                }
                else
                {
                    'ProviderAccepted'
                }
                $restartRequired = $false
                if ($null -ne $outcome) { $restartRequired = [bool]$outcome.RestartRequired }
                elseif ($PSCmdlet.ParameterSetName -eq 'Rearm') { $restartRequired = $true }

                New-LicensingOperationResult `
                    -ComputerName $c `
                    -Operation $operation `
                    -Success $true `
                    -ActivationId $resultActivationId `
                    -ProductName $productName `
                    -RestartRequired $restartRequired `
                    -VerificationState $verificationState
            }
            catch
            {
                $structured = New-LicensingOperationError `
                    -ErrorRecord $_ `
                    -ComputerName $c `
                    -Operation $operation `
                    -ActivationId $requestedActivationId `
                    -RestartRequired ($PSCmdlet.ParameterSetName -eq 'Rearm')
                $activationFailures.Add($structured.ErrorRecord)
                Write-Output $structured.Result
            }
            finally
            {
                if ($null -ne $session)
                {
                    Remove-CimSession -CimSession $session -ErrorAction Ignore | Out-Null
                }
            }
        }
    }
    End
    {
        Complete-LicensingOperationBatch -Failures $activationFailures
    }
}