Écriture d’une ressource DSC personnalisée avec des classes PowerShell

S’applique à : Windows PowerShell 5.0

Avec l’introduction des classes PowerShell dans Windows PowerShell 5.0, vous pouvez maintenant définir une ressource DSC en créant une classe. La classe définit à la fois le schéma et l’implémentation de la ressource. Il n’est donc pas nécessaire de créer un fichier MOF distinct. La structure de dossiers d’une ressource basée sur une classe est également plus simple, car un dossier DSCResources n’est pas nécessaire.

Dans une ressource DSC basée sur une classe, le schéma est défini en tant que propriétés de la classe qui peuvent être modifiées avec des attributs pour spécifier le type de propriété. La ressource est implémentée par des méthodes Get(), Set()et Test() (équivalentes aux fonctions Get-TargetResource, Set-TargetResourceet Test-TargetResource dans une ressource de script.

Dans cet article, nous allons créer une ressource simple nommée NewFile qui gère un fichier dans un chemin d’accès spécifié.

Pour plus d’informations sur les ressources DSC, consultez Générer des ressources de configuration d’état souhaité Windows PowerShell personnalisées

Note

Les collections génériques ne sont pas prises en charge dans les ressources basées sur des classes.

Structure de dossiers pour une ressource de classe

Pour implémenter une ressource personnalisée DSC avec une classe PowerShell, créez la structure de dossiers suivante. La classe est définie dans MyDscResource.psm1 et le manifeste du module est défini dans MyDscResource.psd1.

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

Créer la classe

Vous utilisez le mot clé de classe pour créer une classe PowerShell. Pour spécifier qu’une classe est une ressource DSC, utilisez l’attribut DscResource(). Le nom de la classe est le nom de la ressource DSC.

[DscResource()]
class NewFile {
}

Déclarer des propriétés

Le schéma de ressource DSC est défini en tant que propriétés de la classe. Nous déclarons trois propriétés comme suit.

[DscProperty(Key)]
[string] $path

[DscProperty(Mandatory)]
[ensure] $ensure

[DscProperty()]
[string] $content

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

Notez que les propriétés sont modifiées par les attributs. La signification des attributs est la suivante :

  • DscProperty(Key) : la propriété est requise. La propriété est une clé. Les valeurs de toutes les propriétés marquées comme clés doivent être combinées pour identifier de manière unique une instance de ressource au sein d’une configuration.
  • DscProperty(Obligatoire) : la propriété est requise.
  • DscProperty(NotConfigurable): la propriété est en lecture seule. Les propriétés marquées avec cet attribut ne peuvent pas être définies par une configuration, mais sont remplies par la méthode Get() lorsqu’elles sont présentes.
  • DscProperty() : la propriété est configurable, mais elle n’est pas obligatoire.

Les propriétés $Path et $SourcePath sont les deux chaînes. Le $CreationTime est une propriété DateTime. La propriété $Ensure est un type d’énumération, défini comme suit.

enum Ensure
{
    Absent
    Present
}

Classes d’incorporation

Si vous souhaitez inclure un nouveau type avec des propriétés définies que vous pouvez utiliser dans votre ressource, créez simplement une classe avec des types de propriétés, comme décrit ci-dessus.

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

    [DscProperty()]
    [string] $Phrase
}

Note

La classe MyDscResourceReason est déclarée ici avec le nom du module comme préfixe. Bien que vous puissiez donner des classes incorporées n’importe quel nom, si deux modules ou plus définissent une classe portant le même nom et sont tous deux utilisés dans une configuration, PowerShell déclenche une exception.

Pour éviter les exceptions provoquées par des conflits de noms dans DSC, préfixez les noms de vos classes incorporées avec le nom du module. Si le nom de votre classe incorporée est déjà peu susceptible d’être en conflit, vous pouvez l’utiliser sans préfixe.

Si votre ressource DSC est conçue pour être utilisée avec la fonctionnalité de configuration de l’ordinateur d’Azure Automanage, préfixez toujours le nom de la classe incorporée que vous créez pour la propriété Raisons.

Fonctions publiques et privées

Vous pouvez créer des fonctions PowerShell dans le même fichier de module et les utiliser dans les méthodes de votre ressource de classe DSC. Les fonctions doivent être déclarées comme publiques, mais les blocs de script au sein de ces fonctions publiques peuvent appeler des fonctions privées. La seule différence est de savoir s’ils sont répertoriés dans la propriété FunctionsToExport du manifeste du module.

<#
   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
}

Implémentation des méthodes

Les méthodes Get(), Set()et Test() sont analogues aux fonctions Get-TargetResource, Set-TargetResourceet Test-TargetResource dans une ressource de script.

En guise de meilleure pratique, réduisez la quantité de code dans l’implémentation de classe. Au lieu de cela, déplacez la majorité de votre code vers des fonctions publiques dans le module, qui peuvent ensuite être testées indépendamment.

<#
    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
}

Fichier complet

Le fichier de classe complet suit.

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
    }
}

Créer un manifeste

Pour rendre une ressource basée sur une classe disponible pour le moteur DSC, vous devez inclure une instruction DscResourcesToExport dans le fichier manifeste qui indique au module d’exporter la ressource. Notre manifeste ressemble à ceci :

@{

    # 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

    }
}

Tester la ressource

Après avoir enregistré les fichiers de classe et de manifeste dans la structure de dossiers comme décrit précédemment, vous pouvez créer une configuration qui utilise la nouvelle ressource. Pour plus d’informations sur l’exécution d’une configuration DSC, consultez Mettre en œuvre des configurations. La configuration suivante vérifie si le fichier à /tmp/test.txt existe et si le contenu correspond à la chaîne fournie par la propriété « Content ». Si ce n’est pas le cas, l’intégralité du fichier est écrite.

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

Prise en charge de PsDscRunAsCredential

[Remarque] psDscRunAsCredential est pris en charge dans PowerShell 5.0 et versions ultérieures.

La propriété PsDscRunAsCredential peut être utilisée dans configurations DSC bloc de ressources pour spécifier que la ressource doit être exécutée sous un ensemble d’informations d’identification spécifié. Pour plus d’informations, consultez Exécution de DSC avec des informations d’identification utilisateur.

Exiger ou interdire psDscRunAsCredential pour votre ressource

L’attribut DscResource() prend un paramètre facultatif RunAsCredential. Ce paramètre prend l’une des trois valeurs suivantes :

  • Optional PsDscRunAsCredential est facultatif pour les configurations qui appellent cette ressource. Il s’agit de la valeur par défaut.
  • Mandatory psDscRunAsCredential doit être utilisé pour toute configuration qui appelle cette ressource.
  • NotSupported Configurations qui appellent cette ressource ne peuvent pas utiliser psDscRunAsCredential.
  • Default Identique à Optional.

Par exemple, utilisez l’attribut suivant pour spécifier que votre ressource personnalisée ne prend pas en charge l’utilisation de PsDscRunAsCredential:

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

Déclaration de plusieurs ressources de classe dans un module

Un module peut définir plusieurs ressources DSC basées sur des classes. Vous devez simplement déclarer toutes les classes dans le même fichier .psm1 et inclure chaque nom dans le manifeste .psd1.

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

Accéder au contexte utilisateur

Pour accéder au contexte utilisateur à partir d’une ressource personnalisée, vous pouvez utiliser la variable automatique $global:PsDscContext.

Par exemple, le code suivant écrit le contexte utilisateur sous lequel la ressource s’exécute dans le flux de sortie détaillé :

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

Voir aussi

créer des ressources de configuration d’état souhaité Windows PowerShell personnalisées