Написание пользовательских ресурсов DSC с использованием классов PowerShell

Область применения: Windows PowerShell 5.0

С добавлением классов PowerShell в Windows PowerShell 5.0 появилась возможность определить ресурс DSC, создав отдельный класс. Класс определяет схему и реализацию ресурса, а значит, отдельный MOF-файл создавать не нужно. Кроме того, для ресурса на основе класса используется более простая структура папок, поскольку не требуется папка DSCResources.

В ресурсе DSC на основе класса схема определяется как свойства класса , которые можно изменить с помощью атрибутов, чтобы указать тип свойства. Ресурс реализуется с помощью методов Get() , Set() и Test() , эквивалентных функциям Get-TargetResource, Set-TargetResource и Test-TargetResource в ресурсе сценария.

В этой статье мы создадим простой ресурс с именем NewFile , который управляет файлом по указанному пути.

Дополнительные сведения о ресурсах DSC см. в статье Создание настраиваемых ресурсов для настройки требуемого состояния Windows PowerShell.

Примечание

Универсальные коллекции не поддерживаются в ресурсах на основе классов.

Структура папок для ресурса класса

Для реализации настраиваемого ресурса DSC с помощью класса PowerShell создайте указанную ниже структуру папок. Класс определяется в файле MyDscResource.psm1, а манифест модуля — в файле MyDscResource.psd1.

$env:ProgramFiles\WindowsPowerShell\Modules (folder)
    |- MyDscResource (folder)
        MyDscResource.psm1
        MyDscResource.psd1

Создание класса

Для создания класса PowerShell необходимо ключевое слово class. Чтобы указать, что класс является ресурсом DSC, используйте атрибут DscResource() . Имя класса — это имя ресурса DSC.

[DscResource()]
class NewFile {
}

Объявление свойств

Схема ресурсов DSC определяется как свойства класса. Необходимо объявить три свойства описанным ниже образом.

[DscProperty(Key)]
[string] $path

[DscProperty(Mandatory)]
[ensure] $ensure

[DscProperty()]
[string] $content

[DscProperty(NotConfigurable)]
[MyDscResourceReason[]] $Reasons

Обратите внимание, что для изменения свойств используются атрибуты. Значения атрибутов выглядит следующим образом:

  • DscProperty(Key) : свойство является обязательным. Это свойство является ключом. Значения всех свойств, помеченных как ключи, необходимо объединять для уникальной идентификации экземпляра ресурсов в конфигурации.
  • DscProperty(Mandatory) : свойство является обязательным.
  • DscProperty(NotConfigurable) : свойство доступно только для чтения. Свойства с таким атрибутом задаются не конфигурацией, а методом Get() (если они есть).
  • DscProperty() : свойство доступно для настройки, но не является обязательным.

Свойства $Path и $SourcePath являются строками. $CreationTime — это свойство DateTime. Свойство $Ensure является перечислением и определяется следующим образом:

enum Ensure
{
    Absent
    Present
}

Внедрение классов

Если вы хотите добавить новый тип с определенными свойствами, которые можно использовать в ресурсе, просто создайте класс с типами свойств, как описано выше.

class MyDscResourceReason {
    [DscProperty()]
    [string] $Code

    [DscProperty()]
    [string] $Phrase
}

Примечание

Класс MyDscResourceReason объявляется здесь с именем модуля в качестве префикса. Хотя внедренным классам можно присвоить любое имя, если два или более модулей определяют класс с одинаковым именем и оба используются в конфигурации, PowerShell вызывает исключение.

Чтобы избежать исключений, вызванных конфликтами имен в DSC, в префиксируйте имена внедренных классов именем модуля. Если имя внедренного класса уже вряд ли конфликтует, его можно использовать без префикса.

Если ресурс DSC предназначен для использования с функцией конфигурации компьютера службы "Автоматическое управление Azure", всегда введите префикс имени создаваемого внедренного класса для свойства Reasons .

Определение общедоступных и частных функций

Вы можете создать функции PowerShell в одном файле модуля и использовать их в методах ресурса класса DSC. Функции должны быть объявлены как открытые, однако блоки скриптов в этих открытых функциях могут вызывать закрытые функции. Единственное отличие заключается в том, что они могут быть указаны в свойстве FunctionsToExport манифеста модуля.

<#
   Public Functions
#>

