DscResources/Carbon_Service/Carbon_Service.psm1

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

$psModulesPath = Join-Path -Path $PSScriptRoot -ChildPath '..\..\Modules' -Resolve
Import-Module -Name (Join-Path -Path $psModulesPath -ChildPath 'Carbon' -Resolve) `
              -Function @('Install-CService', 'Test-CService', 'Uninstall-CService' )
Import-Module -Name (Join-Path -Path $psModulesPath -ChildPath 'Carbon.Accounts' -Resolve) `
              -Function @('Resolve-CIdentity')

function Get-TargetResource
{
    [CmdletBinding()]
    [OutputType([hashtable])]
    param(
        [Parameter(Mandatory=$true)]
        [String]
        # The name of the service.
        $Name,

        [String]
        # The path to the service.
        $Path,

        [ValidateSet('Automatic','Manual','Disabled')]
        [String]
        # The startup type: automatic, manual, or disabled. Default is automatic.
        $StartupType,

        [switch]
        $Delayed,

        [Carbon.Service.FailureAction]
        # What to do on the service's first failure. Default is to take no action.
        $OnFirstFailure,

        [Carbon.Service.FailureAction]
        # What to do on the service's second failure. Default is to take no action.
        $OnSecondFailure,

        [Carbon.Service.FailureAction]
        # What to do on the service' third failure. Default is to take no action.
        $OnThirdFailure,

        [int]
        # How many seconds after which the failure count is reset to 0.
        $ResetFailureCount,

        [int]
        # How many milliseconds to wait before restarting the service. Default is 60,0000, or 1 minute.
        $RestartDelay,

        [int]
        # How many milliseconds to wait before handling the second failure. Default is 60,000 or 1 minute.
        $RebootDelay,

        [string[]]
        # What other services does this service depend on?
        $Dependency,

        [String]
        # The command to run when a service fails, including path to the command and arguments.
        $Command,

        [int]
        # How many milliseconds to wait before running the failure command. Default is 0, or immediately.
        $RunCommandDelay,

        [String]
        # The service's display names.
        $DisplayName,

        [String]
        # The service's description.
        $Description,

        [ValidateSet("LocalSystem", "LocalService", "NetworkService")]
        [String]
        # The system account the service should run as.
        $UserName,

        [pscredential]
        # The credentials of the custom account the service should run as.
        $Credential,

        [string[]]
        # The arguments/startup parameters for the service
        $ArgumentList,

        [ValidateSet('Present','Absent')]
        [String]
        # If `Present`, the service is installed/updated. If `Absent`, the service is removed.
        $Ensure = 'Present'
    )

    Set-StrictMode -Version 'Latest'

    $resource = @{
                    Name = $Name;
                    Path = $null;
                    StartupType = $null;
                    Delayed = $null;
                    OnFirstFailure = $null;
                    OnSecondFailure = $null;
                    OnThirdFailure = $null;
                    ResetFailureCount = $null;
                    RestartDelay = $null;
                    RebootDelay = $null;
                    Command = $null;
                    RunCommandDelay = $null;
                    Dependency = $null;
                    DisplayName = $null;
                    Description = $null;
                    UserName = $null;
                    Credential = $null;
                    Ensure = 'Absent';
                    ArgumentList = $null;
                }

    if( Test-CService -Name $Name )
    {
        $service = Get-Service -Name $Name

        $resource.Path,$resource.ArgumentList = [Carbon.Shell.Command]::Split($service.Path)
        $resource.StartupType = $service.StartMode
        $resource.Delayed = $service.DelayedAutoStart
        $resource.OnFirstFailure = $service.FirstFailure
        $resource.OnSecondFailure = $service.SecondFailure
        $resource.OnThirdFailure = $service.ThirdFailure
        $resource.ResetFailureCount = $service.ResetPeriod
        $resource.RestartDelay = $service.RestartDelay
        $resource.RebootDelay = $service.RebootDelay
        $resource.Command = $service.FailureProgram
        $resource.RunCommandDelay = $service.RunCommandDelay
        $resource.DisplayName = $service.DisplayName
        $resource.Description = $service.Description
        $resource.UserName = $service.UserName
        $actualUserName = ''
        if( $service.UserName )
        {
            $actualUserName = Resolve-CIdentity -Name $service.UserName -ErrorAction Ignore
            if( $actualUserName )
            {
                $resource.UserName = $actualUserName.FullName
            }
        }
        [string[]]$resource.Dependency = $service.ServicesDependedOn | Select-Object -ExpandProperty Name
        $resource.Ensure = 'Present'
    }
    $resource
}


