Wednesday, January 25, 2017

Change service accounts on multiple servers with advanced logging

A colleague of mine had to update a service account of multiple servers. +100 servers. Thank god, he's a smart guy (all my colleagues are). So he said, I'm not going to do that manually, there must exist a PowerShell script to do that. And so he googled and found a script to do that, just a couple of lines actually. But it was for a customer and wanted a more polished version of it. Logging, error handling, the lots. So here is what I wrote for him.


Logging

First of all, if you are writing PowerShell-code and you want some decent logging, don't reinvent the wheel.  In fact, if you want logging in your PowerShell code, have a look at my previous post "logging the professional way".  In this case I used my logging module.

Module

Second, i like things the way Microsoft meant it to be.  That means, with a module & manifest (see my post about creating modules & manifests).  I also like adding some help and since changing the credentials might disruptive, I also implemented "ShouldProcess" with a confirmImpact to "high".

For the rest it's pretty straightforward. Here is the package and below is the code.

Download ServiceCredential Module Download WriteLog Module


# service helper function
function SetServiceCredential {
    [CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact="High")]
    param(
        [parameter(Position=0,Mandatory=$true)]
        $serviceName,

        [parameter(Position=1,Mandatory=$true)]
        $ComputerName,

        [parameter(Position=2,Mandatory=$true)]
        $ServiceCredential,

        [parameter(Position=3,Mandatory=$false)]
        $ConnectionCredential,

        [parameter(Mandatory=$false)]
        $RestartService=$true,

        [parameter(Mandatory=$false)]
        $StopStartTimeoutSeconds=10
    )

    Write-LogVerbose "[$ComputerName][$ServiceName][$($ServiceCredential.UserName)]"

    # Get computer name if passed by property name.
    if ( $ComputerName.ComputerName ) {
        $ComputerName = $ComputerName.ComputerName
    }

    # Empty computer name or . is local computer.
    if ( (-not $ComputerName) -or $ComputerName -eq "." ) {
        $ComputerName = [Net.Dns]::GetHostName()
        Write-LogVerbose "Assuming localhost ($ComputerName)"
    }

    $wmiFilter = "Name='{0}' OR DisplayName='{0}'" -f $serviceName
    $params = @{
        "Namespace" = "root\CIMV2"
        "Class" = "Win32_Service"
        "ComputerName" = $ComputerName
        "Filter" = $wmiFilter
        "ErrorAction" = "Stop"
    }

    if ( $ConnectionCredential ) {

        # Specify connection credentials only when not connecting to the local computer.
        if ( $ComputerName -ne [Net.Dns]::GetHostName() ) {
        $params.Add("Credential", $ConnectionCredential)
        Write-LogVerbose "Connecting with : $($ConnectionCredential.UserName)"
        }else{
        Write-LogVerbose "Connecting with local account passthru"
        }
    }

    try {
        $service = Get-WmiObject @params -OutVariable out
    }
    catch [System.Management.Automation.RuntimeException],[System.Runtime.InteropServices.COMException] {
        Write-LogError "Unable to connect to '$ComputerName'"
        throw $_
    }

    if ( -not $service ) {
        Write-LogError "Unable to find service named '$serviceName' on '$ComputerName'."
        throw $_
    }

    if ( $PSCmdlet.ShouldProcess("Service '$serviceName' on '$ComputerName'","Set credentials") ) {
        # See https://msdn.microsoft.com/en-us/library/aa384901.aspx
        $returnValue =($service.Change($null,               # DisplayName
            $null,                                               # PathName
            $null,                                               # ServiceType
            $null,                                               # ErrorControl
            $null,                                               # StartMode
            $null,                                               # DesktopInteract
            $ServiceCredential.UserName,                         # StartName
            $ServiceCredential.GetNetworkCredential().Password,  # StartPassword
            $null,                                               # LoadOrderGroup
            $null,                                               # LoadOrderGroupDependencies
            $null)).ReturnValue                                  # ServiceDependencies

        $errorMessage = "Error setting credentials for service '$serviceName' on '$ComputerName'"

        switch ( $returnValue ) {
            0  { Write-LogInfo "Set credentials for service '$serviceName' on '$ComputerName'" }
            1  { Write-LogError "$errorMessage - Not Supported" }
            2  { Write-LogError "$errorMessage - Access Denied" }
            3  { Write-LogError "$errorMessage - Dependent Services Running" }
            4  { Write-LogError "$errorMessage - Invalid Service Control" }
            5  { Write-LogError "$errorMessage - Service Cannot Accept Control" }
            6  { Write-LogError "$errorMessage - Service Not Active" }
            7  { Write-LogError "$errorMessage - Service Request timeout" }
            8  { Write-LogError "$errorMessage - Unknown Failure" }
            9  { Write-LogError "$errorMessage - Path Not Found" }
            10 { Write-LogError "$errorMessage - Service Already Stopped" }
            11 { Write-LogError "$errorMessage - Service Database Locked" }
            12 { Write-LogError "$errorMessage - Service Dependency Deleted" }
            13 { Write-LogError "$errorMessage - Service Dependency Failure" }
            14 { Write-LogError "$errorMessage - Service Disabled" }
            15 { Write-LogError "$errorMessage - Service Logon Failed" }
            16 { Write-LogError "$errorMessage - Service Marked For Deletion" }
            17 { Write-LogError "$errorMessage - Service No Thread" }
            18 { Write-LogError "$errorMessage - Status Circular Dependency" }
            19 { Write-LogError "$errorMessage - Status Duplicate Name" }
            20 { Write-LogError "$errorMessage - Status Invalid Name" }
            21 { Write-LogError "$errorMessage - Status Invalid Parameter" }
            22 { Write-LogError "$errorMessage - Status Invalid Service Account" }
            23 { Write-LogError "$errorMessage - Status Service Exists" }
            24 { Write-LogError "$errorMessage - Service Already Paused" }
        }
        if($returnValue){throw $_}

        # If we get here, it worked
        if($RestartService){
            Write-LogInfo "Restarting service"
            Write-LogVerbose "Stopping service..."
            $null = $service.StopService()
            $timeoutcounter=0
            while (($service.State -ne "Stopped") -and ($timeoutcounter -lt $StopStartTimeoutSeconds)){
                sleep 1
                Write-LogVerbose "waiting..."
                $timeoutcounter++
                $service = Get-WmiObject @params -OutVariable out
            }
            if($service.State -ne "Stopped"){
                Write-LogError "Failed to stop the service"
            }else{
                Write-LogVerbose "Starting service..."
                $null = $service.StartService()
                $timeoutcounter=0
                while (($service.State -ne "Running") -and ($timeoutcounter -lt $StopStartTimeoutSeconds)){
                    sleep 1
                    Write-LogVerbose "waiting..."
                    $timeoutcounter++
                    $service = Get-WmiObject @params -OutVariable out
                }
                if($service.State -ne "Running"){
                    Write-LogError "Failed to start the service"
                }else{
                    Write-LogSuccess "Service is succesfully restarted"
                }
            }

        }
    }
}