function Get-File {
    param(
        [ensure]$ensure,

        [parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]$path,

        [String]$content
    )
    $fileContent        = [MyDscResourceReason]::new()
    $fileContent.code   = 'file:file:content'

    $filePresent        = [MyDscResourceReason]::new()
    $filePresent.code   = 'file:file:path'

    $ensureReturn = 'Absent'

    $fileExists = Test-path $path -ErrorAction SilentlyContinue

    if ($true -eq $fileExists) {
        $filePresent.phrase     = "The file was expected to be: $ensure`nThe file exists at path: $path"

        $existingFileContent    = Get-Content $path -Raw
        if ([string]::IsNullOrEmpty($existingFileContent)) {
            $existingFileContent = ''
        }

        if ($false -eq ([string]::IsNullOrEmpty($content))) {
            $content = $content | ConvertTo-SpecialChars
        }

        $fileContent.phrase     = "The file was expected to contain: $content`nThe file contained: $existingFileContent"

        if ($content -eq $existingFileContent) {
            $ensureReturn = 'Present'
        }
    }
    else {
        $filePresent.phrase     = "The file was expected to be: $ensure`nThe file does not exist at path: $path"
        $path = 'file not found'
    }

    return @{
        ensure  = $ensureReturn
        path    = $path
        content = $existingFileContent
        Reasons = @($filePresent,$fileContent)
    }
}

function Set-File {
    param(
        [ensure]$ensure = "Present",

        [parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]$path,

        [String]$content
    )
    Remove-Item $path -Force -ErrorAction SilentlyContinue
    if ($ensure -eq "Present") {
        New-Item $path -ItemType File -Force
        if ([ValidateNotNullOrEmpty()]$content) {
            $content | ConvertTo-SpecialChars | Set-Content $path -NoNewline -Force
        }
    }
}

function Test-File {
    param(
        [ensure]$ensure = "Present",

        [parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]$path,

        [String]$content
    )
    $test = $false
    $get = Get-File @PSBoundParameters

    if ($get.ensure -eq $ensure) {
        $test = $true
    }
    return $test
}

<#
   Private Functions
#>

function ConvertTo-SpecialChars {
    param(
        [parameter(Mandatory = $true,ValueFromPipeline)]
        [ValidateNotNullOrEmpty()]
        [string]$string
    )
    $specialChars = @{
        '`n' = "`n"
        '\\n' = "`n"
        '`r' = "`r"
        '\\r' = "`r"
        '`t' = "`t"
        '\\t' = "`t"
    }
    foreach ($char in $specialChars.Keys) {
        $string = $string -replace ($char,$specialChars[$char])
    }
    return $string
}

Реализация методов

Методы Get() , Set() и Test() эквивалентны функциям Get-TargetResource, Set-TargetResource и Test-TargetResource в ресурсе сценария.

Рекомендуется минимизировать объем кода в реализации класса. Вместо этого переместите большую часть кода в общедоступные функции в модуле, которые затем можно протестировать независимо.

<#
    This method is equivalent of the Get-TargetResource script function.
    The implementation should use the keys to find appropriate
    resources. This method returns an instance of this class with the
    updated key properties.
#>
[NewFile] Get() {
    $get = Get-File -ensure $this.ensure -path $this.path -content $this.content
    return $get
}

<#
    This method is equivalent of the Set-TargetResource script function.
    It sets the resource to the desired state.
#>
[void] Set() {
    $set = Set-File -ensure $this.ensure -path $this.path -content $this.content
}

<#
    This method is equivalent of the Test-TargetResource script
    function. It should return True or False, showing whether the
    resource is in a desired state.
#>
[bool] Test() {
    $test = Test-File -ensure $this.ensure -path $this.path -content $this.content
    return $test
}

Полный файл

Полный файл класса:

enum ensure {
    Absent
    Present
}

<#
    This class is used within the DSC Resource to standardize how data
    is returned about the compliance details of the machine. Note that
    the class name is prefixed with the module name - this helps prevent
    errors raised when multiple modules with DSC Resources define the
    Reasons property for reporting when they're out-of-state.
#>
class MyDscResourceReason {
    [DscProperty()]
    [string] $Code

    [DscProperty()]
    [string] $Phrase
}

<#
   Public Functions
#>