function Set-TargetResource
{
    <#
    .SYNOPSIS
    DSC resource for configuring Windows services.
 
    .DESCRIPTION
    The `Carbon_Service` resource configures Windows services, including name, credentials, startup type, state, failure actions, and dependencies.
 
    The service is installed when the `Ensure` property is set to `Present`. If the service already exists, and its configuration doesn't match the properties being set, the service is stopped, its configuration updated, and the service is restarted. Properties not passed are ignored/left as-is.
 
    In addition to installing the service, this resource also grants the service user the logon as a service privilege and execute permissions on the service executable.
 
    The service is uninstalled when the `Ensure` property is set to `Absent`. The service is stopped, then uninstalled.
 
    `Carbon_Service` is new in Carbon 2.0.
 
    .LINK
    Grant-CPrivilege
 
    .LINK
    Install-CService
 
    .LINK
    Uninstall-CService
 
    .EXAMPLE
    >
    Demonstrates how to install a service that runs as a custom account and has custom failure actions.
 
        Carbon_Service InstallNoOpService
        {
            Name = 'CarbonNoOpService';
            Path = 'C:\Projects\Carbon\bin\NoOpService.bin';
            StartupType = 'Automatic';
            Credential = $noOpServiceCreential';
            OnFirstFailure = 'RunCommand';
            Command = 'example.exe /fail %1%';
            RunCommandDelay = 1000;
            OnSecondFailure = 'Restart';
            RestartDelay = (1000*60*5); # 5 minutes as milliseconds
        }
 
    .EXAMPLE
    >
    Demonstrates how to install a service that runs as a built-in account.
 
        Carbon_Service InstallNoOpService
        {
            Name = 'CarbonNoOpService';
            Path = 'C:\Projects\Carbon\bin\NoOpService.bin';
            StartupType = 'Automatic';
            UserName = 'LocalService';
        }
 
    .EXAMPLE
    >
    Demonstrates how to remove a service.
 
        Carbon_Service InstallNoOpService
        {
            Name = 'CarbonNoOpService';
            Ensure = 'Absent';
        }
 
    .EXAMPLE
    >
    Demonstrates how to set a service's start type `Automatic (Delayed)`. This functionality was added in Carbon 2.5.
 
        Carbon_Service InstallNoOpService
        {
            Name = 'CarbonNoOpService';
            Path = 'C:\Projects\Carbon\bin\NoOpService.bin';
            StartupType = 'Automatic';
            Delayed = $true;
            Ensure = 'Present';
        }
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [String]
        # The name of the service.
        $Name,

        [String]
        # The path to the service.
        $Path,

        [ValidateSet('Automatic','Manual','Disabled')]
        [String]
        # The startup type: automatic, manual, or disabled. Default is automatic.
        $StartupType,

        [switch]
        # Used in combination with the `StartupType` parameter to set a service's startup type to `Automatic (Delayed)`.
        #
        # If `Delayed` is true true, and `StartupType` is `Automatic` sets the service's startup type to `Automatic (Delayed)`.
        #
        # If `Delayed` is false and `StartupType` is `Automatic, sets the service's startup type to `Automatic`.
        #
        # For all other values of `StartupType`, this parameter is ignored.
        #
        # This parameter was added in Carbon 2.5.
        $Delayed,

        [Carbon.Service.FailureAction]
        # What to do on the service's first failure. Default is to take no action.
        $OnFirstFailure,

        [Carbon.Service.FailureAction]
        # What to do on the service's second failure. Default is to take no action.
        $OnSecondFailure,

        [Carbon.Service.FailureAction]
        # What to do on the service' third failure. Default is to take no action.
        $OnThirdFailure,

        [int]
        # How many seconds after which the failure count is reset to 0.
        $ResetFailureCount,

        [int]
        # How many milliseconds to wait before restarting the service. Default is 60,0000, or 1 minute.
        $RestartDelay,

        [int]
        # How many milliseconds to wait before handling the second failure. Default is 60,000 or 1 minute.
        $RebootDelay,

        [string[]]
        # What other services does this service depend on?
        $Dependency,

        [String]
        # The command to run when a service fails, including path to the command and arguments.
        $Command,

        [int]
        # How many milliseconds to wait before running the failure command. Default is 0, or immediately.
        $RunCommandDelay,

        [String]
        # The service's display names.
        $DisplayName,

        [String]
        # The service's description.
        $Description,

        [ValidateSet("LocalSystem", "LocalService", "NetworkService")]
        [String]
        # The system account the service should run as.
        $UserName,

        [pscredential]
        # The credentials of the custom account the service should run as.
        $Credential,

        [string[]]
        # The arguments/startup parameters for the service
        $ArgumentList,

        [ValidateSet('Present','Absent')]
        [String]
        # If `Present`, the service is installed/updated. If `Absent`, the service is removed.
        $Ensure = 'Present'
    )

    Set-StrictMode -Version 'Latest'

    $serviceExists = Test-CService -Name $Name
    if( $Ensure -eq 'Absent' )
    {
        if( $serviceExists )
        {
            Write-Verbose ('Removing service ''{0}''' -f $Name)
            Uninstall-CService -Name $Name
        }
        return
    }

    if( -not $Path )
    {
        Write-Error ('Property ''Path'' mandatory when installing/updating a service.')
        return
    }

    if( $UserName -and $Credential )
    {
        Write-Error ('UserName and Credential properties are mutually exclusive. Please provide either Credential or UserName, not both.')
        return
    }

    $PSBoundParameters.Remove('Ensure')
    if( $serviceExists )
    {
        Write-Verbose ('Updating service ''{0}''' -f $Name)
    }
    else
    {
        Write-Verbose ('Installing service ''{0}''' -f $Name)
    }

    Install-CService @PSBoundParameters
}


