Compare commits
6 Commits
10fb3792cb
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
793c4acddd | ||
|
|
edf7c8ad34 | ||
| 39f7a0bd60 | |||
|
|
971839f30c | ||
|
|
cfe2a8099e | ||
|
|
c93f4ebc5c |
111
.sample/Deployment.psd1
Normal file
111
.sample/Deployment.psd1
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
@{
|
||||||
|
Templates = @(
|
||||||
|
@{
|
||||||
|
TemplateName = "HZD Template"
|
||||||
|
TemplatePath = "./Configurations/HZD.psd1"
|
||||||
|
},
|
||||||
|
@{
|
||||||
|
TemplateName = "SQL Server Template"
|
||||||
|
TemplatePath = "./Configurations/Services/Template-SQLServer.psd1"
|
||||||
|
},
|
||||||
|
@{
|
||||||
|
TemplateName = "Umgebungs Template"
|
||||||
|
TemplatePath = "./Configurations/Umgebungen/Template-Test.psd1"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
AllNodes = @(
|
||||||
|
@{
|
||||||
|
NodeName = "JURZMAZSSQL901"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
NonNodeData =@{
|
||||||
|
Services = @{
|
||||||
|
SQLServer = @{
|
||||||
|
Instances = @(
|
||||||
|
@{
|
||||||
|
Name = 'MSSQLSERVER'
|
||||||
|
|
||||||
|
ServiceAccounts = @{
|
||||||
|
SQLEngineAccount = @{
|
||||||
|
ServiceAccountName = "gmsaE2Af901"
|
||||||
|
AccountType = "Group"
|
||||||
|
Path = "OU=DBA,OU=Dienste_Verfahren,OU=Administration,DC=justiz,DC=hessen,DC=de"
|
||||||
|
ManagedPasswordPrincipals = @(
|
||||||
|
@{
|
||||||
|
GroupName = "sicE2AfE2ADB901"
|
||||||
|
Path = "OU=DBA,OU=Dienste_Verfahren,OU=Administration,DC=justiz,DC=hessen,DC=de"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
SQLAgentAccount = @{
|
||||||
|
ServiceAccountName = "gmsaE2Aa901"
|
||||||
|
AccountType = "Group"
|
||||||
|
Path = "OU=DBA,OU=Dienste_Verfahren,OU=Administration,DC=justiz,DC=hessen,DC=de"
|
||||||
|
ManagedPasswordPrincipals = @(
|
||||||
|
@{
|
||||||
|
GroupName = "sicE2AfE2ADB901"
|
||||||
|
Path = "OU=DBA,OU=Dienste_Verfahren,OU=Administration,DC=justiz,DC=hessen,DC=de"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AlwaysOn = @{
|
||||||
|
StartIndex = "B"
|
||||||
|
AlwaysOnGroups = @(
|
||||||
|
@{
|
||||||
|
Name = "TestAG"
|
||||||
|
FailoverMode ="Automatic"
|
||||||
|
SeedingMode = "Automatic"
|
||||||
|
Listener = @{
|
||||||
|
Provision = $true
|
||||||
|
IpAddress = "'10.96.x.x/255.255.255.0','10.96.x.x/255.255.255.0'"
|
||||||
|
Port = 1433
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfigurationOptions= @()
|
||||||
|
|
||||||
|
AdditionalScripts = @()
|
||||||
|
},
|
||||||
|
@{
|
||||||
|
Name = 'MSSQLSERVERTEST'
|
||||||
|
|
||||||
|
ServiceAccounts = @{
|
||||||
|
SQLEngineAccount = @{
|
||||||
|
ServiceAccountName = "gmsaE2Af901"
|
||||||
|
AccountType = "Group"
|
||||||
|
Path = "OU=DBA,OU=Dienste_Verfahren,OU=Administration,DC=justiz,DC=hessen,DC=de"
|
||||||
|
ManagedPasswordPrincipals = @(
|
||||||
|
@{
|
||||||
|
GroupName = "sicE2AfE2ADB901"
|
||||||
|
Path = "OU=DBA,OU=Dienste_Verfahren,OU=Administration,DC=justiz,DC=hessen,DC=de"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
SQLAgentAccount = @{
|
||||||
|
ServiceAccountName = "gmsaE2Aa901"
|
||||||
|
AccountType = "Group"
|
||||||
|
Path = "OU=DBA,OU=Dienste_Verfahren,OU=Administration,DC=justiz,DC=hessen,DC=de"
|
||||||
|
ManagedPasswordPrincipals = @(
|
||||||
|
@{
|
||||||
|
GroupName = "sicE2AfE2ADB901"
|
||||||
|
Path = "OU=DBA,OU=Dienste_Verfahren,OU=Administration,DC=justiz,DC=hessen,DC=de"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfigurationOptions= @()
|
||||||
|
|
||||||
|
AdditionalScripts = @()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
157
.sample/Template.psd1
Normal file
157
.sample/Template.psd1
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
@{
|
||||||
|
NonNodeData =@{
|
||||||
|
Services = @{
|
||||||
|
SQLServer = @{
|
||||||
|
General = @{
|
||||||
|
Version = ""
|
||||||
|
Release = "2022"
|
||||||
|
SourcePath = "\\JURZMAZSFIL02.justiz.hessen.de\DbaDSCSources\Binaries"
|
||||||
|
DestinationPath = "C:\HZD\SQL_Install"
|
||||||
|
}
|
||||||
|
Basic = @{
|
||||||
|
Registry = @(
|
||||||
|
@{
|
||||||
|
Key = "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa"
|
||||||
|
ValueName = "DisableLoopbackCheck"
|
||||||
|
ValueData = 1
|
||||||
|
ValueType = "Dword"
|
||||||
|
},
|
||||||
|
@{
|
||||||
|
Ensure = "Present"
|
||||||
|
Key = "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL16.MSSQLSERVER\SQLServerAgent"
|
||||||
|
ValueName = "MsxEncryptChannelOptions"
|
||||||
|
ValueData = 1
|
||||||
|
ValueType = "Dword"
|
||||||
|
}
|
||||||
|
|
||||||
|
@{
|
||||||
|
Ensure = 'Present'
|
||||||
|
Key = 'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL16.MSSQLSERVER\SQLServerAgent'
|
||||||
|
ValueName = 'ErrorLogFile'
|
||||||
|
ValueData = 'D:\MSSQL16.MSSQLSERVER\MSSQL\Log\SQLAGENT.OUT'
|
||||||
|
ValueType = 'String'
|
||||||
|
},
|
||||||
|
@{
|
||||||
|
Ensure = 'Present'
|
||||||
|
Key = 'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL16.MSSQLSERVER\SQLServerAgent'
|
||||||
|
ValueName = 'WorkingDirectory'
|
||||||
|
ValueData = 'D:\MSSQL16.MSSQLSERVER\MSSQL\JOBS'
|
||||||
|
ValueType = 'String'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
WindowsFeatures = @(
|
||||||
|
@{
|
||||||
|
Name = "RSAT-AD-PowerShell"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Modules = @(
|
||||||
|
@{
|
||||||
|
Name = "SQLServer"
|
||||||
|
AllowClobber = $true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Instances = @(
|
||||||
|
@{
|
||||||
|
Name = '*'
|
||||||
|
Features = 'SQLENGINE,FULLTEXT'
|
||||||
|
SQLSysAdminAccounts = 'sicDBServer_ServerAdmins'
|
||||||
|
Directories = @{
|
||||||
|
InstallSharedDir = 'D:\Microsoft SQL Server'
|
||||||
|
InstallSharedWOWDir = 'D:\Microsoft SQL Server (x86)'
|
||||||
|
InstanceDir = 'D:\Microsoft SQL Server'
|
||||||
|
InstallSQLDataDir = 'D:'
|
||||||
|
SQLSysAdminAccounts = 'sicDBServer_ServerAdmins'
|
||||||
|
SQLBackupDir = '\\JURZMAZSFIL02.justiz.hessen.de\DbaDSCSources\Backups'
|
||||||
|
SQLUserDBDir = ''
|
||||||
|
SQLUserDBLogDir = ''
|
||||||
|
SQLTempDBDir = ''
|
||||||
|
SQLTempDBLogDir = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
Drives = @{
|
||||||
|
}
|
||||||
|
|
||||||
|
MountPoints = @{
|
||||||
|
}
|
||||||
|
|
||||||
|
Memory = @{
|
||||||
|
UseDynamic = $true
|
||||||
|
minValue = 1024
|
||||||
|
maxValue = 8192
|
||||||
|
}
|
||||||
|
|
||||||
|
ServiceAccounts = @{
|
||||||
|
}
|
||||||
|
|
||||||
|
AlwaysOn = @{
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfigurationOptions= @(
|
||||||
|
@{
|
||||||
|
OptionName = 'show advanced options'
|
||||||
|
OptionValue = '1'
|
||||||
|
},
|
||||||
|
@{
|
||||||
|
OptionName = 'cost threshold for parallelism'
|
||||||
|
OptionValue = '50'
|
||||||
|
},
|
||||||
|
@{
|
||||||
|
OptionName = 'blocked process threshold (s)'
|
||||||
|
OptionValue = '30'
|
||||||
|
},
|
||||||
|
@{
|
||||||
|
OptionName = 'backup compression default'
|
||||||
|
OptionValue = '1'
|
||||||
|
}
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
AdditionalScripts = @(
|
||||||
|
|
||||||
|
@{
|
||||||
|
ScriptName = 'AdminDB'
|
||||||
|
ScriptPath = '\\JURZMAZSFIL02.justiz.hessen.de\DbaDSCSources\SQL_Scripts\AdminDB'
|
||||||
|
DependsOn = '[SqlSetup]Instance-MSSQLSERVER'
|
||||||
|
},
|
||||||
|
|
||||||
|
@{
|
||||||
|
ScriptName = 'RenameSA'
|
||||||
|
ScriptPath = '\\JURZMAZSFIL02.justiz.hessen.de\DbaDSCSources\SQL_Scripts\RenameSA'
|
||||||
|
DependsOn = '[SqlScript]AdminDB'
|
||||||
|
|
||||||
|
},
|
||||||
|
@{
|
||||||
|
ScriptName = 'Maintenance_Solution'
|
||||||
|
ScriptPath = '\\JURZMAZSFIL02.justiz.hessen.de\DbaDSCSources\SQL_Scripts\Maintenance_Solutions'
|
||||||
|
DependsOn = '[SqlScript]RenameSA'
|
||||||
|
},
|
||||||
|
|
||||||
|
@{
|
||||||
|
ScriptName = 'WhoIsActive'
|
||||||
|
ScriptPath = '\\JURZMAZSFIL02.justiz.hessen.de\DbaDSCSources\SQL_Scripts\WhoIsActive_Monitoring'
|
||||||
|
DependsOn = '[SqlScript]Maintenance_Solution'
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
@{
|
||||||
|
ScriptName = 'Alerts'
|
||||||
|
ScriptPath = '\\JURZMAZSFIL02.justiz.hessen.de\DbaDSCSources\SQL_Scripts\Alerts_Email'
|
||||||
|
DependsOn = '[SqlScript]WhoIsActive'
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
@{
|
||||||
|
ScriptName = 'TempDB'
|
||||||
|
ScriptPath = '\\JURZMAZSFIL02.justiz.hessen.de\DbaDSCSources\SQL_Scripts\TempDB_Size'
|
||||||
|
DependsOn = '[SqlScript]Alerts'
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
132
.tests/Merge-DSCConfigurationData.Tests.ps1
Normal file
132
.tests/Merge-DSCConfigurationData.Tests.ps1
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
$script:ModuleRoot = Split-Path -Parent $PSScriptRoot
|
||||||
|
$script:SampleRoot = Join-Path -Path $script:ModuleRoot -ChildPath ".sample"
|
||||||
|
|
||||||
|
Import-Module (Join-Path -Path $script:ModuleRoot -ChildPath "Merge-DSCConfigurationData.psd1") -Force
|
||||||
|
|
||||||
|
Describe "Merge-DSCConfigurationData" {
|
||||||
|
It "merges wildcard instance templates into all deployment instances" {
|
||||||
|
$Template = Import-PowerShellDataFile (Join-Path -Path $script:SampleRoot -ChildPath "Template.psd1")
|
||||||
|
$Deployment = Import-PowerShellDataFile (Join-Path -Path $script:SampleRoot -ChildPath "Deployment.psd1")
|
||||||
|
|
||||||
|
$Result = Merge-DSCConfigurationData -Template $Template -Deployment $Deployment
|
||||||
|
$Instances = @($Result.NonNodeData.Services.SQLServer.Instances)
|
||||||
|
|
||||||
|
$Instances.Count | Should Be 2
|
||||||
|
($Instances.Name -contains "*") | Should Be $false
|
||||||
|
|
||||||
|
foreach($Name in @("MSSQLSERVER", "MSSQLSERVERTEST")){
|
||||||
|
$Instance = $Instances | Where-Object { $_.Name -eq $Name }
|
||||||
|
|
||||||
|
$Instance | Should Not BeNullOrEmpty
|
||||||
|
$Instance.Features | Should Be "SQLENGINE,FULLTEXT"
|
||||||
|
$Instance.ConfigurationOptions.Count | Should Be 4
|
||||||
|
$Instance.AdditionalScripts.Count | Should Be 6
|
||||||
|
$Instance.Memory | Should Not BeNullOrEmpty
|
||||||
|
$Instance.Directories | Should Not BeNullOrEmpty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
It "does not merge an exact instance template into another instance name" {
|
||||||
|
$Template = @{
|
||||||
|
NonNodeData = @{
|
||||||
|
Services = @{
|
||||||
|
SQLServer = @{
|
||||||
|
Instances = @(
|
||||||
|
@{
|
||||||
|
Name = "MSSQLSERVER"
|
||||||
|
Marker = "exact-only"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$Deployment = @{
|
||||||
|
NonNodeData = @{
|
||||||
|
Services = @{
|
||||||
|
SQLServer = @{
|
||||||
|
Instances = @(
|
||||||
|
@{ Name = "MSSQLSERVER" },
|
||||||
|
@{ Name = "MSSQLSERVERTEST" }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$Result = Merge-DSCConfigurationData -Template $Template -Deployment $Deployment
|
||||||
|
$Instances = @($Result.NonNodeData.Services.SQLServer.Instances)
|
||||||
|
|
||||||
|
($Instances | Where-Object { $_.Name -eq "MSSQLSERVER" }).Marker | Should Be "exact-only"
|
||||||
|
($Instances | Where-Object { $_.Name -eq "MSSQLSERVERTEST" }).Marker | Should BeNullOrEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
It "uses the name-key fallback for unknown arrays" {
|
||||||
|
$Template = @{
|
||||||
|
Databases = @(
|
||||||
|
@{
|
||||||
|
DatabaseName = "AppDb"
|
||||||
|
RecoveryModel = "Full"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
$Deployment = @{
|
||||||
|
Databases = @(
|
||||||
|
@{
|
||||||
|
DatabaseName = "AppDb"
|
||||||
|
Owner = "dbo"
|
||||||
|
},
|
||||||
|
@{
|
||||||
|
DatabaseName = "LogDb"
|
||||||
|
Owner = "dbo"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
$Result = Merge-DSCConfigurationData -Template $Template -Deployment $Deployment
|
||||||
|
$Databases = @($Result.Databases)
|
||||||
|
|
||||||
|
($Databases | Where-Object { $_.DatabaseName -eq "AppDb" }).RecoveryModel | Should Be "Full"
|
||||||
|
($Databases | Where-Object { $_.DatabaseName -eq "LogDb" }).RecoveryModel | Should BeNullOrEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
It "uses Key and ValueName together for registry array merges" {
|
||||||
|
$Template = @{
|
||||||
|
Basic = @{
|
||||||
|
Registry = @(
|
||||||
|
@{
|
||||||
|
Key = "K1"
|
||||||
|
ValueName = "Enabled"
|
||||||
|
Default = "Template-K1"
|
||||||
|
},
|
||||||
|
@{
|
||||||
|
Key = "K2"
|
||||||
|
ValueName = "Enabled"
|
||||||
|
Default = "Template-K2"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$Deployment = @{
|
||||||
|
Basic = @{
|
||||||
|
Registry = @(
|
||||||
|
@{
|
||||||
|
Key = "K1"
|
||||||
|
ValueName = "Enabled"
|
||||||
|
ValueData = "Deployment-K1"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$Result = Merge-DSCConfigurationData -Template $Template -Deployment $Deployment
|
||||||
|
$Registry = @($Result.Basic.Registry)
|
||||||
|
|
||||||
|
($Registry | Where-Object { $_.Key -eq "K1" }).Default | Should Be "Template-K1"
|
||||||
|
($Registry | Where-Object { $_.Key -eq "K1" }).ValueData | Should Be "Deployment-K1"
|
||||||
|
($Registry | Where-Object { $_.Key -eq "K2" }).Default | Should Be "Template-K2"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
@{
|
@{
|
||||||
RootModule = "Merge-DSCConfigurationData.psm1"
|
RootModule = "Merge-DSCConfigurationData.psm1"
|
||||||
ModuleVersion = "1.0.1"
|
ModuleVersion = "1.1.0"
|
||||||
GUID = "c1c7e70d-9049-4eaa-a3c9-44a424c35ef5"
|
GUID = "c1c7e70d-9049-4eaa-a3c9-44a424c35ef5"
|
||||||
Author = "Torsten Brendgen"
|
Author = "Torsten Brendgen"
|
||||||
Copyright = "(c) Torsten Brendgen. All rights reserved."
|
Copyright = "(c) Torsten Brendgen. All rights reserved."
|
||||||
@@ -9,7 +9,8 @@
|
|||||||
FunctionsToExport = @(
|
FunctionsToExport = @(
|
||||||
"Merge-DSCConfigurationData",
|
"Merge-DSCConfigurationData",
|
||||||
"Export-PowerShellDataFile",
|
"Export-PowerShellDataFile",
|
||||||
"New-DSCConfigurationDataDeployment"
|
"New-DSCConfigurationDataDeployment",
|
||||||
|
"Test-DSCConfigurationDataDeployment"
|
||||||
)
|
)
|
||||||
CmdletsToExport = @()
|
CmdletsToExport = @()
|
||||||
VariablesToExport = @()
|
VariablesToExport = @()
|
||||||
|
|||||||
@@ -11,5 +11,6 @@ foreach($File in @($Private + $Public)){
|
|||||||
Export-ModuleMember -Function @(
|
Export-ModuleMember -Function @(
|
||||||
"Merge-DSCConfigurationData",
|
"Merge-DSCConfigurationData",
|
||||||
"Export-PowerShellDataFile",
|
"Export-PowerShellDataFile",
|
||||||
"New-DSCConfigurationDataDeployment"
|
"New-DSCConfigurationDataDeployment",
|
||||||
|
"Test-DSCConfigurationDataDeployment"
|
||||||
)
|
)
|
||||||
|
|||||||
34
Private/Get-ConfigurationDataSourceFileInfo.ps1
Normal file
34
Private/Get-ConfigurationDataSourceFileInfo.ps1
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
function Get-ConfigurationDataSourceFileInfo {
|
||||||
|
[CmdletBinding()]
|
||||||
|
Param(
|
||||||
|
[Parameter(Mandatory=$true)]
|
||||||
|
[string]
|
||||||
|
$Path,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$true)]
|
||||||
|
[string]
|
||||||
|
$BasePath,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[switch]
|
||||||
|
$UseRelativePath
|
||||||
|
)
|
||||||
|
|
||||||
|
$ResolvedPath = Resolve-ConfigurationDataTemplatePath -Path $Path -BasePath $BasePath
|
||||||
|
$Item = Get-Item -LiteralPath $ResolvedPath
|
||||||
|
$Hash = Get-FileHash -LiteralPath $ResolvedPath -Algorithm SHA256
|
||||||
|
|
||||||
|
$DisplayPath = if($UseRelativePath){
|
||||||
|
ConvertTo-ConfigurationDataRelativePath -Path $ResolvedPath -BasePath $BasePath
|
||||||
|
}else{
|
||||||
|
$ResolvedPath
|
||||||
|
}
|
||||||
|
|
||||||
|
[ordered]@{
|
||||||
|
Path = $DisplayPath
|
||||||
|
Hash = $Hash.Hash
|
||||||
|
Algorithm = $Hash.Algorithm
|
||||||
|
Size = $Item.Length
|
||||||
|
LastWriteTimeUtc = $Item.LastWriteTimeUtc.ToString("o")
|
||||||
|
}
|
||||||
|
}
|
||||||
19
Private/Resolve-ConfigurationDataSourcePath.ps1
Normal file
19
Private/Resolve-ConfigurationDataSourcePath.ps1
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
function Resolve-ConfigurationDataSourcePath {
|
||||||
|
[CmdletBinding()]
|
||||||
|
Param(
|
||||||
|
[Parameter(Mandatory=$true)]
|
||||||
|
[string]
|
||||||
|
$Path,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$true)]
|
||||||
|
[string]
|
||||||
|
$DeploymentPath
|
||||||
|
)
|
||||||
|
|
||||||
|
$BasePath = Split-Path -Path (Repair-ConfigurationDataPathEncoding -Path $DeploymentPath) -Parent
|
||||||
|
if([string]::IsNullOrWhiteSpace($BasePath)){
|
||||||
|
$BasePath = (Get-Location).Path
|
||||||
|
}
|
||||||
|
|
||||||
|
return Resolve-ConfigurationDataTemplatePath -Path $Path -BasePath $BasePath
|
||||||
|
}
|
||||||
14
Private/Test-ConfigurationDataNodeSealed.ps1
Normal file
14
Private/Test-ConfigurationDataNodeSealed.ps1
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
function Test-ConfigurationDataNodeSealed {
|
||||||
|
[CmdletBinding()]
|
||||||
|
Param(
|
||||||
|
[Parameter(Mandatory=$true)]
|
||||||
|
[System.Collections.IDictionary]
|
||||||
|
$Node
|
||||||
|
)
|
||||||
|
|
||||||
|
if(-not (Test-ConfigurationDataDictionaryKey -Dictionary $Node -Key "Sealed")){
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
return [System.Management.Automation.LanguagePrimitives]::ConvertTo($Node["Sealed"], [bool])
|
||||||
|
}
|
||||||
@@ -20,7 +20,11 @@ function Merge-DSCConfigurationData {
|
|||||||
|
|
||||||
[Parameter(Mandatory=$false, ParameterSetName="Path")]
|
[Parameter(Mandatory=$false, ParameterSetName="Path")]
|
||||||
[switch]
|
[switch]
|
||||||
$PassThru
|
$PassThru,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false, ParameterSetName="Data")]
|
||||||
|
[string]
|
||||||
|
$MergePath = '$'
|
||||||
)
|
)
|
||||||
|
|
||||||
begin {
|
begin {
|
||||||
@@ -37,6 +41,10 @@ function Merge-DSCConfigurationData {
|
|||||||
$Output = $Deployment
|
$Output = $Deployment
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if((Test-ConfigurationDataNodeSealed -Node $Template) -and $Deployment.Count -gt 0){
|
||||||
|
throw "Configuration data path [$MergePath] is sealed and cannot be modified."
|
||||||
|
}
|
||||||
|
|
||||||
foreach($Property in $Template.GetEnumerator()){
|
foreach($Property in $Template.GetEnumerator()){
|
||||||
if($Property.Name -eq "Metadata"){
|
if($Property.Name -eq "Metadata"){
|
||||||
Write-Verbose "Skipping metadata block during configuration data merge"
|
Write-Verbose "Skipping metadata block during configuration data merge"
|
||||||
@@ -47,7 +55,7 @@ function Merge-DSCConfigurationData {
|
|||||||
Write-Verbose "Key [$($Property.Name)] is a Dictionary"
|
Write-Verbose "Key [$($Property.Name)] is a Dictionary"
|
||||||
if(Test-ConfigurationDataDictionaryKey -Dictionary $Deployment -Key $Property.Name){
|
if(Test-ConfigurationDataDictionaryKey -Dictionary $Deployment -Key $Property.Name){
|
||||||
Write-Verbose "Key [$($Property.Name)] is present in Deployment Data"
|
Write-Verbose "Key [$($Property.Name)] is present in Deployment Data"
|
||||||
$Output[$Property.Name] = Merge-DSCConfigurationData -Template $Template[$Property.Name] -Deployment $Deployment[$Property.Name] -Output $Output[$Property.Name]
|
$Output[$Property.Name] = Merge-DSCConfigurationData -Template $Template[$Property.Name] -Deployment $Deployment[$Property.Name] -Output $Output[$Property.Name] -MergePath "$MergePath.$($Property.Name)"
|
||||||
}else{
|
}else{
|
||||||
Write-Verbose "Key [$($Property.Name)] is not present in Deployment Data"
|
Write-Verbose "Key [$($Property.Name)] is not present in Deployment Data"
|
||||||
$Output.Add($Property.Name,(Copy-ConfigurationDataValue -Value $Template[$Property.Name]))
|
$Output.Add($Property.Name,(Copy-ConfigurationDataValue -Value $Template[$Property.Name]))
|
||||||
@@ -89,7 +97,7 @@ function Merge-DSCConfigurationData {
|
|||||||
if($MatchingDeploymentItems.Count -gt 0){
|
if($MatchingDeploymentItems.Count -gt 0){
|
||||||
foreach($DeploymentItem in $MatchingDeploymentItems){
|
foreach($DeploymentItem in $MatchingDeploymentItems){
|
||||||
$OutputItem = @($Output[$Property.Name] | Where-Object { Test-ConfigurationDataItemKeyMatch -Left $_ -Right $DeploymentItem -KeyNames $SearchKeyNames })[0]
|
$OutputItem = @($Output[$Property.Name] | Where-Object { Test-ConfigurationDataItemKeyMatch -Left $_ -Right $DeploymentItem -KeyNames $SearchKeyNames })[0]
|
||||||
Merge-DSCConfigurationData -Template $TemplateItem.Value -Deployment $DeploymentItem -Output $OutputItem | Out-Null
|
Merge-DSCConfigurationData -Template $TemplateItem.Value -Deployment $DeploymentItem -Output $OutputItem -MergePath "$MergePath.$($Property.Name)[$(Format-ConfigurationDataMergeKey -Item $DeploymentItem -KeyNames $SearchKeyNames)]" | Out-Null
|
||||||
}
|
}
|
||||||
}else{
|
}else{
|
||||||
if($TemplateItem.IsWildcard){
|
if($TemplateItem.IsWildcard){
|
||||||
|
|||||||
@@ -91,11 +91,7 @@ function New-DSCConfigurationDataDeployment {
|
|||||||
})
|
})
|
||||||
|
|
||||||
$SourceFiles = @($MergeResult.Files | ForEach-Object {
|
$SourceFiles = @($MergeResult.Files | ForEach-Object {
|
||||||
if($UseRelativePaths){
|
Get-ConfigurationDataSourceFileInfo -Path $_ -BasePath $OutputDirectory -UseRelativePath:$UseRelativePaths
|
||||||
ConvertTo-ConfigurationDataRelativePath -Path $_ -BasePath $OutputDirectory
|
|
||||||
}else{
|
|
||||||
$_
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
$ConfigurationData["Metadata"] = [ordered]@{
|
$ConfigurationData["Metadata"] = [ordered]@{
|
||||||
|
|||||||
125
Public/Test-DSCConfigurationDataDeployment.ps1
Normal file
125
Public/Test-DSCConfigurationDataDeployment.ps1
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
function Test-DSCConfigurationDataDeployment {
|
||||||
|
[CmdletBinding()]
|
||||||
|
Param(
|
||||||
|
[Parameter(Mandatory=$true)]
|
||||||
|
[string]
|
||||||
|
$Path,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[switch]
|
||||||
|
$PassThru
|
||||||
|
)
|
||||||
|
|
||||||
|
$ResolvedPath = Repair-ConfigurationDataPathEncoding -Path $Path
|
||||||
|
$Issues = @()
|
||||||
|
|
||||||
|
if(-not (Test-Path -LiteralPath $ResolvedPath -PathType Leaf)){
|
||||||
|
$Issues += [PSCustomObject]@{
|
||||||
|
Severity = "Error"
|
||||||
|
Code = "DeploymentNotFound"
|
||||||
|
Path = $ResolvedPath
|
||||||
|
Message = "Deployment file was not found."
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
try {
|
||||||
|
$DeploymentData = Import-PowerShellDataFile -LiteralPath $ResolvedPath
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
$Issues += [PSCustomObject]@{
|
||||||
|
Severity = "Error"
|
||||||
|
Code = "DeploymentImportFailed"
|
||||||
|
Path = $ResolvedPath
|
||||||
|
Message = $_.Exception.Message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if($null -ne $DeploymentData){
|
||||||
|
if(-not (Test-ConfigurationDataDictionaryKey -Dictionary $DeploymentData -Key "Metadata")){
|
||||||
|
$Issues += [PSCustomObject]@{ Severity = "Error"; Code = "MetadataMissing"; Path = $ResolvedPath; Message = "Metadata block is missing." }
|
||||||
|
}elseif($DeploymentData.Metadata.TemplateType -ne "Deployment"){
|
||||||
|
$Issues += [PSCustomObject]@{ Severity = "Error"; Code = "InvalidTemplateType"; Path = $ResolvedPath; Message = "Metadata.TemplateType must be [Deployment]." }
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach($Key in @("Parameters", "Variables", "Resources")){
|
||||||
|
if(-not (Test-ConfigurationDataDictionaryKey -Dictionary $DeploymentData -Key $Key)){
|
||||||
|
$Issues += [PSCustomObject]@{ Severity = "Error"; Code = "$Key`Missing"; Path = $ResolvedPath; Message = "Required block [$Key] is missing." }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if((Test-ConfigurationDataDictionaryKey -Dictionary $DeploymentData -Key "Resources") -and
|
||||||
|
(-not (Test-ConfigurationDataDictionaryKey -Dictionary $DeploymentData.Resources -Key "AllNodes"))){
|
||||||
|
$Issues += [PSCustomObject]@{ Severity = "Error"; Code = "AllNodesMissing"; Path = $ResolvedPath; Message = "Resources.AllNodes is missing." }
|
||||||
|
}
|
||||||
|
|
||||||
|
if((Test-ConfigurationDataDictionaryKey -Dictionary $DeploymentData -Key "Resources") -and
|
||||||
|
(Test-ConfigurationDataDictionaryKey -Dictionary $DeploymentData.Resources -Key "AllNodes")){
|
||||||
|
$NodeNames = @{}
|
||||||
|
foreach($Node in @($DeploymentData.Resources.AllNodes)){
|
||||||
|
if(-not (Test-ConfigurationDataDictionaryKey -Dictionary $Node -Key "NodeName") -or [string]::IsNullOrWhiteSpace([string]$Node.NodeName)){
|
||||||
|
$Issues += [PSCustomObject]@{ Severity = "Error"; Code = "NodeNameMissing"; Path = $ResolvedPath; Message = "An AllNodes item has no NodeName." }
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$NodeName = [string]$Node.NodeName
|
||||||
|
if($NodeNames.ContainsKey($NodeName)){
|
||||||
|
$Issues += [PSCustomObject]@{ Severity = "Error"; Code = "DuplicateNodeName"; Path = $ResolvedPath; Message = "Duplicate NodeName [$NodeName]." }
|
||||||
|
}else{
|
||||||
|
$NodeNames[$NodeName] = $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if($DeploymentData.Metadata -is [System.Collections.IDictionary] -and
|
||||||
|
(Test-ConfigurationDataDictionaryKey -Dictionary $DeploymentData.Metadata -Key "Sources") -and
|
||||||
|
$DeploymentData.Metadata.Sources -is [System.Collections.IDictionary] -and
|
||||||
|
(Test-ConfigurationDataDictionaryKey -Dictionary $DeploymentData.Metadata.Sources -Key "Files")){
|
||||||
|
foreach($Source in @($DeploymentData.Metadata.Sources.Files)){
|
||||||
|
$SourcePath = if($Source -is [System.Collections.IDictionary]){ [string]$Source.Path } else { [string]$Source }
|
||||||
|
if([string]::IsNullOrWhiteSpace($SourcePath)){
|
||||||
|
$Issues += [PSCustomObject]@{ Severity = "Warning"; Code = "SourcePathMissing"; Path = $ResolvedPath; Message = "A source file entry has no path." }
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$ActualPath = Resolve-ConfigurationDataSourcePath -Path $SourcePath -DeploymentPath $ResolvedPath
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
$Issues += [PSCustomObject]@{ Severity = "Warning"; Code = "SourceMissing"; Path = $SourcePath; Message = $_.Exception.Message }
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if($Source -is [System.Collections.IDictionary] -and (Test-ConfigurationDataDictionaryKey -Dictionary $Source -Key "Hash")){
|
||||||
|
$Algorithm = [string](Get-ConfigurationDataDictionaryValue -Dictionary $Source -Key "Algorithm" -DefaultValue "SHA256")
|
||||||
|
$ActualHash = Get-FileHash -LiteralPath $ActualPath -Algorithm $Algorithm
|
||||||
|
if($ActualHash.Hash -ne [string]$Source.Hash){
|
||||||
|
$Issues += [PSCustomObject]@{
|
||||||
|
Severity = "Warning"
|
||||||
|
Code = "SourceChanged"
|
||||||
|
Path = $SourcePath
|
||||||
|
Message = "Source file hash has changed."
|
||||||
|
ExpectedHash = [string]$Source.Hash
|
||||||
|
ActualHash = $ActualHash.Hash
|
||||||
|
Algorithm = $Algorithm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
$Issues += [PSCustomObject]@{ Severity = "Warning"; Code = "SourceMetadataMissing"; Path = $ResolvedPath; Message = "Metadata.Sources.Files is missing." }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$Result = [PSCustomObject]@{
|
||||||
|
Path = $ResolvedPath
|
||||||
|
IsValid = @($Issues | Where-Object { $_.Severity -eq "Error" }).Count -eq 0
|
||||||
|
IsStale = @($Issues | Where-Object { $_.Code -in @("SourceChanged", "SourceMissing") }).Count -gt 0
|
||||||
|
Issues = $Issues
|
||||||
|
}
|
||||||
|
|
||||||
|
if($PassThru){
|
||||||
|
return $Result
|
||||||
|
}
|
||||||
|
|
||||||
|
return $Result.IsValid -and (-not $Result.IsStale)
|
||||||
|
}
|
||||||
@@ -212,6 +212,8 @@ Describe 'Merge-DSCConfigurationData Extends' {
|
|||||||
Required = $true
|
Required = $true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Variables = @{
|
||||||
|
}
|
||||||
}
|
}
|
||||||
'@ | Set-Content -Path $DefaultPath -Encoding UTF8
|
'@ | Set-Content -Path $DefaultPath -Encoding UTF8
|
||||||
|
|
||||||
@@ -250,6 +252,8 @@ Describe 'Merge-DSCConfigurationData Extends' {
|
|||||||
$DeploymentPath | Should Exist
|
$DeploymentPath | Should Exist
|
||||||
$Result.ConfigurationData.Metadata.TemplateType | Should Be 'Deployment'
|
$Result.ConfigurationData.Metadata.TemplateType | Should Be 'Deployment'
|
||||||
$Result.ConfigurationData.Metadata.Sources.Files.Count | Should Be 2
|
$Result.ConfigurationData.Metadata.Sources.Files.Count | Should Be 2
|
||||||
|
$Result.ConfigurationData.Metadata.Sources.Files[0].Hash.Length | Should Be 64
|
||||||
|
$Result.ConfigurationData.Metadata.Sources.Files[0].Algorithm | Should Be 'SHA256'
|
||||||
$Result.ConfigurationData.Parameters.DomainFQDN.Value | Should Be 'contoso.local'
|
$Result.ConfigurationData.Parameters.DomainFQDN.Value | Should Be 'contoso.local'
|
||||||
$Result.ConfigurationData.Resources.AllNodes[0].NodeName | Should Be 'Node01'
|
$Result.ConfigurationData.Resources.AllNodes[0].NodeName | Should Be 'Node01'
|
||||||
$Result.ConfigurationData.Resources.AllNodes[0].RunCentralAdministration | Should Be $true
|
$Result.ConfigurationData.Resources.AllNodes[0].RunCentralAdministration | Should Be $true
|
||||||
@@ -257,6 +261,10 @@ Describe 'Merge-DSCConfigurationData Extends' {
|
|||||||
$Imported = Import-PowerShellDataFile -Path $DeploymentPath
|
$Imported = Import-PowerShellDataFile -Path $DeploymentPath
|
||||||
$Imported.Metadata.Name | Should Be 'Contoso-Test'
|
$Imported.Metadata.Name | Should Be 'Contoso-Test'
|
||||||
$Imported.Metadata.Sources.Templates.Count | Should Be 1
|
$Imported.Metadata.Sources.Templates.Count | Should Be 1
|
||||||
|
|
||||||
|
$Validation = Test-DSCConfigurationDataDeployment -Path $DeploymentPath -PassThru
|
||||||
|
$Validation.IsValid | Should Be $true
|
||||||
|
$Validation.IsStale | Should Be $false
|
||||||
}
|
}
|
||||||
|
|
||||||
It 'exports materialized deployment data as JSON' {
|
It 'exports materialized deployment data as JSON' {
|
||||||
@@ -271,6 +279,8 @@ Describe 'Merge-DSCConfigurationData Extends' {
|
|||||||
Value = 'contoso.local'
|
Value = 'contoso.local'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Variables = @{
|
||||||
|
}
|
||||||
}
|
}
|
||||||
'@ | Set-Content -Path $DefaultPath -Encoding UTF8
|
'@ | Set-Content -Path $DefaultPath -Encoding UTF8
|
||||||
|
|
||||||
@@ -288,4 +298,112 @@ Describe 'Merge-DSCConfigurationData Extends' {
|
|||||||
$Imported.Metadata.TemplateType | Should Be 'Deployment'
|
$Imported.Metadata.TemplateType | Should Be 'Deployment'
|
||||||
$Imported.Parameters.DomainFQDN.Value | Should Be 'contoso.local'
|
$Imported.Parameters.DomainFQDN.Value | Should Be 'contoso.local'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
It 'detects changed deployment source files by hash' {
|
||||||
|
$DefaultPath = Join-Path -Path $TestDrive -ChildPath 'Default.psd1'
|
||||||
|
$DeploymentPath = Join-Path -Path $TestDrive -ChildPath 'Deployment_123.psd1'
|
||||||
|
|
||||||
|
@'
|
||||||
|
@{
|
||||||
|
Parameters = @{
|
||||||
|
DomainFQDN = @{
|
||||||
|
Type = 'string'
|
||||||
|
Value = 'contoso.local'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Variables = @{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'@ | Set-Content -Path $DefaultPath -Encoding UTF8
|
||||||
|
|
||||||
|
New-DSCConfigurationDataDeployment `
|
||||||
|
-Path $DeploymentPath `
|
||||||
|
-Name 'Contoso-Test' `
|
||||||
|
-DeploymentId '123' `
|
||||||
|
-SourceTemplatePath $DefaultPath `
|
||||||
|
-AllNodes @(
|
||||||
|
@{
|
||||||
|
NodeName = 'Node01'
|
||||||
|
}
|
||||||
|
) `
|
||||||
|
-Export `
|
||||||
|
-Force
|
||||||
|
|
||||||
|
@'
|
||||||
|
@{
|
||||||
|
Parameters = @{
|
||||||
|
DomainFQDN = @{
|
||||||
|
Type = 'string'
|
||||||
|
Value = 'changed.local'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Variables = @{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'@ | Set-Content -Path $DefaultPath -Encoding UTF8
|
||||||
|
|
||||||
|
$Validation = Test-DSCConfigurationDataDeployment -Path $DeploymentPath -PassThru
|
||||||
|
$Validation.IsValid | Should Be $true
|
||||||
|
$Validation.IsStale | Should Be $true
|
||||||
|
@($Validation.Issues | Where-Object { $_.Code -eq 'SourceChanged' }).Count | Should Be 1
|
||||||
|
}
|
||||||
|
|
||||||
|
It 'prevents overriding a sealed parameter definition' {
|
||||||
|
$Template = @{
|
||||||
|
Parameters = @{
|
||||||
|
DomainNetBIOS = @{
|
||||||
|
Type = 'string'
|
||||||
|
Value = 'CONTOSO'
|
||||||
|
Sealed = $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$Deployment = @{
|
||||||
|
Parameters = @{
|
||||||
|
DomainNetBIOS = @{
|
||||||
|
Value = 'OTHER'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{ Merge-DSCConfigurationData -Template $Template -Deployment $Deployment } | Should Throw
|
||||||
|
}
|
||||||
|
|
||||||
|
It 'prevents adding values below a sealed resource tree' {
|
||||||
|
$Template = @{
|
||||||
|
Resources = @{
|
||||||
|
NonNodeData = @{
|
||||||
|
Services = @{
|
||||||
|
SharePoint = @{
|
||||||
|
Farm = @{
|
||||||
|
ManagedAccounts = @{
|
||||||
|
Sealed = $true
|
||||||
|
FarmAccount = "[parameters('FarmCredential')]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$Deployment = @{
|
||||||
|
Resources = @{
|
||||||
|
NonNodeData = @{
|
||||||
|
Services = @{
|
||||||
|
SharePoint = @{
|
||||||
|
Farm = @{
|
||||||
|
ManagedAccounts = @{
|
||||||
|
SearchAccount = "[parameters('SearchCredential')]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{ Merge-DSCConfigurationData -Template $Template -Deployment $Deployment } | Should Throw
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user