function Get-File {
    param(
        [ensure]$ensure,

        [parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]$path,

        [String]$content
    )
    $fileContent        = [MyDscResourceReason]::new()
    $fileContent.code   = 'file:file:content'

    $filePresent        = [MyDscResourceReason]::new()
    $filePresent.code   = 'file:file:path'

    $ensureReturn = 'Absent'

    $fileExists = Test-path $path -ErrorAction SilentlyContinue

    if ($true -eq $fileExists) {
        $filePresent.phrase     = "The file was expected to be: $ensure`nThe file exists at path: $path"

        $existingFileContent    = Get-Content $path -Raw
        if ([string]::IsNullOrEmpty($existingFileContent)) {
            $existingFileContent = ''
        }

        if ($false -eq ([string]::IsNullOrEmpty($content))) {
            $content = $content | ConvertTo-SpecialChars
        }

        $fileContent.phrase     = "The file was expected to contain: $content`nThe file contained: $existingFileContent"

        if ($content -eq $existingFileContent) {
            $ensureReturn = 'Present'
        }
    }
    else {
        $filePresent.phrase     = "The file was expected to be: $ensure`nThe file does not exist at path: $path"
        $path = 'file not found'
    }

    return @{
        ensure  = $ensureReturn
        path    = $path
        content = $existingFileContent
        Reasons = @($filePresent,$fileContent)
    }
}

function Set-File {
    param(
        [ensure]$ensure = "Present",

        [parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]$path,

        [String]$content
    )
    Remove-Item $path -Force -ErrorAction SilentlyContinue
    if ($ensure -eq "Present") {
        New-Item $path -ItemType File -Force
        if ([ValidateNotNullOrEmpty()]$content) {
            $content | ConvertTo-SpecialChars | Set-Content $path -NoNewline -Force
        }
    }
}

function Test-File {
    param(
        [ensure]$ensure = "Present",

        [parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]$path,

        [String]$content
    )
    $test = $false
    $get = Get-File @PSBoundParameters

    if ($get.ensure -eq $ensure) {
        $test = $true
    }
    return $test
}

<#
   Private Functions
#>

function ConvertTo-SpecialChars {
    param(
        [parameter(Mandatory = $true,ValueFromPipeline)]
        [ValidateNotNullOrEmpty()]
        [string]$string
    )
    $specialChars = @{
        '`n' = "`n"
        '\\n' = "`n"
        '`r' = "`r"
        '\\r' = "`r"
        '`t' = "`t"
        '\\t' = "`t"
    }
    foreach ($char in $specialChars.Keys) {
        $string = $string -replace ($char,$specialChars[$char])
    }
    return $string
}

<#
    This resource manages the file in a specific path.
    [DscResource()] indicates the class is a DSC resource
#>

[DscResource()]
class NewFile {

    <#
        This property is the fully qualified path to the file that is
        expected to be present or absent.

        The [DscProperty(Key)] attribute indicates the property is a
        key and its value uniquely identifies a resource instance.
        Defining this attribute also means the property is required
        and DSC will ensure a value is set before calling the resource.

        A DSC resource must define at least one key property.
    #>
    [DscProperty(Key)]
    [string] $path

    <#
        This property indicates if the settings should be present or absent
        on the system. For present, the resource ensures the file pointed
        to by $Path exists. For absent, it ensures the file point to by
        $Path does not exist.

        The [DscProperty(Mandatory)] attribute indicates the property is
        required and DSC will guarantee it is set.

        If Mandatory is not specified or if it is defined as
        Mandatory=$false, the value is not guaranteed to be set when DSC
        calls the resource.  This is appropriate for optional properties.
    #>
    [DscProperty(Mandatory)]
    [ensure] $ensure

    <#
        This property is optional. When provided, the content of the file
        will be overwridden by this value.
    #>
    [DscProperty()]
    [string] $content

    <#
        This property reports the reasons the machine is or is not compliant.

        [DscProperty(NotConfigurable)] attribute indicates the property is
        not configurable in DSC configuration.  Properties marked this way
        are populated by the Get() method to report additional details
        about the resource when it is present.
    #>
    [DscProperty(NotConfigurable)]
    [MyDscResourceReason[]] $Reasons

    <#
        This method is equivalent of the Get-TargetResource script function.
        The implementation should use the keys to find appropriate
        resources. This method returns an instance of this class with the
        updated key properties.
    #>
    [NewFile] Get() {
        $get = Get-File -ensure $this.ensure -path $this.path -content $this.content
        return $get
    }

    <#
        This method is equivalent of the Set-TargetResource script function.
        It sets the resource to the desired state.
    #>
    [void] Set() {
        $set = Set-File -ensure $this.ensure -path $this.path -content $this.content
    }

    <#
        This method is equivalent of the Test-TargetResource script
        function. It should return True or False, showing whether the
        resource is in a desired state.
    #>
    [bool] Test() {
        $test = Test-File -ensure $this.ensure -path $this.path -content $this.content
        return $test
    }
}