function Test-TargetResource
{
    [CmdletBinding(SupportsShouldProcess)]
    [OutputType([bool])]
    param(
        [Parameter(Mandatory=$true)]
        [String]
        # The name of the service.
        $Name,

        [String]
        # The path to the service.
        $Path,

        [ValidateSet('Automatic','Manual','Disabled')]
        [String]
        # The startup type: automatic, manual, or disabled. Default is automatic.
        $StartupType,

        [switch]
        $Delayed,

        [Carbon.Service.FailureAction]
        # What to do on the service's first failure. Default is to take no action.
        $OnFirstFailure,

        [Carbon.Service.FailureAction]
        # What to do on the service's second failure. Default is to take no action.
        $OnSecondFailure,

        [Carbon.Service.FailureAction]
        # What to do on the service' third failure. Default is to take no action.
        $OnThirdFailure,

        [int]
        # How many seconds after which the failure count is reset to 0.
        $ResetFailureCount,

        [int]
        # How many milliseconds to wait before restarting the service. Default is 60,0000, or 1 minute.
        $RestartDelay,

        [int]
        # How many milliseconds to wait before handling the second failure. Default is 60,000 or 1 minute.
        $RebootDelay,

        [string[]]
        # What other services does this service depend on?
        $Dependency,

        [String]
        # The command to run when a service fails, including path to the command and arguments.
        $Command,

        [int]
        # How many milliseconds to wait before running the failure command. Default is 0, or immediately.
        $RunCommandDelay,

        [String]
        # The service's display names.
        $DisplayName,

        [String]
        # The service's description.
        $Description,

        [ValidateSet("LocalSystem", "LocalService", "NetworkService")]
        [String]
        # The system account the service should run as.
        $UserName,

        [pscredential]
        # The custom account the service should run as.
        $Credential,

        [string[]]
        # The arguments/startup parameters for the service
        $ArgumentList,

        [ValidateSet('Present','Absent')]
        [String]
        # If `Present`, the service is installed/updated. If `Absent`, the service is removed.
        $Ensure = 'Present'
    )

    Set-StrictMode -Version 'Latest'

    $resource = Get-TargetResource -Name $Name

    if( $Ensure -eq 'Absent' )
    {
        if( $resource.Ensure -eq 'Absent' )
        {
            return $true
        }

        Write-Verbose ('Service ''{0}'' found.' -f $Name)
        return $false
    }

    if( $resource.Ensure -eq 'Absent' )
    {
        Write-Verbose ('Service ''{0}'' not found.' -f $Name)
        return $false
    }

    if( $PSBoundParameters.ContainsKey( 'UserName' ) )
    {
        $identity = Resolve-CIdentity -Name $UserName
        if( $identity )
        {
            $PSBoundParameters['UserName'] = $identity.FullName
        }
    }

    if( $resource.ContainsKey('Credential') )
    {
        [void]$resource.Remove('Credential')
    }

    if( $PSBoundParameters.ContainsKey('Credential') )
    {
        [void]$PSBoundParameters.Remove('Credential')
        $identity = Resolve-CIdentity -Name $Credential.UserName -ErrorAction Ignore
        if( $identity )
        {
            $PSBoundParameters.UserName = $identity.FullName
        }
    }

    return Test-CDscTargetResource -TargetResource $resource -DesiredResource $PSBoundParameters -Target ('Service ''{0}''' -f $Name)
}