<#
.SYNOPSIS
Sets start credentials for one or more services on one or more computers.

.DESCRIPTION
Sets start credentials for one or more services on one or more computers.

.PARAMETER ServiceName
Specifies one or more service names. You can specify either the Name or DisplayName property for the services. Wildcards are not supported.

.PARAMETER ComputerName
Specifies one or more computer names. The default is the current computer. This parameter accepts pipeline input containing computer names or objects with a ComputerName property.

.PARAMETER ServiceCredential
Specifies the credentials to use to start the service(s).

.PARAMETER ConnectionCredential
Specifies credentials that have permissions to change the service(s) on the computer(s).

.NOTES
Default confirm impact is High. To suppress the prompt, specify -Confirm:$false or set the $ConfirmPreference variable to "None".
#>
function Set-ServiceCredential{
    [CmdletBinding(SupportsShouldProcess=$true)]
    param(
        #Name of the service
        [parameter(Position=0,Mandatory=$true)]
        [String[]] $ServiceName,

        #Name of the computer
        [parameter(Position=1,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
        $ComputerName,

        #Credential to set
        [parameter(Position=2,Mandatory=$true)]
        [Management.Automation.PSCredential] $ServiceCredential,

        #Credential to execute the change
        [Management.Automation.PSCredential] $ConnectionCredential,

        # The logging level for logfiles
        [ValidateSet('off','debug','info','warn','error','fatal')]
        [string]$logLevel = "info",

        # The logging level for eventviewer
        [ValidateSet('off','debug','info','warn','error','fatal')]
        [string]$eventLevel = "warn",

        #Restart the service after success ?  [Default = true]
        [switch] $RestartService=$true,

        #Timeout For Stop & Start Service in seconds [Default = 10]
        $StopStartTimeOutSeconds = 10
    )

    begin {
        Import-Module WriteLog
        $prevVerbose = $VerbosePreference
        if($logLevel -eq "debug"){
            $VerbosePreference = "Continue"
            $Verbose = $true
        }else{
            $VerbosePreference = "SilentlyContinue"
            $Verbose = $false            
        }
        Remove-Logger
        Initialize-Logger -LoggerName "ServiceCredential" -logLevel $logLevel -eventLevel $eventLevel -Verbose:$Verbose
        Write-LogTitle "Start Set-ServiceCredential"
    }

    process {
      foreach ( $ComputerNameItem in $ComputerName ) {
        foreach ( $serviceNameItem in $ServiceName ) {
          try{
            SetServiceCredential $serviceNameItem $ComputerNameItem $ServiceCredential $ConnectionCredential -RestartService:$RestartService -StopStartTimeoutSeconds $StopStartTimeOutSeconds -Verbose:$Verbose
          }catch{
            Write-LogErrorObject -ErrorObject $_
          }
        }
      }
    }

    end {
        Write-LogTitle "Stop Set-ServiceCredential"
        $VerbosePreference = $prevVerbose
    }
}


And this how you call it

PS C:\jumpstart\TomService> Import-Module ServiceCredential

PS C:\jumpstart\TomService> Set-ServiceCredential -ServiceName MySQL57 -ComputerName localhost -ServiceCredential mirko-laptop\mirko -Confirm:$false -logLevel debug
VERBOSE: [LOG] Logger initialization
VERBOSE: [LOG] Log4net dll path is : 'C:\Users\Mirko\Documents\WindowsPowerShell\Modules\WriteLog\log4net.dll'
VERBOSE: [LOG] Repository already created
VERBOSE: [LOG] LogFile path is : 'C:\jumpstart\TomService\ServiceCredential.log'
VERBOSE: [LOG] Logger is initialized
VERBOSE: [LOG] Tip : If your eventviewer is not showing anything, first time must be run as administrator to
 create the eventsource.  Don't forget to set your eventLevel (default off)

Start Set-ServiceCredential
---------------------------
VERBOSE: [localhost][MySQL57][mirko-laptop\mirko]
VERBOSE: Performing the operation "Set credentials" on target "Service 'MySQL57' on 'localhost'".
Set credentials for service 'MySQL57' on 'localhost'
Restarting service
VERBOSE: Stopping service...
VERBOSE: waiting...
VERBOSE: waiting...
VERBOSE: Starting service...
VERBOSE: waiting...
Service is succesfully restarted

Stop Set-ServiceCredential
--------------------------

PS C:\jumpstart\TomService> Set-ServiceCredential -ServiceName MySQL57 -ComputerName localhost -ServiceCredential mirko-laptop\mirko -Confirm:$false -logLevel info

Start Set-ServiceCredential
---------------------------
Set credentials for service 'MySQL57' on 'localhost'
Restarting service
Service is succesfully restarted

Stop Set-ServiceCredential
--------------------------

PS C:\jumpstart\TomService> get-help Set-ServiceCredential -Full

NAME
    Set-ServiceCredential
    
SYNOPSIS
    Sets start credentials for one or more services on one or more computers.
    
    
SYNTAX
    Set-ServiceCredential [-ServiceName] <String[]> [[-ComputerName] <Object>] [-ServiceCredential] <PSCred
    ential> [-ConnectionCredential <PSCredential>] [-logLevel <String>] [-eventLevel <String>] [-RestartSer
    vice] [-StopStartTimeOutSeconds <Object>] [-WhatIf] [-Confirm] [<CommonParameters>]
    
    
DESCRIPTION
    Sets start credentials for one or more services on one or more computers.
    

PARAMETERS
    -ServiceName <String[]>
        Specifies one or more service names. You can specify either the Name or DisplayName property for th
        e services. Wildcards are not supported.
        
        Required?                    true
        Position?                    1
        Default value                
        Accept pipeline input?       false
        Accept wildcard characters?  false
        
    -ComputerName <Object>
        Specifies one or more computer names. The default is the current computer. This parameter accepts p
        ipeline input containing computer names or objects with a ComputerName property.
        
        Required?                    false
        Position?                    2
        Default value                
        Accept pipeline input?       true (ByValue, ByPropertyName)
        Accept wildcard characters?  false
        
    -ServiceCredential <PSCredential>
        Specifies the credentials to use to start the service(s).
        
        Required?                    true
        Position?                    3
        Default value                
        Accept pipeline input?       false
        Accept wildcard characters?  false
        
    -ConnectionCredential <PSCredential>
        Specifies credentials that have permissions to change the service(s) on the computer(s).
        
        Required?                    false
        Position?                    named
        Default value                
        Accept pipeline input?       false
        Accept wildcard characters?  false
        
    -logLevel <String>
        The logging level for logfiles
        
        Required?                    false
        Position?                    named
        Default value                info
        Accept pipeline input?       false
        Accept wildcard characters?  false
        
    -eventLevel <String>
        The logging level for eventviewer
        
        Required?                    false
        Position?                    named
        Default value                warn
        Accept pipeline input?       false
        Accept wildcard characters?  false
        
    -RestartService [<SwitchParameter>]
        Restart the service after success ?  [Default = true]
        
        Required?                    false
        Position?                    named
        Default value                True
        Accept pipeline input?       false
        Accept wildcard characters?  false
        
    -StopStartTimeOutSeconds <Object>
        Timeout For Stop & Start Service in seconds [Default = 10]
        
        Required?                    false
        Position?                    named
        Default value                10
        Accept pipeline input?       false
        Accept wildcard characters?  false

No comments :

Post a Comment