Создание манифеста

Чтобы сделать ресурс на основе класса доступным для модуля DSC, необходимо добавить в файл манифеста оператор DscResourcesToExport, который указывает модулю, что нужно экспортировать этот ресурс. Наш манифест выглядит следующим образом:

@{

    # Script module or binary module file associated with this manifest.
    RootModule = 'NewFile.psm1'

    # Version number of this module.
    ModuleVersion = '1.0.0'

    # ID used to uniquely identify this module
    GUID = 'fad0d04e-65d9-4e87-aa17-39de1d008ee4'

    # Author of this module
    Author = 'Microsoft Corporation'

    # Company or vendor of this module
    CompanyName = 'Microsoft Corporation'

    # Copyright statement for this module
    Copyright = ''

    # Description of the functionality provided by this module
    Description = 'Create and set content of a file'

    # Minimum version of the Windows PowerShell engine required by this module
    PowerShellVersion = '5.0'

    # Functions to export from this module
    FunctionsToExport = @('Get-File','Set-File','Test-File')

    # DSC resources to export from this module
    DscResourcesToExport = @('NewFile')

    # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell.
    PrivateData = @{

        PSData = @{

            # Tags applied to this module. These help with module discovery in online galleries.
            # Tags = @(Power Plan, Energy, Battery)

            # A URL to the license for this module.
            # LicenseUri = ''

            # A URL to the main website for this project.
            # ProjectUri = ''

            # A URL to an icon representing this module.
            # IconUri = ''

            # ReleaseNotes of this module
            # ReleaseNotes = ''

        } # End of PSData hashtable

    }
}

Тестирование ресурса

Сохранив файлы класса и манифеста в структуру папок, как описано выше, вы можете создать конфигурацию, использующую новый ресурс. Инструкции по запуску конфигурации DSC см. в статье Активирование конфигураций. Следующая конфигурация проверяет, существует ли файл /tmp/test.txt, и совпадает ли содержимое со строкой, предоставленной свойством Content. В противном случае выполняется запись всего файла.

Configuration MyConfig
{
    Import-DSCResource -ModuleName NewFile
    NewFile testFile
    {
        Path = "/tmp/test.txt"
        Content = "DSC Rocks!"
        Ensure = "Present"
    }
}
MyConfig

Поддержка PsDscRunAsCredential

[Примечание.] PsDscRunAsCredential поддерживается в PowerShell 5.0 и более поздних версий.

Свойство PsDscRunAsCredential может использоваться в блоке ресурса конфигураций DSC, чтобы указать, что ресурс должен выполняться с указанным набором учетных данных. Дополнительные сведения см. в разделе Запуск DSC с учетными данными пользователя.

Требование параметра PsDscRunAsCredential для ресурса или его запрещение

Атрибут DscResource() принимает необязательный параметр RunAsCredential. Этот параметр принимает одно из трех значений:

  • OptionalPsDscRunAsCredential является необязательным для конфигураций, вызывающих этот ресурс. Это значение по умолчанию.
  • MandatoryPsDscRunAsCredential необходимо использовать для любой конфигурации, которая вызывает этот ресурс.
  • Конфигурации NotSupported, которые вызывают этот ресурс, не могут использовать PsDscRunAsCredential.
  • Default аналогичен Optional.

Например, используйте следующий атрибут, чтобы указать, что настраиваемый ресурс не поддерживает использование PsDscRunAsCredential:

[DscResource(RunAsCredential=NotSupported)]
class NewFile {
}

Объявление нескольких ресурсов класса в модуле

Модуль может определять несколько ресурсов DSC на основе классов. Необходимо просто объявить все классы в одном .psm1 файле и включить каждое имя в .psd1 манифест.

$env:ProgramFiles\WindowsPowerShell\Modules (folder)
     |- MyDscResource (folder)
        |- MyDscResource.psm1
           MyDscResource.psd1

Доступ к контексту пользователя

Чтобы получить доступ к пользовательскому контексту из настраиваемого ресурса, можно использовать автоматическую переменную $global:PsDscContext.

Например, следующий код пропишет пользовательский контекст, по которому выполняется ресурс, в подробный выходной поток:

if (PsDscContext.RunAsUser) {
    Write-Verbose "User: $global:PsDscContext.RunAsUser";
}

См. также:

Создание пользовательских ресурсов DSC Windows PowerShell