DSCResources/cOctopusSeqLogger/cOctopusSeqLogger.psm1
$octopusServerExePath = "$($env:ProgramFiles)\Octopus Deploy\Octopus\Octopus.Server.exe" $tentacleExePath = "$($env:ProgramFiles)\Octopus Deploy\Tentacle\Tentacle.exe" function Get-TargetResource { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSDSCUseVerboseMessageInDSCResource", "")] [OutputType([HashTable])] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [ValidateSet("OctopusServer", "Tentacle")] [string]$InstanceType, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [ValidateSet("Present", "Absent")] [string]$Ensure, [string]$SeqServer, [PSCredential]$SeqApiKey, [Microsoft.Management.Infrastructure.CimInstance[]]$Properties ) $propertiesAsHashTable = ConvertTo-HashTable $properties return Get-TargetResourceInternal -InstanceType $InstanceType ` -Ensure $Ensure ` -SeqServer $SeqServer ` -SeqApiKey $SeqApiKey ` -Properties $propertiesAsHashTable } function Set-TargetResource { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSDSCUseVerboseMessageInDSCResource", "")] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [ValidateSet("OctopusServer", "Tentacle")] [string]$InstanceType, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [ValidateSet("Present", "Absent")] [string]$Ensure, [string]$SeqServer, [PSCredential]$SeqApiKey, [Microsoft.Management.Infrastructure.CimInstance[]]$Properties ) $propertiesAsHashTable = ConvertTo-HashTable $properties Set-TargetResourceInternal -InstanceType $InstanceType ` -Ensure $Ensure ` -SeqServer $SeqServer ` -SeqApiKey $SeqApiKey ` -Properties $propertiesAsHashTable } function Test-TargetResource { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSDSCUseVerboseMessageInDSCResource", "")] [OutputType([boolean])] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [ValidateSet("OctopusServer", "Tentacle")] [string]$InstanceType, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [ValidateSet("Present", "Absent")] [string]$Ensure, [string]$SeqServer, [PSCredential]$SeqApiKey, [Microsoft.Management.Infrastructure.CimInstance[]]$Properties ) $propertiesAsHashTable = ConvertTo-HashTable $properties return Test-TargetResourceInternal -InstanceType $InstanceType ` -Ensure $Ensure ` -SeqServer $SeqServer ` -SeqApiKey $SeqApiKey ` -Properties $propertiesAsHashTable } function Get-TargetResourceInternal { [OutputType([Hashtable])] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [ValidateSet("OctopusServer", "Tentacle")] [string]$InstanceType, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [ValidateSet("Present", "Absent")] [string]$Ensure, [string]$SeqServer, [PSCredential]$SeqApiKey, [HashTable]$Properties ) if (-not (Test-Path -Path $octopusServerExePath) -and ($InstanceType -eq "OctopusServer") -and ($Ensure -eq "Present")) { throw "Unable to find Octopus (checked for existence of file '$octopusServerExePath')." } if (-not (Test-Path -Path $tentacleExePath) -and ($InstanceType -eq "Tentacle") -and ($Ensure -eq "Present")) { throw "Unable to find Tentacle (checked for existence of file '$tentacleExePath')." } if ($InstanceType -eq "Tentacle") { $nlogConfigFile = "$tentacleExePath.nlog" } elseif ($InstanceType -eq "OctopusServer") { $nlogConfigFile = "$octopusServerExePath.nlog" } $existingApiKey = $null $nlogExtensionElementExists = $false $nlogTargetElementExists = $false $nlogRuleElementExists = $false $existingServerUrl = $null $existingProperties = @{} if (Test-Path $nlogConfigFile) { $nlogConfig = Get-NLogConfig $nlogConfigFile $nlogExtensionElementExists = $null -ne ($nlogConfig.nlog.extensions.add.assembly | where-object { $_ -eq "Seq.Client.Nlog" }) $nlogTargetElement = ($nlogConfig.nlog.targets.target | where-object {$_.name -eq "seq"}) $nlogTargetElementExists = $null -ne $nlogTargetElement $nlogRuleElement = ($nlogConfig.nlog.rules.logger | where-object {$_.writeTo -eq "seq"}) $nlogRuleElementExists = $null -ne $nlogRuleElement $plainTextPassword = ($nlogConfig.nlog.targets.target | where-object { $_.name -eq "seq" -and $_.type -eq "Seq" }).ApiKey if ($null -ne $plainTextPassword) { $password = new-object securestring # Avoid using "ConvertTo-SecureString ... -AsPlaintext", to fix PSAvoidUsingConvertToSecureStringWithPlainText # Not sure it actually solves the underlying issue though $plainTextPassword.ToCharArray() | Foreach-Object { $password.AppendChar($_) } $existingApiKey = New-Object System.Management.Automation.PSCredential ("ignored", $password) } $existingServerUrl = $nlogTargetElement.serverUrl if ($null -ne $nlogTargetElementExists -and ($null -ne $nlogTargetElement.property)) { $nlogTargetElement.property | Sort-Object -Property Name | Foreach-Object { $existingProperties[$_.name] = $_.value } } } if ($InstanceType -eq "Tentacle") { $dllPath = "$($env:ProgramFiles)\Octopus Deploy\Tentacle\Seq.Client.NLog.dll" } elseif ($InstanceType -eq "OctopusServer") { $dllPath = "$($env:ProgramFiles)\Octopus Deploy\Octopus\Seq.Client.NLog.dll" } $existingEnsure = "Absent" if ((Test-NLogDll $dllPath) -and $nlogExtensionElementExists -and $nlogTargetElementExists -and $nlogRuleElementExists) { $existingEnsure = "Present" } $result = @{ InstanceType = $InstanceType; Ensure = $existingEnsure SeqServer = $existingServerUrl SeqApiKey = $existingApiKey Properties = $existingProperties } return $result } function Set-TargetResourceInternal { param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [ValidateSet("OctopusServer", "Tentacle")] [string]$InstanceType, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [ValidateSet("Present", "Absent")] [string]$Ensure, [string]$SeqServer, [PSCredential]$SeqApiKey, [HashTable]$Properties ) if ((($null -eq $SeqServer) -or ("" -eq $SeqServer)) -and ($Ensure -eq 'Present')) { throw "Property 'SeqServer' should be supplied if 'Ensure' is set to 'Present'" } Get-TargetResource -InstanceType $InstanceType ` -Ensure $Ensure ` -SeqServer $SeqServer ` -SeqApiKey $SeqApiKey ` -Properties $null if ($InstanceType -eq "Tentacle") { $dllPath = "$($env:ProgramFiles)\Octopus Deploy\Tentacle\Seq.Client.NLog.dll" } elseif ($InstanceType -eq "OctopusServer") { $dllPath = "$($env:ProgramFiles)\Octopus Deploy\Octopus\Seq.Client.NLog.dll" } if ($InstanceType -eq "Tentacle") { $nlogConfigFile = "$tentacleExePath.nlog" } elseif ($InstanceType -eq "OctopusServer") { $nlogConfigFile = "$octopusServerExePath.nlog" } if ($Ensure -eq "Absent") { if (Test-Path $dllPath) { try { Remove-Item $dllPath -Force -ErrorAction SilentlyContinue } catch { Write-Verbose "We tried to removing the seq dll from $dllPath" Write-Verbose "But, we couldn't actually remove it, as its locked by Octopus.Server.exe / Tentacle.exe" } } if (Test-Path $nlogConfigFile) { Write-Verbose "Removing settings from $nlogConfigFile" $nlogConfig = Get-NLogConfig $nlogConfigFile $nlogExtensionElement = ($nlogConfig.nlog.extensions.add | where-object { $_.assembly -eq "Seq.Client.NLog" }) if ($null -ne $nlogExtensionElement) { $nlogConfig.nlog.extensions.RemoveChild($nlogExtensionElement) } $nlogTargetElement = ($nlogConfig.nlog.targets.target | where-object {$_.name -eq "seq"}) if ($null -ne $nlogTargetElement) { $nlogConfig.nlog.targets.RemoveChild($nlogTargetElement) } $nlogRuleElement = ($nlogConfig.nlog.rules.logger | where-object {$_.writeTo -eq "seq"}) if ($null -ne $nlogRuleElement) { $nlogConfig.nlog.rules.RemoveChild($nlogRuleElement) } Write-Verbose "Saving updated config file $nlogConfigFile" Save-NlogConfig $nlogConfig $nlogConfigFile } } else { if (-not (Test-Path $dllPath)) { Request-SeqClientNlogDll $dllPath } Write-Verbose "Modifying config file $nlogConfigFile" $nlogConfig = Get-NLogConfig $nlogConfigFile #remove then re-add "<add assembly="Seq.Client.NLog"/>" to //nlog/extensions $nlogExtensionElement = ($nlogConfig.nlog.extensions.add | where-object { $_.assembly -eq "Seq.Client.Nlog" }) if ($null -ne $nlogPropertyElement) { $nlogConfig.nlog.extensions.RemoveChild($nlogExtensionElement) } $newChild = $nlogConfig.CreateElement("add", $nlogConfig.DocumentElement.NamespaceURI) $newChild.Attributes.Append((New-XmlAttribute $nlogConfig "assembly" "Seq.Client.NLog")) $nlogConfig.nlog.extensions.AppendChild($newChild) #remove then re-add " # <target name="seq" xsi:type="Seq" serverUrl="https://seq.example.com" apiKey="my-magic-api-key"> # <property name="Application" value="Octopus" /> # </target> #to //nlog/targets" $nlogTargetElement = ($nlogConfig.nlog.targets.target | where-object {$_.name -eq "seq" -and $_.type -eq "Seq"}) if ($null -ne $nlogTargetElement) { $nlogConfig.nlog.targets.RemoveChild($nlogTargetElement) } $newChild = $nlogConfig.CreateElement("target", $nlogConfig.DocumentElement.NamespaceURI) $newChild.Attributes.Append((New-XmlAttribute $nlogConfig "name" "seq")) $attribute = $nlogConfig.CreateAttribute("xsi:type", "http://www.w3.org/2001/XMLSchema-instance") $attribute.Value = "Seq" $newChild.Attributes.Append($attribute) $newChild.Attributes.Append((New-XmlAttribute $nlogConfig "serverUrl" $SeqServer)) if ($null -ne $SeqApiKey) { $newChild.Attributes.Append((New-XmlAttribute $nlogConfig "apiKey" $SeqApiKey.GetNetworkCredential().Password)) } if ($null -ne $properties) { $sortedProperties = ($Properties.GetEnumerator() | Sort-Object -Property Key) foreach($property in $sortedProperties) { $propertyChild = $nlogConfig.CreateElement("property", $nlogConfig.DocumentElement.NamespaceURI) $propertyChild.Attributes.Append((New-XmlAttribute $nlogConfig "name" $property.Key)) $propertyChild.Attributes.Append((New-XmlAttribute $nlogConfig "value" $property.value)) $newChild.AppendChild($propertyChild) } } $nlogConfig.nlog.targets.AppendChild($newChild) # remove then re-add "<logger name="*" minlevel="Info" writeTo="seq" />" to //nlog/rules" $nlogRuleElement = ($nlogConfig.nlog.rules.logger | where-object {$_.writeTo -eq "seq"}) if ($null -ne $nlogRuleElement) { $nlogConfig.nlog.rules.RemoveChild($nlogRuleElement) } $newChild = $nlogConfig.CreateElement("logger", $nlogConfig.DocumentElement.NamespaceURI) $newChild.Attributes.Append((New-XmlAttribute $nlogConfig "name" "*")) $newChild.Attributes.Append((New-XmlAttribute $nlogConfig "minlevel" "Info")) $newChild.Attributes.Append((New-XmlAttribute $nlogConfig "writeTo" "seq")) $nlogConfig.nlog.rules.AppendChild($newChild) Write-Verbose "Saving config file $nlogConfigFile" Save-NlogConfig $nlogConfig $nlogConfigFile } } function Test-TargetResourceInternal { [OutputType([boolean])] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [ValidateSet("OctopusServer", "Tentacle")] [string]$InstanceType, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [ValidateSet("Present", "Absent")] [string]$Ensure, [string]$SeqServer, [PSCredential]$SeqApiKey, [HashTable]$Properties ) $currentResource = (Get-TargetResourceInternal -InstanceType $InstanceType ` -Ensure $Ensure ` -SeqServer $SeqServer ` -SeqApiKey $SeqApiKey ` -Properties $Properties) $params = Get-OctopusDSCParameter $MyInvocation.MyCommand.Parameters $currentConfigurationMatchesRequestedConfiguration = $true foreach($key in $currentResource.Keys) { $currentValue = $currentResource.Item($key) $requestedValue = $params.Item($key) if ($currentValue -is [PSCredential]) { if (-not (Test-PSCredential $currentValue $requestedValue)) { $currentConfigurationMatchesRequestedConfiguration = $false } } elseif ($currentValue -is [HashTable]) { if (-not (Test-HashTable $currentValue $requestedValue)) { $currentConfigurationMatchesRequestedConfiguration = $false } } elseif ($currentValue -ne $requestedValue) { Write-Verbose "(FOUND MISMATCH) Configuration parameter '$key' with value '$currentValue' mismatched the specified value '$requestedValue'" $currentConfigurationMatchesRequestedConfiguration = $false } else { Write-Verbose "Configuration parameter '$key' matches the requested value '$requestedValue'" } } return $currentConfigurationMatchesRequestedConfiguration } function Get-NLogConfig ([string] $fileName) { return [xml] (Get-Content $fileName) } function Test-NLogDll ([string] $fileName) { return Test-Path $fileName } function New-XmlAttribute($xml, $name, $value) { $attribute = $xml.CreateAttribute($name) $attribute.Value = $value return $attribute } function ConvertTo-HashTable { [CmdletBinding()] [OutputType([HashTable])] param ( [Microsoft.Management.Infrastructure.CimInstance[]] $tokens ) $HashTable = @{} foreach($token in $tokens) { $HashTable.Add($token.Key, $token.Value) } return $HashTable } function Request-SeqClientNlogDll ($dllPath) { Write-Verbose "Downloading Seq.Client.NLog.dll version 2.3.25 from nuget to $dllPath" $ProgressPreference = "SilentlyContinue" $folder = [System.IO.Path]::GetTempPath() Invoke-WebRequest https://dist.nuget.org/win-x86-commandline/latest/nuget.exe -outfile "$folder\nuget.exe" & "$folder\nuget.exe" install Seq.Client.NLog -outputdirectory $folder -version 2.3.24 Copy-Item "$folder\Seq.Client.NLog.2.3.24\lib\net40\Seq.Client.NLog.dll" $dllPath } function Save-NlogConfig ($nlogConfig, $filename) { $nlogConfig.Save($filename) } function Test-HashTable($currentValue, $requestedValue) { $currentConfigurationMatchesRequestedConfiguration = $true if ($currentValue.Count -ne $requestedValue.Count) { Write-Verbose "(FOUND MISMATCH) Configuration parameter '$key' with $($currentValue.count) values mismatched the specified $($requestedValue.Count) values" $currentConfigurationMatchesRequestedConfiguration = $false } else { foreach($value in $currentValue.Keys) { $curr = $currentValue[$value] $req = $requestedValue[$value] if ($curr -ne $req) { Write-Verbose "(FOUND MISMATCH) Configuration parameter `"$key['$value']`" with value '$curr' mismatched the specified value '$req'" $currentConfigurationMatchesRequestedConfiguration = $false } } } return $currentConfigurationMatchesRequestedConfiguration } function Test-PSCredential($currentValue, $requestedValue) { if ($null -ne $currentValue) { $currentUsername = $currentValue.GetNetworkCredential().UserName $currentPassword = $currentValue.GetNetworkCredential().Password } else { $currentUserName = "" $currentPassword = "" } if ($null -ne $requestedValue) { $requestedUsername = $requestedValue.GetNetworkCredential().UserName $requestedPassword = $requestedValue.GetNetworkCredential().Password } else { $requestedUsername = "" $requestedPassword = "" } if ($currentPassword -ne $requestedPassword -or $currentUsername -ne $requestedUserName) { Write-Verbose "(FOUND MISMATCH) Configuration parameter '$key' with value '********' mismatched the specified value '********'" return $false } else { Write-Verbose "Configuration parameter '$key' matches the requested value '********'" } return $true } function Get-OctopusDSCParameter($parameters) { # unfortunately $PSBoundParameters doesn't contain parameters that weren't supplied (because the default value was okay) # credit to https://www.briantist.com/how-to/splatting-psboundparameters-default-values-optional-parameters/ $params = @{} foreach($h in $parameters.GetEnumerator()) { $key = $h.Key $var = Get-Variable -Name $key -ErrorAction SilentlyContinue if ($null -ne $var) { $val = Get-Variable -Name $key -ErrorAction Stop | Select-Object -ExpandProperty Value -ErrorAction Stop $params[$key] = $val } } return $params } |