使用 PowerShell 类编写自定义 DSC 资源

适用于:Windows PowerShell 5.0

了解了在 Windows PowerShell 5.0 中使用 PowerShell 类的简介后,现在你可以通过创建一个类来定义 DSC 资源。 类可以同时定义资源的架构和实现,因此无需创建单独的 MOF 文件。 因为无需 DSCResources 文件夹,基于类的资源的文件夹结构也得以简化。

在基于类的 DSC 资源中,架构定义为类的属性,可以使用属性对其进行修改以指定属性类型。 资源通过 Get()Set()Test() 的方法得到实现(相当于脚本资源中的 Get-TargetResourceSet-TargetResourceTest-TargetResource 函数)。

在本文中,我们将创建一个名为 NewFile 的简单资源,用于管理指定路径中的文件。

有关 DSC 资源的详细信息,请参阅构建自定义 Windows PowerShell Desired State Configuration 资源

注意

基于类的资源中不支持泛型集合。

类资源的文件夹结构

想要使用 PowerShell 类实现 DSC 自定义资源,请创建下列文件夹结构。 在 MyDscResource.psm1 中定义类,并且在 MyDscResource.psd1 中定义模块清单。

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

创建类

使用类关键字创建 PowerShell 类。 使用 DscResource() 特性来指定类是 DSC 资源。 类的名称就是 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 Automanage 的计算机配置功能,请始终在为 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-TargetResourceSet-TargetResourceTest-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 及更高版本中受支持。

可以在 DSC 配置资源块中使用 PsDscRunAsCredential 属性,以指定应使用指定的一组凭据运行资源。 有关详细信息,请参阅使用用户凭据运行 DSC

需要或禁止对资源使用 PsDscRunAsCredential

DscResource() 属性使用可选参数 RunAsCredential。 此参数可取以下三个值之一:

  • Optional对于调用此资源的配置,PsDscRunAsCredential 是可选的。 这是默认值。
  • MandatoryPsDscRunAsCredential 必须用于调用此资源的任何配置。
  • NotSupported:调用此资源的配置无法使用 PsDscRunAsCredential。
  • DefaultOptional 相同。

例如,使用以下属性指定自定义资源不支持使用 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";
}

另请参阅

构建自定义 Windows PowerShell Desired State Configuration 资源