source/Public/New-GuestConfigurationPackage.ps1 (340 lines of code) (raw):
<#
.SYNOPSIS
Creates a package to run code on machines through Azure Guest Configuration.
.PARAMETER Name
The name of the Guest Configuration package.
.PARAMETER Configuration
The path to the compiled DSC configuration file (.mof) to base the package on.
.PARAMETER Version
The semantic version of the Guest Configuration package.
The default value is '1.0.0'.
.PARAMETER Type
Sets a tag in the metaconfig data of the package specifying whether or not this package is
Audit-only or can support Set/Apply functionality.
Audit indicates that the package will only monitor settings and cannot set the state of
the machine.
AuditAndSet indicates that the package can be used for both monitoring and setting the
state of the machine.
The default value is Audit.
.PARAMETER FrequencyMinutes
The frequency at which Guest Configuration should run this package in minutes.
The default value is 15.
15 is also the mimimum value.
Guest Configuration cannot run a package less-frequently than every 15 minutes.
.PARAMETER Path
The path to a folder to output the package under.
By default the package will be created under the current working directory.
.PARAMETER ChefInspecProfilePath
The path to a folder containing Chef InSpec profiles to include with the package.
The compiled DSC configuration (.mof) provided must include a reference to the native Chef
InSpec resource with the reference name of the resources matching the name of the profile
folder to use.
If the compiled DSC configuration (.mof) provided includes a reference to the native Chef
InSpec resource, then specifying a Chef InSpec profile to include with this parameter is
required.
.PARAMETER FilesToInclude
The path(s) to any extra files or folders to include under the Modules path within the package.
Please note, any files added here may not be accessible by custom modules.
Files needed for custom modules need to be included within those modules.
.PARAMETER Force
If present, this function will overwrite any existing package files at the output path.
.EXAMPLE
New-GuestConfigurationPackage `
-Name 'WindowsTLS' `
-Configuration ./custom_policy/WindowsTLS/localhost.mof `
-Path ./git/repository/release/policy/WindowsTLS
.OUTPUTS
Returns a PSCustomObject with the name and path of the new Guest Configuration package.
[PSCustomObject]@{
PSTypeName = 'GuestConfiguration.Package'
Name = (Same as the Name parameter)
Path = (Path to the newly created package zip file)
}
#>
function New-GuestConfigurationPackage
{
[CmdletBinding()]
[OutputType([PSCustomObject])]
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
param
(
[Parameter(Position = 0, Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
[ValidateNotNullOrEmpty()]
[System.String]
$Name,
[Parameter(Position = 1, Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
[ValidateNotNullOrEmpty()]
[System.IO.FileInfo]
$Configuration,
[Parameter(Position = 2, ValueFromPipelineByPropertyName = $true)]
[ValidateNotNullOrEmpty()]
[System.String]
$Version = '1.0.0',
[Parameter()]
[ValidateSet('Audit', 'AuditAndSet')]
[ValidateNotNullOrEmpty()]
[String]
$Type = 'Audit',
[Parameter()]
[int]
$FrequencyMinutes = 15,
[Parameter()]
[ValidateNotNullOrEmpty()]
[System.IO.DirectoryInfo]
$Path = $(Get-Item -Path $(Get-Location)),
[Parameter()]
[System.IO.DirectoryInfo]
$ChefInspecProfilePath,
[Parameter()]
[String[]]
$FilesToInclude,
[Parameter()]
[Switch]
$Force
)
Write-Verbose -Message 'Starting New-GuestConfigurationPackage'
$Configuration = Resolve-RelativePath -Path $Configuration
$Path = Resolve-RelativePath -Path $Path
if (-not [String]::IsNullOrEmpty($ChefInspecProfilePath))
{
$ChefInspecProfilePath = Resolve-RelativePath -Path $ChefInspecProfilePath
}
#-----VALIDATION-----
if ($FrequencyMinutes -lt 15)
{
throw "FrequencyMinutes must be 15 or greater. Guest Configuration cannot run packages more frequently than every 15 minutes."
}
# Validate mof
if (-not (Test-Path -Path $Configuration -PathType 'Leaf'))
{
throw "No file found at the path '$Configuration'. Please specify the file path to a compiled DSC configuration (.mof) with the Configuration parameter."
}
$sourceMofFile = Get-Item -Path $Configuration
if ($sourceMofFile.Extension -ine '.mof')
{
throw "The file found at the path '$Configuration' is not a .mof file. It has extension '$($sourceMofFile.Extension)'. Please specify the file path to a compiled DSC configuration (.mof) with the Configuration parameter."
}
# Validate dependencies
$resourceDependencies = @( Get-MofResouceDependencies -MofFilePath $Configuration )
if ($resourceDependencies.Count -le 0)
{
throw "Failed to determine resource dependencies from the mof at the path '$Configuration'. Please specify the file path to a compiled DSC configuration (.mof) with the Configuration parameter."
}
$usingInSpecResource = $false
$moduleDependencies = @()
$inSpecProfileNames = @()
foreach ($resourceDependency in $resourceDependencies)
{
if ($resourceDependency['ResourceName'] -ieq 'MSFT_ChefInSpecResource')
{
$usingInSpecResource = $true
$inSpecProfileNames += $resourceDependency['ResourceInstanceName']
continue
}
$getModuleDependenciesParameters = @{
ModuleName = $resourceDependency['ModuleName']
ModuleVersion = $resourceDependency['ModuleVersion']
}
$moduleDependencies += Get-ModuleDependencies @getModuleDependenciesParameters
}
if ($moduleDependencies.Count -gt 0)
{
Write-Verbose -Message "Found the module dependencies: $($moduleDependencies.Name)"
}
$duplicateModules = @( $moduleDependencies | Group-Object -Property 'Name' | Where-Object { $_.Count -gt 1 } )
foreach ($duplicateModule in $duplicateModules)
{
$uniqueVersions = @( $duplicateModule.Group.Version | Get-Unique )
if ($uniqueVersions.Count -gt 1)
{
$moduleName = $duplicateModule.Group[0].Name
throw "Cannot include more than one version of a module in one package. Detected versions $uniqueVersions of the module '$moduleName' are needed for this package."
}
}
$inSpecProfileSourcePaths = @()
if ($usingInSpecResource)
{
Write-Verbose -Message "Expecting the InSpec profiles: $($inSpecProfileNames)"
if ($Type -ieq 'AuditAndSet')
{
throw "The type of this package was specified as 'AuditAndSet', but native InSpec resource was detected in the provided .mof file. This resource does not currently support the set scenario and can only be used for 'Audit' packages."
}
if ([String]::IsNullOrEmpty($ChefInspecProfilePath))
{
throw "The native InSpec resource was detected in the provided .mof file, but no InSpec profiles folder path was provided. Please provide the path to an InSpec profiles folder via the ChefInspecProfilePath parameter."
}
else
{
$inSpecProfileFolder = Get-Item -Path $ChefInspecProfilePath -ErrorAction 'SilentlyContinue'
if ($null -eq $inSpecProfileFolder)
{
throw "The native InSpec resource was detected in the provided .mof file, but the specified path to the InSpec profiles folder does not exist. Please provide the path to an InSpec profiles folder via the ChefInspecProfilePath parameter."
}
elseif ($inSpecProfileFolder -isnot [System.IO.DirectoryInfo])
{
throw "The native InSpec resource was detected in the provided .mof file, but the specified path to the InSpec profiles folder is not a directory. Please provide the path to an InSpec profiles folder via the ChefInspecProfilePath parameter."
}
else
{
foreach ($expectedInSpecProfileName in $inSpecProfileNames)
{
$inSpecProfilePath = Join-Path -Path $ChefInspecProfilePath -ChildPath $expectedInSpecProfileName
$inSpecProfile = Get-Item -Path $inSpecProfilePath -ErrorAction 'SilentlyContinue'
if ($null -eq $inSpecProfile)
{
throw "Expected to find an InSpec profile at the path '$inSpecProfilePath', but there is no item at this path."
}
elseif ($inSpecProfile -isnot [System.IO.DirectoryInfo])
{
throw "Expected to find an InSpec profile at the path '$inSpecProfilePath', but the item at this path is not a directory."
}
else
{
$inSpecProfileYmlFileName = 'inspec.yml'
$inSpecProfileYmlFilePath = Join-Path -Path $inSpecProfilePath -ChildPath $inSpecProfileYmlFileName
if (Test-Path -Path $inSpecProfileYmlFilePath -PathType 'Leaf')
{
$inSpecProfileSourcePaths += $inSpecProfilePath
}
else
{
throw "Expected to find an InSpec profile at the path '$inSpecProfilePath', but there file named '$inSpecProfileYmlFileName' under this path."
}
}
}
}
}
}
elseif (-not [String]::IsNullOrEmpty($ChefInspecProfilePath))
{
throw "A Chef InSpec profile path was provided, but the native InSpec resource was not detected in the provided .mof file. Please provide a compiled DSC configuration (.mof) that references the native InSpec resource or remove the reference to the ChefInspecProfilePath parameter."
}
# Check extra files if needed
foreach ($file in $FilesToInclude)
{
$filePath = Resolve-RelativePath -Path $file
if (-not (Test-Path -Path $filePath))
{
throw "The item to include from the path '$filePath' does not exist. Please update or remove the FilesToInclude parameter."
}
}
# Check destination
$packageDestinationPath = Join-Path -Path $Path -ChildPath "$Name.zip"
if (Test-Path -Path $packageDestinationPath)
{
if (-not $Force)
{
throw "A file already exists at the package destination path '$packageDestinationPath'. Please remove it or use the Force parameter. With -Force the cmdlet will remove this file for you."
}
}
#-----PACKAGE CREATION-----
# Clear the temp directory
$tempFolderPath = Reset-GCWorkerTempDirectory
try
{
# Create the package root folder
$packageRootPath = Join-Path -Path $tempFolderPath -ChildPath $Name
Write-Verbose -Message "Creating the package root folder at the path '$packageRootPath'..."
$null = New-Item -Path $packageRootPath -ItemType 'Directory' -Force
# Create the Modules folder
$modulesFolderPath = Join-Path -Path $packageRootPath -ChildPath 'Modules'
Write-Verbose -Message "Creating the package Modules folder at the path '$modulesFolderPath'..."
$null = New-Item -Path $modulesFolderPath -ItemType 'Directory'
# Create the metaconfig file
$metaconfigFileName = "$Name.metaconfig.json"
$metaconfigFilePath = Join-Path -Path $packageRootPath -ChildPath $metaconfigFileName
$metaconfig = @{
Type = $Type
Version = $Version
}
if ($FrequencyMinutes -gt 15)
{
$metaconfig['configurationModeFrequencyMins'] = $FrequencyMinutes
}
$metaconfigJson = $metaconfig | ConvertTo-Json
Write-Verbose -Message "Setting the content of the package metaconfig at the path '$metaconfigFilePath'..."
$null = Set-Content -Path $metaconfigFilePath -Value $metaconfigJson -Encoding 'ascii'
# Copy the mof into the package
$mofFilePath = Join-Path -Path $packageRootPath -ChildPath "$Name.mof"
Write-Verbose -Message "Copying the compiled DSC configuration (.mof) from the path '$Configuration' to the package path '$mofFilePath'..."
$null = Copy-Item -Path $Configuration -Destination $mofFilePath
# Edit the native Chef InSpec resource parameters in the mof if needed
if ($usingInSpecResource)
{
Edit-GuestConfigurationPackageMofChefInSpecContent -PackageName $Name -MofPath $mofFilePath
}
# Copy resource dependencies
foreach ($moduleDependency in $moduleDependencies)
{
$moduleDestinationPath = Join-Path -Path $modulesFolderPath -ChildPath $moduleDependency['Name']
Write-Verbose -Message "Copying module from '$($moduleDependency['SourcePath'])' to '$moduleDestinationPath'"
$null = Copy-Item -Path $moduleDependency['SourcePath'] -Destination $moduleDestinationPath -Container -Recurse -Force
}
# Copy native Chef InSpec resource if needed
if ($usingInSpecResource)
{
$nativeResourcesFolder = Join-Path -Path $modulesFolderPath -ChildPath 'DscNativeResources'
Write-Verbose -Message "Creating the package native resources folder at the path '$nativeResourcesFolder'..."
$null = New-Item -Path $nativeResourcesFolder -ItemType 'Directory'
$inSpecResourceFolder = Join-Path -Path $nativeResourcesFolder -ChildPath 'MSFT_ChefInSpecResource'
Write-Verbose -Message "Creating the native Chef InSpec resource folder at the path '$inSpecResourceFolder'..."
$null = New-Item -Path $inSpecResourceFolder -ItemType 'Directory'
$dscResourcesFolderPath = Join-Path -Path $PSScriptRoot -ChildPath 'DscResources'
$inSpecResourceSourcePath = Join-Path -Path $dscResourcesFolderPath -ChildPath 'MSFT_ChefInSpecResource'
$installInSpecScriptSourcePath = Join-Path -Path $inSpecResourceSourcePath -ChildPath 'install_inspec.sh'
Write-Verbose -Message "Copying the Chef Inspec install script from the path '$installInSpecScriptSourcePath' to the package path '$modulesFolderPath'..."
$null = Copy-Item -Path $installInSpecScriptSourcePath -Destination $modulesFolderPath
$inSpecResourceLibrarySourcePath = Join-Path -Path $inSpecResourceSourcePath -ChildPath 'libMSFT_ChefInSpecResource.so'
Write-Verbose -Message "Copying the native Chef Inspec resource library from the path '$inSpecResourceLibrarySourcePath' to the package path '$inSpecResourceFolder'..."
$null = Copy-Item -Path $inSpecResourceLibrarySourcePath -Destination $inSpecResourceFolder
$inSpecResourceSchemaMofSourcePath = Join-Path -Path $inSpecResourceSourcePath -ChildPath 'MSFT_ChefInSpecResource.schema.mof'
Write-Verbose -Message "Copying the native Chef Inspec resource schema from the path '$inSpecResourceSchemaMofSourcePath' to the package path '$inSpecResourceFolder'..."
$null = Copy-Item -Path $inSpecResourceSchemaMofSourcePath -Destination $inSpecResourceFolder
foreach ($inSpecProfileSourcePath in $inSpecProfileSourcePaths)
{
Write-Verbose -Message "Copying the Chef Inspec profile from the path '$inSpecProfileSourcePath' to the package path '$modulesFolderPath'..."
$null = Copy-Item -Path $inSpecProfileSourcePath -Destination $modulesFolderPath -Container -Recurse
}
}
# Copy extra files
foreach ($file in $FilesToInclude)
{
$filePath = Resolve-RelativePath -Path $file
if (Test-Path -Path $filePath -PathType 'Leaf')
{
Write-Verbose -Message "Copying the custom file to include from the path '$filePath' to the package module path '$modulesFolderPath'..."
$null = Copy-Item -Path $filePath -Destination $modulesFolderPath
}
else
{
Write-Verbose -Message "Copying the custom folder to include from the path '$filePath' to the package module path '$modulesFolderPath'..."
$null = Copy-Item -Path $filePath -Destination $modulesFolderPath -Container -Recurse
}
}
# Clear the package destination
if (Test-Path -Path $packageDestinationPath)
{
Write-Verbose -Message "Removing an existing item at the path '$packageDestinationPath'..."
$null = Remove-Item -Path $packageDestinationPath -Recurse -Force
}
# Create the destination parent directory if needed
if (-not (Test-Path -Path $Path))
{
$null = New-Item -Path $Path -ItemType 'Directory' -Force
}
# Zip the package
# NOTE: We are NOT using Compress-Archive here because it does not zip empty folders (like an empty Modules folder) into the package
Write-Verbose -Message "Compressing the generated package from the path '$packageRootPath' to the package path '$packageDestinationPath'..."
$null = [System.IO.Compression.ZipFile]::CreateFromDirectory($packageRootPath, $packageDestinationPath)
}
finally
{
# Clear the temp directory
$null = Reset-GCWorkerTempDirectory
}
return [PSCustomObject]@{
PSTypeName = 'GuestConfiguration.Package'
Name = $Name
Path = $packageDestinationPath
}
}