allcommands.ps1
<#
.SYNOPSIS Add's ACLs to a folder. .DESCRIPTION Adds the acl to the specified folder - will also attempt to repair cannonical ordering issue .PARAMETER FolderPath Folder to set ACLs on. .PARAMETER User User to grant permission to. .PARAMETER Permission Permission to add .EXAMPLE Add-AclFolder -Folder ".\test" -User "hqcatalyst\dev.test" -Permission "Read" #> function Add-AclFolder { [CmdletBinding()] param( [Parameter(Mandatory=$true)] [ValidateScript({ if (!(Test-Path $_ -PathType Container)) { Write-DosMessage -Level "Fatal" -Message "FolderPath either $_ does not exist or is not a folder. Please enter valid folder path." } else { $true } })] [string] $FolderPath, [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [string] $User, [Parameter(Mandatory=$true)] [ValidateSet("Read", "Write")] [string] $Permission ) Write-DosMessage -Level "Verbose" -Message "Adding ACL to give $Permission to $User on folder $FolderPath" $directoryAcl = (Get-Item $FolderPath).GetAccessControl("Access") $accessRule = New-Object System.Security.AccessControl.FileSystemAccessRule($User, $Permission, "ContainerInherit,ObjectInherit", "None", "Allow") try{ $directoryAcl.AddAccessRule($accessRule) } catch [System.InvalidOperationException]{ Write-DosMessage -Level "Warning" -Message "Error attempting to add ACL to list on $FolderPath, attempting to repair" Repair-AclCanonicalOrder -Acl $directoryAcl -Path $FolderPath $directoryAcl.AddAccessRule($accessRule) } try{ Write-DosMessage -Level "Verbose" -Message "Adding ACL to give $Permission to $User on folder $FolderPath" Set-Acl -Path $FolderPath $directoryAcl } catch [System.InvalidOperationException]{ Write-DosMessage -Level "Warning" -Message "Error attempting to add ACL to $FolderPath, attempting to repair" Repair-AclCanonicalOrder -Acl $directoryAcl -Path $FolderPath Set-Acl -Path $FolderPath $directoryAcl } } function Assert-DependencySemVerRequirementsMet { [CmdletBinding()] param ( [string]$ServiceName, [array]$DiscoveryServiceEntries, [array]$CurrentDependencies ) $allDependenciesMet = $true foreach ($dependency in $CurrentDependencies ) { $installedDependencyBuildNumber = ($DiscoveryServiceEntries | Where-Object { $_.ServiceName -eq $dependency.serviceName }).BuildNumber $s1 = [SemVer]::New($installedDependencyBuildNumber) $s2 = [SemVer]::New($dependency.serviceBuildVersion) $dependencyMet = Assert-SatisfiesSemVer -SemVer1 $s1 -SemVer2 $s2 if (!$dependencyMet) { Write-DosMessage -Level "Warning" -Message "$($dependency.serviceName) dependency not met. Expected $($dependency.serviceBuildVersion), existing $($installedDependencyBuildNumber)" $allDependenciesMet = $false } else { Write-DosMessage -Level "Information" -Message "$($dependency.serviceName) dependency met. Expected $($dependency.serviceBuildVersion), existing $($installedDependencyBuildNumber)" } } return $allDependenciesMet } function Get-PossibleConstraints { return @('~', '^', '=', 'v', '>', '<', '>=', '<=') } function Compare-Numbers { [CmdletBinding()] param ( $A, $B ) if ([int]$A -eq [int]$B) { return 0 } elseif ([int]$A -lt [int]$B) { return -1 } else { return 1 } } enum VersionIdentifier { Major Minor Patch } class SemVer { [string]$Constraint [string]$Major [string]$Minor [string]$Patch SemVer( [string]$Version ) { $this.Parse($Version) } [void]Parse([string]$Version) { $possibleOperators = Get-PossibleConstraints $splitVersion = @() if ($Version[0] -in $possibleOperators) { $versionSubstring = "" if (($Version[0] -eq '<') -or ($Version[0] -eq '>') -and ($Version[1] -eq '=')) { $this.Constraint = "$($Version[0])$($Version[1])" $versionSubstring = $Version.Substring(2, $Version.length - 2) } else { $this.Constraint = $Version[0] $versionSubstring = $Version.Substring(1, $Version.length - 1) } $splitVersion = $versionSubstring.split('.') } else { $splitVersion = $Version.split('.') } if ($null -ne $splitVersion[0]) { $this.Major = "$($splitVersion[0])" } if ($null -ne $splitVersion[1]) { $this.Minor = "$($splitVersion[1])" } if ($null -ne $splitVersion[2]) { $this.Patch = "$($splitVersion[2])" } } [int]Compare([SemVer] $other) { if (Compare-Numbers -A $this.Major -B $other.Major) { return Compare-Numbers -A $this.Major -B $other.Major } elseif (Compare-Numbers -A $this.Minor -B $other.Minor) { return Compare-Numbers -A $this.Minor -B $other.Minor } else { return Compare-Numbers -A $this.Patch -B $other.Patch } } [string]ToString() { return "$($this.Major).$($this.Minor).$($this.Patch)" } [void]Increment([VersionIdentifier] $Identifier) { switch ($Identifier) { Major { # 1.2.3 -> 2.0.0 $this.Major = (+$this.Major + 1).ToString() $this.Minor = "0" $this.Patch = "0" } Minor { # 1.2.3 -> 1.3.0 $this.Minor = (+$this.Minor + 1).ToString() $this.Patch = "0" } Patch { # 1.2.3 -> 1.2.4 $this.Patch = (+$this.Patch + 1).ToString() } } } } function Assert-Equal { [CmdletBinding()] param ( [SemVer]$SemVer1, [SemVer]$SemVer2 ) # $SemVer2 # 1 >=1.0.0 <2.0.0 # 1.0 >=1.0.0 <1.1.0. # 1.0.0 1.0.0 exact $lessThanSemVer = [SemVer]::new($SemVer2.ToString()) if ($SemVer2.Patch) { return $SemVer1.Compare($SemVer2) -eq 0 } elseif ($SemVer2.Minor) { $lessThanSemVer.Increment([VersionIdentifier]::Minor) } else { $lessThanSemVer.Increment([VersionIdentifier]::Major) } return (Assert-GreaterThanEqual $SemVer1 $SemVer2) -and (Assert-LessThan $SemVer1 $lessThanSemVer) } function Assert-LessThan { [CmdletBinding()] param ( [SemVer]$SemVer1, [SemVer]$SemVer2 ) return $SemVer1.Compare($SemVer2) -lt 0 } function Assert-LessThanEqual { [CmdletBinding()] param ( [SemVer]$SemVer1, [SemVer]$SemVer2 ) $lteSemVer = [SemVer]::new($SemVer2.ToString()) if ($SemVer2.Patch) { return $SemVer1.Compare($SemVer2) -le 0 } elseif ($SemVer2.Minor) { $lteSemVer.Increment([VersionIdentifier]::Minor) } else { $lteSemVer.Increment([VersionIdentifier]::Major) } return $SemVer1.Compare($lteSemVer) -lt 0 } function Assert-GreaterThan { [CmdletBinding()] param ( [SemVer]$SemVer1, [SemVer]$SemVer2 ) $gtSemVer = [SemVer]::new($SemVer2.ToString()) if ($SemVer2.Patch) { return $SemVer1.Compare($SemVer2) -gt 0 } elseif ($SemVer2.Minor) { $gtSemVer.Increment([VersionIdentifier]::Minor) } else { $gtSemVer.Increment([VersionIdentifier]::Major) } return $SemVer1.Compare($gtSemVer) -ge 0 } function Assert-GreaterThanEqual { [CmdletBinding()] param ( [SemVer]$SemVer1, [SemVer]$SemVer2 ) return $SemVer1.Compare($SemVer2) -ge 0 } function Assert-TildeConstraint { [CmdletBinding()] param ( [SemVer]$SemVer1, [SemVer]$SemVer2 ) # $SemVer2 # ~1: >=1.0.0 <2.0.0. # ~1.1: >=1.1.0 <1.2.0. # ~1.1.1: >=1.1.1 <1.2.0. $lessThanSemVer = [SemVer]::new($SemVer2.ToString()) if ($SemVer2.Patch -or $SemVer2.Minor) { # ~1.1.1: >=1.1.1 <1.2.0. # ~1.1: >=1.1.0 <1.2.0. $lessThanSemVer.Increment([VersionIdentifier]::Minor) } else { # ~1: >=1.0.0 <2.0.0. $lessThanSemVer.Increment([VersionIdentifier]::Major) } return (Assert-GreaterThanEqual $SemVer1 $SemVer2) -and (Assert-LessThan $SemVer1 $lessThanSemVer) } function Assert-CaretConstraint { [CmdletBinding()] param ( [SemVer]$SemVer1, [SemVer]$SemVer2 ) # $SemVer2 # ^1 >=1.0.0 <2.0.0 # ^1.1 >=1.1.0 <2.0.0 # ^1.1.1 >=1.1.1 <2.0.0 # ^0 >=0.0.0 <1.0.0 # ^0.0 >=0.0.0 <0.1.0 # ^0.0.0 >=0.0.0 <0.0.1 $lessThanSemVer = [SemVer]::new($SemVer2.ToString()) if (($SemVer2.Patch -eq 0) -and ($SemVer2.Minor -eq 0) -and ($SemVer2.Major -eq 0)) { # ^0.0.0 >=0.0.0 <0.0.1 $lessThanSemVer.Increment([VersionIdentifier]::Patch) } elseif (($SemVer2.Minor -eq 0) -and ($SemVer2.Major -eq 0)) { # ^0.0 >=0.0.0 <0.1.0 $lessThanSemVer.Increment([VersionIdentifier]::Minor) } else { # ^1 >=1.0.0 <2.0.0 # ^0 >=0.0.0 <1.0.0 $lessThanSemVer.Increment([VersionIdentifier]::Major) } return (Assert-GreaterThanEqual $SemVer1 $SemVer2) -and (Assert-LessThan $SemVer1 $lessThanSemVer) } function Assert-XRangeConstraint { [CmdletBinding()] param ( [SemVer]$SemVer1, [SemVer]$SemVer2 ) # $SemVer2 # * any # 1.* >=1.0.0 <2.0.0 # 1.1.* >=1.1.0 <1.2.0 $lessThanSemVer = [SemVer]::new($SemVer2.ToString()) $semVer2ReplaceStar = [SemVer]::new($SemVer2.ToString()) if ($SemVer2.Patch -eq '*') { # 1.1.* >=1.1.0 <1.2.0 $lessThanSemVer.Increment([VersionIdentifier]::Minor) $semVer2ReplaceStar.Patch = "0" } elseif ($SemVer2.Minor -eq '*') { # 1.* >=1.0.0 <2.0.0 $lessThanSemVer.Increment([VersionIdentifier]::Major) $semVer2ReplaceStar.Patch = "0" $semVer2ReplaceStar.Minor = "0" } else { # * return $true } return (Assert-GreaterThanEqual $SemVer1 $semVer2ReplaceStar) -and (Assert-LessThan $SemVer1 $lessThanSemVer) } function Assert-SatisfiesSemVer { [CmdletBinding()] param ( [SemVer]$SemVer1, [SemVer]$SemVer2 ) if ($SemVer2.Constraint) { Switch ($SemVer2.Constraint) { '=' { return Assert-Equal -SemVer1 $SemVer1 -SemVer2 $SemVer2 } 'v' { return Assert-Equal -SemVer1 $SemVer1 -SemVer2 $SemVer2 } '<' { return Assert-LessThan -SemVer1 $SemVer1 -SemVer2 $SemVer2 } '<=' { return Assert-LessThanEqual -SemVer1 $SemVer1 -SemVer2 $SemVer2 } '>' { return Assert-GreaterThan -SemVer1 $SemVer1 -SemVer2 $SemVer2 } '>=' { return Assert-GreaterThanEqual -SemVer1 $SemVer1 -SemVer2 $SemVer2 } '~' { return Assert-TildeConstraint -SemVer1 $SemVer1 -SemVer2 $SemVer2 } '^' { return Assert-CaretConstraint -SemVer1 $SemVer1 -SemVer2 $SemVer2 } } } if (($SemVer2.Major -eq '*') -or ($SemVer2.Minor -eq '*') -or ($SemVer2.Patch -eq '*')) { return Assert-XRangeConstraint -SemVer1 $SemVer1 -SemVer2 $SemVer2 } return Assert-Equal -SemVer1 $SemVer1 -SemVer2 $SemVer2 } <# .SYNOPSIS Validates ConfigStore object to be sure configuration values will be returned accuratelly and appropriately. .DESCRIPTION Checks if config store object properties are provided with valid values. .PARAMETER ConfigStore Accepts a powershell hashtable or dictionary with key value pairs following the example, $configStore = @{Type = "File"; Format = "XML"; Path = "C:\Path\To\File"}. .EXAMPLE Confirm-ConfigStore -ConfigStore $configHashtable #> function Confirm-ConfigStore { [CmdletBinding()] param( [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [hashtable] $ConfigStore ) Write-DosMessage -Level "Debug" -Message "Validating Config Store object." $isValid = $true $isValid = Confirm-GenericConfigStore -ConfigStore $ConfigStore if ($ConfigStore.Type -eq "File") { $isValid = Confirm-FileConfigStore -ConfigStore $ConfigStore } if ($ConfigStore.Type -eq "External") { $isValid = Confirm-ExternalConfigStore -ConfigStore $ConfigStore } if ($isValid -eq $true) { Write-DosMessage -Level "Debug" -Message "Config Store is valid." } return $isValid } function Confirm-GenericConfigStore { param ( $ConfigStore ) $isValid = $true $validConfigTypes = @("File", "External") if (!($validConfigTypes -contains $ConfigStore.Type)){ Write-DosMessage -Level "Warning" -Message "$($ConfigStore.Type) is not a supported configuration type. Supported types include, $validConfigTypes" $isValid = $false } $validConfigFormats = @("XML", "AzureTable") if (!($validConfigFormats -contains $ConfigStore.Format)){ Write-DosMessage -Level "Warning" -Message "$($ConfigStore.Format) is not a supported configuration format. Supported formats include, $validConfigFormats" $isValid = $false } if ([string]::IsNullOrEmpty($ConfigStore.Type)){ Write-DosMessage -Level "Warning" -Message "ConfigStore 'Type' attribute is missing. Please provide a valid value." $isValid = $false } if ([string]::IsNullOrEmpty($ConfigStore.Format)){ Write-DosMessage -Level "Warning" -Message "ConfigStore 'Format' attribute is missing. Please provide a valid value." $isValid = $false } return $isValid } function Confirm-FileConfigStore { param ( $ConfigStore ) $isValid = $true $validFileConfigFormats = @("XML") if (!($validFileConfigFormats -contains $ConfigStore.Format)){ Write-DosMessage -Level "Warning" -Message "$($ConfigStore.Format) is not a supported 'File' type configuration format. Supported formats include, $validFileConfigFormats" $isValid = $false } if ([string]::IsNullOrEmpty($ConfigStore.Path)){ Write-DosMessage -Level "Warning" -Message "ConfigStore 'Path' attribute cannot be empty when ConfigStore type is 'File'. Please provide an appropriate path." $isValid = $false } if (![string]::IsNullOrEmpty($ConfigStore.Path) -and !(Test-Path $ConfigStore.Path)){ Write-DosMessage -Level "Warning" -Message "Path $($ConfigStore.Path) does not exist or user does not have access. Please enter a valid path in the ConfigStore object." $isValid = $false } if (![string]::IsNullOrEmpty($ConfigStore.Path) -and !(Test-Path $ConfigStore.Path -PathType Leaf)){ Write-DosMessage -Level "Warning" -Message "Path $($ConfigStore.Path) is not a file. Please enter a valid path in the ConfigStore object." $isValid = $false } return $isValid } function Confirm-ExternalConfigStore { param ( $ConfigStore ) $isValid = $true $validExternalConfigFormats = @("AzureTable") if (!($validExternalConfigFormats -contains $ConfigStore.Format)){ Write-DosMessage -Level "Warning" -Message "$($ConfigStore.Format) is not a supported 'External' type configuration format. Supported formats include, $validFileConfigFormats" $isValid = $false } if ([string]::IsNullOrEmpty($ConfigStore.Uri)){ Write-DosMessage -Level "Warning" -Message "ConfigStore 'Uri' attribute cannot be empty when ConfigStore type is 'External'. Please provide an appropriate Uri." $isValid = $false } return $isValid } function Confirm-IsBoolean { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $name, [Parameter(Mandatory = $true)] [string] $type, $value ) $result = @{check = "IsBoolean"; name = $name; type = $type; value = $value } if ($value -isnot [boolean]) { $result.Add("errorFlag", 1) $result.Add("level", "Fatal") $result.Add("message", "You must specify a valid boolean value (ex: `$true or `$false) for the ""$name"" configuration.") } else { $result.Add("errorFlag", 0) } return (New-CheckResult @result) } function Confirm-IsNotNull { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $name, [Parameter(Mandatory = $true)] [string] $type, $value ) $result = @{check = "IsNotNull"; name = $name; type = $type; value = $value} if ([string]::IsNullOrEmpty($value)) { $result.Add("errorFlag", 1) $result.Add("level", "Fatal") $result.Add("message", "A null or empty ""$name"" value was found as a configuration.") } else { $result.Add("errorFlag", 0) } return (New-CheckResult @result) } function Confirm-IsValidConnection { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $name, [Parameter(Mandatory = $true)] [string] $type, $value ) $result = @{check = "IsValidConnection"; name = $name; type = $type; value = $value} if ($value -is [Hashtable] -and $value.ContainsKey("sqlConnection") -and $value.ContainsKey("sqlTestCommand")) { # confirm string is a valid try { $connection = New-Object System.Data.SqlClient.SQLConnection($value.sqlConnection) # confirm access to connection try { $connection.Open() try { $command = New-Object System.Data.SqlClient.SqlCommand($value.sqlTestCommand, $connection) $out = $command.ExecuteReader() if (($out | Measure-Object).Count -eq 0) { throw } $result.Add("errorFlag", 0) } catch { $result.Add("errorFlag", 1) $result.Add("level", "Fatal") $result.Add("message", "Test sql command failed '$($value.sqlTestCommand)' please check database connection settings.") } } catch { $result.Add("errorFlag", 1) $result.Add("level", "Fatal") $result.Add("message", "Could not connect to '$($value.sqlConnection)' please check database connection settings.") } finally { $connection.Close(); } } catch { $result.Add("errorFlag", 1) $result.Add("level", "Fatal") $result.Add("message", "Invalid connection string '$($value.sqlConnection)'.") } } else { $result.Add("errorFlag", 1) $result.Add("level", "Fatal") $result.Add("message", "Valid connection string requires a hashtable with both sqlConnection and sqlTestCommand keys (example: connection = @{sqlConnection=""Data Source=<server>;Initial Catalog=<database>;Integrated Security=True;"";sqlTestCommand=""SELECT <test> FROM <schema><table>""}") } return (New-CheckResult @result) } function Confirm-IsValidDir { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $name, [Parameter(Mandatory = $true)] [string] $type, [string] $value ) $result = @{check = "IsValidDir"; name = $name; type = $type; value = $value } if (!(Test-Path (Split-Path $value -Parent))) { $result.Add("errorFlag", 1) $result.Add("level", "Fatal") $result.Add("message", """$value"" does not have a valid directory. Please specify a valid directory for the ""$name"" configuration") } else { $result.Add("errorFlag", 0) } return (New-CheckResult @result) } function Confirm-IsValidEndpoint { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $name, [Parameter(Mandatory = $true)] [string] $type, $value ) $result = @{check = "IsValidEndpoint"; name = $name; type = $type; value = $value } try { Invoke-WebRequest -Uri $value -Method GET -UseDefaultCredentials -UseBasicParsing $result.Add("errorFlag", 0) } catch [System.Net.WebException] { $result.Add("errorFlag", 1) $result.Add("level", "Fatal") $result.Add("message", "There was an error communicating with the configured $name endpoint. Request: $value. Status Code: $($_.Exception.Response.StatusCode.value__). Message: $($_.Exception.Response.StatusDescription)") } return (New-CheckResult @result) } function Confirm-IsValidPath { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $name, [Parameter(Mandatory = $true)] [string] $type, [string] $value ) $result = @{check = "IsValidPath"; name = $name; type = $type; value = $value } if (!(Test-Path $value)) { $result.Add("errorFlag", 1) $result.Add("level", "Fatal") $result.Add("message", """$value"" is not a valid path. Please specify a valid path for the ""$name"" configuration") } else { $result.Add("errorFlag", 0) } return (New-CheckResult @result) } function Confirm-IsValidValue { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string[]] $validateSet, [Parameter(Mandatory = $true)] [string] $name, [Parameter(Mandatory = $true)] [string] $type, [string] $value ) $result = @{check = "IsValidValue"; name = $name; type = $type; value = $value } if ($validateSet -notcontains $value) { $result.Add("errorFlag", 1) $result.Add("level", "Fatal") $result.Add("message", """$value"" is not a valid value. Please specify one of these valid configuration values ""$($validateSet -join ", ")""") } else { $result.Add("errorFlag", 0) } return (New-CheckResult @result) } function Get-AzureTableStorageInputsFromUri { param ( $storageUri ) $uriString = [System.Uri]$storageUri # Host returns "<stroage_account>.table.core.windows.net" $storageAccountName = $uriString.Host.Substring(0,$uriString.Host.IndexOf(".")) Write-DosMessage -Level "Debug" -Message "Extracting storage account name from storage URI. Storage Account Name: $storageAccountName" # AbsolutePath returns "/<table_name>" $tableName = $uriString.AbsolutePath.Substring(1) Write-DosMessage -Level "Debug" -Message "Extracting table name from storage URI. Table Name: $tableName" # Query returns everyting after "?" in the url $sasToken = $uriString.Query Write-DosMessage -Level "Debug" -Message "Extracting SAS token from storage URI." return @{ storageSas = $sasToken; tableName = $tableName; storageAccountName = $storageAccountName } } <# .SYNOPSIS Private function for the Get-DosConfigScopes, but just for the azuretable type. .DESCRIPTION Private function for the Get-DosConfigScopes, but just for the azuretable type. #> function Get-DosConfigScopesAzureTable { [CmdletBinding()] param ( [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [hashtable] $ConfigStore ) Write-DosMessage -Level "Verbose" -Message "Grabbing the scopes from the external config store.." try { $values = Get-DosConfigValuesAzureTable -ConfigStore $ConfigStore return $values.scope | select-object -unique } catch { Write-DosMessage -Level "Fatal" -Message "Error grabbing the scopes from the external config store. Exception: $($_.Exception)" return $null } } <# .SYNOPSIS Private function for the Get-DosConfigScopes, but just for the xml type. .DESCRIPTION Private function for the Get-DosConfigScopes, but just for the xml type. #> function Get-DosConfigScopesXml { [CmdletBinding()] param ( [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [hashtable] $ConfigStore ) Write-DosMessage -Level "Verbose" -Message "Attempting to parse XML content from $installConfigPath." try { $installConfigXml = [xml](Get-Content $ConfigStore.Path) } catch { Write-DosMessage -Level "Warning" -Message "Error parsing XML content from $installConfigPath. Exception: $($_.Exception)" return $null } $scopeArray = $installConfigXml.installation.settings.scope.name | select-object -unique return $scopeArray } <# .SYNOPSIS Private function for the Get-DosConfigValues, but just for the AzureTable type. .DESCRIPTION Private function for the Get-DosConfigValues, but just for the AzureTable type. #> function Get-DosConfigValuesAzureTable { [CmdletBinding()] param ( [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] $ConfigStore, $Scope ) $filter = "" if ($Scope) { # parse requested scope to be returned Write-DosMessage -Level "Debug" -Message "Adding $Scope scope filter" $filter = "`$filter=(PartitionKey eq '$Scope')" } $header = @{ Accept = 'application/json;odata=nometadata' } try { $oldProgressPreference = $progresspreference $global:progressPreference = 'silentlyContinue' $progresspreference = 'SilentlyContinue' $result = Invoke-WebRequest -Method GET -Uri "$($ConfigStore.Uri)&$filter" -Headers $header -UseBasicParsing $oldProgressPreference = $oldProgressPreference $global:progressPreference = $oldProgressPreference $progresspreference = $oldProgressPreference } catch { Write-DosMessage -Level "Fatal" -Message "Failed to retrieve configuration values from $($configStore.Type) configstore. Excetion $($_.Exception)." } $configValues = ($result.Content | ConvertFrom-Json).value <# PartitionKey RowKey Timestamp Value ------------ ------ --------- ----- common clientEnvironment 2020-09-23T23:30:28.8063443Z common clientName 2020-09-23T23:08:49.5847473Z dfma common testVariable 2020-09-23T23:28:52.2270564Z test123 #> #convert the array of PSCustomObjects to an array of PSCustomObjects with names that we want. if([string]::IsNullOrEmpty($Scope)){ #return an array of pscustomobjects $returnArray = @() foreach($row in $configValues){ $myCustomRow= [PSCustomObject] @{ Name = $row.RowKey Value = $row.Value Scope = $row.PartitionKey Timestamp = $row.Timestamp } $returnArray += $myCustomRow } return $returnArray } else { #return a hashtable $returnHashtable = @{} foreach($row in $configValues){ $returnHashtable[$row.RowKey] = $row.Value } return $returnHashtable } } <# .SYNOPSIS Private function for the Get-DosConfigValues, but just for the Xml type. .DESCRIPTION Private function for the Get-DosConfigValues, but just for the Xml type. #> function Get-DosConfigValuesXml { param( [Parameter(Mandatory=$true)] [string] $ConfigSection, [ValidateScript({ if (!(Test-Path $_)) { Write-DosMessage -Level "Fatal" -Message "Path $_ does not exist. Please enter valid path to the install.config." } if (!(Test-Path $_ -PathType Leaf)) { Write-DosMessage -Level "Fatal" -Message "Path $_ is not a file. Please enter a valid path to the install.config." } return $true })] [string] $InstallConfigPath = "install.config" ) Write-DosMessage -Level "Verbose" -Message "Attempting to parse XML content from $installConfigPath." try { $installConfigXml = [xml](Get-Content $installConfigPath) } catch { Write-DosMessage -Level "Warning" -Message "Error parsing XML content from $installConfigPath. Exception: $($_.Exception)" return $null } Write-DosMessage -Level "Verbose" -Message "Searching XML content for $configSection scoped values." $sectionSettings = $installConfigXml.installation.settings.scope | Where-Object {$_.name -eq $configSection} if($null -eq $sectionSettings){ Write-DosMessage -Level "Warning" -Message "The '$ConfigSection' scope doesn't exist in '$installConfigPath'." return $null } $installationSettings = @{} Write-DosMessage -Level "Verbose" -Message "Reading scoped values if they are not null or empty." foreach($variable in $sectionSettings.variable){ if($variable.name){ $installationSettings.Add($variable.name, $variable.value) } } if ($installationSettings.Count -eq 0){ Write-DosMessage -Level "Warning" -Message "There were no configuration values provided in '$ConfigSection' scope." } return $installationSettings } <# .SYNOPSIS Checks Registry for .net core .DESCRIPTION Checks the array of .net core versions #> function Get-DotNetCoreVersion { param ( [PSCustomObject] $Value, [PSCustomObject] $Registry ) if($Registry) { $item = $null ForEach ($item in $Registry) { if($item.DisplayVersion -like "$($Value.softwareVersion)*") { Write-Host "Registry:"$item.DisplayVersion, " Manifest:"$Value.softwareVersion break } } It "dependent software $($Value.softwareName) $($Value.softwareVersion) version check" { $item.DisplayVersion | Should -BeLike "$($Value.softwareVersion)*" } } } function Get-DotNetVersion { param ( [PSCustomObject] $Value ) $netVersion = @{ 378389 = [version]'4.5' 378675 = [version]'4.5.1' 378758 = [version]'4.5.1' 379893 = [version]'4.5.2' 393295 = [version]'4.6' 393297 = [version]'4.6' 394254 = [version]'4.6.1' 394271 = [version]'4.6.1' 394802 = [version]'4.6.2' 394806 = [version]'4.6.2' 460798 = [version]'4.7' 460805 = [version]'4.7' 461308 = [version]'4.7.1' 461310 = [version]'4.7.1' 461808 = [version]'4.7.2' 461814 = [version]'4.7.2' 528040 = [version]'4.8' 528049 = [version]'4.8' } $netVersionArray = @() foreach ($item in $netVersion.GetEnumerator()) { #If the variable software i.e '4.6.2' is equal to the Value of '4.6.2' in the Hash table then set the #variable softwareConverted to the value of the Key or i.e '394806'' if($Value.softwareVersion -eq $item.Value) { #Append to array as there are multiple DWORDs for each Value of our Table $netVersionArray += $item.Key } else { #Do Nothing (currently looping through entire Hash Table to make sure we dont miss anything) continue } } #This looks directly into the registry and compares against our netVersionArray to make sure the requested .NET version matches the DWORD inside our Windows registry $itemProperty = Get-ItemProperty -Path "HKLM:\Software\Microsoft\NET Framework Setup\NDP\v4\full\*" | Where-Object { $_.Release -ige $netVersionArray[0] -Or $_.Release -ige $netVersionArray[1] } | Select-Object Release $releaseProperty = [string]$itemProperty $dword = $releaseProperty.Replace("@{Release=", "").Replace("}", "") Write-Host "Registry:"$dword, " Manifest:"$Value.softwareVersion $versionNumber = $null foreach ($item in $netVersion.GetEnumerator()) { if($dword -eq $item.Key) { $versionNumber = $item.Value break } } It ".net dependent software version check" { $versionNumber| Should -Not -BeNullOrEmpty $versionNumber | Should -BeGreaterOrEqual $Value.softwareVersion } } <# .SYNOPSIS Retrieves a list of applications from the windows registry. .DESCRIPTION Scans the windows registry for installed applications and returns a list of summary objects. .EXAMPLE $x = Get-InstalledApps () .OUTPUTS Returns an array of registered applications. Each item in the array contains the application's DisplayName, Publisher, InstallDate, DisplayVersion and UninstallString 32 Bit NOTE: IF this function is called from a 32 bit process, the apps returned may differ from the list when called fomr a 64 bit process! This is becuase microsfot redirects 32 bit apps in the registry. See: https://docs.microsoft.com/en-us/windows/desktop/winprog64/registry-reflection #> function Get-InstalledApps { if (![Environment]::Is64BitProcess) { $regpath = 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*' } else { $regpath = @( 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*' 'HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*' ) } Get-ItemProperty $regpath | .{process{if($_.DisplayName -and $_.UninstallString) { $_ } }} | Select DisplayName, Publisher, InstallDate, DisplayVersion, UninstallString |Sort DisplayName } <# .SYNOPSIS Creates a new IntegrationServices object .DESCRIPTION Instantiates an IntegrationServices object using a specified connection string .PARAMETER ConnectionString Connection string to target Integration Services .INPUTS None. You cannot pipe objects to Get-IntegrationServices. .OUTPUTS Integration Services object .EXAMPLE PS> Get-IntegrationServices #> function Get-IntegrationServices { [cmdletbinding(SupportsShouldProcess=$true)] [OutputType()] param([parameter(Mandatory=$false)][string]$ConnectionString = 'Data Source=localhost;Initial Catalog=EDWAdmin;Integrated Security=True') $connection = New-Object Data.SqlClient.SqlConnection $ConnectionString if ($pscmdlet.ShouldProcess($CatalogName, "Provisioning SSIS catalog")) { $connection.Open() $integrationServices = New-Object "Microsoft.SqlServer.Management.IntegrationServices.IntegrationServices" $connection } return $integrationServices } function Get-SqlServerVersion { $sqlVersionDict = @{ 13 = 2016 11 = 2012 } $sqlVersion = Get-ItemProperty HKLM:\SOFTWARE\Microsoft\MSSQLServer\MSSQLServer\CurrentVersion | Select-Object -ExpandProperty "CurrentVersion" $sqlVersion = [int]$sqlVersion.split('.')[0] $currentSqlVersion = $sqlVersionDict[$sqlVersion] if ($null -eq $currentSqlVersion) { Write-DosMessage -Level 'Error' -Message 'SQL Server Version not in Dictionary' } return $currentSqlVersion } function Invoke-DependentSoftwareCheck { param ( [array] $Data ) Describe "DependentSoftwareCheck" { ForEach ($value in $Data) { # Continue if sqlVersion is not specified or is different than sqlVersion of machine or if the $value is $null if ($null -eq $value -or ($value.PSObject.Properties['sqlVersion'] -and $value.sqlVersion -ne $(Get-SqlServerVersion))) { continue } $w64 = Get-ItemProperty HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\* | where-Object DisplayName -like "*$($value.softwareName)*" $w32 = Get-ItemProperty HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\* | where-Object DisplayName -like "*$($value.softwareName)*" if ($value.softwareName -like "*.net framework*") { Get-DotNetVersion $value } elseif ($value.softwareName -like "*.net core*" -and $w64){ Get-DotNetCoreVersion $value $w64 } elseif ($value.softwareName -like "*.net core*" -and $w32) { Get-DotNetCoreVersion $value $w32 } else { if($w64) { Get-RegistryAndLocationCheck $w64 $value } elseif ($w32) { Get-RegistryAndLocationCheck $w32 $value } else { Write-Host "Software not found in Registry - Manifest Path: $($value.softwareLocation)" It "Dependent software $($value.softwareName) $($value.softwareVersion) location exists" { $value.softwareLocation | Should exist } } } } } } function Get-RegistryAndLocationCheck { param ( [PSCustomObject] $Registry, [PSCustomObject] $Value ) if ($Registry) { Write-Host "Registry:"$Registry.DisplayName," Manifest:"$Value.softwareName It "Dependent software $($Value.softwareName) $($Value.softwareVersion) exists in registry" { $Registry.DisplayName | Should -Match "$($Value.softwareName)*" } if ($Value.versionCheckType -eq 'exact') { Write-Host "Registry:"$Registry.DisplayVersion, " Manifest:"$Value.softwareVersion It "Dependent software $($Value.softwareName) $($Value.softwareVersion) version check" { $Registry.DisplayVersion | Should -BeExactly $Value.softwareVersion } } elseif ($Value.versionCheckType -eq 'min') { Write-Host "Registry:"$Registry.DisplayVersion, " Manifest:"$Value.softwareVersion It "Dependent software $($Value.softwareName) $($Value.softwareVersion) version check" { $Registry.DisplayVersion | Should -BeGreaterOrEqual $Value.softwareVersion } } } } <# .SYNOPSIS Checks whether the OS Version from a manifest json file is equal to that on the machine this is being ran on .DESCRIPTION .PARAMETER Data Accepts an array of OS Version checktype objects that have been converted from JSON #> function Invoke-OsVersionCheck { param ( [array] $AllowedVersions ) $currentOsVersion = (Get-ComputerInfo -Property osname).osname Describe "Os Version Check" { $matchedOSVersion = $null ForEach ($value in $AllowedVersions) { if ($currentOsVersion -like "*$value*") { $matchedOSVersion = $value break } } It "'$currentOsVersion' is an allowed OS" { $matchedOSVersion | Should -Not -BeNullOrEmpty $currentOsVersion | Should -BeLike "*$matchedOSVersion*" } } } <# .SYNOPSIS Checks whether the Powershell Version from a manifest json file is equal to that on the machine this is being ran on .DESCRIPTION .PARAMETER Data Accepts an array of powershell objects that have been converted from JSON #> function Invoke-PowershellVersionCheck { param ( [array] $Data ) $currentVersion = "$($psversiontable.psversion.major).$($psversiontable.psversion.minor)" Write-Host "Powershell version is $currentVersion)" Describe "PowershellVersionCheck" { ForEach ($value in $Data) { if ($null -eq $value) { continue } #process check if ($value.versionCheckType -eq 'min'){ It "Version of powershell minimum version $($value.powershellVersion)" { $currentVersion | Should -BeGreaterOrEqual $value.powershellVersion } } elseif ($value.versionCheckType -eq 'exact') { It "Version of powershell exactly $($value.powershellVersion)" { $currentVersion | Should -BeExactly $value.powershellVersion } } } } } <# .SYNOPSIS Checks whether the Windows Features from a manifest json file are installed and enabled on the executing environmnet .DESCRIPTION .PARAMETER Data Accepts an array of powershell objects that have been converted from JSON #> function Invoke-WindowsFeatureCheck { param ( [array] $Data ) $currentOsVersion = (Get-ComputerInfo -Property osname).osname $amIAServer = $currentOsVersion.StartsWith("Microsoft Windows Server") $amIWindowsTen = $currentOsVersion.StartsWith("Microsoft Windows 10") Describe "Windows Feature Check" { ForEach ($value in $Data) { if ($amIAServer) { if ($value.os -like "*Server*") { ForEach ($feature in $value.featureList) { It "'$feature' windows feature is installed & enabled" { Get-WindowsFeature -Name $feature | Should -Not -BeNullOrEmpty (Get-WindowsFeature -Name $feature)."InstallState" | Should -Be "Installed" } } } else { Write-Warning "Check designed for $($value.os) does not match the Current OS: $currentOsVersion. Skipping Test" } } elseif ($amIWindowsTen) { if ($value.os -like "*10*") { ForEach ($feature in $value.featureList) { It "'$feature' windows feature is installed & enabled" { Get-WindowsOptionalFeature -Online -FeatureName $feature | Should -Not -BeNullOrEmpty (Get-WindowsOptionalFeature -Online -FeatureName $feature).State | Should -Be "Enabled" } } } else { Write-Warning "Check designed for $($value.os) does not match the Current OS: $currentOsVersion. Skipping Test" } } else { Write-Warning "Unrecognized OS: $currentOsVersion" } } } } <# .SYNOPSIS Creates an IIS Application Pool with the specified options. .DESCRIPTION Uses the WebAdministration powershell module to create IIS app pools. .PARAMETER IISAppPoolName Name of App Pool to create. .PARAMETER IdentityCredential PSCredential with a username and password. .PARAMETER RuntimeDotNetVersion Runtime version to be used in app pool creation. Defaults to 'v4.0' .PARAMETER PipelineMode Managed pipeline mode to be used in app pool creation. Defaults to 'Integrated' .EXAMPLE New-AppPool -IISAppPoolName "CatalystAppPool" -IdentityCredential $psCredential #> function New-AppPool { [CmdletBinding()] param( [Parameter(Mandatory=$true)] [ValidateLength(1,64)] [ValidateScript({ if ($_ -match '[^a-zA-Z0-9]') { Write-DosMessage -Level "Error" -Message "$_ must only contain alphanumeric values. Please remove special characters." } else { $true } })] [string] $IISAppPoolName, [PSCredential] $IdentityCredential, [string] $RuntimeDotNetVersion = "v4.0", [string] $PipelineMode = "Integrated" ) # Tests if current session has elevated permissions required to create IIS App Pool Test-ElevatedPermission # TODO: Validate bitness for powershell session. Import-Module WebAdministration most likely requires 64 bit Import-Module WebAdministration if ($IdentityCredential) { Write-DosMessage -Level "Information" -Message "Attempting to validate credential" if (!((Confirm-DosCredential -Credential $IdentityCredential).isValid)) { Write-DosMessage -Level "Fatal" -Message "Username or password is not valid" } } if(!(Test-Path "IIS:\AppPools\$IISAppPoolName" -PathType Container)) { Write-DosMessage -Level "Information" -Message "Creating AppPool $IISAppPoolName." $appPool = New-WebAppPool $IISAppPoolName $appPool | Set-ItemProperty -Name "managedRuntimeVersion" -Value "$RuntimeDotNetVersion" $appPool | Set-ItemProperty -Name "managedPipelineMode" -Value "$PipelineMode" Set-AppPoolSettings -IISAppPoolName $IISAppPoolName -IdentityCredential $IdentityCredential $appPool.Stop() }else{ Write-DosMessage -Level "Error" -Message "AppPool: $IISAppPoolName already exists." return } } function New-CheckResult { [CmdletBinding()] param ( $name, $type, $value, $check, $errorFlag, $level, $message ) $result = New-Object PSObject $result | Add-Member -Type NoteProperty -Name name -Value $name $result | Add-Member -Type NoteProperty -Name type -Value $type $result | Add-Member -Type NoteProperty -Name value -Value $value $result | Add-Member -Type NoteProperty -Name check -Value $check $result | Add-Member -Type NoteProperty -Name errorFlag -Value $errorFlag $result | Add-Member -Type NoteProperty -Name level -Value $level $result | Add-Member -Type NoteProperty -Name message -Value $message return $result } <# .SYNOPSIS NON PUBLIC - creates the xml file for the XML/File config store .DESCRIPTION creates the xml file for the XML/File config store .PARAMETER configStore The xml object that represents the install.config .EXAMPLE New-DosConfigStoreXml -configStore $configStore .NOTES General notes #> function New-DosConfigStoreXml { [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] param( [Parameter(Mandatory=$true)] [hashtable] $configStore ) if(Test-Path -Path $configStore.Path){ Write-DosMessage -Level Warning -Message "$($configStore.Path) already exists. No config store created" } else { $folder = split-path -path $configStore.Path if(-not(Test-Path -Path $folder)){ Write-DosMessage -Level Information -Message "Creating $folder." if($PSCmdlet.ShouldProcess("Create $folder")){ New-Item -ItemType directory -Path $folder | Out-Null } } if(-not(Test-Path -Path $configStore.Path)){ Write-DosMessage -Level Information -Message "Creating $($configStore.Path) with template settings." $installationConfigTemplate = "<installation>`n`t<settings>`n`t</settings>`n</installation>" if($PSCmdlet.ShouldProcess("Create $($configStore.Path))")){ New-Item -Path $configStore.Path -ItemType "file" -Value $installationConfigTemplate -Force | Out-Null } } } return $configStore } <# .SYNOPSIS Creates a new SSIS catalog .DESCRIPTION Adds a new SSIS catalog with specified name and encryption key .PARAMETER IntegrationServices IntegrationServices object where catalog should be created .PARAMETER CatalogEncryptionKey Key to use for encrypting catalog, if it must be created .PARAMETER CatalogName Name of SSIS catalog to contain project .INPUTS None. You cannot pipe objects to New-SsisCatalog .OUTPUTS Created SSIS catalog .EXAMPLE PS> New-SsisCatalog -IntegrationServices $integrationServices -CatalogEncryptionKey 'password' #> function New-SsisCatalog { [cmdletbinding(SupportsShouldProcess=$true)] [OutputType()] param( [parameter(Mandatory=$true)]$IntegrationServices, [parameter(Mandatory=$true)][string]$CatalogEncryptionKey, [parameter(Mandatory=$false)][string]$CatalogName = 'SSISDB') if ($pscmdlet.ShouldProcess($CatalogName, "Provisioning SSIS catalog")) { $catalog = New-Object "Microsoft.SqlServer.Management.IntegrationServices.Catalog" ($IntegrationServices, $CatalogName, $CatalogEncryptionKey) $catalog.Create() } return $catalog } <# .SYNOPSIS Creates a new SSIS folder .DESCRIPTION Adds a new SSIS folder with specified name .PARAMETER SsisCatalog IntegrationServices Catalog object where folder should be created .PARAMETER FolderName Name of folder to create .INPUTS None. You cannot pipe objects to New-SsisFolder .OUTPUTS Created SSIS folder .EXAMPLE PS> New-SsisFolder -SsisCatalog $catalog -FolderName 'Catalyst' #> function New-SsisFolder { [cmdletbinding(SupportsShouldProcess=$true)] [OutputType()] param( [parameter(Mandatory=$true)]$SsisCatalog, [parameter(Mandatory=$true)][string]$FolderName) if ($pscmdlet.ShouldProcess($FolderName, "Creating SSIS folder")) { $folder = New-Object "Microsoft.SqlServer.Management.IntegrationServices.CatalogFolder" ($SsisCatalog, $FolderName, "Folder to contain SSIS projects") $folder.Create() } return $folder } <# .SYNOPSIS Creates a new SSIS project .DESCRIPTION Deploys an ISPAC into an existing SSIS catalog and folder .PARAMETER SsisFolder IntegrationServices Folder object where project should be created .PARAMETER ProjectName Name of project to create .PARAMETER IspacPath Path to ISPAC to deploy to initialize project .INPUTS None. You cannot pipe objects to New-SsisProject .OUTPUTS None. .EXAMPLE PS> New-SsisProject -SsisFolder $folder -ProjectName 'CatalystLoader' -IspacPath "C:\a\place" #> function New-SsisProject { [cmdletbinding(SupportsShouldProcess=$true)] [OutputType()] param( [parameter(Mandatory=$true)]$SsisFolder, [parameter(Mandatory=$true)][string]$ProjectName, [parameter(Mandatory=$true)][string]$IspacPath) if ($pscmdlet.ShouldProcess($ProjectName, "Deploying SSIS project")) { [byte[]] $projectFile = [System.IO.File]::ReadAllBytes($IspacPath) $SsisFolder.DeployProject($ProjectName, $projectFile) } } <# .SYNOPSIS Publishes a .net core web applications. .DESCRIPTION Creates the necessary folder and expands the archive containing the .net core web app to the folder. Also creates the appropriate IIS Site and associates the specified App Pool with the site. .PARAMETER WebApplicationPackagePath Path to the zip file containing the .net core web applications .PARAMETER AppPoolName Application pool to associate with the web application. This must already exist .PARAMETER IISWebSite IIS Site to install the application to. Defaults to "Default Web Site" if not specified .PARAMETER AppName Application name - used for both the site AND the folder created underneath the IISWebSite root .PARAMETER PathsToPreserve Array of paths to preserve during a deployment, such as logs, relative to the install directory in IIS, so they are not removed during the upgrade of an application. Ignored for new installs. .EXAMPLE Publish-DotNetCoreWebApp -WebApplicationPackagePath $WebAppPackagePath -AppPoolCredentials $AppPoolCredential -AppName $AppName -IISWebSite $IISWebSite -AppPoolName $AppPoolName -PathsToPreserve @("logs") #> function Publish-DotNetCoreWebApp{ [CmdletBinding()] param( [Parameter(Mandatory=$true)] [ValidateScript({ if (!(Test-Path $_)) { Write-DosMessage -Level "Fatal" -Message "WebApplicationPackagePath $_ does not exist. Please enter valid path." } else { $true } })] [string] $WebApplicationPackagePath, [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [ValidateLength(1,64)] [ValidateScript({ if ($_ -match '[^a-zA-Z0-9]') { Write-DosMessage -Level "Fatal" -Message "$_ must only contain alphanumeric values. Please remove special characters." } else { $true } })] [string] $AppPoolName, [string] $IISWebSite = "Default Web Site", [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [string] $AppName, [string[]] $PathsToPreserve ) # Tests if current session has elevated permissions required to create IIS App Pool Test-ElevatedPermission Import-Module WebAdministration #Stop app pool if it's running (upgrade scenario) [string] $appPoolUser = $null if(Test-Path "IIS:\AppPools\$AppPoolName" -PathType Container){ $currentState = Get-WebAppPoolState -Name $AppPoolName if($currentState.Value -ne "Stopped"){ Write-DosMessage -Level "Information" -Message "App pool $AppPoolName not stopped, current state $currentState" Stop-WebAppPool -Name $AppPoolName Wait-AppPoolState -AppPoolName $AppPoolName -AppPoolState "Stopped" } $appPool = Get-Item "IIS:\AppPools\$AppPoolName" $appPoolUser = $appPool.processModel.userName } else{ Write-DosMessage -Level "Error" -Message "No app pool found named $AppPoolName" return } #Get/Create folder $physicalWebPath = Join-Path (Get-IISWebSitePath -WebSiteName $IISWebSite) $AppName if(!(Test-Path $physicalWebPath)){ Write-DosMessage -Level "Information" -Message "Creating directory $physicalWebPath" New-Item -Path $physicalWebPath -ItemType Directory | Out-Null } else { Write-DosMessage -Level "Information" -Message "Directory $physicalWebPath exists, removing previous installation files" if ($PathsToPreserve) { Get-ChildItem -Path "$physicalWebPath\*" -Exclude $PathsToPreserve | Remove-Item -Recurse -Force } else { Remove-Item -Path "$physicalWebPath\*" -Recurse -Force } } #Folder ACL ops if (!([string]::IsNullOrEmpty($appPoolUser))) { Write-DosMessage -Level "Information" -Message "Adding read and write to $physicalWebPath for $appPoolUser" Add-AclFolder -FolderPath $physicalWebPath -User $appPoolUser -Permission "Read" Add-AclFolder -FolderPath $physicalWebPath -User $appPoolUser -Permission "Write" } #Extract zip file Write-DosMessage -Level "Information" -Message "Extracting $WebApplicationPackagePath to $physicalWebPath" Expand-DosArchive -ArchiveFile $WebApplicationPackagePath -DestinationPath $physicalWebPath -Overwrite Write-DosMessage -Level "Information" -Message "Creating web site $AppName on site $IISWebSite using application pool $AppPoolName" New-WebApplication -Name $AppName -Site $IISWebSite -PhysicalPath $physicalWebPath -ApplicationPool $AppPoolName -Force | Out-Null Start-WebAppPool -Name $AppPoolName } [string] $script:WebDeployExecutableName = "msdeploy.exe" # alternatively the directory is located in registry: HKLM\SOFTWARE\Microsoft\IIS Extensions\MSDeploy [Array] $script:MsdeployLocations = @([System.IO.Path]::Combine(([System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::ProgramFilesX86)), "IIS", "Microsoft Web Deploy V3")) <# .SYNOPSIS Publishes the target Web Deploy Web application. .DESCRIPTION Uses WebDeploy to deploy specified web application with specified parameters .PARAMETER WebDeployPackageFilePath File path to web deploy zip file to publish to server. Mandatory .PARAMETER WebDeployParameterFilePath File path to the set parameter XML file that contains transforms for the web.config file (connection strings, etc) .PARAMETER WebParameters Arraylist object to the set parameters for the web.config file (connection strings, etc) .EXAMPLE Publish-DosWebApp -WebDeployPackageFilePath ".\test.zip" -WebDeployParameterFilePath ".\test.params.xml" #> function Publish-WebDeployWebApp { [CmdletBinding()] param( [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [string] $WebDeployPackageFilePath, [string] $WebDeployParameterFilePath, [System.Collections.ArrayList] $WebParameters, [string] $AppName, [string] $IISWebSite = "Default Web Site", [string] $AppPoolName, [string[]] $PathsToPreserve ) Write-DosMessage -Level "Verbose" -Message "Confirming process is running with elevated permissions." Test-ElevatedPermission if(!(Test-Path $WebDeployPackageFilePath)){ Write-DosMessage -Level "Error" -Message "Unable to locate $WebDeployPackageFilePath" return } Write-DosMessage -Level "Verbose" -Message "Generating WebDeploy arguments with '$WebDeployPackageFilePath' file path." # --% escapes out the rest of the line. otherwise we would need to add a tick(`) for semi-colons, apostrophes, and quotes. [string] $webDeployArguments = "--%"` +" -source:package='$WebDeployPackageFilePath'"` +" -dest:auto,includeAcls=""False"""` +" -verb:sync"` +" -disableLink:AppPoolExtension"` +" -disableLink:ContentExtension"` +" -disableLink:CertificateExtension" Write-DosMessage -Level "Verbose" -Message "Ensuring that WebDeploy parameter file path exists." if(!([string]::IsNullOrEmpty($WebDeployParameterFilePath))){ if ((Test-Path $WebDeployParameterFilePath) -and $WebParameters){ Write-DosMessage -Level "Error" -Message "It is not supported to provide both an XML settings file and deploy arg objects. Provide only one." Return } if(!(Test-Path $WebDeployParameterFilePath)){ Write-DosMessage -Level "Error" -Message "Unable to locate $WebDeployParameterFilePath" Return } else{ $webDeployArguments = $webDeployArguments + " -setParamFile:""$WebDeployParameterFilePath""" } } else{ foreach ($param in $WebParameters) { $webDeployArguments = $webDeployArguments + $(" -setParam:name='{0}',value='{1}'" -f $param.Name, $param.Value) } } Write-DosMessage -Level "Information" -Message "Attempting to retrieve WebDeploy executable path." [string] $webDeployExecutablePath = Resolve-FilePath -PathsToSearch $script:MsdeployLocations -FilePattern $script:WebDeployExecutableName if([String]::IsNullOrEmpty($webDeployExecutablePath)){ Write-DosMessage -Level "Error" -Message "Unable to locate web deploy, unable to publish DOS application" return } if ($PathsToPreserve) { foreach ($path in $PathsToPreserve) { $webDeployArguments += " -skip:skipaction='Delete',objectName='dirPath',absolutepath='$path$'" $webDeployArguments += " -skip:skipaction='Delete',objectName='filePath',absolutepath='$path\\.*$'" } } Write-DosMessage -Level "Verbose" -Message "Running: $webDeployExecutablePath $webDeployArguments" $output = Start-CommandAndReturnOutput -Command "& ""$webDeployExecutablePath"" $webDeployArguments | Out-String" if ([string]::IsNullOrEmpty($output)) { Write-DosMessage -Level "Fatal" -Message "Web Deploy output returned empty or null. Please validate Web Deploy 3.5 is correctly installed." return } $physicalWebPath = Join-Path (Get-IISWebSitePath -WebSiteName $IISWebSite) $AppName $appPool = Get-Item "IIS:\AppPools\$AppPoolName" $appPoolUser = $appPool.processModel.userName Write-DosMessage -Level "Information" -Message "Adding read and write to $physicalWebPath for $appPoolUser" if (!([string]::IsNullOrEmpty($appPoolUser))) { Add-AclFolder -FolderPath $physicalWebPath -User $appPoolUser -Permission "Read" Add-AclFolder -FolderPath $physicalWebPath -User $appPoolUser -Permission "Write" } Write-DosMessage -Level "Verbose" -Message $output } <# .SYNOPSIS Private function for the Remove-DosConfigSection function, but just for the azuretable type. .DESCRIPTION Private function for the Remove-DosConfigSection function, but just for the azuretable type. #> function Remove-DosConfigSectionAzureTable { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Justification="WhatIf support not implemented.")] [CmdletBinding()] param ( $configStore, $configSection ) $configValues = Get-DosConfigValuesAzureTable -ConfigStore $configStore -Scope $configSection $responseObject = $null foreach ($configValue in $configValues) { Write-DosMessage -Level "Debug" -Message "Attepmting to remove $($configValue.Scope).$($configValue.Name)" $responseObject = Remove-DosConfigValueAzureTable -ConfigStore $configStore -configSection "$($configValue.Scope)" -configSetting "$($configValue.Name)" if ($responseObject.StatusCode -eq 204) { Write-DosMessage -Level "Debug" -Message "Successfully removed $($configValue.Scope).$($configValue.Name) from $($configStore.Type) configstore." } } return $responseObject } <# .SYNOPSIS Private function for the Remove-DosConfigSection function, but just for the Xml type. .DESCRIPTION Private function for the Remove-DosConfigSection function, but just for the Xml type. #> function Remove-DosConfigSectionXml { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Justification="WhatIf support not implemented.")] [CmdletBinding()] param ( $configStore, $configSection ) $installConfigXml = [xml](Get-Content "$($configStore.Path)") $sectionSettings = $installConfigXml.installation.settings.scope | Where-Object {$_.name -eq $configSection} if (-not($sectionSettings) -or ($sectionSettings.Count -eq 0)) { #if the scope doesn't exist no problem, do nothing Write-DosMessage -Level Information -Message "$($configStore.Path) didn't have a $configSection scope. No action taken." return } #make the config section lowercase and then get the node to delete (case insensitive) $configSection = $configSection.ToLower() $nodeToDelete = $installConfigXml.selectnodes("/installation/settings/scope[translate(@name,'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz')='$configSection']") if ($nodeToDelete) { $nodeToDelete | Foreach-Object{$_.parentnode.removechild($_)} | out-null } else { Write-DosMessage -Level "Warning" -Message "$($configStore.Path) had a $configSection.$configSetting value but we couldn't find it with XPath. No action taken." $installConfigXml = $null } return $installConfigXml } <# .SYNOPSIS Private function for the Remove-DosConfigValue function, but just for the AzureTable type. .DESCRIPTION Private function for the Remove-DosConfigValue function, but just for the AzureTable type. #> function Remove-DosConfigValueAzureTable { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Justification="WhatIf support not implemented.")] [CmdletBinding()] param ( $configStore, $configSection, $configSetting ) Write-DosMessage -Level "Debug" -Message "Attempting to remove $configSection.$configSetting from $($configStore.Type) configstore." $tableStorageInputs = Get-AzureTableStorageInputsFromUri -storageUri "$($configStore.Uri)" $resource = "$($tableStorageInputs.tableName)(PartitionKey='$configSection',RowKey='$configSetting')" $deleteUri = "https://$($tableStorageInputs.storageAccountName).table.core.windows.net/$resource$($tableStorageInputs.storageSas)" $header = @{ Accept = "application/json;odata=nometadata" 'If-Match' = "*" } $responseObject = $null try { $responseObject = Invoke-WebRequest -Method DELETE -Uri "$deleteUri" -Headers $header -UseBasicParsing } catch { Write-DosMessage -Level "Fatal" -Message "Failed to remove $($configValue.PartitionKey).$($configValue.RowKey) configuration from $($configStore.Type) config store. Exception: $($_.Exception)" } if ($responseObject.StatusCode -eq 204) { Write-DosMessage -Level "Debug" -Message "Successfully removed $configSection.$configSetting from $($configStore.Type) configstore." } return $responseObject } <# .SYNOPSIS Private function for the Remove-DosConfigValue function, but just for the Xml type. .DESCRIPTION Private function for the Remove-DosConfigValue function, but just for the Xml type. #> function Remove-DosConfigValueXml { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Justification="WhatIf support not implemented.")] [CmdletBinding()] param ( $configStore, $configSection, $configSetting ) Write-DosMessage -Level "Debug" -Message "Attempting to remove $configSection.$configSetting from $($configStore.Path)." $installConfigXml = [xml](Get-Content "$($configStore.Path)") $sectionSettings = $installConfigXml.installation.settings.scope | Where-Object {$_.name -eq $configSection} if (-not($sectionSettings) -or ($sectionSettings.Count -eq 0)) { #if the scope doesn't exist no problem, do nothing Write-DosMessage -Level 'Debug' -Message "$($configStore.Path) didn't have a $configSection scope. No action taken." return } $existingSetting = $sectionSettings.variable | Where-Object {$_.name -eq $configSetting} if(!$existingSetting){ #if the existing variable in the scope doesn't exist, do nothing Write-DosMessage -Level 'Debug' -Message "$($configStore.Path) didn't have a $configSection.$configSetting value. No action taken." return } $configSection = $configSection.ToLower() $configSetting = $configSetting.ToLower() $nodeToDelete = $installConfigXml.selectnodes("/installation/settings/scope[translate(@name,'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz')='$configSection']/variable[translate(@name,'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz')='$configSetting']") if ($nodeToDelete) { $nodeToDelete | Foreach-Object{$_.parentnode.removechild($_)} | out-null } else { Write-DosMessage -Level "Warning" -Message "$($configStore.Path) had a $configSection.$configSetting value but we couldn't find it with XPath. No action taken." $installConfigXml = $null } return $installConfigXml } <# .SYNOPSIS Looks for the v1/v2/v3/vN in the URL and removes it. .DESCRIPTION There are various times when we want the version number removed from the URL, thus we have this helper function to make it easy to remove the version number. .PARAMETER Url Name the URL .EXAMPLE Remove-VersionFromLocalPath -Url 'https://www.example.com/DiscoveryService/v1/Services #> function Remove-VersionFromLocalPath { [CmdletBinding()] param ( [Parameter(Mandatory = $true, Position = 0)] [string] $Url ) $uri = [System.Uri]$Url $uriLocalPath = $($uri.LocalPath) -replace "/v(\d+)([/]?)", "/" $uriLocalPath = $uriLocalPath -replace "//","/" if($($uri.LocalPath).EndsWith("/") -and -not($uriLocalPath.endswith("/"))){ #the original ended with a slash, and the new one doesn't, add a slash $uriLocalPath = "$uriLocalPath/" } if(-not($($uri.LocalPath).EndsWith("/")) -and $uriLocalPath.endswith("/")){ #the original didn't end with a slash, but the new one does. #removing trailing slashes, remove it. $uriLocalPath = $uriLocalPath.substring(0,$uriLocalPath.Length-1) } $newUrl = "$($uri.Scheme)://$($uri.Host)$uriLocalPath$($uri.Query)" #This was the original code, but it didn't handle all the cases that I wanted it to handle. # So the following line was replaced by all the code above. #$NewUrl = $Url -replace "/v(\d+)([/]?)", "/" if ($Url -ne $NewUrl) { Write-DosMessage -Level "Debug" -Message "Removed version from url. From: $Url To: $NewUrl" } return $newUrl } <# .SYNOPSIS Repairs non-cannonical ACLs .DESCRIPTION Attempts to reorder ACEs in the specified ACL. .PARAMETER Acl ACL needing repair .PARAMETER Path Path to the item needing ACL repairs - needed because sometimes the ACL passed in doesn't have an associated path. .EXAMPLE Repair-AclCanonicalOrder -Acl $x #> function Repair-AclCanonicalOrder { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [ValidateNotNull()] [System.Security.AccessControl.DirectorySecurity] $Acl, [string] $Path ) if ($Acl.AreAccessRulesCanonical) { Write-DosMessage -Level "Debug" -Message "Acls are canonical" return } Write-DosMessage -Level "Debug" -Message "Acls are not canonnical, attempting to fix Acls" # Convert ACL to a raw security descriptor: $RawSD = New-Object System.Security.AccessControl.RawSecurityDescriptor($Acl.Sddl) # Create a new, empty DACL $NewDacl = New-Object System.Security.AccessControl.RawAcl( [System.Security.AccessControl.RawAcl]::AclRevision, $RawSD.DiscretionaryAcl.Count # Capacity of ACL ) # Put in reverse canonical order and insert each ACE (I originally had a different method that # preserved the order as much as it could, but that order isn't preserved later when we put this # back into a DirectorySecurity object, so I went with this shorter command) $RawSD.DiscretionaryAcl | Sort-Object @{E={$_.IsInherited}; Descending=$true}, AceQualifier | ForEach-Object { $NewDacl.InsertAce(0, $_) } # Replace the DACL with the re-ordered one $RawSD.DiscretionaryAcl = $NewDacl # Commit those changes back to the original SD object (but not to disk yet): $Acl.SetSecurityDescriptorSddlForm($RawSD.GetSddlForm("Access")) # Commit changes $Acl | Set-Acl -Path $Path } #List of common locations for assemblies [System.Collections.ArrayList] $script:commonFilePaths = @("$PSScriptRoot\..\assemblies","C:\Program Files (x86)\Microsoft SQL Server") <# .SYNOPSIS Attempts to resolve the the assembly file name specified to a fully qualified path. .DESCRIPTION Looks through the commonAssemblyFilePaths for files matching the specified file name. .PARAMETER FileName Assembly file name. Ex. "Microsoft.Test.dll" .EXAMPLE Resolve-AssemblyFilePath -AssemblyFileName ".\test.dll" .OUTPUTS $null if no file found, otherwise the fully qualitifed path to the file. #> function Resolve-CommonFilePath{ [CmdletBinding()] param( [string] $AssemblyFileName ) return Resolve-FilePath -PathsToSearch $script:commonFilePaths -FilePattern $AssemblyFileName } <# .SYNOPSIS Attempts to resolve the the assembly file name specified to a fully qualified path. .DESCRIPTION Looks through the commonAssemblyFilePaths for files matching the specified file name. .PARAMETER AssemblyFileName Assembly file name. Ex. "Microsoft.Test.dll" .EXAMPLE Resolve-FilePath -PathsToSearch "C:\Path\To\Search" -FilePatter "Executable.exe" .OUTPUTS $null if no file found, otherwise the fully qualitifed path to the file. #> function Resolve-FilePath{ [CmdletBinding()] param( [Array] $PathsToSearch, [string] $FilePattern ) Write-DosMessage -Level "Verbose" -Message "Attempting to retrieve file path that matches '$FilePattern'." foreach($filePath in $PathsToSearch){ try { Write-DosMessage -Level "Verbose" -Message "Searching in '$filePath'" $files = Get-ChildItem -Path $filePath -Filter $FilePattern -Recurse | Where-Object { $_.PSIsContainer -ne $true } } catch { Write-DosMessage -Level "Error" -Message "Error retrieving path that matches pattern '$FilePattern'. Exception: $($_.Exception)." } if(($files -ne $null) -and ($files.Length -gt 0)){ Write-DosMessage -Level "Verbose" -Message "Found $($files.Length) files under $assemblyFilePath for $AssemblyFileName" return $files[0].FullName } } Write-DosMessage -Level "Verbose" -Message "Unable to find path that matches pattern '$FilePattern'. Returning null." return $null } <# .SYNOPSIS Adds to the common search paths used when attempting to resolve an assembly file location. .DESCRIPTION Adds a path to the list of paths to search. .EXAMPLE Add-CommonPath -Path "C:\Windows" #> function Add-CommonPath{ [CmdletBinding()] param( [string] $Path ) $script:commonFilePaths.Add($Path) } <# .SYNOPSIS Clears the common search paths. .DESCRIPTION Clears the search path variable. .EXAMPLE Clear-CommonPath #> function Clear-CommonPath{ $script:commonFilePaths = @() } <# .SYNOPSIS Configure an IIS Application Pool with the specified settings. .DESCRIPTION Uses the WebAdministration powershell module to create IIS app pools. .PARAMETER IISAppPoolName Name of App Pool to configure. .PARAMETER IdentityCredential PSCredential with a username and password. .EXAMPLE Set-AppPoolSettings -IISAppPoolName "CatalystAppPool" -IdentityCredential $psCredential #> function Set-AppPoolSettings { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [ValidateLength(1, 64)] [ValidateScript( { if ($_ -match '[^a-zA-Z0-9]') { Write-DosMessage -Level "Error" -Message "$_ must only contain alphanumeric values. Please remove special characters." } else { $true } })] [string] $IISAppPoolName, [PSCredential] $IdentityCredential, [switch] $NoCredential ) $poolpath = "IIS:\AppPools\$IISAppPoolName" try { $appPool = Get-Item -Path $poolpath -ErrorAction Stop } Catch { Write-DosMessage -Level "Fatal" -Message "Failed to get application pool. Exception: $($_.Exception)" Break } Write-DosMessage -Level "Information" -Message "Configuring AppPool $IISAppPoolName." if ($IdentityCredential) { if (!((Confirm-DosCredential -Credential $IdentityCredential).isValid)) { Write-DosMessage -Level "Fatal" -Message "Username or password is not valid" } if (![string]::IsNullOrEmpty($IdentityCredential.UserName) -and $IdentityCredential.GetNetworkCredential().Password -ne $null) { Write-DosMessage -Level "Information" -Message "Configuring '$IISAppPoolName' app pool's identity with the credentials provided." $appPool.processModel.userName = $IdentityCredential.UserName $appPool.processModel.password = $IdentityCredential.GetNetworkCredential().Password } # IdentityType 3 references 'SpecificUser' https://docs.microsoft.com/en-us/iis/configuration/system.applicationhost/applicationpools/add/processmodel if (!($appPool.processModel.identityType -eq "SpecificUser")) { Write-DosMessage -Level "Information" -Message "Identity Type was not set to SpecificUser, setting to 3" $appPool.processModel.identityType = 3 } } else { Write-DosMessage -Level "Information" -Message "No identity credential was provided. '$IISAppPoolName' identity configuration will not be altered." } if ($NoCredential.IsPresent) { Write-DosMessage -Level "Information" -Message "NoCredential parameter was provided. Attempting to configure '$IISAppPoolName' identity type." if (!($appPool.processModel.identityType -eq "ApplicationPoolIdentity")) { Write-DosMessage -Level "Information" -Message "Identity Type was not set to ApplicationPoolIdentity, setting to 4" $appPool.processModel.identityType = 4 } Write-DosMessage -Level "Information" -Message "'$IISAppPoolName' identity type successfully set to 'ApplicationPoolIdentity'" } if ($appPool.processModel.loaduserprofile -eq $false) { Write-DosMessage -Level "Verbose" -Message "loaduserprofile was not set to true, setting to true" $appPool.processModel.loaduserprofile = $true } if (!($appPool.startMode -eq "alwaysrunning")) { Write-DosMessage -Level "Verbose" -Message "startmode was not set to alwaysrunning, setting to alwaysrunning" $appPool.startMode = "alwaysrunning" } if (!($appPool.processmodel.idletimeout -eq [TimeSpan]::FromMinutes(0))) { $appPool.processmodel.idletimeout = [TimeSpan]::FromMinutes(0) } if (!($appPool.processmodel.idletimeoutaction -eq "suspend")) { Write-DosMessage -Level "Verbose" -Message "idletimeoutaction was not set to suspend, setting to suspend" $appPool.processmodel.idletimeoutaction = "suspend" } if (!($appPool.cpu.action -eq "ThrottleUnderLoad")) { Write-DosMessage -Level "Verbose" -Message "cpu limit action was not set, setting ThrottleUnderLoad to 10%" $appPool.cpu.action = "ThrottleUnderLoad" $appPool.cpu.limit = 10000 } try { $appPool | Set-Item -Verbose -ErrorAction Stop } catch { Write-DosMessage -Level "Fatal" -Message "Failed to set application pool settings. Exception: $($_.Exception)" Break } } <# .SYNOPSIS Private function for the Set-DosConfigValue function, but just for the AzureTable type. .DESCRIPTION Private function for the Set-DosConfigValue function, but just for the AzureTable type. #> function Set-DosConfigValueAzureTable { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Justification="WhatIf support not implemented.")] [CmdletBinding()] param ( $configStore, $configSection, $configSetting, $configValue ) Write-DosMessage -Level "Debug" -Message "Attempting to add $configSection.$configSetting=$configValue into $($ConfigStore.Format)." # Check if value already exists $configValues = Get-DosConfigValues -ConfigStore $configStore -Scope $configSection $currentValue = $configValues.$configSetting $tableStorageInputs = Get-AzureTableStorageInputsFromUri -storageUri $ConfigStore.Uri $headers = @{ Accept = 'application/json;odata=nometadata' } $entity = @{ PartitionKey = "$configSection" RowKey = "$configSetting" Value = "$configValue" } $body = $entity | ConvertTo-Json if ($currentValue -eq $configValue) { Write-DosMessage -Level "Debug" -Message "$configSection.$configSetting is already configured with $configValue" $responseObject = $null } else { #upsert Write-DosMessage -Level "Debug" -Message "Changing $configSection.$configSetting from '$($currentValue)' to '$($configValue)'" $resource = "$($tableStorageInputs.tableName)(PartitionKey='$configSection',RowKey='$configSetting')" $putUri = "https://$($tableStorageInputs.storageAccountName).table.core.windows.net/$resource$($tableStorageInputs.storageSas)" try { $oldProgressPreference = $progresspreference $global:progressPreference = 'silentlyContinue' $progresspreference = 'SilentlyContinue' $responseObject = Invoke-WebRequest -Method PUT -Uri $putUri -Headers $headers -Body $body -ContentType 'application/json' -UseBasicParsing $progresspreference = $oldProgressPreference $global:progressPreference = $oldProgressPreference } catch { Write-DosMessage -Level "Fatal" -Message "Failed to set $configSection.$configSetting=$configValue on $($configStore.Type) configStore. Exception: $($_.Exception)" } } if ($responseObject.StatusCode -eq 204) { Write-DosMessage -Level "Debug" -Message "Successfully set $configSection.$configSetting in $($configStore.Type) configstore." } return $responseObject } <# .SYNOPSIS Private function for the Set-DosConfigValue function, but just for the Xml type. .DESCRIPTION Private function for the Set-DosConfigValue function, but just for the Xml type. #> function Set-DosConfigValueXml { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Justification="WhatIf support not implemented.")] [CmdletBinding()] param ( $configStore, $configSection, $configSetting, $configValue, $KeepExisting ) Write-DosMessage -Level "Debug" -Message "Attempting to add $configSection.$configSetting=$configValue with KeepExisting=($KeepExisting) into $($configStore.Path)." $somethingChanged = $false $installConfigXml = [xml](Get-Content "$($configStore.Path)") $sectionSettings = $installConfigXml.installation.settings.scope | Where-Object {$_.name -eq $configSection} if (!$sectionSettings) { #if the scope doesn't exist, create it Write-DosMessage -Level "Debug" -Message "Scope ""$configSection"" doesn't exist, creating it." $sectionSettings = $installConfigXml.CreateElement("scope") $nameAttribute = $installConfigXml.CreateAttribute("name") $nameAttribute.Value = $configSection $sectionSettings.Attributes.Append($nameAttribute) | Out-Null $installConfigXml.installation.SelectSingleNode("settings").AppendChild($sectionSettings) | Out-Null $somethingChanged = $true } $existingSetting = $sectionSettings.variable | Where-Object {$_.name -eq $configSetting} if(!$existingSetting){ #if the existing variable in the scope doesn't exist, create it. $setting = $installConfigXml.CreateElement("variable") $nameAttribute = $installConfigXml.CreateAttribute("name") $nameAttribute.Value = $configSetting $setting.Attributes.Append($nameAttribute) | Out-Null #now that the existing variable is created, set the value. $valueAttribute = $installConfigXml.CreateAttribute("value") $valueAttribute.Value = $configValue $setting.Attributes.Append($valueAttribute) | Out-Null Write-DosMessage -Level "Debug" -Message "Adding setting ""$configSetting"" with value ""$configValue"" to the ""$configSection"" scope" $sectionSettings.AppendChild($setting) | Out-Null $somethingChanged = $true } elseif([string]::IsNullOrEmpty($existingSetting.value)){ #the current value is null or empty, so we are going to overwrite it, regardless if it says keepexisting or not. Write-DosMessage -Level "Debug" -Message "No existing value found for setting ""$configSetting"" in scope ""$configSection"", populating with ""$configValue""" $existingSetting.value = $configValue $somethingChanged = $true } elseif (-not([string]::IsNullOrEmpty($existingSetting.value))) { #There is an existing setting #That existing setting has a value. if($KeepExisting){ #Don't change the value and let the user know. Write-DosMessage -Level "Debug" -Message "Existing value ""$($existingSetting.value)"" found for setting ""$configSetting"" in scope ""$configSection"" but KeepExisting was passed in, leaving value as-is." } else { #Do change the value and let the user know. Write-DosMessage -Level "Debug" -Message "Existing value ""$($existingSetting.value)"" found for setting ""$configSetting"" in scope ""$configSection"", replacing with ""$configValue""" $existingSetting.value = $configValue $somethingChanged = $true } } else { Write-DosMessage -Level "Fatal" -Message "You've reached an else block that you shouldn't have been able to reach. The cake is a lie." } if (!$somethingChanged) { $installConfigXml = $null } return $installConfigXml } <# .SYNOPSIS Alters IIS authentication type based off parameter values provided by user. .DESCRIPTION Pull current web.config values, unlocking the configurations, and altering based of provided values. .PARAMETER AuthenticationType Array of strings allowing for either 'Windows', 'Anonymous' or both. .PARAMETER SiteName Name of IIS website being used. .PARAMETER ApplicationName Name of IIS application being altered. .EXAMPLE Set-IISAuthentication -AuthenticationType 'Windows' -SiteName 'Default Web Site' -ApplicationName 'HCApp' #> function Set-IISAuthentication { [CmdletBinding()] param( [Parameter(Mandatory=$true)] [ValidateSet("Windows", "Anonymous")] [string[]] $AuthenticationType, [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [string] $SiteName, [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [string] $ApplicationName ) Add-Assembly -Assemblies "$env:systemroot\system32\inetsrv\Microsoft.Web.Administration.dll" $manager = New-Object Microsoft.Web.Administration.ServerManager if ($AuthenticationType.Contains("Anonymous")){ Edit-AuthenticationType -AuthenticationType "Anonymous" -SiteName $SiteName -AppName $ApplicationName -ApplicationHostManager $manager -Enable } else { Edit-AuthenticationType -AuthenticationType "Anonymous" -SiteName $SiteName -AppName $ApplicationName -ApplicationHostManager $manager } if ($AuthenticationType.Contains("Windows")){ Edit-AuthenticationType -AuthenticationType "Windows" -SiteName $SiteName -AppName $ApplicationName -ApplicationHostManager $manager -Enable } else { Edit-AuthenticationType -AuthenticationType "Windows" -SiteName $SiteName -AppName $ApplicationName -ApplicationHostManager $manager } $manager.CommitChanges() } <# .SYNOPSIS Used to alter authentication type of an IIS web site/application .DESCRIPTION .PARAMETER AuthenticationType Authentication Mode that will be altered .PARAMETER SiteName Name of the IIS Website .PARAMETER AppName Name of the IIS application .PARAMETER Enable Switch toggling whether the specified Authentication will be enabled or disabled .EXAMPLE Alter-AuthenticationType -AuthenticationType "Windows" -SiteName "TestSite" -AppName "TestApp" -ApplicationConfiguration $config -Enable #> function Edit-AuthenticationType { param( [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [ValidateSet("Windows", "Anonymous")] [string] $AuthenticationType, [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [string] $SiteName, [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [string] $AppName, [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [Microsoft.Web.Administration.ServerManager] $ApplicationHostManager, [switch] $Enable ) if ($AuthenticationType -eq "Windows") { $authenticationString = "windowsAuthentication" } else { $authenticationString = "anonymousAuthentication" } $config = $ApplicationHostManager.GetApplicationHostConfiguration() $section = $config.GetSection("system.webServer/security/authentication/$authenticationString") $section.OverrideMode = "Allow" $ApplicationHostManager.CommitChanges() # When Invoke-Pester is called on the Publish-DosWebApplication Integration tests there seems to be inconsistent behavior when ran in and out of a debug session. Start-Sleep -s 3 Write-DosMessage -Level "Information" -Message "Unlocked system.webServer/security/authentication/$authenticationString for configuration" Set-WebConfigurationProperty -Filter "/system.webServer/security/authentication/$authenticationString" -Name Enabled -Value $Enable.IsPresent -PSPath "IIS:\Sites\$SiteName\$AppName" if ($Enable.IsPresent){ Write-DosMessage -Level "Information" -Message "Enabled $AuthenticationType Authentication on $SiteName/$AppName" } else { Write-DosMessage -Level "Information" -Message "Disabled $AuthenticationType Authentication on $SiteName/$AppName" } } function Start-CommandAndReturnOutput { [CmdletBinding()] param( [ValidateNotNullOrEmpty()] [string] $Command ) Write-DosMessage -Level "Verbose" -Message "Running: $Command" $output = Invoke-Expression "$Command" return $output } <# .SYNOPSIS NON-PUBLIC Validates if the assembly is loaded into the current PSSession .DESCRIPTION Checkes if the specified DLL is already loaded. .PARAMETER AssemblyFile DLL file to check if it is loaded .PARAMETER ExactVersion Specifies if we require an exact version match. If not, we will return true if a higher level version is loaded than the specified version .EXAMPLE Test-AssemblyLoaded -assemblyFile "C:\Sql\Microsoft.Sql.Smo.Dll" .OUTPUTS True if the assembly or higher version is loaded (if exactVersion = $false). Throws an exception if a down level version is found to be loaded (this can cause issues) #> function Test-AssemblyLoaded{ [CmdletBinding()] param( [string] $AssemblyFile, [bool] $ExactVersion = $false ) if(!(Test-Path $AssemblyFile)){ Write-DosMessage -Level "Error" -Message "Can't find $AssemblyFile" } [System.Reflection.AssemblyName] $targetName = [System.Reflection.AssemblyName]::GetAssemblyName($AssemblyFile) Write-DosMessage -Level "Verbose" -Message "Assembly info $targetName for $AssemblyFile" [Array] $loadedAssemblies = [AppDomain]::CurrentDomain.GetAssemblies() foreach($loadedAssembly in $loadedAssemblies){ [System.Reflection.AssemblyName] $loadedName = $loadedAssembly.GetName() #Have to do deep comparison - Equals and -eq just compare references if(($loadedName.Name -eq $targetName.Name) -and ($loadedName.Version -eq $targetName.Version)){ Write-DosMessage -Level "Verbose" -Message "Exact assembly $($loadedName.Name) already loaded" return $true } if(!$ExactVersion){ if($loadedName.Name -eq $targetName.Name){ if($loadedName.Version -ge $targetName.Version){ Write-DosMessage -Level "Verbose" -Message "Found assembly $($loadedName.Name) with version $($loadedName.Version) which is greater or equal to targeted version $($targetName.Version)" return $true } else{ Write-DosMessage -Level "Error" -Message "Older assembly version loaded than $($targetName.Name) with specified version $($targetName.Version)" } } } } return $false } <# .SYNOPSIS Tests if the powershell session is running with elevated permissions. .DESCRIPTION Tests if the powershell session is running with elevated permissions. #> function Test-ElevatedPermission { $elevated = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) if (-not $elevated ) { Write-DosMessage -Level "Fatal" -Message "This procedure requires elevated permissions." } } <# .SYNOPSIS Checks for a specific version of an installed application. .DESCRIPTION Checks the windows registry for an installed application has the exact version specified. .PARAMETER appName Application name pattern. The function uses "like" to match against the DisplayName in the registry. .PARAMETER supportedVersion The specific version to check. This is a string paramreter and should match the registered version exactly. .EXAMPLE Test-PrerequisiteExact -appName "Dot Net Runtime" -supportedVersion "1.2.3.4" .OUTPUTS boolean. Ture if and only if the the application us installed at the exact version. A newer dersion will return false. #> function Test-PrerequisiteExact { [CmdletBinding()] param ( [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] $appName, $supportedVersion ) $installedAppResults = Get-InstalledApps | Where-Object {$_.DisplayName -like $appName} if($null -eq $installedAppResults){ return $false; } if($null -eq $supportedVersion) { return $true; } $supportedVersionAsSystemVersion = [System.Version]$supportedVersion Foreach($version in $installedAppResults) { $installedVersion = [System.Version]$version.DisplayVersion if($installedVersion -eq $supportedVersionAsSystemVersion) { return $true; } } return $false; } <# .SYNOPSIS Waits for the application pool to enter the specified state .DESCRIPTION Waits for the application pool to enter the specified state. Needed to allow files to be overwritten during upgrade .PARAMETER AppPoolName App pool to wait for state change on .PARAMETER AppPoolState State to wait for the app pool to enter. Current allowed values of "Started" and "Stopped" .PARAMETER TimeOut Amount of time to wait before throwing an error. Default is 2 minutes (240 seconds) .EXAMPLE Wait-AppPoolStateChange -AppPoolName "Identity" -AppPoolState "Started" #> function Wait-AppPoolState{ [CmdletBinding()] param( [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [ValidateLength(1,64)] [ValidateScript({ if ($_ -match '[^a-zA-Z0-9]') { Write-DosMessage -Level "Fatal" -Message "$_ must only contain alphanumeric values. Please remove special characters." } else { $true } })] [string] $AppPoolName, [Parameter(Mandatory=$true)] [ValidateSet("Started", "Stopped")] [string] $AppPoolState, [int] $TimeOut = 240 ) Test-ElevatedPermission Import-Module WebAdministration $currentState = Get-WebAppPoolState -Name $AppPoolName Write-DosMessage -Level "Information" -Message "Waiting for app pool '$AppPoolName' to enter the '$AppPoolState' state" $loopTimes = 0 while($currentState.Value -ne $AppPoolState) { Write-DosMessage -Level "Debug" -Message "Waiting 1 second" Start-Sleep 1 $currentState = Get-WebAppPoolState -Name $AppPoolName if($loopTimes -ge $TimeOut) { Write-DosMessage -Level "Error" -Message "Timed out waiting for $AppPoolName to enter state $AppPoolState - timeout $TimeOut seconds" return } $loopTimes++ } Write-DosMessage -Level "Information" -Message "App Pool $AppPoolName successfully entered state $AppPoolState" } <# .SYNOPSIS Adds the target assembly - essentially loads the assembly .DESCRIPTION Attempts to load the specified DLL files (assemblies). Will attempt to resolve from a couple of common paths during an attempt to load .PARAMETER Assemblies DLL files to attempt to load .OUTPUTS $true if the specified assemblies are already loaded, $false if the assemblies aren't loaded due to not being found or other issues. .EXAMPLE Add-Assembly -Assemblies "Microsoft.Sql.Smo.Dll" #> function Add-Assembly { [CmdletBinding()] param( [Object[]] $Assemblies, [string] $AssemblyFilter = "*.dll" ) if(($null -eq $Assemblies) -or ([String]::IsNullOrEmpty($Assemblies[0]))){ Write-DosMessage -Level "Error" -Message "Must specify one or more assemblies to load" return $false } foreach($assemblyFilePath in $Assemblies){ Write-DosMessage -Level "Verbose" -Message "Attempting to load assembly $assemblyFilePath" if([String]::IsNullOrEmpty($assemblyFilePath)){ Write-DosMessage -Level "Error" -Message "Specified assembly file path is null or empty" return $false } [string] $targetAssemblyFilePath = $assemblyFilePath if(!(Test-Path -Path $targetAssemblyFilePath)){ Write-DosMessage -Level "Verbose" -Message "Unable to resolve $assemblyFilePath, searching common folder" $targetAssemblyFilePath = Resolve-CommonFilePath -AssemblyFileName $targetAssemblyFilePath } if([String]::IsNullOrEmpty($targetAssemblyFilePath)){ Write-DosMessage -Level "Error" -Message "Unable to find specified assembly $assemblyFilePath" return $false } Write-DosMessage -Level "Verbose" -Message "Found $targetAssemblyFilePath for specified file $assemblyFilePath" if(!(Test-AssemblyLoaded -assemblyFile $targetAssemblyFilePath)){ try{ Write-DosMessage -Level "Verbose" -Message "Attempting to load $targetAssemblyFilePath" Add-Type -Path $targetAssemblyFilePath } catch{ Write-DosMessage -Level "Error" -Message "Unable to load. Exception: $($_.Exception)" } } } Write-DosTelemetry -Message "Add-Assembly called." } <# .SYNOPSIS Adds or updates a registration with the Discovery Service .DESCRIPTION Uses the Discovery Service /Services endpoint to POST a new service or update an existing registration. .PARAMETER DiscoveryPostBody An hashtable that is used in the body of the request to create or update a registration. The following are required members on said object: ServiceName, Version, ServiceUrl, DiscoveryType, IsHidden DiscoveryPostBody has the following types: Required: - ServiceName (string) - Version (integer) - ServiceUrl (string) Optional: - DiscoveryServiceId (integer) only required when updating a service - Heartbeat (string) - DiscoveryType (string, usually "Service" or "Application") - IsHidden (boolean) - Icon (string) - FriendlyName (string) - Description (string) - BuildNumber (string) - ShallowHealthCheckUrl (string) - DeepHealthCheckUrl (string) Since this function just posts to the DiscoveryService Services endpoint, please see the Discovery Service documentation for additional information .PARAMETER ConfigStore An object that describes the location of the installation configuration file. Used to get the url for the discovery service from the common scope. Accepts a powershell hashtable or dictionary with key value pairs following the example, $configStore = @{Type = "File"; Format = "XML"; Path = "C:\Path\To\File"}. .EXAMPLE Add-DosDiscoveryRegistration -DiscoveryPostBody $postBody -ConfigStore $configHashTable #> function Add-DosDiscoveryRegistration { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [hashtable]$DiscoveryPostBody, [Parameter(Mandatory = $true)] [hashtable]$ConfigStore ) begin { function Add-IsHiddenAndIconDefault { param( [Parameter(Mandatory=$true)] [hashtable]$PostBody ) if ($null -eq $PostBody.IsHidden) { $PostBody.IsHidden = $true } if ($null -eq $PostBody.Icon) { $PostBody.Icon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGwAAABsCAYAAACPZlfNAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAZdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAuMTZEaa/1AAARf0lEQVR4Xu2deYyURRrGXV3dqHHVGHc12XjsJhv/UGM8spt4ZDUeCN73bVSIGo8ooKgIion3hSDHeCHXDMoxHiOKcigiCoo3qMBwDAJeIIjAOAxT+/xqqoZv+pqe7q+756gnedPd31Ff1fvU8b5vVX29XUBAQEB6vPzyyztNnDjxH+PHjz9U34/R58kvvfRSN4TvHCsvLz+Ua7jW3RZQaEyZMmXXcePGHSUiekgGS6ZJlku2igiTjejaen0uc/cO0vfuIvXIUaNG7eoeE5ArysrKdnQE9ZNiZ+lzs1c8MmHCBPPKK6+Yd955x8yaNcvMnTvXfPbZZ+bLL780X3/9tRW+c4xz77//vr2We7g3mpbS3qTPmZK7JUfMmDHjzy4bAZkgZe1QUVFxrBRIC1qNMhG1AvPWW2+Zjz/+2FRXV5s1a9aYP/74w+QK7iUN0iJN0uYZ/nl69irJIFWYo++5557tXfYCPDS+/E0K6y0lLfJKoyV8+OGHZtmyZXmRky1qa2vN0qVL7TMrKyuj5C3UZy997u2y23khJRwoGSqhS7Jd1QcffGBWrlxp6uvrnSqLD579/fff267WtzzlcaNksMg8wGW/82DMmDH7q/BlUkQdynj99dfN/PnzzebNm53K2g42bdpk8/baa6/5VlcrGSbZzxWn40Ld3G4iaoAKaw2IN954w44jW7dudeppuyCP5JU8k3d6BUn/DmthagDvqgIup7CvvvqqWbx4cbsgKhHkedGiRbYMjjhciy6umO0fkyZN2kvjwBgVroEx6vPPPzd1dXWu+O0XGEK4DJRJhDVIRlZVVe3pit0+IaL+J6JWUBPxgX799VdX3I4DykTZKKOkRgQe54rffqBybK8a11dSTw3EiW2P3V+2oGw46ViUKvMWSZ924785w6KSGodl9dNPP7lidXxQVm9NSgfj27xBIgd4X2V2HhmeNm1amzTTCw3cgKlTp/ouci5BAaeetgV1ff9UBqvJKNGCUjq+pQZlnz17tidtsVrbgU5NbQMi6yBlbCUZxApsaGhwWe+8QAdYkY60moqKin87dZUWrmVZsjAuAprjq6++aiJNutrfqa00YMxSc7fdYCArPSKkLZbs49RXXGAN6uHWwKAbDMgM3z2qgs8pKyvbxamxONDz8bOs6Y6BEdAyGNO8ISLdTSiqn6YH9uXBmO6d2RpsLdAVOnOk9XHqLCwIN+lh9TiIndHPyhf4aS5wvEW6PNaptTAgkKsHrSDc1JkiGHHjxx9/9GGspdLn7k698UMPGEtzDhZh/iD2iC4lo5164wXzWUq8gch0cIzzBwHjt99+m1bG1Ey882kuoLucrrAjTpGUCqzccl1jtWRnp+78ocSY1g/+VgEQ8c/uderODyyYUYKbsWw6wkxxWwMz1+hWhG0gcuTUnjuUEKub7BqMgMKANSKulQ13as8NSuBAJVTHSqGOPGNcauBQs9RP+q6rqKjIfd2jEhgK8yzvCigs0LFrZYOd+lsHZkp18yaYD62r8NiyZYuPgKyvrKzcw9GQPURWbxhn1WtAcYCuXSvr7WjIDrppB920CL8rxAuLB+KM+GXS/4JWRfPZ8gPTbEwIKC7YgKHG0lBeXv4fR0fL0A2DIYxdJAHFxYoVK3y3OMjRkRnsPtTFq9mfFea6ig+MDxl8ELZCP//kaEkP9aFHwnCYSS4d0D0cjBs37nBHS3qIWfYU252PAaUBO0HhQHK3oyU9dNEsLJVibFMNSA1vLarxzHC0pAavVtBFm9mkHVA6MN84efJkWthaSfp3i/CaBZoiO+vzxYYNG8ybb75pli9f7o40AkNGFcNs3LjRHWk85qcZxowZY2bOnGlrWSZMnz7d7kMG/lnffPON/R0F6aQ7995775k5c+a4X82B0r799lszadIkM3r0aKvAH374wZ1tBGmSdir55Zdf3FW5AQ7QhxrQIY6eZOhkDy6KI3ZIP3ziiSeaa665xlo+HuzW79atW5Oy165da2644QZz8cUXmyeeeMI8/fTT5rrrrjPnn39+xqUI5513niUeUCl41rXXXmt/R0FvcdJJJ5mhQ4e6I43g+eTjjDPOML///rs72gjyKMfVnHPOOebhhx82Q4YMMXfccYc59dRT7e4UD45fdNFF5sEHH0wSyp8PIrHFHo6eZOik9b+YCc0XZPi0004zN954oxk1apQ72pwwavGdd95pbrnllmYtithlWVmZOffcc5u1xCgSCUOZl156qZ2q8CD9m2++2VxxxRVJhD333HPm/vvvNzfddJNtRVHw7MsuuyxJD3J5zPXXX2/LACDsrrvust/jBi3UEZbeH9PJaYSj4jA4IKxr167mu+++s7UYhxBECWP1ELU/VbySkNiZZ55pu6JUSEXYsGHDzJNPPmmPgZqaGnvd448/3owwykcL/vTTT236PXr0aFqnwrmzzjrLTne0hEIShp4gTDLV0ZMMEbYchzkOQBhKBM8884xtRSglShivFILUdA76rbfemtQyPFIRRjdy9tlnN1W4F154wQwaNMgMHDiwWTos5qTVkR+6QyrGggUL7DmW71GJosMC+SPfXnx+IYyu/LHHHmsmlDcO0PLFyRJHT3OIyZ10ciurouJAlDC6NbqYqqqqZoQx4ENYuumb3r17t4ow0qGLY0MdyxlQJoqPEua7Sbq99evXW3nggQesAN/qo4TxZhzy6cW3egi7+uqr7cb0qDDhGwfcqqraTz75ZEdH0zZMbHw9nX2JVhyIEgawehjEV69e3UTYqlWrrHKwxhKBwmktkJwKqQgDKKtnz572ed4IiRLGUodTTjnFph0ViPjtt99shUrsEjnGOYQ0/blCdokALkRYvazU5LUectR436DtpuJAImHgoYceMn379jWnn356k9GBcrHAErtFajWKpAWkQjrC1q1bZ9Pv169fkzERJYwukjzwPC9UDqxZyg8g4vLLL096Nl3tlVde2VSJCk0YXChPvC7jIEfTNujEMWQYfygOpCIMq4tWhgkOYYAZAcYTaq5qk511ve+++2wt/+ijj+w1qZCOMIApTivGPwOeMH5DZqp0Ifeqq66y3+nCb7vtNnPhhReaF1980RKEVYkVSpfrl6l7YjmXKHH4st43lX/8X0fTNqiF8fZOu4Q4DkDOs88+635tA+MW4wctwQOLkK4MxWLRKYMtOp64Cr4r5VkoyYPxJ+ov4SCz1WfJkiXWEElszYD8DB8+vGmhLNcwHwgpjz76qDUkNJY0G285T1lSSabKli38cm5V5BMcTdugg904GdbNtx3AhSMseSl3IKztISNhcXeJAfkjY5eoE7EaHQH5wxsdakzJ6zt4VTgn4zLrA/JHRrM+bsc5IH94x1m8JL8qQgcJTdXHFZoCBHzxb37++Wf7G0cZ38nH7RJBBgnIesybN8/6ZdwXBUFd3AB/nNggQeZMwAcjLwsXLnRHGkEaLHrhHEIsFZM8OkvAFBCOfKqgOMf8vYmCO5EPfGhKbkJyaAqItGVxBX8B8Tki9VF/DH8G5zMRhH0IwuIvARRxySWXWEc30QllmoNou/eJcLrx3TIBUkgLhzhaAfC3jj/+eBsZwQd85JFHbHwQZ977ZEzZEM6K+o4eBI8JBBDxwGGPytixY91VuQHSRVi1oycZOhnb9Aq1kmgDUQIm+YjHAVodhScSEgUtBoI8CRBHwPj55583/fv3t8c8WksYpEAAzjQxw+h6S09YdIrHxzF9JCUbwlLFQ/MB+mKIEicZp1f4e4tYJjCVlo0TohAI890Dtfv22283gwcPtr8B1zAxGJ3oZGJz5MiRtvuDeCqAR2sJo5tlMpSKyEwykQiPVIRBAj0DkQ1QCsIYRuBC8pSjJxk62Z2LolMLuQBF0joYWwBrInr16tXUFRHSIabod3Qy/hAL9BWFOCMtwYenuDdKZmsJGzBgQFPwF8uLOKRv8Z4wWjFLFOgSiQ9yvc9vtl0iwe2o5OMiMasAFypXd0dPMmTv20Wk+QYuUQpdCl0PSsfIoMAEaQFrPOj+vIFDBB2levgJT+5F6MuZ20K5oDWEcX+XLl2s8vjOQhry5iuTJ4yul8UzzHUx5hLoZZ07yIawESNGJC3E8bPsucCZ9JTrYEdPMnjlqbqyTfkuc2P6gmg7XaEXBnym8D1YHQUpFBgF+ggLaztoARASvR+l+9XIrSGM1s3akmhaPI/uGqTqEgFWIdF7nlHsLpGWzZguLtaItMx/oaULZqqlNXUZrQU1GOUmdquY7BdccEFTuszs0u3xLKwy3/0wWwxhngwPxjwqAsiWMLpcpkgS14X4PNLi0xFGhcJQKQVhkYWk0xwt6aGC85dMSVZctqBrYKVUIlAMRECIB90gBgWWKYA0pu9TrYmgAqBkBuNUhNFiuM8L81hUElyFVHvc+vTpY5fUecIYu5h6oWtkNRWtEj8IQBgz41Sa6DPoMj1hmPHRcwjpRZf4ZQt0DwcirK+jJT104RFcnOtmCEzndAFkTHUWiXowP0VN9kvc+MR3YaxJBGQylmGgsO4fv8q3SloQ6USFMvC8d999116TCFoETjmk0236+3g+x1m+4IExFE3bC+MMrTjVOYRWnwth5Jv8Sw5ztKSHai/bjVbRh1P7AooLKgCVUWTV6GfL242ACLP+mJ/GDygeIhv6Bjo6Woaa8tHc5M3agOLBBXwbxMFRjo6WwYZo3bQQSyW6hDqgsIhYh5is2XWHHmphvWhlqZZRBxQGkSUBPR0N2UM37S3ZiNWX6BMFxA+sSWZKRNg6Wa1/dTS0DiLM7mbJN7YY0DJ87FCSPtjbEmTaH6AEagmThFZWOOA+uZeD1U7I998jRBh/0Gm9/YDCgFlwdCzChji15w4ltJ8S2oT376dDAuIDc3Ru7FoviedvPkRYf2pAWAIXP5hcRbeSll/xkC3ctEt4SXPMIEaJ3yWyFk+ePPkvTt3xQIR1kdjXoAcDJH+gQ2YDRNZWkXayU3O8EGEjab5hOXf++OKLL2xXKJ2OcOqNH1VVVXvqITU04/BXHrmDCVTXFVbL4NjNqbcw0Dh2nGrFFiIgIc7YerBAFYtbOqwjyO7UWljoYX1ozswehzmz7IGusAHQnaSXU2fh4aL543kws6N+5jcgPdARy/vQmXTHApTWRePzxZQpU3bVw+eSgeCfZQZksU/AkTU7dhM+W5SXl/9dmeCPOO0fcwakRmRj3iLJ3k59pYEywL9H1ATSkkHL8mRJ+HO3/ZzaSgv+VNqTRvcYxrRGsiJhp6XS0b+cutoGmBZQxmz3iCHSma1Hyh4xMBbps220rEQoY/sog3PIKGvXO6Ofhp+Fu+PIml3yMasllJWV7aJMTiDDOIgsye4soKyU2ZE1rmTWYGvh/DSca/761g68HTlgTNmIDVJWlbtO5e6lw8X1s+KACnCsCoB1ZCPTcWwUbGugTC7qjlQXLdxUKKgQu0tGi7gGaiBWZBxbcksNyoAV6IK4W1W+EQUP5BYTKhDzadXURPp51oi0R0uSPLMGw03rI4tFWmHms0oNEbaz5F7JBgrLSiGW0OWyw6PYII8sRSPPjijWYNzdbgyLfDBx4sR9RdpwCQO0bXGsMG6LbgB5YkWub1HKc61kiL7Hs2CmPYE/6FThWaxKbbXjARsw2MFRylbH6jDyQF7cGIWskzyV97rBjoDKyso9RBx/4bhAnw0oiD1SbCpkNyK1vJDhLtLmGTyLKI3bn0Vr4m/n50t65rx8uiMD/41/ppOCBklWoDSEWs4uS95uwJjHDs1c92AD7mXrLWMSuyhZ2RxpSUiNnj+QLT+6vP35U6UAipLCDpfy2HM9XcIfx0SVarfQ4gOxpwrF4zLgpDPmIHznGOe4hmu5JzEdkbNGMk3SV78PCyTFACmSl5cdIukhoQWi4CUSjADeeGa70jRC11bvrq2WTNWxp1QhuksO1vfMr1YIiA+8hF/jy74yBg5St0Z3eoIEnw85gWOcEyn7pH0rWkBAQICw3Xb/B+xpzGHxSTPuAAAAAElFTkSuQmCC" } } function Confirm-RequiredMembersArePresent { param( [Parameter(Mandatory=$true)] [hashtable]$PostBody ) $requiredMembers = @( 'ServiceName' 'Version' 'ServiceUrl' ) $missingRequiredMembers = $requiredMembers | Where-Object { !$PostBody.ContainsKey($_) } if ($missingRequiredMembers.Length -gt 0) { Throw "These parameters are required but missing from the discovery registration payload: $($missingRequiredMembers -join ", ")" } } function Get-DiscoveryServiceUrl { param( [Parameter(Mandatory=$true)] [hashtable]$ConfigStore ) $commonConfig = Get-DosConfigValues -ConfigStore $ConfigStore -Scope "common" $discoveryService = $commonConfig.discoveryService if ($null -eq $discoveryService) { Throw "The discoveryService variable was not found in the common scope of the configuration file" } return $discoveryService } function Add-CurrentUserToDiscoveryServiceUserRole { param( [Parameter(Mandatory=$true)] [hashtable]$ConfigStore ) $commonConfig = Get-DosConfigValues -ConfigStore $configStore -Scope 'common' Write-DosMessage -Level 'Information' -Message "Granting DiscoveryServiceUser role to the current user, $(whoami), in the '$($commonConfig.metadataDbName)' database, on '$($commonConfig.metadataSqlServerInstanceAddress)'" $MetadataConnectionString = "Server=$($commonConfig.metadataSqlServerInstanceAddress);Database=$($commonConfig.metadataDbName);Trusted_Connection=True;MultipleActiveResultSets=True;Connect Timeout=10" $sqlQuery = ' DECLARE @RoleID INT; IF NOT EXISTS(SELECT * FROM [CatalystAdmin].[IdentityBASE] WHERE [IdentityNM] = SYSTEM_USER) INSERT INTO [CatalystAdmin].[IdentityBASE] ([IdentityNM]) VALUES (SYSTEM_USER) DECLARE @IdentityID INT = (SELECT [IdentityID] FROM [CatalystAdmin].[IdentityBASE] WHERE [IdentityNM] = SYSTEM_USER) SET @RoleID = (SELECT [RoleID] FROM [CatalystAdmin].[RoleBASE] WHERE [RoleNM] = ''DiscoveryServiceUser'') IF NOT EXISTS(SELECT * FROM [CatalystAdmin].[IdentityRoleBASE] WHERE [RoleID] = @RoleID AND [IdentityID] = @IdentityID) INSERT INTO [CatalystAdmin].[IdentityRoleBASE] ([IdentityID], [RoleID]) VALUES(@IdentityID, @RoleID)' Invoke-DosSqlQuery -ConnectionString $MetadataConnectionString -Query $sqlQuery -NonQuery } } process { $postBody = $DiscoveryPostBody.PSObject.Copy() Add-IsHiddenAndIconDefault -PostBody $postBody Confirm-RequiredMembersArePresent -PostBody $postBody Add-CurrentUserToDiscoveryServiceUserRole -configstore $ConfigStore $discoveryServiceUrl = Get-DiscoveryServiceUrl -ConfigStore $ConfigStore $postHeaders = @{ "Accept" = "application/json" "Content-Type" = "application/json" } $postBodyJson = ConvertTo-Json $postBody Write-DosMessage -Level "Information" -Message "Registering $($postBody.ServiceName) v$($postBody.Version) with Discovery Service" try { Invoke-RestMethod -Method Post -Uri "$discoveryServiceUrl/Services" -Body $postBodyJson -Headers $postHeaders -UseDefaultCredentials | Out-Null Write-DosMessage -Level "Information" -Message "Registration Successful" $postBody.Remove("Icon") Write-DosMessage -Level "Information" -Message "Registration summary: $(($postBody | ConvertTo-Json -Compress).ToString())" } catch { if($_.Exception.Message -like '*403*'){ Write-DosMessage -Level "Warning" -Message "Please make sure the current user, $(whoami), has the right DiscoveryService permissions" } Write-DosMessage -Level "Fatal" -Message "Registration failed: $($_.Exception.Message)" } } } <# .SYNOPSIS Adds a SQL login to a database role .DESCRIPTION Adds a SQL user to a database role .PARAMETER InstanceName Full instance name of SQL server .PARAMETER ConnectionString Connection string to a SQL instnace .PARAMETER SqlConnection SQL connection to target SQL Server .PARAMETER DatabaseName Database in which the role exists .PARAMETER UserName The full name of the user to add to the role .PARAMETER RoleName The full name of the database role .PARAMETER Force Will force the addition of the user to the database role even if it is not needed. .INPUTS None .OUTPUTS None .EXAMPLE PS> Add-DosSqlDatabaseRoleMembership ... #> function Add-DosSqlDatabaseRoleMembership { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Passing through the -WhatIf and -Confirm to the Invoke-DosSqlQuery - this is a supported scenario")] [cmdletbinding(SupportsShouldProcess=$true)] [OutputType()] param( [parameter(Mandatory=$true,ParameterSetName='Connection')][Data.SqlClient.SqlConnection]$SqlConnection, [parameter(Mandatory=$true,ParameterSetName='ConnectionString')][string]$ConnectionString, [parameter(Mandatory=$true,ParameterSetName='AdHoc')][string]$InstanceName, [parameter(Mandatory=$true,ParameterSetName='AdHoc')][string]$DatabaseName, [parameter(Mandatory=$false,ParameterSetName='AdHoc')][PSCredential]$Credential, [parameter(Mandatory=$true)][string]$UserName, [parameter(Mandatory=$true)][string[]]$RoleName, [switch] $force ) #remove this from the parameters $PSBoundParameters.Remove('ErrorAction') | Out-Null if(!$force){ #query for all roles first: $roleQuery = "SELECT DP1.name AS DatabaseRoleName, isnull (DP2.name, 'No members') AS DatabaseUserName FROM sys.database_role_members AS DRM RIGHT OUTER JOIN sys.database_principals AS DP1 ON DRM.role_principal_id = DP1.principal_id LEFT OUTER JOIN sys.database_principals AS DP2 ON DRM.member_principal_id = DP2.principal_id WHERE DP1.type = 'R' ORDER BY DP1.name; " $queryResult = Invoke-DosSqlQuery @PSBoundParameters -Query $roleQuery } foreach($role in $RoleName){ $userExistsInRole = $queryResult | where-object -property 'DatabaseRoleName' -eq $role | where-object -property 'DatabaseUserName' -eq $UserName if(!$userExistsInRole -or $force.IsPresent){ $query += "ALTER ROLE [$role] ADD MEMBER [$UserName]" } else { Write-DosMessage -Level 'Information' -Message "User '$UserName' already has role '$role' in database." } } if($query){ #if ($pscmdlet.ShouldProcess($Query, "Executing SQL Query")){ Write-DosMessage -Level 'Information' -Message "Executing ALTER ROLE Query: $query" Invoke-DosSqlQuery @PSBoundParameters -Query $query -NonQuery | Out-Null #} } } function Add-IISUrlRewriteRule { <# .SYNOPSIS Add URL rewrite rule .DESCRIPTION Adds a given URL rewrite rule to IIS with a Redirect action. The IIS URL Rewrite extension must be installed in order to use this function. .PARAMETER IISWebSite The IIS site from which to remove the application. Defaults to "Default Web Site" .PARAMETER RuleName A unique name for the rule. .PARAMETER MatchUrl A regular expression that specifies the URL match pattern. .PARAMETER RedirectUrl The URL in which to redirect on a match. .PARAMETER UseRewrite Use a Rewrite action instead of a Redirect. Defaults to $false. .PARAMETER StopProcessing Enable the StopProcessing flag. When the rule action is performed (i.e. the rule matched) and the StopProcessing flag is turned on, it means that no more subsequent rules will be processed and the request will be passed to the IIS request pipeline. Defaults to $true. .EXAMPLE Add-IISUrlRewriteRule -RuleName "Atlas4-Atlas-Redirect" -MatchUrl "^Atlas4(.*)" -RedirectUrl "/Atlas" -UseRewrite $false -StopProcessing $true -IISWebSite "Default Web Site" .NOTES Rules are always appended to the list of existing rules, which are evaluated in order by IIS. Rules may be viewed from within the "URL Rewrite" feature within IIS Manager. Refer to the "URL Rewrite Module Configuration Reference" at https://docs.microsoft.com/en-us/iis/extensions/url-rewrite-module/url-rewrite-module-configuration-reference for further details. #> Param ( [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [string] $RuleName, [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [string] $MatchUrl, [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [string] $RedirectUrl, [bool] $UseRewrite = $false, [bool] $StopProcessing = $true, [string] $IISWebSite = "Default Web Site" ) Import-Module WebAdministration -Force $iisPath = "IIS:\Sites\$($IISWebSite)" $ruleFilter = "/system.webserver/rewrite/rules/rule[@name='$RuleName']" $rulesXPath = "/system.webserver/rewrite/rules" $exists = Get-WebConfigurationProperty -PSPath $iisPath -Filter $ruleFilter -Name * if ($exists) { Write-DosMessage -Level "Information" -Message "Removing existing URL Rewrite rule '$ruleFilter'." Clear-WebConfiguration -PSPath $iisPath -Filter $ruleFilter } $actionType = "Redirect" if ($UseRewrite) { $actionType = "Rewrite" } Write-DosMessage -Level "Information" -Message "Adding URL Rewrite rule '$RuleName'." Add-WebConfigurationProperty -PSPath $iisPath -Filter $rulesXPath -Name "." -value @{ name = $RuleName; patternSyntax = "ECMAScript"; stopProcessing = $StopProcessing } Set-WebConfigurationProperty -PSPath $iisPath -Filter "$ruleFilter" -Name "match" -value @{ url = $MatchUrl } Set-WebConfigurationProperty -PSPath $iisPath -Filter "$ruleFilter" -Name "action" -value @{ type = $actionType; url = $RedirectUrl; appendQueryString = "false"; } } <# .SYNOPSIS Asserts if an application install meets dependency version requirements. .DESCRIPTION Returns true if all dependencies for an application meet SemVer requirements. Returns false otherwise. .PARAMETER DiscoveryServiceUrl The parameter DiscoveryServiceUrl is used to define the Discovery Service URL. .PARAMETER DependencyManifestPath The parameter DependencyManifestPath is used to define the location of the dependency manifest file for the service/app being installed. dependency.manifest example { "$schema": "./InstallReadinessTool.schema.json", "manifestName": "Analytics Manifest", "operationMode": "validate", "readinessChecks": [ { "name": "Authorization Service Dependency", "checkType": "dependentService", "serviceName": "AuthorizationService", "serviceVersion": "1", "serviceBuildVersion": "^1.7" }, { "name": "Identity Service Dependency", "checkType": "dependentService", "serviceName": "IdentityService", "serviceVersion": "1", "serviceBuildVersion": "^1.7" } ] } .PARAMETER ServiceName The parameter ServiceName is used to define the service name to lookup in Discovery Service. .EXAMPLE $vars = @{ DiscoveryServiceUrl = "https://test/DiscoveryService/v1" DependencyManifestPath = "." ServiceName = "TestService" } Assert-DosValidDependencySet @vars .EXAMPLE Assert-DosValidDependencySet -DiscoveryServiceUrl "https://test/DiscoveryService/v1" -DependencyManifestPath "." -ServiceName "TestService" #> function Assert-DosValidDependencySet { [CmdletBinding()] param ( [Parameter(Mandatory=$true)] [string]$DiscoveryServiceUrl, [Parameter(Mandatory=$true)] [ValidateScript({Test-Path $_})] [string]$DependencyManifestPath, [Parameter(Mandatory=$true)] [string]$ServiceName ) $currentDependencies = Get-CurrentDependencyList -Path $DependencyManifestPath $discoveryServiceEntries = Get-ServiceList -CurrentDependencies $currentDependencies.readinessChecks -DiscoveryServiceUrl $DiscoveryServiceUrl if (!(Assert-DependencySemVerRequirementsMet -ServiceName $ServiceName -DiscoveryServiceEntries $discoveryServiceEntries -CurrentDependencies $currentDependencies.readinessChecks)) { return $false } return $true } function Get-CurrentDependencyList { [CmdletBinding()] param ( [string]$Path ) return Get-Content -Path $Path | ConvertFrom-Json } function Get-ServiceList { [CmdletBinding()] param ( [array]$CurrentDependencies, [string]$DiscoveryServiceUrl ) $discoveryResponses = @() foreach ($dependency in $CurrentDependencies) { $discoveryResponse = Get-DosService -DiscoveryServiceUrl $DiscoveryServiceUrl -ServiceName $dependency.serviceName -ServiceVersion $dependency.serviceVersion $discoveryResponses += $discoveryResponse } return $discoveryResponses } <# .SYNOPSIS Compares the scopes, variables, and values in two configstores. .DESCRIPTION Compares the scopes, variables, and values in two configstores. Note: the config stores do not need to be of the same type. .PARAMETER Configstore1 The first configstore. .PARAMETER Configstore2 The second config store .PARAMETER skipSensitiveVariables A switch parameter that will tell the Compare-DosConfigStore function to ignore sensitive variables when doing the compare. .EXAMPLE $uri = 'https://mystoragetable.table.core.windows.net/Common?sp=raud&st=2020-10-04T21:34:00Z&se=2020-10-21T21:33:00Z&sv=2019-12-12&sig=cDMUXyvpozHJYkTfdQBhKiQ3PL05jV8gLvCXqc6GOgI%3D&tn=Common' $econfigstore = @{Type = "External"; Format = "AzureTable"; Uri = "$uri"} $configStore = @{Type = "File"; Format = "XML"; Path = "c:\Program Files\Health Catalyst\install.config"} Compare-DosConfigStore -configstore1 $econfigstore -configstore2 $configstore #> function Compare-DosConfigStore { [CmdletBinding()] param ( [Parameter(Mandatory=$true)] [hashtable]$Configstore1, [Parameter(Mandatory=$true)] [hashtable]$Configstore2, [switch]$skipSensitiveVariables ) $equal = $true $sensitiveVariables = @('common.fabricInstallerSecret') for($i=0; $i -le 1; $i++){ if($i -eq 0){ $c1 = $Configstore1 $c2 = $Configstore2 $first = 'first' $second = 'second' }else{ $c1 = $Configstore2 $c2 = $Configstore1 $first = 'second' $second = 'first' } $scopesConfigStore1 = get-dosconfigscopes -configstore $c1 $scopesConfigStore2 = get-dosconfigscopes -configstore $c2 #for each scope foreach($scope1 in $scopesConfigStore1){ if($scopesConfigStore2 -contains $scope1){ $myScopeHashTable1 = get-dosconfigvalues -ConfigStore $c1 -Scope $scope1 $myScopeHashTable2 = get-dosconfigvalues -ConfigStore $c2 -Scope $scope1 foreach($key in $myScopeHashTable1.keys){ if($skipSensitiveVariables -and $sensitiveVariables -contains "$scope1.$key"){ #because we are skipping sensitive variables _and_ at this point in the foreach we are comparing a sensitive variable, just continue. continue } if($myScopeHashTable2.keys -contains $key){ if($myScopeHashTable1.$key -ne $myScopeHashTable2.$key){ $equal = $false Write-DosMessage -Level 'Warning' -Message "Value for variable '$key' in scope '$scope1' in the $first configstore doesn't match the value in the $second configstore ($($myScopeHashTable1.$key) != $($myScopeHashTable2.$key))." } } else { $equal = $false Write-DosMessage -Level 'Warning' -Message "Variable '$key' in scope '$scope1' is in the $first configstore, but not the $second configstore." } } } else { $equal = $false Write-DosMessage -Level 'Warning' -Message "Scope '$scope1' is in the $first configstore, but not the $second configstore." } } } return $equal } <# .SYNOPSIS Compares the version of an install to the currently installed version. .DESCRIPTION Returns 1 if application upgrade version is greater than the currently installed version or if an entry for the current install is not found. Returns 0 if application upgrade version is equal to the currently installed version. Returns -1 if application upgrade version is less than the currently installed version. .PARAMETER DiscoveryServiceUrl The parameter DiscoveryServiceUrl is used to define the Discovery Service URL. .PARAMETER ServiceName The parameter ServiceName is used to define the service name to lookup in Discovery Service. .PARAMETER ServiceVersion The parameter ServiceVersion is used to define the service version to lookup in Discovery Service. .PARAMETER InstallBuildNumber The parameter InstallBuildNumber is used to define the installer build number. .EXAMPLE $vars = @{ DiscoveryServiceUrl = "https://test/DiscoveryService/v1" ServiceName = "TestService" ServiceVersion = "1" InstallBuildNumber = "1.2.3" } Compare-DosServiceVersion @vars .EXAMPLE Compare-DosServiceVersion -DiscoveryServiceUrl "https://test/DiscoveryService/v1" -ServiceName "TestService" -ServiceVersion "1" -InstallBuildNumber "1.2.3" #> function Compare-DosServiceVersion { [CmdletBinding()] param ( [Parameter(Mandatory=$true)] [string]$DiscoveryServiceUrl, [Parameter(Mandatory=$true)] [string]$ServiceName, [Parameter(Mandatory=$true)] [string]$ServiceVersion, [Parameter(Mandatory=$true)] [string]$InstallBuildNumber ) $service = Get-DosService -DiscoveryServiceUrl $DiscoveryServiceUrl -ServiceName $ServiceName -ServiceVersion $ServiceVersion -Exists if (!$service.Exists) { # treat as though new version is greater return 1; } $currentBuildNumber = $service.BuildNumber $comparison = Compare-BuildNumber -CurrentBuildNumber $currentBuildNumber -InstallBuildNumber $InstallBuildNumber if ($comparison -lt 0) { Write-DosMessage -Level "Warning" -Message "Installer version of $($ServiceName) $($InstallBuildNumber) is not greater than current version $($currentBuildNumber)" } return $comparison } function Compare-BuildNumber { [CmdletBinding()] param ( [string]$CurrentBuildNumber, [string]$InstallBuildNumber ) Write-DosMessage -Level "Information" -Message "Comparing upgrade build number to installed package" $currentBuildNumberWithoutDecimals = $CurrentBuildNumber.Split('.') -join '' $installBuildNumberWithoutDecimals = $InstallBuildNumber.Split('.') -join '' return $installBuildNumberWithoutDecimals.CompareTo($currentBuildNumberWithoutDecimals) } <# .SYNOPSIS Validates the install.config file against the configuration.manifest file .DESCRIPTION For the scopes given, removes variables from the installation config that are not also found in the manifest. Validates that all variables in the manifest are also in the installation config. .PARAMETER InstallationConfigStore An object that describes the location of the installation configuration file. Accepts a powershell hashtable or dictionary with key value pairs following the example, $configStore = @{Type = "File"; Format = "XML"; Path = "C:\Path\To\File"}. .PARAMETER ManifestConfigStore An object that describes the location of the configuration manifest file. Accepts a powershell hashtable or dictionary with key value pairs following the example, $configStore = @{Type = "File"; Format = "XML"; Path = "C:\Path\To\File"}. .PARAMETER CleanupScopes An array of scopes for which variables should be removed from the installation config file if they are not also found in the manifest. .EXAMPLE Confirm-DosConfigStoreContent -InstallationConfigStore $installationConfigHashTable -ManifestConfigStore $manifestConfigHashTable -CleanupScopes @( "common" ) #> function Confirm-DosConfigStoreContent { [CmdletBinding()] param ( [Parameter(Mandatory=$true)] [hashtable]$InstallationConfigStore, [Parameter(Mandatory=$true)] [hashtable]$ManifestConfigStore, [array] $CleanupScopes ) begin { function ConvertFrom-ContentToObject { [CmdletBinding()] param ( [Parameter(Position = 0, Mandatory = $true)] [hashtable]$ConfigStore ) $scopes = Get-DosConfigScopes -ConfigStore $ConfigStore $variables = @() foreach ($scope in $scopes) { $values = Get-DosConfigValues -ConfigStore $ConfigStore -Scope $scope foreach ($value in $values.GetEnumerator()) { $variable = New-Object PSObject $variable | Add-Member -Type NoteProperty -Name scope_variable -Value "$($scope).$($value.Name)" $variable | Add-Member -Type NoteProperty -Name scope -Value ($scope) $variable | Add-Member -Type NoteProperty -Name variable -Value ($value.Name) $variable | Add-Member -Type NoteProperty -Name value -Value ($value.Value) $variables += $variable } } return $variables } function Write-XmlToScreen { [CmdletBinding()] param ( [xml]$xml ) $StringWriter = New-Object System.IO.StringWriter; $XmlWriter = New-Object System.Xml.XmlTextWriter $StringWriter; $XmlWriter.Formatting = "indented"; $xml.WriteTo($XmlWriter); $XmlWriter.Flush(); $StringWriter.Flush(); Write-Output $StringWriter.ToString(); } } process { $configVariables = ConvertFrom-ContentToObject -ConfigStore $InstallationConfigStore $manifestVariables = ConvertFrom-ContentToObject -ConfigStore $ManifestConfigStore # clean up outdated variables from the filter scope if ($CleanupScopes) { foreach ($cleanupScope in $CleanupScopes) { foreach ($configVariable in $configVariables | Where-Object scope -eq $cleanupScope) { if ($manifestVariables.scope_variable -notcontains $configVariable.scope_variable) { Remove-DosConfigValue -configStore $InstallationConfigStore -configSection $cleanupScope -configSetting $configVariable.variable } } } } $missingVariables = @() foreach ($manifestVariable in $manifestVariables) { if ($configVariables.scope_variable -notcontains $manifestVariable.scope_variable) { $missingVariables += $manifestVariable } } if ($missingVariables) { $missingScopes = $missingVariables | Group-Object scope $text = @" MISSING CONFIGURATIONS REPORT Manifest: $($ConfigManifestPath) Generated: $(Get-Date) After doing a comparison of the configuration manifest with the install.config store it appears that there are some missing configurations that need to be added to this computers install.config store before there can be a successful installation. Below are the missing variables with template/default values that are to be only used as a guide. Please review the values carefully to ensure they are correct and then copy and paste them to the install.config file. COPY TO LOCATION: $($ConfigStorePath) "@ [xml]$doc = New-Object System.Xml.XmlDocument $doc.AppendChild($doc.CreateComment($text)) | Out-Null $installation = $doc.CreateElement("installation") $settings = $doc.CreateElement("settings") foreach ($missingScope in $missingScopes) { $scope = $doc.CreateElement("scope") $scope.SetAttribute("name", $missingScope.Name) foreach ($missingVariable in $missingScope.Group) { $variable = $doc.CreateElement("variable") $variable.SetAttribute("name", $missingVariable.variable) $variable.SetAttribute("value", $missingVariable.value) $scope.AppendChild($variable) | Out-Null } $settings.AppendChild($scope) | Out-Null } $installation.AppendChild($settings) | Out-Null $doc.AppendChild($installation) | Out-Null Write-DosMessage -Level "Fatal" -Message "$(Write-XmlToScreen $doc)" } Write-DosMessage -Level "Information" -Message "All required configurations found in the install.config" } } function Confirm-DosConfiguration { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [Hashtable] $config, [Parameter(Mandatory = $true)] [Hashtable] $checkList ) begin { $results = @(); function Invoke-Check { [CmdletBinding()] param ( [Parameter(Position = 0, Mandatory = $true)] [string] $name, [Parameter(Position = 1)] [object[]] $checkList ) $result = @(); $splat = @{ name = $name type = $config["_type_"][$name] value = $config[$name] } switch -Wildcard ($checkList) { "isNotNull" { $result += Confirm-IsNotNull @splat } "isValidPath" { $result += Confirm-IsValidPath @splat } "isValidDir" { $result += Confirm-IsValidDir @splat } "isBoolean" { $result += Confirm-IsBoolean @splat } "isValidConnection" { $result += Confirm-IsValidConnection @splat } "isValidEndpoint" { $result += Confirm-IsValidEndpoint @splat } "isValidValue=(*)" { $list = $checkList.Split("'*',", [System.StringSplitOptions]::RemoveEmptyEntries) [string[]]$list = $list[(1..($list.Length - 2))] $splat.Add('validateSet', $list) $result += Confirm-IsValidValue @splat } } return $result } } process { foreach ($item in $checkList.Keys) { if ($config.ContainsKey($item)) { $results += Invoke-Check $item $checkList[$item] } else { Write-DosMessage -Level "Warning" -Message "Unable to find $item as a configuration. Please remove from checklist." } } } end { $errors = $results | Where-Object errorFlag -eq 1 $warnings = $results | Where-Object errorFlag -eq -1 $success = $results | Where-Object errorFlag -eq 0 $errorsCnt = ($errors | Measure-Object).Count $warningsCnt = ($warnings | Measure-Object).Count $successCnt = ($success | Measure-Object).Count if ($warnings) { $msg = ($warnings | Sort-Object type, name | Format-Table name, type, check, @{Label = "result"; Expression = {"warning"}}, message -Wrap) Write-DosMessage -Level "Warning" -Message "WARNINGS: $warningsCnt >>>`n$(($msg | Out-String).trim())" } if ($errors) { if ($errors | Where-Object type -eq "store") { $msg = ($errors | Where-Object type -eq "store" | Sort-Object type, name | Format-Table name, type, check, @{Label = "result"; Expression = {"failed"}}, message -Wrap) $errorsCnt = ($errors | Where-Object type -eq "store" | Measure-Object).Count } else { $msg = ($errors | Sort-Object type, name | Format-Table name, type, check, @{Label = "result"; Expression = {"failed"}}, message -Wrap) } Write-DosMessage -Level "Fatal" -Message "ERRORS: $errorsCnt >>>`n$(($msg | Out-String).trim())" } Write-DosMessage -Level "Information" -Message "Configuration checks summary - Success: $successCnt, Warnings: $warningsCnt, Errors: $errorsCnt" } } <# .SYNOPSIS Checks if the credential provided is valid .DESCRIPTION Checks that the provided credential (username/password pairing) is valid and returns you and object with the validity response ($returnObject.isValid) and the validated credential ($returnObject.credential). Note: If the isValid property of the return object is $false, the credential property will be invalid and should be handled accordingly. .PARAMETER Credential Credential object used to check if the username and password combination are correct .PARAMETER promptOnInvalid Switch - If provided and credential is invalid, will prompt for the correct password. .EXAMPLE $validationResponse = (Confirm-DosCredential -Credential $credential).isValid $returnedCredential = (Confirm-DosCredential -Credential $credential -PromptOnInvalid).credential #> function Confirm-DosCredential { [CmdletBinding()] param( [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [PSCredential] $Credential, [switch] $PromptOnInvalid ) $iisUser = $Credential.UserName $isValid = Assert-DosCredential($Credential) if(!$isValid){ if ($PromptOnInvalid) { Write-DosMessage -Level "Warning" -Message "Incorrect credentials for $iisUser" Write-DosMessage -Level "Verbose" -Message "PromptOnInvalid parameter provided. Please input appropriate credential information." $count = 0 do { $count++ $Credential = Read-DosCredential -UserName $Credential.UserName $isValid = Assert-DosCredential($Credential) if(!$isValid){ Write-DosMessage -Level "Warning" -Message "Credential you provide is incorrect for user: $iisUser. Please try again." } }until ($isValid -or ($count -ge 3)) if (!$isValid) { Write-DosMessage -Level "Error" -Message "Maximum number of credential validation attempts reached for user: $iisUser" Write-DosMessage -Level "Verbose" -Message "Credential is invalid for $iisUser. Returning false." } }else { Write-DosMessage -Level "Error" -Message "Incorrect credentials provided for user: $iisUser" } } $credentialValidationResult = @{ isValid = $isValid; credential = $Credential } return $credentialValidationResult } <# .SYNOPSIS Helper Function: Checks if the credential provided is valid .DESCRIPTION Uses System.DirectoryServices.AccountManagement.ContextType to check that the credential provided is valid .PARAMETER Credential Credential object used to check if the username and password combination are correct .EXAMPLE Assert-DosCredential -Credential $credential #> function Assert-DosCredential { [CmdletBinding()] param( [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [PSCredential] $Credential ) Write-DosMessage -Level "Information" -Message "Checking credential validity." $contextName = $Credential.GetNetworkCredential().Domain [System.Reflection.Assembly]::LoadWithPartialName("System.DirectoryServices.AccountManagement") | Out-Null if($contextName -eq $(hostname)){ #validate locally Write-DosMessage -Level "Debug" -Message "Local credential." $principalContext = "machine" $pc = New-Object System.DirectoryServices.AccountManagement.PrincipalContext($principalContext, $contextName) $isValid = $pc.ValidateCredentials($Credential.GetNetworkCredential().UserName, $Credential.GetNetworkCredential().Password) return $true } else { Write-DosMessage -Level "Debug" -Message "Domain credential." $principalContext = "domain" $pc = New-Object System.DirectoryServices.AccountManagement.PrincipalContext($principalContext, $contextName) $isValid = $pc.ValidateCredentials($Credential.GetNetworkCredential().UserName, $Credential.GetNetworkCredential().Password, [System.DirectoryServices.AccountManagement.ContextOptions]::Negotiate) } return $isValid } <# .SYNOPSIS Helper Function: Prompts for username and password for a credential object .DESCRIPTION Prompts for username and password and returns a credential object .PARAMETER Credential Credential object used to check if the username and password combination are correct .EXAMPLE Prompt-DosCredential -UserName 'testaccount' -Password 'password' #> function Read-DosCredential { param ( [string] $UserName, [securestring] $UserPassword ) if (!$UserName){ $UserName = Read-Host "Enter the domain\username to use for the credential" } if (!$UserPassword) { $UserPassword = Read-Host "Enter the password for $UserName" -AsSecureString } Write-DosMessage -Level "Information" -Message "Creating credential from inputs provided." $Credential = New-Object -TypeName "System.Management.Automation.PSCredential" -ArgumentList $UserName, $UserPassword return $Credential } <# .SYNOPSIS Copies the scopes, variables, and values from the $SourceConfigStore to the $DestinationConfigStore. Note: it will not create the ConfigStores. .DESCRIPTION .PARAMETER SourceConfigStore The source configstore you want to copy from. .PARAMETER DestinationConfigStore The destination configstore you want to copy from. .PARAMETER skipSensitiveVariables A switch parameter that will tell the copy-dosconfigstore function to skip the copying of sensitive variables. Note: when copying from a File based configstore to an external configstore, skipSensitiveVariables is always set to true. .EXAMPLE $uri = 'https://mystoragetable.table.core.windows.net/Common?sp=raud&st=2020-10-04T21:34:00Z&se=2020-10-21T21:33:00Z&sv=2019-12-12&sig=cDMUXyvpozHJYkTfdQBhKiQ3PL05jV8gLvCXqc6GOgI%3D&tn=Common' $econfigstore = @{Type = "External"; Format = "AzureTable"; Uri = "$uri"} $configStore = @{Type = "File"; Format = "XML"; Path = "c:\Program Files\Health Catalyst\install.config"} New-DosConfigStore -configStore $configStore Copy-DosConfigStore -SourceConfigStore $econfigstore -DestinationConfigStore $configstore #> function Copy-DosConfigStore { [CmdletBinding()] param ( [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [hashtable] $SourceConfigStore, [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [hashtable] $DestinationConfigStore, [switch]$skipSensitiveVariables, [switch]$keepExisting ) # Confirm External ConfigStore if(!(Confirm-ConfigStore -ConfigStore $SourceConfigStore)) { Write-DosMessage -Level "Fatal" -Message "SourceConfigStore is invalid" return } if(!(Confirm-ConfigStore -ConfigStore $DestinationConfigStore)) { Write-DosMessage -Level "Fatal" -Message "DestinationConfigStore is invalid" return } $sensitiveVariables = @('common.fabricInstallerSecret') $okayToCopySensitiveValues = $false if($skipSensitiveVariables){ #do not change $okayToCopySensitiveValues because $skipSensitiveVariables is true (and thus $okayToCopySensitiveValues remains false) } else { #$skipSensitiveVariables was no passed in, so we can see if it is okay to copy sensitive variables if($DestinationConfigStore.Type -eq "File"){ $okayToCopySensitiveValues = $true } } #get all the scopes for the SourceConfigStore $scopes = Get-DosConfigScopes -ConfigStore $SourceConfigStore #foreach scope in the SourceConfigStore, call the set-dosconfigvalue on the destination config store. foreach($scope in $scopes){ $valuesForScope = Get-DosConfigValues -ConfigStore $SourceConfigStore -Scope $scope foreach($key in $($valuesForScope.keys | sort-object)){ if($okayToCopySensitiveValues){ Set-DosConfigValue -ConfigStore $DestinationConfigStore -configSection $scope -configSetting $key -configValue $valuesForScope[$key] -KeepExisting:$keepExisting.IsPresent } else { if($sensitiveVariables -contains "$scope.$key"){ #do not copy Write-DosMessage -Level "Warning" -Message "Unable to copy senstive variable $scope.$key" } else { Set-DosConfigValue -ConfigStore $DestinationConfigStore -configSection $scope -configSetting $key -configValue $valuesForScope[$key] -KeepExisting:$keepExisting.IsPresent } } } } } <# .SYNOPSIS Extracts the target zip file to the target directory. .DESCRIPTION Either uses Expand-Archive in PS 5.0+ or [System.IO.Compression.ZipFile] and associated calls in PS 4.0 .PARAMETER ArchiveFile Target zip file to extract .PARAMETER DestinationPath Destination directory to extract the zip file into. .PARAMETER OverWrite Forces overwriting existing files. If not specified and similar files already exists in the target directory, errors will be displayed for each file NOT overwritten .EXAMPLE Expand-DosArchive -ArchiveFile "x.zip" -DestinationPath "c:\inetpub\wwwroot\x" -OverWrite #> function Expand-DosArchive{ [CmdletBinding()] param( [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [ValidateScript({ if (!(Test-Path $_)) { Write-DosMessage -Level "Error" -Message "ArchiveFile $_ does not exist. Please enter valid path." } else { $true } })] [string] $ArchiveFile, [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [string] $DestinationPath, [switch] $OverWrite ) Expand-Archive -Path $ArchiveFile -DestinationPath $DestinationPath -Force:$OverWrite.IsPresent Write-DosTelemetry -Message "Expand-DosArchive called." } <# .SYNOPSIS Obtains the path to the root of the install directory. .DESCRIPTION Traverses up the path until it finds the root of the install directory. E.g. C:\install\DosInstaller2016_20.2.2019.11\DosInstaller2016 .EXAMPLE Get-DosBaseInstallerPath #> #replaces get-baseinstallerpath function Get-DosBaseInstallerPath { $parentPath = Get-Location $installerPath = $parentPath $rootDrivePath = (Split-Path -Path $installerPath -Qualifier) + "\" do { if(Get-ChildItem $installerPath | Where-Object {$_.Name -eq "CatalystSetup.exe"}) { break } else { $installerPath = Split-Path -Path $installerPath -Parent } if($installerPath -eq $rootDrivePath) { Write-DosMessage -Level "Error" -Message "Could not resolve the base path to the installer starting from $parentPath." } } until ($installerPath -eq $rootDrivePath) return $installerPath } <# .SYNOPSIS Fixes default PowerShell url dispatch decoding .DESCRIPTION PowerShell by default has a url parser setting which decodes a backslash before the request dispatch. This has caused problems when attempting to send web requests with urls that contain backslashes in them. This function fixes that issue and returns a url that can be passed to Invoke-WebRequest or Invoke-RestMethod. .PARAMETER url The URL string of a given web request. .EXAMPLE PS C:\> Get-DosCleanUri -url "http://localhost/DOMAIN\My User" .NOTES This function was created by following a helpful article found on Stack Overflow Topic: "Percent-encoded slash (“/â€) is decoded before the request dispatch" Article: https://stackoverflow.com/a/30927141 #> function Get-DosCleanUri { [CmdletBinding()] param ( [Parameter(Mandatory = $true, Position = 0)] [string]$url ) try { $escapedUrl = Format-UriString -Url $url $escapedUrl.PathAndQuery | Out-Null $m_Flags = Get-MFlagsFieldDotNet [uint64]$flags = Get-MFlagsValue -Url $escapedUrl -mFlags $m_Flags Set-MFlagsValue -Url $escapedUrl -mFlags $m_Flags -Flags $flags Write-DosMessage -Level "Information" -Message "Url cleaned to prevent decoding on dispatch ($($escapedUrl.OriginalString))." } catch { Write-DosMessage -Level "Error" -Message "An error occurred while attempting to clean the url ($($escapedUrl.OriginalString)). Exception: $($_.Exception)." } return $escapedUrl } function Format-UriString { [CmdletBinding()] param ( [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [string]$Url ) [Uri]$escapedUrl = [System.Uri]::EscapeUriString($Url) return $escapedUrl } function Get-MFlagsFieldDotNet { [CmdletBinding()] $m_Flags = [Uri].GetField("m_Flags", $([Reflection.BindingFlags]::Instance -bor [Reflection.BindingFlags]::NonPublic)) return $m_Flags } function Get-MFlagsValue { [CmdletBinding()] param ( [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [Uri]$Url, [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] $mFlags ) [uint64]$flags = $mFlags.GetValue($Url) return $flags } function Set-MFlagsValue { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Justification="WhatIf support not implemented.")] [CmdletBinding()] param ( [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [Uri]$Url, [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] $mFlags, [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] $Flags ) $mFlags.SetValue($Url, $($Flags -bxor 0x30)) } <# .SYNOPSIS Returns the scopes within a config store. .DESCRIPTION Returns the scopes within a config store. .PARAMETER ConfigStore The configstore object for which you want the scopes. .EXAMPLE Get-DosConfigScopes -configstore $configstore #> function Get-DosConfigScopes { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "", Justification="Values should be plural.")] [CmdletBinding()] param ( [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [hashtable] $ConfigStore ) if(-not(Confirm-ConfigStore -ConfigStore $ConfigStore)){ Write-DosMessage -Level 'Fatal' -Message "Invalid ConfigStore. Unable to Get-DosConfigScopes" return } if ($ConfigStore.Type -eq "File") { return Get-DosConfigScopesXml -ConfigStore $ConfigStore } if ($ConfigStore.Type -eq "External") { return Get-DosConfigScopesAzureTable -ConfigStore $ConfigStore } } <# .SYNOPSIS Gets configuration values from a configuration store of a certain type and format. .DESCRIPTION Given a user has a valid Configuration Store object, when the user asks for all values for a particular scope, values are returned in a hash table. If user provides an invalid Configuration Store object, warning messages will be displayed & logged, and Get-DosConfigValues will return $null. .PARAMETER ConfigStore Accepts a powershell hashtable or dictionary with key value pairs following the example, $configStore = @{Type = "File"; Format = "XML"; Path = "C:\Path\To\File"}. .PARAMETER Scope The configuration values you wish to return. Passing in an application specific scope will return values within the configuration that are contained in that application scope. Additionally, users can pass in a "common" scope to return values contained in the common scope. .EXAMPLE Get-DosConfigScopeValues -ConfigStore $configHashtable -Scope "common" Get-DosConfigScopeValues -ConfigStore $configHashtable -Scope "terminology" #> function Get-DosConfigValues { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "", Justification="Values should be plural.")] param( [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [hashtable] $ConfigStore, [Alias("ConfigSection", "ConfigScope")] [string] $Scope ) $validConfigStore = Confirm-ConfigStore -ConfigStore $ConfigStore if (!$validConfigStore) { Write-DosMessage -Level "Warning" -Message "ConfigStore object is invalid. No configuration values will be returned." return $null } Write-DosMessage -Level "Verbose" -Message "ConfigStore object is valid." $configValues = $null if ($ConfigStore.Type -eq "External") { Write-DosMessage -Level "Debug" -Message "Attempting to retrieve configuration values from external config store using external uri" $configValues = Get-DosConfigValuesAzureTable -ConfigStore $ConfigStore -Scope $Scope } if ($ConfigStore.Type -eq "File") { if ([string]::IsNullOrEmpty($Scope)) { Write-DosMessage -Level "Fatal" -Message "The required paramter 'Scope' was not provided. Please provide the required parameter and try agian." } Write-DosMessage -Level "Debug" -Message "Retrieving '$Scope' scoped configuration values from '$($ConfigStore.Path)'" $configValues = Get-DosConfigValuesXml -ConfigSection $Scope -InstallConfigPath $ConfigStore.Path } Write-DosTelemetry -Message "Get-DosConfigValues called with the following parameters. Scope: $Scope. ConfigStore: $($ConfigStore | Out-String)" return $configValues } <# .SYNOPSIS Replacement for dbatools Get-DbaDatabase .DESCRIPTION Replacement for dbatools Get-DbaDatabase #> function Get-DosDbaDatabase { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $SqlInstance, [Parameter(Mandatory = $true)] [string] $Database ) $MasterConnectionString = "Server=$SqlInstance;Database=master;Trusted_Connection=True;MultipleActiveResultSets=True;Connect Timeout=10" $sysDbQuery = "select name as [Name] , recovery_model_desc as RecoveryModel , suser_sname( owner_sid ) as [Owner] FROM sys.databases WHERE name = @Database" $queryResult = Invoke-DosSqlQuery -ConnectionString $MasterConnectionString -Query $sysDbQuery -Parameters @{Database = $database} $queryResultCount = $($queryResult | Measure-Object).Count if($queryResultCount -gt 1){ Write-DosMessage -Level 'Error' -Message "Get-DosDbaDatabase query resulted in $queryResultCount rows when we only expected 0 or 1." } return $queryResult } <# .SYNOPSIS Returns a service url from Discovery Service. .DESCRIPTION Given a valid Discovery Service url and valid service name, Get-DosServiceUrl will return a service url string. If a valid service version is provided, Get-DosServiceUrl will reutrn the service url string for the specified version. If a valid credential is provided, Get-DosServiceUrl will query Discovery Service with the credential provided, else it will use the local powershell session user. .PARAMETER DiscoveryServiceUrl REQUIRED Accepts a valid (not null or empty) Discovery Service Url string. .PARAMETER ServiceName REQUIRED Accepts a valid (not null or empty) service name that will be used to query Discovery Service .PARAMETER ServiceVersion REQUIRED Accepts a valid (not null or empty) service name that will be used to query Discovery Service .PARAMETER Credential Accepts a valid (not null or empty) service name that will be used to query Discovery Service .PARAMETER Exists Switch - If provided, appends an 'Exists' property to the output object and does not write a fatal message if the ServiceName is not found. .EXAMPLE Get-DosService -DiscoveryServiceUrl "https://server/DiscoveryService/v1" -ServiceName "AnalyticsService" -ServiceVersion 1 $credential = Get-Credential Get-DosService -DiscoveryServiceUrl "https://server/DiscoveryService/v1" -ServiceName "AnalyticsService" -ServiceVersion 1 -Credential $credential Get-DosService -DiscoveryServiceUrl "https://server/DiscoveryService/v1" -ServiceName "AnalyticsService" -ServiceVersion 1 -Exists #> function Get-DosService { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateNotNullorEmpty()] [string] $DiscoveryServiceUrl, [Parameter(Mandatory = $true)] [ValidateNotNullorEmpty()] [string] $ServiceName, [int] $ServiceVersion, [pscredential] $Credential, [switch]$Exists ) if ($ServiceVersion) { Write-DosMessage -Level "Verbose" -Message "Service Version provided. Request formed with specified version." $discoveryRequest = "$DiscoveryServiceUrl/Services(ServiceName='$ServiceName',Version=$ServiceVersion)" } else { Write-DosMessage -Level "Verbose" -Message "Service Version was not provided. Request formed without specified version." $discoveryRequest = "$DiscoveryServiceUrl/Services?`$filter=ServiceName eq $ServiceName" } $discoveryResponse = @{ } if ($null -ne $Credential) { try { Write-DosMessage -Level "Information" -Message "Attempting to retrieve Discovery Service object from Discovery Service at '$discoveryRequest' using '$($Credential.UserName)' credential." $discoveryResponse = Invoke-RestMethod -Method Get -Uri $discoveryRequest -Credential $Credential Write-DosMessage -Level "Information" -Message "Successfully retrieved Discovery Service object from Discovery Service." } catch { $statusCode = $_.Exception.Response.StatusCode.value__ if ($statusCode -eq "400" -and $Exists.IsPresent) { $discoveryResponse.Exists = $false return $discoveryResponse } Write-DosMessage -Level "Fatal" -Message "Error retrieving service registration with '$($Credential.UserName)' for '$ServiceName'. Please confirm Discovery Service installation and/or credential permissions. Exception: $($_.Exception)." } } else { try { Write-DosMessage -Level "Information" -Message "Attempting to retrieve Discovery Service object from Discovery Service at '$discoveryRequest' using '$env:UserName' credential." $discoveryResponse = Invoke-RestMethod -Method Get -Uri $discoveryRequest -UseDefaultCredentials Write-DosMessage -Level "Information" -Message "Successfully retrieved Discovery Service object from Discovery Service." } catch { $statusCode = $_.Exception.Response.StatusCode.value__ if ($statusCode -eq "400" -and $Exists.IsPresent) { $discoveryResponse.Exists = $false return $discoveryResponse } Write-DosMessage -Level "Fatal" -Message "Error retrieving service registration with '$($Credential.UserName)' for '$ServiceName'. Please confirm Discovery Service installation and/or credential permissions. Exception: $($_.Exception)." } } if ($Exists.IsPresent) { $discoveryResponse | Add-Member -NotePropertyName "Exists" -NotePropertyValue $true } return $discoveryResponse } function Get-DosServiceUrl { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateNotNullorEmpty()] [string] $DiscoveryServiceUrl, [Parameter(Mandatory = $true)] [ValidateNotNullorEmpty()] [string] $ServiceName, [Parameter(Mandatory = $true)] [ValidateNotNullorEmpty()] [int] $ServiceVersion, [pscredential] $Credential ) if ($Credential) { $discoveryResponse = Get-DosService -DiscoveryServiceUrl $DiscoveryServiceUrl -ServiceName $ServiceName -ServiceVersion $ServiceVersion -Credential $Credential } else { $discoveryResponse = Get-DosService -DiscoveryServiceUrl $DiscoveryServiceUrl -ServiceName $ServiceName -ServiceVersion $ServiceVersion } Write-DosMessage -Level "Verbose" -Message "Pulling 'ServiceUrl' from the Discovery Service response." $serviceUrl = Get-ServiceUrlString -DiscoveryServiceResponse $discoveryResponse Write-DosMessage -Level "Information" -Message "Checking if a value was returned for '$ServiceName version $ServiceVersion'." if ([string]::IsNullOrWhiteSpace($serviceUrl)) { Write-DosMessage -Level "Fatal" -Message "The service $ServiceName is not registered with the Discovery service. Make sure that '$ServiceName $ServiceVersion' is registered with Discovery Service. Exception: $($_.Exception)" } Write-DosMessage -Level "Information" -Message "Returning url '$serviceUrl' for '$ServiceName' service." return $serviceUrl } function Get-ServiceUrlString { [CmdletBinding()] param ( $DiscoveryServiceResponse ) $serviceUrl = $DiscoveryServiceResponse.ServiceUrl return $serviceUrl } <# .SYNOPSIS Returns the physical root of the specified web site .DESCRIPTION Uses the WebAdministration powershell module to locate the web site's physical root .PARAMETER WebSiteName Name of the IIS site - defaults to "Defautl Web Site" .EXAMPLE Get-IISWebSitePath -WebSiteName $IISWebSite #> function Get-IISWebSitePath{ [CmdletBinding()] param( [string] $WebSiteName = "Default Web Site" ) Import-Module WebAdministration Test-ElevatedPermission try{ [Microsoft.IIs.PowerShell.Framework.ConfigurationElement] $webSite = Get-Item "IIS:\Sites\$WebSiteName" return [System.Environment]::ExpandEnvironmentVariables($webSite.PhysicalPath) } catch{ Write-DosMessage -Level Error -Message "Unable to get inforomation for $WebSiteName : error $($_.Exception)" } } <# .SYNOPSIS Downloads the specified Uri using a WebRequest to the specified OutFile .DESCRIPTION Wraps the PowerShell native Invoke-WebRequest and performs the request without the default progress bar behavior. .PARAMETER Uri Specifies the Uniform Resource Identifier (URI) of the Internet resource to which the web request is sent. Enter a URI. This parameter supports HTTP, HTTPS, FTP, and FILE values. This parameter is required. .PARAMETER OutFile Specifies the output file for which this cmdlet saves the response body. Enter a path and file name. If you omit the path, the default is the current location. .PARAMETER NoCache When supplied will provide the "Cache-Control" header set to "no-cache" to the underlying WebRequest. .EXAMPLE Download-WebRequest -Uri http://some.valid.uri/file -OutFile theFile.txt #> function Get-WebRequestDownload { [CmdletBinding()] param( [Parameter(Mandatory=$true)] [string] $Uri, [Parameter(Mandatory=$true)] [string] $OutFile, [switch] $NoCache = $false ) $originalProgressPreference = $progressPreference try { $headers = @{} if ($NoCache) { $headers.Add("Cache-Control", "no-cache") } $progressPreference = 'silentlyContinue' Invoke-WebRequest -Uri $Uri -Headers $headers -OutFile $OutFile -UseBasicParsing } finally { $progressPreference = $originalProgressPreference } } <# .SYNOPSIS Installs SSIS project .DESCRIPTION Ensures SSIS catalog and folder exist, and installs SSIS project from specified ISPAC .PARAMETER ProjectName Name to deploy project under .PARAMETER IspacPath Path to ISPAC to deploy .PARAMETER CatalogEncryptionKey Key to use for encrypting catalog, if it must be created .PARAMETER CatalogName Name of SSIS catalog to contain project .PARAMETER FolderName Name of SSIS catalog folder to contain project .PARAMETER ConnectionString Connection string for connecting to SQL Server instance with Integration Services .INPUTS None. You cannot pipe objects to Install-DosIspac. .OUTPUTS None. .EXAMPLE PS> Install-DosIspac -ProjectName 'CatalystLoader' -IspacPath 'SetupContent/SSISLoader2016.ispac' #> function Install-DosIspac { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Passing through the -WhatIf and -Confirm to the private functions - this is a supported scenario")] [cmdletbinding(SupportsShouldProcess=$true)] [OutputType()] param( [parameter(Mandatory=$true)][string]$ProjectName, [parameter(Mandatory=$true)][string]$IspacPath, [parameter(Mandatory=$false)][string]$CatalogEncryptionKey, [parameter(Mandatory=$false)][string]$CatalogName = 'SSISDB', [parameter(Mandatory=$false)][string]$FolderName = 'Catalyst', [parameter(Mandatory=$false)][string]$ConnectionString = 'Data Source=localhost;Initial Catalog=EDWAdmin;Integrated Security=True') [Reflection.Assembly]::LoadWithPartialName("Microsoft.SqlServer.Management.IntegrationServices") $integrationServices = Get-IntegrationServices -ConnectionString $ConnectionString Write-DosMessage -Level Debug -Message "Testing whether catalog $CatalogName exists" if ($integrationServices.Catalogs.Contains($CatalogName)) { Write-DosMessage -Level Information -Message "SSIS catalog already exists, skipping" $catalog = $integrationServices.Catalogs[$CatalogName] } else { Write-DosMessage -Level Information -Message "SSIS catalog does not exist, creating" if ([string]::IsNullOrEmpty($CatalogEncryptionKey)) { Write-DosMessage -Level Error -Message "Please provide a catalog encryption key using the CatalogEncryptionKey parameter so that a new catalog can be created." -ErrorAction Stop } $catalog = New-SsisCatalog -IntegrationServices $integrationServices -CatalogEncryptionKey $CatalogEncryptionKey -CatalogName $CatalogName -WhatIf:$PSBoundParameters.ContainsKey('WhatIf') -Confirm:$PSBoundParameters.ContainsKey('Confirm') Write-DosMessage -Level Debug -Message "SSIS catalog created successfully" } Write-DosMessage -Level Debug -Message "Testing whether folder $FolderName exists" if ($catalog.Folders.Contains($FolderName)) { Write-DosMessage -Level Information -Message "SSIS catalog folder already exists" $folder = $catalog.Folders[$FolderName] } else { Write-DosMessage -Level Information -Message "SSIS catalog folder does not exist, creating" $folder = New-SsisFolder -SsisCatalog $catalog -FolderName $FolderName -WhatIf:$PSBoundParameters.ContainsKey('WhatIf') -Confirm:$PSBoundParameters.ContainsKey('Confirm') Write-DosMessage -Level Debug -Message "SSIS catalog folder created successfully" } Write-DosMessage -Level Information -Message "Deploying SSIS project $ProjectName from $IspacPath" New-SsisProject -SsisFolder $folder -ProjectName $ProjectName -IspacPath $IspacPath -WhatIf:$PSBoundParameters.ContainsKey('WhatIf') -Confirm:$PSBoundParameters.ContainsKey('Confirm') Write-DosMessage -Level Debug -Message "SSIS project $ProjectName deployed successfully" } <# .SYNOPSIS If needed, Installs the correct version of Dot Net Core .DESCRIPTION Checks for a specific version of Dot Net Core. If the version is not installed,download and install it. .PARAMETER version The string representation of the version. This should match the DisplayVersion for the object as installed in the windows registry. .PARAMETER downloadUrl The complete download URL for the exe installer that corresponds to the given version. .EXAMPLE Install-DotNetCoreIfNeeded -version "1.2.3.4" -downloadUrl "https://some.web.site/folder/installFill.exe" #> function Install-DotNetCoreIfNeeded { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [ValidateNotNullorEmpty()] [string] $version, [Parameter(Mandatory = $true)] [ValidateNotNullorEmpty()] [string] $downloadUrl, [string] $filePattern = "*.NET Core*Windows Server Hosting*" ) if (Test-PrerequisiteExact $filePattern $version) { Write-DosMessage -Level "Information" -Message ".NET Core Windows Server Hosting Bundle (v$version) installed and meets expectations." return } try { Write-DosMessage -Level "Information" -Message "Windows Server Hosting Bundle version $version not installed...installing version $version" Get-WebRequestDownload -Uri $downloadUrl -OutFile $env:Temp\bundle.exe Start-Process $env:Temp\bundle.exe -Wait -ArgumentList '/quiet /install /norestart' net stop was /y net start w3svc } catch { Write-DosMessage -Level "Fatal" -Message "Could not install .NET Windows Server Hosting bundle. Please install the hosting bundle before proceeding. $downloadUrl" } if (Test-PrerequisiteExact $filePattern $version) { Write-DosMessage -Level "Information" -Message "Windows Server Hosting Bundle installed version $version" } else { Write-DosMessage -Level "Fatal" -Message "Error executing .NET Windows Server Hosting bundle. Please install the hosting bundle before proceeding. $downloadUrl" } try { Remove-Item $env:Temp\bundle.exe } catch { $e = $_.Exception Write-DosMessage -Level "Warning" -Message "Unable to remove temporary download file for server hosting bundle exe" Write-DosMessage -Level "Warning" -Message $e.Message } } <# .SYNOPSIS Imports/Installs a module required to run this module/function .DESCRIPTION Attempts to load a local module first - if the module isn't available it will attempt to download/install from PSGallery. .PARAMETER ModuleName Name of the module to install .PARAMETER Scope Scope used to install the module - default is CurrentUser .EXAMPLE Install-RequiredModule -ModuleName dbatools #> function Install-RequiredModule{ [CmdletBinding()] param( [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [string] $ModuleName, [string] $Scope = "CurrentUser", [version] $RequiredVersion ) Write-DosTelemetry -Message "Install-RequiredModule start" # Adding PSModule path becuse in some cases the path to user directory was not in the module system path $currentUserPSModulePath = "$home\Documents\WindowsPowerShell\Modules" $replaceCurrentUserModulePath = Add-ToPSModulePath -Path $currentUserPSModulePath #First scenario - required version of the module is already loaded $importedModule = Get-ModuleWorkAround -Name $ModuleName if($null -ne $importedModule){ Write-DosMessage -Level "Information" -Message "Module '$ModuleName' already imported." if ($RequiredVersion) { Write-DosMessage -Level "Verbose" -Message "Checking if imported module '$ModuleName' matches required version '$RequiredVersion'." if ((Compare-ModuleVersion -ModuleToCompare $importedModule -RequiredVersion $RequiredVersion)) { Write-DosMessage -Level "Information" -Message "Confirming '$ModuleName' with version '$RequiredVersion' is loaded into session." if ($importedModule.Count -gt 1) { Write-DosMessage -Level "Information" -Message "Multiple '$ModuleName' modules loaded in session. This should never happen?" Remove-Module -Name $ModuleName try { Import-Module -Name $ModuleName -RequiredVersion $RequiredVersion -Global -ErrorAction Stop } catch [System.Management.Automation.RuntimeException]{ Write-DosMessage -Level "Warning" -Message "'$ModuleName' was previously loaded during this session. If you encounter weird issues with DosInstallUtilities, please restart PowerShell and resolve conflicts with '$ModuleName' imports." } Write-DosMessage -Level "Information" -Message "Successfully imported '$ModuleName' with version '$RequiredVersion' into session." Write-DosTelemetry -Message "Multiple modules found in session. Removed all and successfully imported '$ModuleName' with version '$RequiredVersion' into session." } else { Write-DosMessage -Level "Information" -Message "Using '$ModuleName' that is currently loaded in the session." Write-DosTelemetry -Message "Using '$ModuleName' that is currently loaded in the session." } # Removing user path from ps module to cover cases in it is not included in system path if ($replaceCurrentUserModulePath) { Remove-FromPSModulePath -Path $currentUserPSModulePath } return } else { Write-DosMessage -Level "Information" -Message "Removing '$ModuleName' that does not match required version '$RequiredVersion'." Remove-Module -Name $ModuleName } } else { # Removing user path from ps module to cover cases in it is not included in system path if ($replaceCurrentUserModulePath) { Remove-FromPSModulePath -Path $currentUserPSModulePath } return } } Write-DosMessage -Level "Information" -Message "Did not find module '$ModuleName' loaded in session." # Second scenario is required version of the module is installed on the system Write-DosMessage -Level "Information" -Message "Checking if module '$ModuleName' is already installed." $installedModule = Get-ModuleWorkAround -Name $ModuleName -ListAvailable if($null -ne $installedModule){ Write-DosMessage -Level "Information" -Message "Module '$ModuleName' already installed." if ($RequiredVersion) { Write-DosMessage -Level "Verbose" -Message "Checking if installed module '$ModuleName' matches required version '$RequiredVersion'." if ((Compare-ModuleVersion -ModuleToCompare $installedModule -RequiredVersion $RequiredVersion)) { Write-DosMessage -Level "Information" -Message "Importing '$ModuleName' with version '$RequiredVersion'." try { Import-Module -Name $ModuleName -RequiredVersion $RequiredVersion -Global -ErrorAction Stop } catch [System.Management.Automation.RuntimeException]{ Write-DosMessage -Level "Warning" -Message "'$ModuleName' was previously loaded during this session. If you encounter weird issues with DosInstallUtilities, please restart PowerShell and resolve conflicts with '$ModuleName' imports." } Write-DosTelemetry -Message "'$ModuleName' found on the system and successfully imported with version '$RequiredVersion'" if ($replaceCurrentUserModulePath) { Remove-FromPSModulePath -Path $currentUserPSModulePath } return } } else { Write-DosMessage -Level "Information" -Message "Module '$ModuleName' is installed on system, attempting to import." try { Import-Module -Name $ModuleName -Global -ErrorAction Stop } catch [System.Management.Automation.RuntimeException]{ Write-DosMessage -Level "Warning" -Message "'$ModuleName' was previously loaded during this session. If you encounter weird issues with DosInstallUtilities, please restart PowerShell and resolve conflicts with '$ModuleName' imports." } Write-DosTelemetry -Message "'$ModuleName' found on the system and successfully imported with version" if ($replaceCurrentUserModulePath) { Remove-FromPSModulePath -Path $currentUserPSModulePath } return } } Write-DosMessage -Level "Information" -Message "Did not find module '$ModuleName' installed on system." #third scenario is module is installed from diu\dependencies folder Write-DosMessage -Level "Information" -Message "Seeing if '$ModuleName' is in the DosInstallUtilities dependencies folder." $dependenciesFolder = "$($MyInvocation.MyCommand.Module.ModuleBase)\dependencies\" if(Test-Path -Path $dependenciesFolder){ #dependencies folder exists, let's look through it. #the zip file should be named module_version.zip ( e.g. dbatools_1.0.115.zip (where 1.0.115 is the version)) $foundModuleZip = $false $zipFiles = get-childitem -path "$dependenciesFolder" -file -filter '*.zip' foreach($zipfile in $zipFiles){ $moduleZipFileWithoutTheZip = $($zipfile.name).replace('.zip','') $moduleZipName = $moduleZipFileWithoutTheZip.split("_")[0] $moduleZipVersion = $moduleZipFileWithoutTheZip.split("_")[1] if($moduleZipName -eq $ModuleName){ #we found a zip file, now let's see if it is the right version. if($RequiredVersion){ #we are looking for an exact version, so let's check. if($RequiredVersion -eq $moduleZipVersion){ #yep, this is the exact version. $foundModuleZip = $true } } else { #we don't need an exact version, so we're good. $foundModuleZip = $true } if($foundModuleZip){ #we found the module we were looking for, let's save it off and break out of this foreach loop $moduleZipFile = $zipfile.PSPath break } } } if($moduleZipFile){ Write-DosMessage -Level 'Information' -Message "Found $moduleZipFile in $dependenciesFolder" #now, we want to go to the my documents folder $finalFolder = [Environment]::GetFolderPath("MyDocuments") $finalFolder = "$finalFolder\WindowsPowerShell\Modules\$moduleZipName\$moduleZipVersion" if($(Test-Path -Path $finalFolder) -and $(get-childitem $finalFolder -filter "$moduleZipName.psd1")){ Write-DosMessage -Level 'Information' -Message "$ModuleName already exists in $finalFolder." } else { Write-DosMessage -Level 'Information' -Message "$ModuleName doesn't exist in $finalFolder. We will place it there." if(Test-Path -Path $finalFolder){ Write-DosMessage -Level 'Information' -Message "Removing directory $finalFolder" Remove-item -force $finalFolder } Write-DosMessage -Level 'Information' -Message "Creating directory $finalFolder" new-item $finalFolder -itemtype 'directory' Write-DosMessage -Level 'Information' -Message "Unzipping $moduleZipFile to $finalFolder" Expand-Archive -Path $moduleZipFile -DestinationPath $finalFolder -Force } $modules = @( Get-ChildItem -Path "$finalFolder\*" -Include "$moduleZipName.psd1" -Recurse -ErrorAction SilentlyContinue ) if ($modules) { Write-DosMessage -Level 'Information' -Message "Importing $modules" Import-Module $modules -Force -Global return } else { Write-DosMessage -Level 'Fatal' -Message "Unable to import $ModuleName from $finalFolder when we expected to be able to do so." } } } Write-DosMessage -Level 'Information' -Message "Did not find a zip file for $ModuleName in $dependenciesFolder." #fourth scenario is module is installed from PSGallery Write-DosMessage -Level "Information" -Message "Attempting to fetch '$ModuleName'." $desiredRepo = "PSGallery" $isTrusted = Get-RepositoryTrust -RepositoryName $desiredRepo if (!($isTrusted)) { Write-DosMessage -Level "Information" -Message "'$desiredRepo' is not trusted. Toggling trust to download '$ModuleName'" Set-RepositoryTrust -RepositoryName $desiredRepo -Trust } #Error check here - also assume that PowerShellGet is loaded/available. try { if ($RequiredVersion) { Write-DosMessage -Level "Information" -Message "Module '$ModuleName' version '$RequiredVersion' being downloaded from PSGallery." Install-Module $ModuleName -RequiredVersion $RequiredVersion -Scope $scope Write-DosMessage -Level "Information" -Message "Successfully downloaded module '$ModuleName' with version '$RequiredVersion' from PSGallery." try { Import-Module -Name $ModuleName -RequiredVersion $RequiredVersion -Global -ErrorAction Stop } catch [System.Management.Automation.RuntimeException]{ Write-DosMessage -Level "Warning" -Message "'$ModuleName' was previously loaded during this session. If you encounter weird issues with DosInstallUtilities, please restart PowerShell and resolve conflicts with '$ModuleName' imports." } Write-DosMessage -Level "Information" -Message "Successfully imported module '$ModuleName' with version '$RequiredVersion' from PSGallery." Write-DosTelemetry -Message "Successfully imported module '$ModuleName' with version '$RequiredVersion' from PSGallery." } else { Write-DosMessage -Level "Information" -Message "Module '$ModuleName' being downloaded from PSGallery." Install-Module $ModuleName -Scope $scope Write-DosMessage -Level "Information" -Message "Successfully downloaded module '$ModuleName' from PSGallery." try { Import-Module -Name $ModuleName -Global -ErrorAction Stop } catch [System.Management.Automation.RuntimeException]{ Write-DosMessage -Level "Warning" -Message "'$ModuleName' was previously loaded during this session. If you encounter weird issues with DosInstallUtilities, please restart PowerShell and resolve conflicts with '$ModuleName' imports." } Write-DosMessage -Level "Information" -Message "Successfully imported module '$ModuleName' from PSGallery." Write-DosTelemetry -Message "Successfully imported module '$ModuleName' from PSGallery." } } catch { Write-DosMessage -Level "Error" -Message "Error installing or importing '$ModuleName'. Exception: $($_.Exception)" if (!($isTrusted)) { Write-DosMessage -Level "Information" -Message "Returning '$desiredRepo' to an untrusted state." Set-RepositoryTrust -RepositoryName $desiredRepo } } if (!($isTrusted)) { Write-DosMessage -Level "Information" -Message "Returning '$desiredRepo' to an untrusted state." Set-RepositoryTrust -RepositoryName $desiredRepo } if ($replaceCurrentUserModulePath) { Remove-FromPSModulePath -Path $currentUserPSModulePath } Write-DosTelemetry -Message "Install-RequiredModule completed - successfully" } #Work around for pester issue: https://github.com/pester/Pester/issues/1007 function Get-ModuleWorkAround{ param( [string] $Name, [switch] $ListAvailable ) if($ListAvailable.IsPresent){ return Get-Module -Name $Name -ListAvailable } else { return Get-Module -Name $Name } } function Get-RepositoryTrust { param ( [string] $RepositoryName ) $repo = Get-PSRepository -Name $RepositoryName return $repo.Trusted } function Set-RepositoryTrust { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Justification="WhatIf support not implemented.")] param ( [string] $RepositoryName, [switch] $Trust ) if ($Trust.IsPresent) { Set-PSRepository -Name $RepositoryName -InstallationPolicy Trusted } else { Set-PSRepository -Name $RepositoryName -InstallationPolicy Untrusted } } function Add-ToPSModulePath { [CmdletBinding()] param( [Parameter(Mandatory=$true)] [string] $Path ) if (!($env:PSModulePath.split(";") -contains $Path)){ Write-DosMessage -Level "Information" -Message "Adding '$Path' to PSModulePath" $current = $env:PSModulePath [Environment]::SetEnvironmentVariable("PSModulePath",$current + ";" + $Path, "Machine") $env:PSModulePath = [System.Environment]::GetEnvironmentVariable("PSModulePath","Machine") return $true }else{ Write-DosMessage -Level "Information" -Message "'$Path' is already present in PSModulePath" return $false } } function Remove-FromPSModulePath{ [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Justification="WhatIf support not implemented.")] [CmdletBinding()] param( [Parameter(Mandatory=$true)] [string] $Path ) if ($env:PSModulePath.split(";") -contains $Path){ $newValue = (($env:PSModulePath).Split(";") | Where-Object { $_ -ne $Path }) -join ";" [Environment]::SetEnvironmentVariable("PSModulePath", $newValue, "Machine") $env:PSModulePath = [System.Environment]::GetEnvironmentVariable("PSModulePath","Machine") Write-DosMessage -Level "Information" -Message "$Path removed from PSModulePath." }else{ Write-DosMessage -Level "Information" -Message "$Path is not present in $env:PSModulePath" } } function Compare-ModuleVersion { [CmdletBinding()] param( [object] $ModuleToCompare, [version] $RequiredVersion ) $isMatch = $false foreach($module in $ModuleToCompare) { if ($module.Version.CompareTo($RequiredVersion) -eq 0) { Write-DosMessage -Level "Information" -Message "Found Module '$ModuleName' that meets the version requirements." $isMatch = $true break; } } if (!$isMatch) { Write-DosMessage -Level "Information" -Message "No module with version '$RequiredVersion' was found for module '$ModuleName'." } return $isMatch } <# .SYNOPSIS Makes a web request to the $ServiceUrl that is passed in and returns the odata results. .DESCRIPTION Makes a web request to the $ServiceUrl that is passed in and returns the odata results. If there are not results then it will return an empty object. .PARAMETER ServiceUrl [string] (Required) The URL to make a request to .PARAMETER AccessToken [string] (Required) The AccessToken from Identity needed for authentication .PARAMETER Headers [hashtable] (Optional) Headers to send with the request - by default the code will add: @{Accept = "application/json"; Authorization = "Bearer $AccessToken"} but those can be overwritten Note: It will only add the Authorization header if AccessToken is set .PARAMETER ContentType [string] (Optional) The ContentType of the Body being sent with the request (if a body is required) - by default the code will set it to "application/json" if it is not set .PARAMETER Body [string] (Optional) The JSON body to send with Patch, Put, and Post requests - will fail if included with Get or Delete requests .PARAMETER Get [switch] Sets GET as the method type One of the Get, Patch, Put, Post, or Delete switches MUST be present .PARAMETER Patch [switch] Sets PATCH as the method type One of the Get, Patch, Put, Post, or Delete switches MUST be present .PARAMETER Put [switch] Sets PUT as the method type One of the Get, Patch, Put, Post, or Delete switches MUST be present .PARAMETER Post [switch] Sets POST as the method type One of the Get, Patch, Put, Post, or Delete switches MUST be present .PARAMETER Delete [switch] Sets DELETE as the method type One of the Get, Patch, Put, Post, or Delete switches MUST be present .EXAMPLE Invoke-DosOdataRequest -ServiceUrl "https://mymachine.hqcatalyst.local/MetadataService2/v2/DataMarts" -AccessToken $token $Get or Invoke-DosOdataRequest -ServiceUrl "https://mymachine.hqcatalyst.local/MetadataService2/v2/DataMarts" -AccessToken $token $Post -Body $jsonPayload #> function Invoke-DosOdataRequest { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateNotNullorEmpty()] [ValidateScript( { if ($null -eq ($_ -as [System.URI]).AbsoluteURI) { throw """$_"" is not a valid url" } return $true })] [string] $ServiceUrl, [string] $AccessToken, [hashtable] $Headers = @{}, [string] $ContentType = "application/json", [switch] $UseDefaultCredentials, # The parameter set will require this parameter only when that same parameter set is on another parameter (Put, Patch, Post) # It will fail if it is included when not required (Get, Delete) [Parameter(ParameterSetName = "RequestBody")] [string] $Body, [Parameter(ParameterSetName = "GetRequest")] [switch] $Get, # Body is a required parameter for Patch [Parameter(ParameterSetName = "PatchRequest")] [Parameter(ParameterSetName = "RequestBody")] [switch] $Patch, # Body is a required parameter for Put [Parameter(ParameterSetName = "PutRequest")] [Parameter(ParameterSetName = "RequestBody")] [switch] $Put, # Body is a required parameter for Post [Parameter(ParameterSetName = "PostRequest")] [Parameter(ParameterSetName = "RequestBody")] [switch] $Post, [Parameter(ParameterSetName = "DeleteRequest")] [switch] $Delete ) Write-DosMessage -Level "Debug" -Message "Checking for required headers" if (-Not $Headers.ContainsKey("Accept")) { Write-DosMessage -Level "Debug" -Message "Adding ""Accept"" header" $Headers.Add("Accept", "application/json") } # Only add the header if AccessToken is not null or empty and the Headers doesn't already contain Authorization if (-Not $Headers.ContainsKey("Authorization") -and ![String]::IsNullOrEmpty($AccessToken)) { Write-DosMessage -Level "Debug" -Message "Adding ""Authorization"" header" $Headers.Add("Authorization", "Bearer $AccessToken") } Write-DosMessage -Level "Debug" -Message "Checking that Content-Type is set" if ([String]::IsNullOrEmpty($ContentType)) { Write-DosMessage -Level "Debug" -Message "Setting Content-Type to ""application/json""" $ContentType = "application/json" } Write-DosMessage -Level "Debug" -Message "Determining which request method was selected" if ($Get.IsPresent) { $method = "GET" } elseif ($Post.IsPresent) { $method = "POST" } elseif ($Put.IsPresent) { $method = "PUT" } elseif ($Patch.IsPresent) { $method = "PATCH" } elseif ($Delete.IsPresent) { $method = "DELETE" } Write-DosMessage -Level "Information" -Message "Method selected is ""$method""" $output = @() $url = $ServiceUrl try { Write-DosMessage -Level "Information" -Message "Invoking ""$method"" request to ""$ServiceUrl""" $requestParameters = @{ Method = $method URI = $url Headers = $Headers ContentType = $ContentType UseBasicParsing = $true #required until Powershell 6+ } if ($Body) { Write-DosMessage -Level "Debug" -Message "Testing if the body is already Json" # When we go to Powershell 7 there is a Test-Json method that can take the place of the next several lines of code try { ConvertFrom-Json $Body -ErrorAction Stop | Out-Null $validJson = $true } catch { $validJson = $false } if (-Not $validJson) { Write-DosMessage -Level "Debug" -Message "Attempting to convert the body to Json" $Body = $Body | ConvertTo-Json -Depth 100 -Compress } Write-DosMessage -Level "Debug" -Message "Request body is ""$Body""" $requestParameters.Add("Body", $Body) } if ($UseDefaultCredentials) { $requestParameters.Add("UseDefaultCredentials", $true) } do { $response = Invoke-RestMethod @requestParameters if ($response.PSOjbect.Properties.Name -contains "value") { $output += $response.value } else { $output += $response } $url = $response.'@odata.nextLink'; } while ($url); } catch [System.Net.WebException] { Write-DosMessage -Level "Warning" -Message "A non 200 response was returned from ""$url"". This may be expected.`nRequest: $url`nStatus Code: $($_.Exception.Response.StatusCode.value__)`nMessage: $($_.Exception.Response.StatusDescription)" throw $_ # Rethrow error for downstream catching if desired } catch { Write-DosMessage -Level "Fatal" -Message "An error was encountered while making the web request. Exception: $($_.Exception)" } return $output } function Invoke-DosPingServices { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "", Justification="Values should be plural.")] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $DiscoveryServiceUrl, [string] $AccessToken, [Parameter(Mandatory = $true)] [hashtable[]] $Services ) begin { Write-DosMessage -Level "Information" -Message "PING DISCOVERYSERVICE" -HeaderType H2 Invoke-DosPingService -ServiceName "DiscoveryService" -Uri "$(Remove-VersionFromLocalPath $DiscoveryServiceUrl)/ping" -Headers @{"Accept" = "application/json" } -UseDefaultCredentials -ErrorLevel Fatal } process { foreach ($Service in $Services) { try { Write-DosMessage -Level "Information" -Message "PING $($Service.Name.ToUpper())" -HeaderType H2 $Headers = @{"Accept" = "application/json" } if ($Service.RequireAuthToken) { if ($AccessToken) { $Headers.Add("Authorization", "Bearer $AccessToken") } else { Write-DosMessage -Level "Fatal" -Message "$($Service.Name) ping header did not provide the required AccessToken" } } if([string]::IsNullOrEmpty($Service.AbsoluteEndpoint)){ $UriRoot = Remove-VersionFromLocalPath (Get-DosServiceUrl -DiscoveryServiceUrl $DiscoveryServiceUrl -ServiceName $Service.Name -ServiceVersion $Service.Version) #removing trailing slashes, if there are any if ($UriRoot.EndsWith("/")){ $UriRoot = $UriRoot.substring(0,$UriRoot.Length-1) } $Uri = "$($UriRoot)/$($Service.PingEndpoint)" } else { $Uri = $Service.AbsoluteEndpoint } Write-DosMessage -Level "Information" -Message "Appended ping endpoint to root url --> $Uri" $invokePingServiceParams = @{ ServiceName = $Service.Name Uri = $Uri Headers = $Headers ErrorLevel = $Service.ErrorLevel UseDefaultCredentials = if ($Service.UseDefaultCredentials) { $true } else { $false } } if ($Service.RetryScriptBlock) { $invokePingServiceParams.Add("RetryScriptBlock", $Service.RetryScriptBlock) } Invoke-DosPingService @invokePingServiceParams } catch { Write-DosMessage -Level $Service.ErrorLevel -Message "There was an error pinging the service `"$($Service.Name)`". This installation is configured to return a `"$($Service.ErrorLevel.ToUpper())`" message if this occurs." if ($Service.ErrorLevel -eq "Fatal") { Write-DosMessage -Level $Service.ErrorLevel -Message "Please check and fix the service `"$($Service.Name)`" before trying this installation again." } } } } } function Invoke-DosPingService { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $ServiceName, [Parameter(Mandatory = $true)] [string] $Uri, [Parameter(Mandatory = $true)] [hashtable] $Headers, [string] $Method = "Get", [switch] $UseDefaultCredentials, [ValidateSet("Error", "Fatal", "Warning")] [string] $ErrorLevel = "Error", [int] $RetryAttempts = 5, [int] $RetrySleepSeconds = 3, [scriptblock] $RetryScriptBlock ) $counter = 0 $success = $false try { Write-DosMessage -Level "Information" -Message "Pinging $($ServiceName)...$($Uri)" $webRequestParams = @{ Method = $Method Uri = $Uri Headers = $Headers } if ($UseDefaultCredentials) { $webRequestParams.Add("UseDefaultCredentials", $UseDefaultCredentials) } while (!$success -and $counter -lt $RetryAttempts) { try { $response = Invoke-WebRequest @webRequestParams -UseBasicParsing Write-DosMessage -Level "Information" -Message "$($ServiceName) ping successful! $($response.StatusCode) ($($response.StatusDescription))" $success = $true } catch { $counter++ Write-DosMessage -Level "Warning" -Message "There was an error pinging the service `"$($ServiceName)`"." if ($RetryScriptBlock) { Invoke-Command -ScriptBlock $RetryScriptBlock } Write-DosMessage -Level "Information" -Message "Retrying ping - attempt $($counter) of $($RetryAttempts) ..." Start-Sleep -Seconds $RetrySleepSeconds } } if (!$success) { throw } } catch { Write-DosMessage -Level $ErrorLevel -Message "There was an error pinging the service `"$($ServiceName)`". This installation is configured to return a `"$($ErrorLevel.ToUpper())`" message if this occurs." if ($ErrorLevel -eq "Fatal") { Write-DosMessage -Level $ErrorLevel -Message "Please check and fix the service `"$($ServiceName)`" before trying this installation again." } } } <# .SYNOPSIS Reads in json manifest files and invokes the appropriate readiness checks based of the supported check types. .DESCRIPTION .PARAMETER ManifestPath Accepts an array of valid paths to json manifest files. .PARAMETER ResourceToCheck Accepts a string of either "dbServer", or "webServer". The environment that the readiness checks will run against. .EXAMPLE Invoke-DosPrerequisiteChecks -ManifestPath @("C:/Exampe/Path/Manifest.json", "C:/Another/Example/Manifest.json") -ResourceToCheck "dbServer" -PathToLogRoot "C:\install" #> function Invoke-DosPrerequisiteChecks { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "", Justification="Values should be plural.")] [CmdletBinding()] param( [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [ValidateScript({ foreach ($path in $_) { if (!(Test-Path $path)) { throw "ManfiestPath $path does not exist. Please enter valid path." } else { $true } } })] [string[]] $ManifestPath, [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [ValidateSet("dbServer", "webServer")] [string] $ResourceToCheck, [ValidateScript({ foreach ($path in 0) { if (!(Test-Path $path)) { throw "PathToLogRoot '$path' does not exist. Please enter valid path." } else { $true } } })] [string] $PathToLogRoot = "C:\install\" ) $fileDate = (Get-Date).tostring("dd-MM-yyyy-hh-mm-ss") $logFile = "InstallReadinessTool_" + $fileDate + ".log" $logPath = $PathToLogRoot + $logFile try { New-Item -ItemType File -Path $PathToLogRoot -Name $logFile -Force -ErrorAction Stop | Out-Null } catch { throw "Error creating file, '$logPath'. Exception: $_" } Write-Output "Log file generated in '$PathToLogRoot'" # Potentially Loop for the manifests provided foreach ($manifest in $ManifestPath) { # Get-Content json manfiests $manifestData = Get-Content -Raw -Path "$manifest" | ConvertFrom-Json # Powershell Checktype $powershellData = $manifestData.readinessChecks | Where-Object { $_.checkType -eq "powershellVersion" -and $_.resourceToCheck -contains "$ResourceToCheck" } $psVersionCheckResult = Invoke-PowershellVersionCheck -Data $powershellData *>&1 Write-VariableToConsoleAndFile -VariableToParse $psVersionCheckResult -Log $logPath # Os Version Checktype $allowedOSVersions = @("Server 2012 R2", "Server 2016", "Windows 10") $osVersionCheckResult = Invoke-OsVersionCheck -AllowedVersions $allowedOSVersions *>&1 Write-VariableToConsoleAndFile -VariableToParse $osVersionCheckResult -Log $logPath # Dependent Software Checktype $dependentSoftwareData = $manifestData.readinessChecks | Where-Object { $_.checkType -eq "dependentSoftware" -and $_.resourceToCheck -contains "$ResourceToCheck" } $dependentSoftwareCheckResult = Invoke-DependentSoftwareCheck -Data $dependentSoftwareData *>&1 Write-VariableToConsoleAndFile -VariableToParse $dependentSoftwareCheckResult -Log $logPath # Windows Feature Checktype $windowsFeatureData = $manifestData.readinessChecks | Where-Object { $_.checkType -eq "windowsFeature" -and $_.resourceToCheck -contains "$ResourceToCheck" } $windowsFeatureCheckResult = Invoke-WindowsFeatureCheck -Data $windowsFeatureData *>&1 Write-VariableToConsoleAndFile -VariableToParse $windowsFeatureCheckResult -Log $logPath } } function Write-VariableToConsoleAndFile { param ( [object[]] $VariableToParse, [string] $Log ) ForEach ($line in $($VariableToParse -split "`r`n")) { if ($line -like '*`[+`]*') { Write-Host $line -Foregroundcolor Green $line | Out-File -FilePath $Log -Append } elseif ($line -like '*`[-`]*') { Write-Host $line -Foregroundcolor Red $line | Out-File -FilePath $Log -Append } else { Write-Host $line $line | Out-File -FilePath $Log -Append } } } <# .SYNOPSIS Start or restart an application pool .DESCRIPTION Uses methods provided in the WebAdministration module to make an attempt to start or restart a provided application pool. .PARAMETER AppPoolName IIS Application Pool name .PARAMETER ErrorLevel Error level logged when unsuccessful in starting or restarting the provided application pool. Defaulted to "Error" .EXAMPLE Invoke-DosRecycleAppPool -AppPoolName "x" -ErrorLevel "Fatal" #> function Invoke-DosRecycleAppPool { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateNotNullorEmpty()] [ValidateLength(1, 64)] [ValidateScript( { if ($_ -match '[^a-zA-Z0-9]') { Write-DosMessage -Level "Fatal" -Message "$_ must only contain alphanumeric values. Please remove special characters." } else { $true } })] [string] $AppPoolName, [ValidateSet("Warning", "Error", "Fatal")] [string] $ErrorLevel = "Error" ) Test-ElevatedPermission Import-Module WebAdministration -Force try { $appPoolState = Get-WebAppPoolState -Name $config.appPoolName -ErrorAction Stop if ($appPoolState.Value -ne 'Started') { try { Start-WebAppPool -Name $config.appPoolName -ErrorAction Stop Write-DosMessage -Level "Information" -Message "Started Application Pool: $($config.appPoolName)" } catch { Write-DosMessage -Level $ErrorLevel -Message $_.Exception.message } } else { try { Restart-WebAppPool $config.appPoolName Write-DosMessage -Level "Information" -Message "Restarted Application Pool: $($config.appPoolName)" } catch { Write-DosMessage -Level $ErrorLevel -Message $_.Exception.message } } } catch { Write-DosMessage -Level "Fatal" -Message $_.Exception.message } } <# .SYNOPSIS Executes a query .DESCRIPTION Executes TSQL against a SQL Server .PARAMETER SqlConnection The sql connection to use to execute the query .PARAMETER ConnectionString The connection string used to create a new connection. .PARAMETER InstanceName Instance name for use in creating an AdHoc connection .PARAMETER DatabaseName Database name for use in creating an AdHoc connection .PARAMETER Credentail An optional PsCredential object if using sql auth .PARAMETER Query TSQL to execute .PARAMETER Parameters A hashtable of parameters to build a parameterized query .PARAMETER AsResult Return results as DataSet, DataTable, or array or DataRows .PARAMETER NonQuery Executes the query as ExecuteNonQuery() This will allow you to run the query and then suppress the results output .PARAMETER CommandTimeout The execution timeout in seconds (default is 30) By default, all executions of Invoke-DosSqlQuery have a 30 second timeout. Use this parameter to increase the timeout to any number of seconds. Or pass in 0 for no timeout. .INPUTS None. .OUTPUTS [System.Data.DataSet], [System.Data.DataTable], [System.Data.DataRow[]] .EXAMPLE PSn Invoke-DosSqlQuery ... #> function Invoke-DosSqlQuery { [cmdletbinding(SupportsShouldProcess=$true)] [OutputType([System.Data.DataSet],[System.Data.DataTable],[System.Data.DataRow[]])] param( [parameter(Mandatory=$true,ParameterSetName='Connection')][Data.SqlClient.SqlConnection]$SqlConnection, [parameter(Mandatory=$true,ParameterSetName='ConnectionString')][string]$ConnectionString, [parameter(Mandatory=$true,ParameterSetName='AdHoc')][string]$InstanceName, [parameter(Mandatory=$false,ParameterSetName='AdHoc')][string]$DatabaseName, [parameter(Mandatory=$false,ParameterSetName='AdHoc')][PSCredential]$Credential, [parameter(Mandatory=$true)][string]$Query, [parameter(Mandatory=$false)][hashtable]$Parameters, [parameter(Mandatory=$false)][ValidateSet("DataSet", "DataTable", "Array")][string]$AsResult='Array', [parameter(Mandatory=$false)][switch]$NonQuery, [parameter(Mandatory=$false)][int]$CommandTimeout = 30, [parameter(Mandatory=$false,ValueFromRemainingArguments=$true)]$Arguments ) <# This function is going to be splatted on by other functions $PSBoundParametes as they share common parameter sets for connectivity. Non defined parameters lands in $Arguments which dont care about nor do we want to pass that only New-SqlConnection. Remove $Arguments from our $PSBoundParameters #> $PSBoundParameters.Remove('Arguments') | Out-Null $PSBoundParameters.Remove('ErrorAction') | Out-Null if(@('AdHoc','ConnectionString').Contains($PSCmdlet.ParameterSetName)){ $SqlConnection = New-DosSqlConnection @PSBoundParameters -ErrorAction Stop } $Cmd = New-Object System.Data.SqlClient.SqlCommand($Query,$SqlConnection) $Cmd.CommandTimeout = $CommandTimeout if($null -ne $Parameters){ foreach($key in $Parameters.Keys){ $cmd.Parameters.AddWithValue($Key,$Parameters[$Key]) | Out-Null } } if($NonQuery){ if ($pscmdlet.ShouldProcess($Query, "Executing non SQL query")){ Invoke-ExecuteNonQuery -cmd $cmd } } else{ if ($pscmdlet.ShouldProcess($Query, "Executing SQL Query")){ $results = Invoke-Fill -cmd $cmd -AsResult $AsResult } } if(@('AdHoc','ConnectionString').Contains($PSCmdlet.ParameterSetName)){ if ($pscmdlet.ShouldProcess($ConnectionString, "Closing SQL connection")){ Invoke-CloseAndDispose -SqlConnection $SqlConnection } } if($NonQuery -eq $false){ return $results } } function Invoke-CloseAndDispose{ param( [Data.SqlClient.SqlConnection]$SqlConnection ) $SqlConnection.Close() | Out-Null $SqlConnection.Dispose() | Out-Null } function Invoke-ExecuteNonQuery{ param( [System.Data.SqlClient.SqlCommand]$cmd ) $cmd.ExecuteNonQuery() | Out-Null } function Invoke-Fill{ param( [System.Data.SqlClient.SqlCommand]$cmd ,[string]$AsResult ) $ds=New-Object system.Data.DataSet $da=New-Object system.Data.SqlClient.SqlDataAdapter($cmd) $da.Fill($ds) | Out-Null switch($AsResult){ 'DataSet'{ $results = $ds } 'DataTable'{ $results = $ds.Tables[0] } 'Array'{ $results = @() $results += $ds.Tables[0].Rows } } return $results } <# .SYNOPSIS Merges hashtables together .DESCRIPTION When working with DOS configuration values that come from varying places (some common examples may be a config store, pipeline, or static) it is nice to be able to merge these configurations into a single $config hashtable. Merge-DosHashtable combines all key values pairs creating a single hashtable. This method when used in conjunction with Push-DosConfigType keeps a merged "_type_" attribute. See Push-DosConfigType for more information about this. .OUTPUTS HASHTABLE A: Name Value ---- ----- clientName Health Catalyst _type_ {clientName} HASHTABLE B: Name Value ---- ----- ServiceName Test Service _type_ {ServiceName} OUTPUT: Name Value ---- ----- clientName Health Catalyst ServiceName Test Service _type_ {clientName, ServiceName} .EXAMPLE $storeConfig = @{ clientName = 'Health Catalyst' } | Push-DosConfigType "store" $staticConfig = @{ ServiceName = 'Test Service' } | Push-DosConfigType "static" $config = $storeConfig, $staticConfig | Merge-DosHashtable #> function Merge-DosHashtable { [CmdletBinding()] param ( [Parameter(ValueFromPipeline)] $hash ) $output = @{} $subHash = @{} foreach ($hash in $input) { if ($hash -is [Hashtable]) { foreach ($key in $hash.Keys) { if ($key -ne "_type_") { $output.$key = $hash.$key } else { foreach ($subKey in $hash[$key].Keys) { $subHash.$subKey = $hash[$key].$subKey } } } } } $output.Add("_type_", $subHash) return $output } <# .SYNOPSIS Creates a new ConfigStore .DESCRIPTION Attempts to create a new config store .PARAMETER configStore The configstore hashtable that will be created .OUTPUTS $configStore - the $configStore hashtable is returned .EXAMPLE New-DosConfigStore -configStore @{Type = "File"; Format = "XML"; Path = "$PSScriptRoot\nonexistent\nonexistent.config"} New-DosConfigStore -configStore $configStore #> function New-DosConfigStore { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Justification="WhatIf support not implemented.")] [CmdletBinding()] param( [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [hashtable] $configStore ) Write-DosTelemetry -Message "$($MyInvocation.MyCommand.Name) called." if($configStore.Type -eq "File" -and $configStore.Format -eq "XML"){ return New-DosConfigStoreXml -configStore $configStore } else { Write-DosMessage -Level 'Fatal' -Message "New-DosConfigStore not implemented yet for type: $($configStore.Type), format: $($configStore.Format)" return $null } } function New-DosDacPacPublishFile { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Justification="WhatIf support not implemented.")] param ( [Parameter(Mandatory = $true)] [string] $publishProfilePath, [Parameter(Mandatory = $true)] [hashtable] $publishProfileValues ) begin { Write-DosMessage -Level "Information" -Message "Creating Dacpac Publish Profile $publishProfilePath" [xml]$publishProfileXml = @' <?xml version="1.0" encoding="utf-8"?> <Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> </Project> '@ } process { try { Add-PublishDacPacFile -publishProfileXml $publishProfileXml -publishProfileValues $publishProfileValues -publishProfilePath $publishProfilePath Write-DosMessage -Level "Information" -Message "Saving changes to file $publishProfilePath" } catch { $ErrorMessage = $_.Exception.Message Write-DosMessage -Level "Fatal" -Message $ErrorMessage } } } function Add-PublishDacPacFile { param ( [xml] $publishProfileXml, [hashtable] $publishProfileValues, [string] $publishProfilePath ) $project = $publishProfileXml.Project $itemGroup = $publishProfileXml.CreateElement("ItemGroup", $project.xmlns) foreach ($publishProfileValue in $publishProfileValues.GetEnumerator()) { $sqlCmdVariable = $publishProfileXml.CreateElement("SqlCmdVariable", $project.xmlns) $include = $publishProfileXml.CreateAttribute("Include") $include.Value = $publishProfileValue.Name $value = $publishProfileXml.CreateElement("Value", $project.xmlns) $value.InnerText = $publishProfileValue.Value $sqlCmdVariable.AppendChild($value) | Out-Null $sqlCmdVariable.Attributes.Append($include) | Out-Null $itemGroup.AppendChild($sqlCmdVariable) | Out-Null Write-DosMessage -Level "Information" -Message "Added $($publishProfileValue.Name) -> $($publishProfileValue.Value)" } $project.AppendChild($itemGroup) | Out-Null $publishProfileXml.Save($publishProfilePath) } <# .SYNOPSIS Creates a SQL connection .DESCRIPTION Creates and returns an ***open*** SQL connection. .PARAMETER InstanceName The full <ComputerName>/<NamedInstance> instance name of SQL server .PARAMETER DatabaseName The database name to connect too .PARAMETER Credential A credential containing the SQL auth user and password .PARAMETER ConnectionString A SQL connection string to build the connection from .INPUTS None. .OUTPUTS Data.SqlClient.SqlConnection .EXAMPLE PS> New-DosSqlConnection ... #> function New-DosSqlConnection { [cmdletbinding(DefaultParameterSetName='Default',SupportsShouldProcess=$true)] [OutputType([Data.SqlClient.SqlConnection])] param( [parameter(Mandatory=$true,ParameterSetName='Default')] [parameter(Mandatory=$true,ParameterSetName='SqlAuth')] [string]$InstanceName, [parameter(Mandatory=$false,ParameterSetName='Default')] [parameter(Mandatory=$false,ParameterSetName='SqlAuth')] [string]$DatabaseName='master', [parameter(Mandatory=$true,ParameterSetName='SqlAuth')][PSCredential]$Credential, [parameter(Mandatory=$true,ParameterSetName='ConnectionString')][string]$ConnectionString, [parameter(Mandatory=$false,ParameterSetName='Default',ValueFromRemainingArguments=$true)] [parameter(Mandatory=$false,ParameterSetName='SqlAuth',ValueFromRemainingArguments=$true)] [parameter(Mandatory=$false,ParameterSetName='ConnectionString',ValueFromRemainingArguments=$true)] $Arguments ) switch($PSCmdlet.ParameterSetName){ 'ConnectionString'{} 'SqlAuth'{ $ConnectionString = "Server={0};Database={1};User ID={2};Password={3};Pooling=false" -f $InstanceName, $DatabaseName, $Credential.UserName, $Credential.GetNetworkCredential().Password } 'Default'{ $ConnectionString = "Server={0};Database={1};Integrated Security=True" -f $InstanceName, $DatabaseName } } try{ if ($pscmdlet.ShouldProcess($ConnectionString, "Creating SQL connection")){ $connection = New-Object Data.SqlClient.SqlConnection $ConnectionString $connection.Open() } } catch{ Write-DosMessage -Level 'Fatal' -Message "$_.Exception.Message" } return $connection } <# .SYNOPSIS Creates SQL login .DESCRIPTION Creates an instance level SQL login .PARAMETER InstanceName Full instance name of SQL server .PARAMETER ConnectionString Connection string to a SQL instnace .PARAMETER SqlConnection SQL connection to target SQL Server .PARAMETER LoginName The full name of the login to create .PARAMETER AuthenticationType Windows or SQL .PARAMETER Credential The cred (user/pass) to set for the new login .PARAMETER IfNotExists A switch to only create if login does not exist .INPUTS None .OUTPUTS None .EXAMPLE PS> New-DosSqlLogin -ConnectionString $connectionString -LoginName 'domain\username' -IfNotExists Replaces New-SqlLogin #> function New-DosSqlLogin { [cmdletbinding(SupportsShouldProcess=$true)] [OutputType()] param( [parameter(Mandatory=$true,ParameterSetName='Connection')][Data.SqlClient.SqlConnection]$SqlConnection, [parameter(Mandatory=$true,ParameterSetName='ConnectionString')][string]$ConnectionString, [parameter(Mandatory=$true,ParameterSetName='AdHoc')][string]$InstanceName, [parameter(Mandatory=$false,ParameterSetName='AdHoc')][PSCredential]$Credential, [parameter(Mandatory=$true)][string]$LoginName, [parameter(Mandatory=$false)][ValidateSet("Windows", "SQL")][string]$AuthType='Windows', [parameter(Mandatory=$false)][pscredential]$NewLoginCredential, [parameter(Mandatory=$false)][switch]$IfNotExists ) if($AuthType -eq 'SQL' -and $null -eq $Credential){ throw [System.Management.Automation.ParameterBindingException] "Credential must be provided when using an AuthType of SQL" } elseif($AuthType -eq 'Windows' -and $null -ne $Credential){ throw [System.Management.Automation.ParameterBindingException] "Credential cannot be provided when using an AuthType of Windows" } $query = "" if($IfNotExists){ $query += "IF NOT EXISTS (SELECT * FROM [sys].[server_principals] WHERE name = '$LoginName')`n" } switch($AuthType){ 'Windows'{ $query += "CREATE LOGIN [$LoginName] FROM WINDOWS WITH DEFAULT_DATABASE=[master]" } 'SQL'{ $query += "CREATE LOGIN [$LoginName] WITH PASSWORD=N'$($Credential.GetNetworkCredential().Password.Replace("'","''"))', DEFAULT_DATABASE=[master], CHECK_EXPIRATION=ON, CHECK_POLICY=ON" } } $PSBoundParameters.Remove('ErrorAction') | Out-Null if($PSCmdlet.ShouldProcess($Query,"Creating login: $LoginName")){ Invoke-DosSqlQuery @PSBoundParameters -Query $query -NonQuery | Out-Null } } <# .SYNOPSIS Creates SQL database user .DESCRIPTION Creates an SQL database user in a given database .PARAMETER InstanceName Full instance name of SQL server .PARAMETER ConnectionString Connection string to a SQL instnace .PARAMETER SqlConnection SQL connection to target SQL Server .PARAMETER DatabaseName Database name in which to create the user .PARAMETER UserName The full name of the user to create .PARAMETER IfNotExists A switch to only create if user does not exist .INPUTS None .OUTPUTS None .EXAMPLE PS> New-DosSqlUser -ConnectionString $connectionString -UserName 'domain\username' -IfNotExists Replaces New-SqlUser #> function New-DosSqlUser { [cmdletbinding(SupportsShouldProcess=$true)] [OutputType()] param( [parameter(Mandatory=$true,ParameterSetName='Connection')][Data.SqlClient.SqlConnection]$SqlConnection, [parameter(Mandatory=$true,ParameterSetName='ConnectionString')][string]$ConnectionString, [parameter(Mandatory=$true,ParameterSetName='AdHoc')][string]$InstanceName, [parameter(Mandatory=$true,ParameterSetName='AdHoc')][string]$DatabaseName, [parameter(Mandatory=$false,ParameterSetName='AdHoc')][PSCredential]$Credential, [parameter(Mandatory=$true)][string]$UserName, [parameter(Mandatory=$false)][switch]$IfNotExists ) $query = "" if($IfNotExists){ $query += "IF NOT EXISTS (SELECT * FROM sys.database_principals WHERE name = N'$UserName')`n" } $query += "CREATE USER [$UserName] FOR LOGIN [$UserName] WITH DEFAULT_SCHEMA=[dbo]" $PSBoundParameters.Remove('ErrorAction') | Out-Null if($PSCmdlet.ShouldProcess($Query,"Creating user: $UserName")){ Invoke-DosSqlQuery @PSBoundParameters -Query $query -NonQuery | Out-Null } } <# .SYNOPSIS Publishes the target DOS dac file to the specified database with the specified options .DESCRIPTION Uses Microsoft.SqlServer.Dac.DacServices to install DAC file to specified server. .PARAMETER DacPacFilePath File path to dacpac file to publish to server .PARAMETER TargetSqlInstance Sql server connection string supports specifiying non-default sql instance and port if needed .PARAMETER TargetDb Target database to publish dac pac to. .PARAMETER PublishOptionsFilePath Path to publish options file - Required - See tests/SampleFiles/DefaultDacDeployOptions.xml for an example .PARAMETER ForceMountPointCreation Will attempt to create the mount points specified in PublishOptionsFilePath if the folders don't exist. If the specified on an upgrade, a warning will be displayed stating that mount points will be whatever the current DB has set. .EXAMPLE Publish-DosDacPac -DacPacFilePath ".\test.dac" -TargetSqlInstance "localhost" -TargetDb "EDWAdmin" -PublishOptionsFilePath ".\test.publish.xml" Publish-DosDacPac -DacPacFilePath ".\test.dac" -TargetSqlInstance "localhost,1433\MSSQLServer" -TargetDb "EDWAdmin" -PublishOptionsFilePath ".\test.publish.xml" #> function Publish-DosDacPac { [CmdletBinding()] param( [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [ValidateScript({ if (!(Test-Path $_)) { Write-DosMessage -Level "Error" -Message "DacPacFilePath $_ does not exist. Please enter valid path." -ErrorAction Stop } else { $true } })] [string] $DacPacFilePath, [Parameter(Mandatory=$true)] [string] $TargetSqlInstance, [Parameter(Mandatory=$true)] [string] $TargetDb, [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [ValidateScript({ if (!(Test-Path $_)) { Write-DosMessage -Level "Error" -Message "PublishOptionsFilePath $_ does not exist. Please enter valid path." -ErrorAction Stop } else { $true } })] [string] $PublishOptionsFilePath, [switch] $ForceMountPointCreation, [string] $MountPointComputerName = $TargetSqlInstance ) Write-DosMessage -Level "Information" -Message "Publish-DosDacPac start" Write-DosMessage -Level "Information" -Message "Publishing '$DacPacFilePath' to '$TargetDb'" $dbatoolsVersion = New-Object -TypeName System.Version -ArgumentList "1.0.115" Write-DosMessage -Level "Information" -Message "Attempting to install dbatools with a version of '$dbatoolsVersion'" Write-DosMessage -Level "Information" -Message "Checking for required modules: dbatools '$dbatoolsVersion'" Install-RequiredModule -ModuleName dbatools -RequiredVersion $dbatoolsVersion Write-DosMessage -Level "Information" -Message "Successfully installed dbatools." $previousDataBase = Get-DosDbaDatabase -SqlInstance $TargetSqlInstance -Database $TargetDb $previousRecoveryModel = $null [string] $previousDbOwner = $null if($null -ne $previousDataBase){ Write-DosMessage -Level "Information" -Message "Found existing DB $TargetDb on $TargetSqlInstance, will ensure that previous owner and recovery model are preserved across update" $previousDbOwner = $previousDataBase.Owner $previousRecoveryModel = $previousDataBase.RecoveryModel if($ForceMountPointCreation){ Write-DosMessage -Level "Warning" -Message "Previous database already installed, existing mount points will be used" } } else{ [xml] $parsedPublishOptions = [xml] (Get-Content $PublishOptionsFilePath -ErrorAction Stop) $sqlCmdVariable = $parsedPublishOptions.Project.ItemGroup.SqlCmdVariable try { Write-DosMessage -Level "Information" -Message "Beginning attempt to create database mount points" $dataMountPoints = $sqlCmdVariable | Where-Object {$_.Include -like "*Data*MountPoint" -or $_.Include -like "PrimaryMountPoint"} if($null -eq $dataMountPoints) { Write-DosMessage -Level "Error" -Message "Missing data mount point in $PublishOptionsFilePath" return } foreach($dataMountPoint in $dataMountPoints) { Write-DosMessage -Level "Information" -Message "Beginning attempt to create mount point $dataMountPoint" Add-MountPoint -Path $dataMountPoint.Value -CreateIfForced $ForceMountPointCreation.IsPresent -MountPointComputerName $MountPointComputerName } foreach($logMountPoint in $sqlCmdVariable | Where-Object {$_.Include -like "*Log*MountPoint"}) { Write-DosMessage -Level "Information" -Message "Beginning attempt to create mount point $logMountPoint" Add-MountPoint -Path $logMountPoint.Value -CreateIfForced $ForceMountPointCreation.IsPresent -MountPointComputerName $MountPointComputerName } foreach($indexMountPoint in $sqlCmdVariable | Where-Object {$_.Include -like "*Index*MountPoint"}) { Write-DosMessage -Level "Information" -Message "Beginning attempt to create mount point $indexMountPoint" Add-MountPoint -Path $indexMountPoint.Value -CreateIfForced $ForceMountPointCreation.IsPresent -MountPointComputerName $MountPointComputerName } Write-DosMessage -Level "Information" -Message "Finished attempt to create database mount points - Success" } catch { Write-DosMessage -Level "Fatal" -Message "Error occured while attempting to confirm '$TargetDb' mount points. Validate the connection capabilites of '$MountPointComputerName'. Exception: $($_.Exception)" Write-DosMessage -Level "Information" -Message "Finished attempt to create database mount points - Failure" } } try{ $outputPath = "$(Get-DosBaseInstallerPath)\SetupContent\" } catch { Write-DosMessage -Level 'Warning' -Message "Unable to find SetupContent folder, Publish-DbaDacPackage will record output to $($env:Temp)" $outputPath = $env:TEMP } Write-DosMessage -Level 'Information' -Message "DacPac Deploy Log Folder: $outputPath" try { Write-DosMessage -Level Information -Message "Beginning Dacpac deployment" $dacpacReport = Publish-DbaDacPackage -SqlInstance $TargetSqlInstance -Database $TargetDb -Path $DacPacFilePath -PublishXml $PublishOptionsFilePath -EnableException -GenerateDeploymentScript -OutputPath $outputPath Write-DosMessage -Level Information -Message "$dacpacReport" Write-DosMessage -Level Information -Message "Finished Dacpac deployment" $currentDatabase = Get-DosDbaDatabase -SqlInstance $TargetSqlInstance -Database $TargetDb if($null -ne $previousDataBase){ Write-DosMessage -Level Information -Message "Checking that Recovery Model and DbOwner settings are preserved on the database" if($currentDatabase.RecoveryModel -ne $previousRecoveryModel){ Write-DosMessage -Level "Information" -Message "New recovery model $($currentDatabase.RecoveryModel) doesn't match previous recovery model $previousRecoveryModel, reverting to previous recovery model" Set-DbaDbRecoveryModel -RecoveryModel $previousRecoveryModel.ToString() -SqlInstance $TargetSqlInstance -Database $TargetDb -Confirm:$false -EnableException } if($currentDatabase.Owner -ne $previousDbOwner){ Write-DosMessage -Level "Information" -Message "New DB owner $($currentDatabase.Owner) doesn't match previous owner, reverting to old owner $previousDbOwner" Set-DbaDbOwner -SqlInstance $TargetSqlInstance -Database $TargetDb -TargetLogin $previousDbOwner -Confirm:$false -EnableException } } } catch { Write-DosMessage -Level "Error" -Message "Unable to deploy $DacPacFilePath to $TargetDb on $TargetSqlInstance. Exception: $($_.Exception)" Write-DosMessage -Level "Information" "Finished Dacpac deployment - failure" } Write-DosMessage -Level "Information" -Message "Publish-DosDacPac complete" Write-DosMessage -Level "Information" -Message "Publishing '$DacPacFilePath' to '$TargetDb' completed" } function Add-MountPoint{ [CmdletBinding()] param( [string] $Path, [bool] $CreateIfForced, [string] $MountPointComputerName ) $HostcomputerName = hostname $scriptBlock = { $CreateIfForced = $args[1] $Path = $args[0] if($CreateIfForced){ if(!(Test-Path $Path)){ try { New-Item -ItemType Directory $Path | Out-Null return "Mount point $Path created" } catch { return "Error creating mount point $Path. Exception: $($_.Exception)" } } } else{ if(!(Test-Path $Path)){ return "Mount point $Path not found use -ForceMountPointCreation to enable creation of necessary folders" } } } Test-ElevatedPermission Write-DosMessage -Level "Information" -Message "Validating $Path Mount Point on $MountPointComputerName" $params = @{ ScriptBlock = $scriptBlock ArgumentList = @($Path, $CreateIfForced) } $MountPointHostname = $MountPointComputerName.split('.')[0] if($MountPointHostname -match "^\d+$"){ #if the $MountPointHostname is a number (e.g. because it is actually an ip address), just use the full computer name $MountPointHostname = $MountPointComputerName } if($MountPointHostname -eq 'localhost' -or $MountPointHostname -eq '127.0.0.1'){ #don't add computer name to the parameters Write-DosMessage -Level "Debug" -Message "It appears that '$MountPointComputerName' is localhost, so we run the Invoke-Command locally" } elseif($MountPointHostname -eq $HostcomputerName) { #don't add computer namme to the parameters Write-DosMessage -Level "Debug" -Message "It appears that '$HostcomputerName' is the same as '$MountPointComputerName', so we run the Invoke-Command locally" } else { try{ $IpAddressMountPointHostname = $(Test-NetConnection -computername $MountPointComputerName -ErrorAction SilentlyContinue -WarningAction SilentlyContinue).RemoteAddress.IPAddressToString $IpAddressLocalhost = $(Test-NetConnection -computername $HostcomputerName -ErrorAction SilentlyContinue -WarningAction SilentlyContinue).RemoteAddress.IPAddressToString if($IpAddressMountPointHostname -eq $IpAddressLocalhost){ #don't add computer name to the parameters Write-DosMessage -Level "Debug" -Message "It appears that '$IpAddressMountPointHostname' is the same as '$IpAddressLocalhost', so we run the Invoke-Command locally" } else { Write-DosMessage -Level "Debug" -Message "It appears that '$HostcomputerName' is a different computer than '$MountPointComputerName', so we'll use PS Remoting" $params.Add("ComputerName", $MountPointComputerName) } } catch { Write-DosMessage -Level "Debug" -Message "It appears that '$HostcomputerName' is a different computer than '$MountPointComputerName', so we'll use PS Remoting." $params.Add("ComputerName", $MountPointComputerName) } } Write-DosMessage -Level "Information" -Message "Running invoke-command" $addMountResult = Invoke-Command @params if ($addMountResult -like "*use -ForceMountPointCreation*" -or $addMountResult -like "Error creating mount point*") { Write-DosMessage -Level "Error" -Message $addMountResult } if ($addMountResult -eq "Mount point $Path created") { Write-DosMessage -Level "Information" -Message $addMountResult } } <# .SYNOPSIS Publishes the target DOS web application with the specified options .DESCRIPTION Publishes the target DOS web application with the specified options. Either uses WebDeploy (for older apps), or internal logic for new .Net Core applicaitons. .PARAMETER WebAppPackagePath File path to web application zip to publish .PARAMETER SettingsXmlPath ONLY USED in WebDeploy Applications - File path to xml settings .PARAMETER AppPoolName IIS Application Pool name .PARAMETER AppPoolCredential Credential object used to configure the built-in account the IIS Application Pool runs as .PARAMETER AuthenticationType ONLY USED in WebDeploy Applications - Windows or Anonymous authentication .PARAMETER WebDeploy A toggle for the web application to be deployed via WebDeploy or by other means .PARAMETER AppName ONLY USED in NON-Webdeploy applications. Specifies both the application's name AND the folder name where the application will be placed underneath the IIS site's root folder. .PARAMETER IISWebSite ONLY USED in NON-WebDeploy applications. Specifies the IIS site to publish the application to. Defaults to "Default Web Site" .PARAMETER WebDeployParameters ONLY USED in WebDeploy Applications - Arraylist object containing site settings .PARAMETER PathsToPreserve Array of paths to preserve during a deployment, such as logs, relative to the install directory in IIS, so they are not removed during the upgrade of an application. Ignored for new installs. .EXAMPLE Publish-DosWebApplication -WebAppPackagePath "testapp.zip" -SettingsXmlPath "testapp.settings.xml" -AppPoolName "x" -AppPoolCredential $creds -AuthenticationType "Windows" -WebDeploy -WebDeployParameters $webDeployParams -PathsToPreserve @("logs") #> function Publish-DosWebApplication { [CmdletBinding()] param( [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [ValidateScript({ if (!(Test-Path $_)) { Write-DosMessage -Level "Error" -Message "$_ does not exist. Please enter valid path." -ErrorAction Stop } else { $true } })] [string] $WebAppPackagePath, [string] $SettingsXmlPath, [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [ValidateLength(1,64)] [ValidateScript({ if ($_ -match '[^a-zA-Z0-9]') { Write-DosMessage -Level "Error" -Message "$_ must only contain alphanumeric values. Please remove special characters." -ErrorAction Stop } else { $true } })] [string] $AppPoolName, [PSCredential] $AppPoolCredential, [ValidateSet("Windows", "Anonymous")] [string[]] $AuthenticationType, [switch] $WebDeploy, [string] $AppName, [string] $IISWebSite = "Default Web Site", [System.Collections.ArrayList] $WebDeployParameters, [switch] $NoCredential, [string[]] $PathsToPreserve = @() ) Test-ElevatedPermission Import-Module WebAdministration -Force if ($PathsToPreserve.Length -gt 0) { for ($i = 0; $i -lt $PathsToPreserve.Length; $i++) { $PathsToPreserve[$i] = $PathsToPreserve[$i].Trim("\", "/", " ") } } ###Only allow one source of input for webdeploy args try { if ($NoCredential.IsPresent) { Write-DosMessage -Level "Information" -Message "The NoCredential parameter was provided. Proceeding to application pool validation." if(!(Test-Path "IIS:\AppPools\$AppPoolName" -PathType Container)){ New-AppPool -IISAppPoolName $AppPoolName } else { Set-AppPoolSettings -IISAppPoolName $AppPoolName -NoCredential $NoCredential.IsPresent } } else { if(!(Test-Path "IIS:\AppPools\$AppPoolName" -PathType Container)){ if($AppPoolCredential -eq $null){ Write-DosMessage -Level "Fatal" -Message "No app pool found named $AppPoolName and no credentials specified. Please specify credentials -AppPoolCredential if you want to create a new application pool" return } New-AppPool -IISAppPoolName $AppPoolName -IdentityCredential $AppPoolCredential } else { $existingAppPool = Get-Item -Path "IIS:\AppPools\$AppPoolName" # if app pool exists and credential is passed in / checks if the existing credential is the same / if it differs we fail out if ($null -ne $AppPoolCredential) { if ($existingAppPool.processModel.userName -ne $AppPoolCredential.UserName -or $existingAppPool.processModel.password -ne $AppPoolCredential.GetNetworkCredential().Password) { Write-DosMessage -Level "Fatal" -Message "The '$AppPoolName' app pool has an identity configured that differs from the identity credential provided. Halting deployment." } } Set-AppPoolSettings -IISAppPoolName $AppPoolName -IdentityCredential $AppPoolCredential Write-DosMessage -Level "Information" -Message "Application Pool: $AppPoolName already exists. Deploying '$WebAppPackagePath' to $AppPoolName" } } $appPool = Get-Item "IIS:\AppPools\$AppPoolName" $appPool.Start() } catch { Write-DosMessage -Level "Error" -Message "Error occured getting, creating, or updating a IIS application pool. Exception: $($_.Exception)" return } #Deploy Website to IIS if ($WebDeploy.IsPresent){ if([String]::IsNullOrEmpty($SettingsXmlPath) -and ($null -eq $WebDeployParameters)){ Write-DosMessage -Level "Error" -Message "Must provide parameters through a settings xml file or webdeployparameters when deploying a web deploy application" return } if($SettingsXmlPath) { try { [xml] $parsedXml = Get-Content $SettingsXmlPath } catch { Write-DosMessage -Level "Error" -Message "Error occured parsing xml settings file. Exception: $($_.Exception)" return } } try { Write-DosMessage -Level "Information" -Message "Attempting to retrieve the IIS web application information." $siteName,$appNameFromSettings = Get-IisWebAppInfo -SettingsXmlPath $SettingsXmlPath -WebDeployParameters $WebDeployParameters -ParsedXml $parsedXml Write-DosMessage -Level "Information" -Message "Successfully retrieved the IIS web application information." Write-DosMessage -Level "Information" -Message "Deploying '$appNameFromSettings' through WebDeploy." } catch { Write-DosMessage -Level "Error" -Message "Unable to find node containing IIS Web Application Name values" return } Publish-WebDeployWebApp -WebDeployPackageFilePath $WebAppPackagePath -WebDeployParameterFilePath $SettingsXmlPath -WebParameters $WebDeployParameters -AppName $appNameFromSettings -IISWebSite $IISWebSite -AppPoolName $AppPoolName -PathsToPreserve $PathsToPreserve Set-ApplicationPool -SiteName $siteName -AppName $appNameFromSettings -AppPoolName $AppPoolName Set-AuthenticationType -SiteName $siteName -AppName $appNameFromSettings -AuthenticationType $AuthenticationType Write-DosTelemetry -Message "Publish-DosWebApplication called and published using Publish-WebDeployWebApp." } else { if([string]::IsNullOrEmpty($AppName)){ Write-DosMessage -Level "Fatal" -Message "AppName must be non-null and non empty" } Publish-DotNetCoreWebApp -WebApplicationPackagePath $WebAppPackagePath -AppName $AppName -IISWebSite $IISWebSite -AppPoolName $AppPoolName -PathsToPreserve $PathsToPreserve Set-ApplicationPool -SiteName $IISWebSite -AppName $AppName -AppPoolName $AppPoolName if (-Not ([string]::IsNullOrEmpty($AuthenticationType))) { Install-UrlRewrite Set-AuthenticationType -SiteName $IISWebSite -AppName $AppName -AuthenticationType $AuthenticationType } Write-DosTelemetry -Message "Publish-DosWebApplication called and published using Publish-DotNetCoreWebApp." } } function Get-IisWebAppInfo { param ( [string] $SettingsXmlPath, [System.Collections.ArrayList] $WebDeployParameters, [xml] $ParsedXml ) if($SettingsXmlPath){ # Parameter childnodes can be different $iisParameter = $ParsedXml.parameters.ChildNodes | Where-Object { $_.name -eq "IIS Web Application Name" } # Parameter attribute names differ (eg. defaultValue and value) $iisAppPath = $iisParameter.Attributes | Where-Object { $_ -like "*value" } $siteName,$appNameFromSettings = $iisAppPath.'#text'.split('/') }else{ # Parameter attribute names differ (eg. defaultValue and value) foreach ($param in $WebDeployParameters) { if($param.Name -eq "IIS Web Application Name"){ $iisAppPath = $param.Value } } $siteName,$appNameFromSettings = $iisAppPath.split('/') } return $siteName,$appNameFromSettings } function Set-ApplicationPool { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Justification="WhatIf support not implemented.")] param ( [string] $SiteName, [string] $AppName, [string] $AppPoolName ) try { # Set-Application Pool with specific app Push-Location -Path IIS:\Sites\$SiteName\ Set-ItemProperty -Path $AppName -Name applicationPool -Value $AppPoolName } catch { Write-DosMessage -Level "Error" -Message "Error occured associating application to the app pool. Exception: $($_.Exception)" return } finally { Pop-Location } } function Set-AuthenticationType { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Justification="WhatIf support not implemented.")] param ( [string] $SiteName, [string] $AppName, [string[]] $AuthenticationType ) try{ Push-Location -Path IIS:\Sites\$SiteName\$AppName # Set-Authentication and transform the web config Set-IISAuthentication -AuthenticationType $AuthenticationType -SiteName $SiteName -ApplicationName $AppName } catch { Write-DosMessage -Level "Error" -Message "Error occured altering authentication types. Exception: $($_.Exception)" return } finally { Pop-Location } } function Install-UrlRewrite { # Check if URL Rewrite is installed before setting authentication $urlRewriteRegistry = "HKLM:\SOFTWARE\Microsoft\IIS Extensions\URL Rewrite" if (-Not (Test-Path $urlRewriteRegistry)) { Write-DosMessage -Level "Information" -Message "UrlRewrite not installed" # Install Web Platform Installer if not present if (-Not (Test-Path "$($env:ProgramFiles)\Microsoft\Web Platform Installer")) { Write-DosMessage -Level "Information" -Message "Web Platform Installer not found" Get-WebRequestDownload "https://go.microsoft.com/fwlink/?LinkId=287166" -OutFile "$PSScriptRoot\Web-Platform-Install.msi" Write-DosMessage -Level "Information" -Message "Installing Web Platform Installer" Start-Process "$PSScriptRoot\Web-Platform-Install.msi" '/qn' -PassThru | Wait-Process Remove-Item "$PSScriptRoot\Web-Platform-Install.msi" } # Install UrlRewrite using Web Platform Installer if (Test-Path "$($env:ProgramFiles)\Microsoft\Web Platform Installer\WebpiCmd.exe") { Write-DosMessage -Level "Information" -Message "Installing UrlRewrite" Start-Process "$($env:ProgramFiles)\Microsoft\Web Platform Installer\WebpiCmd.exe" "/Install /Products:'UrlRewrite2' /AcceptEULA" -PassThru | Wait-Process if (-Not (Test-Path $urlRewriteRegistry)) { Write-DosMessage -Level "Warning" -Message "UrlRewrite did not install correctly" } } else { Write-DosMessage -Level "Warning" -Message "Unable to install UrlRewrite, WebpiCmd.exe not found at $($env:ProgramFiles)\Microsoft\Web Platform Installer\WebpiCmd.exe" } } } <# .SYNOPSIS Pushes a new "_type_" property to any hashtable, which stores the key name and "type" provided as a parameter .DESCRIPTION When working with DOS configuration values that come from varying places (some common examples may be a config store, pipeline, or static) it is nice to be able to merge these configurations into a single $config hashtable. However, when we merge hashtables together, we lose the information about where they came from or what type of configuration it is. Push-DosConfigType allows you to provide a "type" parameter which then stores the key names and type into a new property called "_type_". This "_type_" property is then used by the Confirm-DosConfiguration method when generating meaningful messages about issues with configurations, due to the added information about what type of configuration encountered a problem. .OUTPUTS Name Value ---- ----- clientName Health Catalyst appName Test App clientEnvironment Internal appPoolName Test App Pool _type_ {clientName, appName, clientEnvironment, appPoolName} .EXAMPLE $storeConfig = @{ clientName = 'Health Catalyst' clientEnvironment = 'Internal' appName = 'Test App' appPoolName = 'Test App Pool' } | Push-DosConfigType "store" $staticConfig = @{ ServiceName = 'Test Service' PathToDatabaseQuery = 'some path' } | Push-DosConfigType "static" #> function Push-DosConfigType { [CmdletBinding()] param ( [Parameter(ValueFromPipeline = $true, Mandatory = $true)] [Hashtable] $hash, [Parameter(Position = 0, Mandatory = $true)] [String] $type ) $newHash = @{}; foreach ($key in $hash.keys) { $newHash.Add($key, $type) } $hash._type_ = $newHash return $hash } <# .SYNOPSIS Removes an installation scope/section from the provided config store .DESCRIPTION Removes an installation scope/section from the provided config store .PARAMETER configSection Config scope that the contains the configSetting that will be removed .PARAMETER ConfigStore Accepts a powershell hashtable or dictionary with key value pairs following the example, $configStore = @{Type = "File"; Format = "XML"; Path = "C:\Path\To\File"}. Current iteration only supports XML Files. .EXAMPLE Remove-DosConfigSection -configSection "common" -configStore $configStore .NOTES General notes #> function Remove-DosConfigSection { [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] param ( [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [String] $configSection, [Parameter(Mandatory=$true)] [hashtable] $configStore ) if(!(Confirm-ConfigStore -ConfigStore $configStore)) { Write-DosMessage -Level Fatal -Message "configStore is invalid (Type: $($configstore.Type); Format: $($configstore.Format); Path: $($configstore.Path)" return } if(-not($configStore.Format -eq 'XML' -and $configStore.Type -eq 'File')){ Write-DosMessage -Level Fatal -Message "Remove-DosConfigSection is not implemented for this type of configstore (Type: $($configstore.Type); Format: $($configstore.Format); Path: $($configstore.Path)" return } if ($configStore.Type -eq "External") { $configObject = Remove-DosConfigSectionAzureTable -configStore $configStore -configSection $configSection } if ($configStore.Type -eq "File") { $configObject = Remove-DosConfigSectionXml -configStore $configStore -configSection $configSection } if($configObject){ if($PSCmdlet.ShouldProcess("Delete '$configSection' scope in install config")){ Write-DosMessage -Level Information -Message "Removing $configSection from $($configStore.Type) configstore." Save-DosConfigStore -configStoreObject $configObject -configStore $configStore } } else { Write-DosMessage -Level 'Warning' -Message "No changes made to $($configStore.Type) configstore for the $configSection scope" } } <# .SYNOPSIS Removes an installation variable from the provided config scope .DESCRIPTION Removes an installation variable and value from the provided config scope .PARAMETER configSection Config scope that the contains the configSetting that will be removed .PARAMETER configSetting Variable that will be removed .PARAMETER ConfigStore Accepts a powershell hashtable or dictionary with key value pairs following the example, $configStore = @{Type = "File"; Format = "XML"; Path = "C:\Path\To\File"}. Current iteration only supports XML Files. .EXAMPLE Remove-DosConfigValue -InstallConfigPath $path -configSection "common" -configSetting "sqlServerAddress" .NOTES General notes #> function Remove-DosConfigValue { [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] param ( [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [Alias("Scope", "ConfigScope")] [String] $configSection, [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [Alias("ConfigVariable")] [String] $configSetting, [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [hashtable] $configStore ) if(!(Confirm-ConfigStore -ConfigStore $configStore)) { Write-DosMessage -Level "Fatal" -Message "ConfigStore is invalid" return } if ($configStore.Type -eq "External") { $configObject = Remove-DosConfigValueAzureTable -configStore $configStore -configSection $configSection -configSetting $configSetting } if ($configStore.Type -eq "File") { $configObject = Remove-DosConfigValueXml -configStore $configStore -configSection $configSection -configSetting $configSetting } if($configObject){ if($PSCmdlet.ShouldProcess("Delete $configSection.$configSetting in install config")){ Write-DosMessage -Level "Debug" -Message "Removing $configSection.$configSetting from $($configStore.Type) configstore." Save-DosConfigStore -configStoreObject $configObject -configStore $configStore } } else { Write-DosMessage -Level "Warning" -Message "No changes made to $($configStore.Type) configstore for $configSection.$configSetting." } } function Remove-DosWebApplication { <# .SYNOPSIS Remove a DOS web application from IIS .DESCRIPTION Removes a given DOS web application, physical directory, and application pool if no other applications are associated with it. .PARAMETER ApplicationName The name of the DOS Web application in IIS. .PARAMETER IISWebSite Specifies the IIS site from which to remove the application. Defaults to "Default Web Site" .EXAMPLE Remove-DosWebApplication -ApplicationName "Atlas4" -IISWebSite "Default Web Site" #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Justification="WhatIf support not implemented.")] Param ( [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [string] $ApplicationName, [string] $IISWebSite = "Default Web Site" ) Import-Module WebAdministration -Force $iisPath = "IIS:\Sites\$($IISWebSite)\$($ApplicationName)" $webApp = Get-WebApplication -Name $ApplicationName -Site $IISWebSite if ($webApp) { $webApplicationFolder = Get-WebFilePath -PSPath $iisPath Write-DosMessage -Level "Information" -Message "Removing web application '$iisPath'" Remove-WebApplication -Name $ApplicationName -Site $IISWebSite Write-DosMessage -Level "Information" -Message "Removing folder '$webApplicationFolder'" Remove-Item -Path $webApplicationFolder -Recurse # Remove app pool if application count is zero $appPoolName = $webApp.applicationPool $appCount = (Get-WebConfigurationProperty "/system.applicationHost/sites/site/application[@applicationPool='$appPoolName']" "machine/webroot/apphost" -name path).Count if ($appCount -eq 0) { Write-DosMessage -Level "Information" -Message "Removing application pool '$appPoolName'" Remove-WebAppPool -Name $appPoolName } else { Write-DosMessage -Level "Warning" -Message "Application pool '$appPoolName' was not removed since other applications are currently bound to it." } } } function Remove-IISUrlRewriteRule { <# .SYNOPSIS Removes URL rewrite rule .DESCRIPTION Removes a given URL rewrite rule from IIS. .PARAMETER RuleName The unique name of the rule to remove. .PARAMETER IISWebSite The IIS site from which to remove the application. Defaults to "Default Web Site" .EXAMPLE Remove-IISUrlRewriteRule -RuleName "Atlas4-Atlas-Redirect" -IISWebSite "Default Web Site" #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Justification="WhatIf support not implemented.")] Param ( [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [string] $RuleName, [string] $IISWebSite = "Default Web Site" ) Import-Module WebAdministration -Force $iisPath = "IIS:\Sites\$($IISWebSite)" $ruleFilter = "/system.webserver/rewrite/rules/rule[@name='$RuleName']" $exists = Get-WebConfigurationProperty -PSPath $iisPath -Filter $ruleFilter -Name * if ($exists) { Write-DosMessage -Level "Information" -Message "Removing URL Rewrite rule '$RuleName'." Clear-WebConfiguration -PSPath $iisPath -Filter $ruleFilter } } <# .SYNOPSIS Saves the config store .DESCRIPTION Saves the config store (currently only works for XML file types) .PARAMETER configStoreObject The object that represents the configuration settings that need to be saved. .EXAMPLE Save-DosConfigStore -configStoreObject $installConfigXml -configStore @{Type = "File"; Format = "XML"; Path = "install.config"} Save-DosConfigStore -configStoreObject $installConfigXml -configStore $configStore .NOTES General notes #> function Save-DosConfigStore { param ( [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] $configStoreObject ,[Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [hashtable] $configStore ) if($configStore.Type -eq "File" -and $configStore.Format -eq "XML"){ Save-DosConfigStoreXml -installConfigXml $configStoreObject -pathToInstallConfig $configStore.Path } if ($configStore.Type -eq "External") { Write-DosMessage -Level "Debug" -Message "External configstores have no execution requirements in Save-DosConfigStore" } } <# .SYNOPSIS NON PUBLIC - Saves the xml file that represents the install.config .DESCRIPTION Saves the xml file that represents the install.config .PARAMETER installConfigXml The xml object that represents the install.config .PARAMETER pathToInstallConfig Full file path to the install.config file that will be saved .EXAMPLE Save-DosConfigStoreXml -installConfigXml $installConfigXml -pathToInstallConfig 'C:\Program Files\Health Catalyst\install.config' .NOTES General notes #> function Save-DosConfigStoreXml { param ( [Parameter(Mandatory=$true)] [xml]$installConfigXml, [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [String]$pathToInstallConfig ) $installConfigXml.Save("$pathToInstallConfig") } <# .SYNOPSIS Adds an installation variable to the provided config scope .DESCRIPTION Adds or updates an installation variable and value to the provided config scope .PARAMETER configSection Config scope that the variable and value will be saved to .PARAMETER configSetting Variable that will be saved .PARAMETER configValue Value of the variable to be saved .PARAMETER KeepExisting Will not overwrite the existing value if present .PARAMETER ConfigStore Accepts a powershell hashtable or dictionary with key value pairs following the example, $configStore = @{Type = "File"; Format = "XML"; Path = "C:\Path\To\File"}. Current iteration only supports XML Files. .EXAMPLE Set-DosConfigValue -PathToInstallConfig $path -configSection "common" -configSetting "sqlServerAddress" -configValue $dbFQN -configstore $configstore Set-DosConfigValue -PathToInstallConfig $path -configSection "common" -configSetting "sqlServerAddress" -configValue $dbFQN -configstore $configstore -KeepExisting .NOTES General notes #> function Set-DosConfigValue { [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] param ( [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [Alias("Scope", "ConfigScope")] [String] $configSection, [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [Alias("ConfigVariable")] [String] $configSetting, [Parameter(Mandatory=$true)] [AllowEmptyString()] [String] $configValue, [Switch] $KeepExisting, [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [hashtable] $configStore ) if(!(Confirm-ConfigStore -ConfigStore $configStore)) { Write-DosMessage -Level "Fatal" -Message "ConfigStore is invalid" return } $configObject = $null if ($configStore.Type -eq "External") { $configObject = Set-DosConfigValueAzureTable -configStore $configStore -configSection $configSection -configSetting $configSetting -configValue $configValue } if ($configStore.Type -eq "File") { $configObject = Set-DosConfigValueXml -configStore $configStore -configSection $configSection -configSetting $configSetting -configValue $configValue -KeepExisting:$KeepExisting } # Instead of the condition being on if something changed, will detect if the object is returned at all form the specific configstore set functions if($configObject){ if($PSCmdlet.ShouldProcess("Save changes to configuration")){ Save-DosConfigStore -configStoreObject $configObject -configStore $configStore Write-DosMessage -Level "Debug" -Message "Successfully added $configSection.$configSetting=$configValue into $($configStore.Type) configstore." } } else { Write-DosMessage -Level "Debug" -Message "No changes made to $($configStore.Type) configstore for $configSection.$configSetting=$configValue." } } <# .SYNOPSIS Sets the the global IIS configuration authentication settings to allow each application to override authentication with it's own configuration settings .DESCRIPTION For more reading, see: https://docs.microsoft.com/en-us/iis/get-started/planning-for-security/how-to-use-locking-in-iis-configuration and https://docs.microsoft.com/en-us/iis/configuration/system.webserver/security/authentication/ .EXAMPLE Set-GlobalIISAuthentication #> function Set-DosGlobalIISAuthentication { [CmdletBinding(SupportsShouldProcess=$true)] param() BEGIN { Add-Assembly -Assemblies "$env:systemroot\system32\inetsrv\Microsoft.Web.Administration.dll" } PROCESS{ Write-DosMessage -Level "Verbose" -Message "Fetching configuration sections" $manager = new-object Microsoft.Web.Administration.ServerManager $config = $manager.GetApplicationHostConfiguration() $section = $config.GetSection("system.webServer/security/authentication/windowsAuthentication") $section.OverrideMode = "Allow" $section = $config.GetSection("system.webServer/security/authentication/anonymousAuthentication") $section.OverrideMode = "Allow" if($PSCmdlet.ShouldProcess("Committing IIS global authentication settings")){ Write-DosMessage -Level "Verbose" -Message "Committing changes" $manager.CommitChanges() } Write-DosTelemetry -Message "Set-DosGlobalIISAuthentication called." } } <# .SYNOPSIS Configure application logger with specified set of parameters.. .DESCRIPTION Configures and creates a Serilog logger, capable of logging to the console, a file, or both. .PARAMETER LoggingMode Logger message output. Valid modes include: Console, File and Both. .PARAMETER MinimumLoggingLevel The minimum logging level that will be written to the logger (file and console). .PARAMETER LogFilePath Path to logging file. .EXAMPLE Set-DosMessageConfiguration -LoggingMode "Both" -LogFilePath "C:\Path\To\log.txt" #> function Set-DosMessageConfiguration { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Justification="Logging messages will not change system state.")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidGlobalVars", "", Justification="We need a global variable to avoid weird scope issues when turning on serilog selflog. It will normally be null")] [CmdletBinding()] param( [Parameter(Mandatory=$true)] [ValidateSet("File", "Console", "Both")] [string] $LoggingMode, [Parameter(Mandatory=$true)] [ValidateSet("Verbose", "Debug", "Information", "Warning", "Error", "Fatal")] [string] $MinimumLoggingLevel, [ValidateScript({ if (!(Test-Path $_)) { try { New-Item $_ -Type File -Force -ErrorAction Stop } catch { Write-DosMessage -Level "Error" -Message "$_" } $true } else { $true } })] [string] $LogFilePath ) # Parameter Validation added because if $LogFilePath isn't provided the [ValidateScript] will not run # Test case. If user tries to configure a file logger without providing a logfilepath. Associated unit test in Set-DosMessageConfiguration.tests.ps1 line 51-59 if (($LoggingMode -eq "File" -or $LoggingMode -eq "Both") -and [string]::IsNullOrEmpty($LogFilePath)) { Write-DosMessage -Level "Error" -Message "You cannot configure a file logger without providing a the LogFilePath parameter." } try { [SerilogBridge.SerilogBridge]::CreateDosLogger($LoggingMode, $MinimumLoggingLevel, $LogFilePath, $global:serilogSelfLogEnabled) } catch { Write-DosMessage -Level "Error" -Message "Error creating logger. Exception: $($_.Exception)" } Write-DosTelemetry -Message "Set-DosMessageConfiguration called." } <# .SYNOPSIS Configures the telemetry logger with a specific telemetry key (current defaults to DOS Install Application Insights key). .DESCRIPTION Pass in the application insights key and a optional opt out parameter. .PARAMETER TelemetryKey Currently associate the telemetry logger with an application insights key. .PARAMETER TelemetryOptOut Switch parameter, if included will opt out of the telemetry logger. .EXAMPLE Set-DosTelemetry -TelemetryKey "testkey" -TelemetryOptOut #> function Set-DosTelemetry { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Justification="Logging messages will not change system state.")] [CmdletBinding()] param( [ValidateNotNullorEmpty()] [string] $TelemetryKey, [switch] $TelemetryOptOut ) # Converts to the appropriate boolean type used for the C# serilogbridge Write-DosMessage -Level "Verbose" -Message "Converting Powershell boolean into primitive type to be used in Serilog C# class" $telemetryConfirmation = [System.Management.Automation.LanguagePrimitives]::ConvertTo($TelemetryOptOut.IsPresent,[System.Type]::GetType($TelemetryOptOut.IsPresent.GetType().FullName)) try { Write-DosMessage -Level "Information" -Message "Creating Telemetry Logger using '$TelemetryKey' key." [SerilogBridge.TelemetryBridge]::CreateDosTelemetryLogger($TelemetryKey, $telemetryConfirmation) } catch { Write-DosMessage -Level "Error" -Message "Error creating telemetry logger. Exception: $($_.Exception)" } Write-DosTelemetry -Message "Set-DosTelemetry called." } <# .SYNOPSIS Given a certificate thumbprint and an encrypted value, Unprotect-DosInstallerSecret will return a decrypted secret value. .DESCRIPTION Pass in a certificate thumbprint (generally stored in a configuration file) and the encrypted secret value, and Unprotect-DosInstallerSecret will return the decrypted value in string format. .PARAMETER CertificateThumprint Certificate Thumbprint of the certificate that will be used for decryption. .PARAMETER EncryptedInstallerSecretValue Encrypted value that will be unprotected. .EXAMPLE $decryptedValue = Unprotect-DosInstallerSecret -CertificateThumprint $certThumbprint -EncryptedInstallerSecretValue $encryptedSecret #> function Unprotect-DosInstallerSecret { [CmdletBinding()] param( [Parameter(Mandatory=$true, ParameterSetName = "CertificateSecret")] [ValidateNotNullorEmpty()] [string] $CertificateThumprint, [Parameter(Mandatory=$true, ParameterSetName = "CertificateSecret")] [ValidateNotNullorEmpty()] [string] $EncryptedInstallerSecretValue ) $secret = '' if ($PSCmdlet.ParameterSetName -eq "CertificateSecret") { try{ Write-DosMessage -Level "Debug" -Message "Attempting to retrieve encryption certificate using the certificate thumbprint provided." $encryptionCertificate = Get-EncryptionCertificate $CertificateThumprint Write-DosMessage -Level "Debug" -Message "Successfully retrieved encryption certificate." } catch { Write-DosMessage -Level "Error" -Message "Could not get encryption certificte with thumbprint $CertificateThumprint. Exception: $($_.Exception)" } try { Write-DosMessage -Level "Debug" -Message "Using encryption certificate to decrypt the installer secret provided." $encryptedValue = $EncryptedInstallerSecretValue if ($encryptedValue.StartsWith("!!enc!!:")) { $encryptedValue = $encryptedValue.Replace("!!enc!!:", "") } $secret = Get-DecryptedString -Certificate $encryptionCertificate -EncryptedValue $encryptedValue Write-DosMessage -Level "Debug" -Message "Successfully decrypted the installer secret provided." } catch { Write-DosMessage -Level "Error" -Message "Error attempting to decrypt installer secret. Exception: $($_.Exception)." } } return $secret } function Get-EncryptionCertificate { param ( [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [string] $CertificateThumprint ) $localCertPath = "Cert:\LocalMachine\My" Write-DosMessage -Level "Verbose" -Message "Cleaning certificate thumbprint for any invalid characters." $cleanCertificateThumbprint = $CertificateThumprint -replace '[^a-zA-Z0-9]', '' try { Write-DosMessage -Level "Debug" -Message "Pulling certificate from $localCertPath." $certificate = Get-Item "$localCertPath\$cleanCertificateThumbprint" -ErrorAction Stop Write-DosMessage -Level "Debug" -Message "Successfully retrieved certificate from $localCertPath." } catch { Write-DosMessage -Level "Error" -Message "Error retrieving certificate from $localCertPath. Confirm that certificate with $cleanCertificateThumbprint exists on the machine. Exception: $($_.Exception)." } return $certificate } function Get-DecryptedString { param ( [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [X509Certificate] $Certificate, [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [string] $EncryptedValue ) try { Write-DosMessage -Level "Debug" -Message "Decrypting value..." $clearTextValue = Get-DecryptedValueDotNet -Certificate $Certificate -EncryptedValue $EncryptedValue Write-DosMessage -Level "Debug" -Message "Successfully decrypted installer secret." } catch { Write-DosMessage -Level "Error" -Message "Error decrypting value provided. Please verify that encryption certificate is valid. Exception: $($_.Exception)." } return $clearTextValue } function Get-DecryptedValueDotNet { param ( [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [X509Certificate] $Certificate, [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [string] $EncryptedValue ) $rsaEncryptionOid = '1.2.840.113549.1.1.1' # we are currently only using RSA. #$dsaEncryptionOid = '1.2.840.10040.4.1' # possible future #$eccEncryptionOid = '1.2.840.10045.2.1' # possible future if ($Certificate.PublicKey.Oid.Value -eq $rsaEncryptionOid) { Write-DosMessage -Level "Debug" -Message "CNG certificate detected." try { Write-DosMessage -Level "Debug" -Message "Pulling RSA private key from certificate with thumbprint provided." $privateKey = Get-CNGRSAPrivateKey -Certificate $Certificate Write-DosMessage -Level "Debug" -Message "Successfully retrieved RSA private key from certificate." } catch { Write-DosMessage -Level "Error" -Message "Error pulling RSA private key from certificate with thumbprint $($Certificate.Thumbprint). Exception: $($_.Exception)." } try { Write-DosMessage -Level "Debug" -Message "Decrypting value..." $clearTextValue = Get-DecryptedValueCNGKey -privateKey $privateKey -EncryptedValue $EncryptedValue Write-DosMessage -Level "Debug" -Message "Successfully decrypted value." } catch { Write-DosMessage -Level "Error" -Message "Error decrypting value provided. Please verify that encryption certificate is valid. Exception: $($_.Exception)." } } else { Write-DosMessage -Level "Debug" -Message "CSP certificate detected." try { Write-DosMessage -Level "Debug" -Message "Decrypting value..." $clearTextValue = Get-DecryptedValueCSPCertificateKey -Certificate $Certificate -EncryptedValue $EncryptedValue Write-DosMessage -Level "Debug" -Message "Successfully decrypted value." } catch { Write-DosMessage -Level "Error" -Message "Error decrypting value provided. Please verify that encryption certificate is valid. Exception: $($_.Exception)." } } return $clearTextValue } function Get-CNGRSAPrivateKey { param ( [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [X509Certificate] $Certificate ) return [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($Certificate) } function Get-DecryptedValueCNGKey { param ( [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [System.Security.Cryptography.RSA] $privateKey, [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [string] $EncryptedValue ) return [System.Text.Encoding]::UTF8.GetString($privateKey.Decrypt([System.Convert]::FromBase64String($EncryptedValue), [System.Security.Cryptography.RSAEncryptionPadding]::OaepSHA1)) } function Get-DecryptedValueCSPCertificateKey { param ( [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [X509Certificate] $Certificate, [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [string] $EncryptedValue ) return [System.Text.Encoding]::UTF8.GetString($Certificate.PrivateKey.Decrypt([System.Convert]::FromBase64String($EncryptedValue), $true)) } <# .SYNOPSIS Adds or updates settings in a .NET appsettings.json file .DESCRIPTION Modifies a .NET appsettings.json file. If a setting is not found, it is added. If a setting is present but its value is different, the setting is modified. .PARAMETER appSettingsPath The path to the appsettings.json file. .PARAMETER appSettingsValues A hashtable that should mirror an appsettings.json file in structure. .EXAMPLE Update-DosAppSettingsJson -appSettingsPath "C:\SomeFolder\appsettings.json" -appSettingsValues $appSettings #> function Update-DosAppSettingsJson { [CmdletBinding(SupportsShouldProcess)] param ( [Parameter(Mandatory = $true)] [string] $appSettingsPath, [Parameter(Mandatory = $true)] [hashtable] $appSettingsValues ) Write-DosMessage -Level "Information" -Message "Updating app settings $appSettingsPath" $appSettings = (Get-Content $appSettingsPath -Raw) | ConvertFrom-Json foreach ($appSettingValue in $appSettingsValues.GetEnumerator()) { if ($appSettings.psobject.properties.name -notcontains $appSettingValue.Key) { $appSettings | Add-Member -Type NoteProperty -Name $appSettingValue.Key -Value $appSettingValue.Value Write-DosMessage -Level "Information" -Message "Added $($appSettingValue.Name)" } else { if ($appSettings."$($appSettingValue.Key)" -ne $appSettingValue.Value) { $appSettings."$($appSettingValue.Key)" = $appSettingValue.Value Write-DosMessage -Level "Information" -Message "Updated $($appSettingValue.Name)" } } } If ($PSCmdlet.ShouldProcess($appSettingsPath)) { $appSettings | ConvertTo-Json -Depth 100 | Set-Content $appSettingsPath Write-DosMessage -Level "Information" -Message "Finished updating $appSettingsPath" } } <# .SYNOPSIS Formats the given input string by performing a search/replace. .DESCRIPTION Formats the given input string by performing a search/replace against each delimited targetpattern and replacing it with the replacement pattern in the same array position .PARAMETER TargetPatterns Array of strings representing the target patterns to search for and replace. Combines with delimiter to prevent aliasing. Must be equal in length to the replacement array .PARAMETER ReplacementPattern Array of replacement strings. Must be equal in length to the target array .PARAMETER Content String representing the content to perform a search/replace on. Must be non-null and not empty .PARAMETER Delimiter Delimiter to combine with the target replacement patterns to perform search/replace against. May be an empty string if no delimiter is necessary .EXAMPLE $formattedContent = Update-DosConfigContent -TargetPatterns $TargetPatterns -ReplacementPattern $ReplacementPattern -Delimiter $Delimiter -Content $configFileContent #> function Update-DosConfigContent{ [CmdletBinding(SupportsShouldProcess = $true)] param( [ValidateNotNull()] [Array] $TargetPatterns, [ValidateNotNull()] [Array] $ReplacementPattern, [ValidateNotNullOrEmpty()] [string] $Content, [ValidateNotNull()] [string] $Delimiter = "" ) if($TargetPatterns.Length -ne $ReplacementPattern.Length){ Write-DosMessage -Level "Error" -Message "Target patterns and replacement pattern length must be equal" return } for($i = 0 ; $i -lt $TargetPatterns.Length; $i++){ Write-DosMessage -Level "Verbose" -Message "Replacing $($TargetPatterns[$i]) with $($ReplacementPattern[$i])" $searchValue = "$Delimiter$($TargetPatterns[$i])$Delimiter" if($PSCmdlet.ShouldProcess("Modifying content via search and replace")){ $Content = $Content.Replace($searchValue, $ReplacementPattern[$i]) } } return $Content Write-DosTelemetry -Message "Update-DosConfigFile called." } <# .SYNOPSIS Formats the given input file by performing a search/replace. Writes the updated content back into the same file .DESCRIPTION Formats the given input string by performing a search/replace against each delimited targetpattern and replacing it with the replacement pattern in the same array position .PARAMETER TargetPatterns Array of strings representing the target patterns to search for and replace. Combines with delimiter to prevent aliasing. Must be equal in length to the replacement array .PARAMETER ReplacementPattern Array of replacement strings. Must be equal in length to the target array .PARAMETER FilePath Path to the file to update with the search/replace .PARAMETER Delimiter Delimiter to combine with the target replacement patterns to perform search/replace against. May be an empty string if no delimiter is necessary .EXAMPLE Update-DosConfigFile -TargetPatterns $targets -ReplacementPattern $replacements -Delimiter $delimiter -FilePath $ConfigFilePath #> function Update-DosConfigFile{ [CmdletBinding(SupportsShouldProcess = $true)] param( [ValidateNotNull()] [Array] $TargetPatterns, [ValidateNotNull()] [Array] $ReplacementPattern, [ValidateNotNullOrEmpty()] [ValidateScript({ if (!(Test-Path $_)) { Write-DosMessage -Level "Error" -Message "FilePath $_ does not exist. Please enter valid path." -ErrorAction Stop } else { $true } })] [string] $FilePath, [ValidateNotNull()] [string] $Delimiter = "" ) Write-DosMessage -Level "Information" -Message "Performing search and replace on $FilePath" $configFileContent = Get-Content -Path $FilePath -Raw Write-DosMessage -Level "Debug" -Message "Pre-formatted content: $configFileContent" $formattedContent = Update-DosConfigContent -TargetPatterns $TargetPatterns -ReplacementPattern $ReplacementPattern -Delimiter $Delimiter -Content $configFileContent Write-DosMessage -Level "Debug" -Message "Formatted content: $formattedContent" if($PSCmdlet.ShouldProcess("Saving updated content back to file $FilePath")){ Set-Content -Path $FilePath -Value $formattedContent } } <# .SYNOPSIS Formats the given input file by performing a search/replace. .DESCRIPTION Formats the given input string by performing a search/replace agaist pairs in the specified XML file .PARAMETER ConfigFilePath Configuration file to update with a search/replacement of patterns in the ReplacementPatternFilePath .PARAMETER ReplacementPatternFilePath File containing the delimiter and search/replace pairs to use to transform the config file .EXAMPLE Update-DosConfigFileFromInputFile -ConfigFilePath "C:\inetput\wwwroot\testapp\web.config" -ReplacementPatternFilePath ".\testappupdates.xml" #> function Update-DosConfigFileFromInputFile { [CmdletBinding(SupportsShouldProcess = $true)] param( [ValidateNotNullOrEmpty()] [ValidateScript({ if (!(Test-Path $_)) { Write-DosMessage -Level "Error" -Message "ConfigFilePath $_ does not exist. Please enter valid path." -ErrorAction Stop } else { $true } })] [string] $ConfigFilePath, [ValidateNotNullOrEmpty()] [ValidateScript({ if (!(Test-Path $_)) { Write-DosMessage -Level "Error" -Message "ReplacementPatternFilePath $_ does not exist. Please enter valid path." -ErrorAction Stop } else { $true } })] [string] $ReplacementPatternFilePath ) Write-DosMessage -Level "Verbose" -Message "Reading replacements from $ReplacementPatternFilePath" try{ $replacementsXmlFile = [xml] (Get-Content $ReplacementPatternFilePath) } catch{ Write-DosMessage -Level "Error" -Message "Unable to load xml in file $ReplacementPatternFilePath" return } if($null -eq $replacementsXmlFile.replacements) { Write-DosMessage -Level "Error" -Message "No replacements root found in specified replacement file $ReplacementPatternFilePath" return } if ($null -eq $replacementsXmlFile.replacements.pairs){ Write-DosMessage -Level "Error" -Message "No replacements pairs found in specified replacement file $ReplacementPatternFilePath" return } $targets = $replacementsXmlFile.replacements.pairs.ChildNodes | ForEach-Object {$_.target} $replacements = $replacementsXmlFile.replacements.pairs.ChildNodes | ForEach-Object {$_.replacement} $delimiter = "" if($null -eq $replacementsXmlFile.replacements.delimiter){ Write-DosMessage -Level "Warning" -Message "No delimiter specified in replacement file $ReplacementPatternFilePath, assuming no delimiter" } else{ $delimiter = $replacementsXmlFile.replacements.delimiter } Write-DosMessage -Level "Debug" -Message "Have $($targets.Count) replacements and the delimiter is $delimiter" if($PSCmdlet.ShouldProcess("Updating config file $ConfigFilePath")){ Update-DosConfigFile -TargetPatterns $targets -ReplacementPattern $replacements -Delimiter $delimiter -FilePath $ConfigFilePath } } <# .SYNOPSIS Updates the target DOS dac publish file to the specified database mount points .DESCRIPTION Take the passed in mount points and update the DOS dac publish xml file .PARAMETER PublishOptionsFilePath Path to publish options file - Required - See tests/SampleFiles/DefaultDacDeployOptions.xml for an example .PARAMETER dataMountPointFolder Path to the base data mount point folder .PARAMETER indexMountPointFolder Path to the base index mount point folder .PARAMETER logMountPointFolder Path to the base log mount point folder .EXAMPLE Update-DosMountPoint -PublishOptionsFilePath ".\test.publish.xml" -dataMountPointFolder "C:\SQLData" -indexMountPointFolder "C:\SQLData" -logMountPointFolder "C:\SQLData" #> function Update-DosMountPoint { [cmdletbinding(SupportsShouldProcess=$true)] param ( [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [string]$PublishOptionsFilePath, [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [string]$dataMountPointFolder, [string]$indexMountPointFolder, [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [string]$logMountPointFolder ) [xml] $parsedPublishOptions = [xml] (Get-Content $PublishOptionsFilePath -ErrorAction Stop) # Mount Points are stored in the SqlCmdVariables $sqlCmdVariables = $parsedPublishOptions.Project.ItemGroup.SqlCmdVariable Write-DosMessage -Level "Verbose" -Message "Validating that data mount point folder exists at $dataMountPointFolder" if(!(Test-Path "$dataMountPointFolder")){ try { Write-DosMessage -Level "Information" -Message "Data mount point folder does not exist, creating folder at $dataMountPointFolder" New-Item -ItemType directory -Path "$dataMountPointFolder" Write-DosMessage -Level "Verbose" -Message "Created data mount point folder at $dataMountPointFolder for $PublishOptionsFilePath" } catch { Write-DosMessage -Level "Error" -Message "Could not create data mount point folder at $dataMountPointFolder for $PublishOptionsFilePath. Exception: $($_.Exception)" } } else { Write-DosMessage -Level "Information" -Message "Data mount point folder already exists at $dataMountPointFolder" } foreach ($dataMountPoint in $sqlCmdVariables | Where-Object {$_.Include -like "*Data*MountPoint"}) { $dataMountPoint.Value = $dataMountPointFolder } foreach ($dataMountPoint in $sqlCmdVariables | Where-Object {$_.Include -like "PrimaryMountPoint"}) { $dataMountPoint.Value = $dataMountPointFolder } Write-DosMessage -Level "Verbose" -Message "Validating that log mount point folder exists at $logMountPointFolder" if(!(Test-Path "$logMountPointFolder")){ try { Write-DosMessage -Level "Information" -Message "Log mount point folder does not exist, creating folder at $logMountPointFolder" New-Item -ItemType directory -Path "$logMountPointFolder" Write-DosMessage -Level "Verbose" -Message "Created log mount point folder at $logMountPointFolder for $PublishOptionsFilePath" } catch { Write-DosMessage -Level "Error" -Message "Could not create log mount point folder at $logMountPointFolder for $PublishOptionsFilePath. Exception: $($_.Exception)" } } else { Write-DosMessage -Level "Information" -Message "Log mount point folder already exists at $logMountPointFolder" } foreach ($logMountPoint in $sqlCmdVariables | Where-Object {$_.Include -like "*Log*MountPoint"}) { $logMountPoint.Value = $logMountPointFolder } if($indexMountPointFolder){ Write-DosMessage -Level "Verbose" -Message "Validating that index mount point folder exists at $indexMountPointFolder" if(!(Test-Path "$indexMountPointFolder")){ try { Write-DosMessage -Level "Information" -Message "Index mount point folder does not exist, creating folder at $indexMountPointFolder" New-Item -ItemType directory -Path "$indexMountPointFolder" Write-DosMessage -Level "Verbose" -Message "Created index mount point folder at $indexMountPointFolder for $PublishOptionsFilePath" } catch { Write-DosMessage -Level "Error" -Message "Could not create index mount point folder at $indexMountPointFolder for $PublishOptionsFilePath. Exception: $($_.Exception)" } } else { Write-DosMessage -Level "Information" -Message "Index mount point folder already exists at $indexMountPointFolder" } foreach ($indexMountPoint in $sqlCmdVariables | Where-Object {$_.Include -like "*Index*MountPoint"}) { $indexMountPoint.Value = $indexMountPointFolder } } try { Write-DosMessage -Level "Information" -Message "Saving publish profile mount point settings" if ($pscmdlet.ShouldProcess($PublishOptionsFilePath,"Saving modifications to profile mount point settings")){ $parsedPublishOptions.Save($PublishOptionsFilePath) } } catch { Write-DosMessage -Level "Fatal" -Message "Error occured while attempting to save mount point settings. Error $($_.Exception)" Write-DosTelemetry -Message "Finished attempt to update database mount point settings" } } <# .SYNOPSIS Adds or updates a single app setting in a .NET web config file. .DESCRIPTION Adds or updates a single app setting in a .NET web config file. The file must have an existing configuration element. If the appSettings element does not exist, it is created. .PARAMETER webConfigPath The path to the web.config file. .PARAMETER settingKey The key of the setting to add or modify .PARAMETER settingValue The to which to set the setting. .EXAMPLE Update-DosWebConfigAppSetting -webConfigPath "C:\Path\To\web.config" -settingKey "readExample" -settingValue "done" #> function Update-DosWebConfigAppSetting { [CmdletBinding(SupportsShouldProcess)] param ( [Parameter(Mandatory = $true)] [string] $webConfigPath, [Parameter(Mandatory = $true)] [string] $settingKey, [string] $settingValue ) if (!(Test-Path $webConfigPath)) { Write-DosMessage -Level Fatal -Message """$webConfigPath"" is not a valid path. Please specify a valid path to the apps web.config file" } $webConfigDoc = [xml](Get-Content $webConfigPath) if (!$webConfigDoc.configuration) { Write-DosMessage -Level Fatal -Message "This web.config file appears to not have a valid ""configuration"" xml node to write to" } if ($null -eq $webConfigDoc.configuration.appSettings) { $appSettings = $webConfigDoc.CreateElement("appSettings") $webConfigDoc.configuration.AppendChild($appSettings) | Out-Null } $appSettings = $webConfigDoc.configuration.SelectSingleNode('//appSettings') $existingSetting = $appSettings.add | Where-Object {$_.key -eq $settingKey} if ($null -eq $existingSetting) { $setting = $webConfigDoc.CreateElement("add") $keyAttribute = $webConfigDoc.CreateAttribute("key") $keyAttribute.Value = $settingKey; $setting.Attributes.Append($keyAttribute) | Out-Null $valueAttribute = $webConfigDoc.CreateAttribute("value") $valueAttribute.Value = $settingValue $setting.Attributes.Append($valueAttribute) | Out-Null $appSettings.AppendChild($setting) | Out-Null Write-DosMessage -Level "Information" -Message "Added $($settingKey) ($webConfigPath)" } else { $existingSetting.Value = $settingValue Write-DosMessage -Level "Information" -Message "Updated $($settingKey) ($webConfigPath)" } if ($PSCmdlet.ShouldProcess($webConfigPath)) { $webConfigDoc.Save($webConfigPath) } } <# .SYNOPSIS Write a logging message with a given severity to either a file, the console, or both. .DESCRIPTION Uses current logger configuration to log application messages based on severity. .PARAMETER Level Logger message level. Valid levels include: Verbose, Debug, Information, Warning, Error, and Fatal. .PARAMETER Message Message to be written in the log. .PARAMETER HeaderType Optional Header type that can be used to create dividers in a log file. Valid header types include: H1 and H2. .EXAMPLE Write-DosMessage -Level "Information" -Message "***[BEGIN]***" -HeaderType H2 Write-DosMessage -Level "Information" -Message "Main Header" -HeaderType H1 # typically used at the beginning of a script Write-DosMessage -Level "Information" -Message "Step1 Header" -HeaderType H2 # typically used in the middle of script Write-DosMessage -Level "Information" -Message "Regular log message1." Write-DosMessage -Level "Information" -Message "Regular log message2." Write-DosMessage -Level "Information" -Message "Step2 Header" -HeaderType H2 # typically used in the middle of script Write-DosMessage -Level "Information" -Message "Regular log message3." Write-DosMessage -Level "Information" -Message "Regular log message4." Write-DosMessage -Level "Information" -Message "***[END]***" -HeaderType H2 Write-DosMessage -Level "Fatal" -Message "Fatal Error Occured." #> function Write-DosMessage { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [ValidateSet("Verbose", "Debug", "Information", "Warning", "Error", "Fatal")] [string] $Level, [Parameter(Mandatory = $true)] [ValidateNotNullorEmpty()] [string] $Message, [Parameter(Mandatory = $false)] [ValidateSet("H1", "H2")] [string] $HeaderType ) $errorAction = $ErrorActionPreference # Used for mocking/testing if ($HeaderType) { $Width = 60; $Margin = 5; $Spacer = "-"; $Message = "$($Spacer*$Margin)$Message$($Spacer*$Margin)"; if ($Message.Length -gt ($Width - ($Margin * 2))) { $Width = $Message.Length + ($Margin * 2); } switch ($HeaderType) { "H1" { $Padding = 2 } "H2" { $Padding = 0 } default { $Padding = 0 } } LoadSerilog -Level "Information" -Message " " if ($Padding) {1..$Padding | ForEach-Object { LoadSerilog -Level $Level -Message ($Spacer * $Width)}} LoadSerilog -Level $Level -Message "$($Message)$($Spacer * ($Width - $Message.Length))" if ($Padding) {1..$Padding | ForEach-Object { LoadSerilog -Level $Level -Message ($Spacer * $Width)}} } else { LoadSerilog -Level $Level -Message $Message } # Silenty Continue do nothing # Default Throws on Fatal if ($errorAction -eq "Continue" -and $Level -eq "Fatal") { # throw reports the line number here instead of the calling function # Write-Error will report the line number in calling function # override $errorAction with Stop to create Terminating Error Write-Error -Message $Message -ErrorAction Stop } # Stop throws on Error and Fatal Levels if ($errorAction -eq "Stop" -and ($Level -eq "Error" -or $Level -eq "Fatal")) { # throw reports the line number here instead of the calling function # Write-Error will report the line number in calling function Write-Error -Message $Message } } function LoadSerilog { param( [Parameter(Mandatory = $true)] [ValidateSet("Verbose", "Debug", "Information", "Warning", "Error", "Fatal")] [string] $Level, [Parameter(Mandatory = $true)] [ValidateNotNullorEmpty()] [string] $Message ) [SerilogBridge.SerilogBridge]::WriteDosMessage($Level, $Message) } <# .SYNOPSIS Write a telemetry logging message with a severity to application insights. .DESCRIPTION Uses Information severity level by default. Requires a message to be passed in as well. .PARAMETER Message Message to be written in the telemetry log. .EXAMPLE Write-DosTelemetry -Message "Telemetry Message Here." #> function Write-DosTelemetry { [CmdletBinding()] param( [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [string] $Message ) try { # Seperate function call for mocking/testing LoadTelemetryBridge -Message $Message } catch { Write-DosMessage -Level "Error" -Message "Error writing telemetry message. Exception: $($_.Exception)" } } function LoadTelemetryBridge { param( [Parameter(Mandatory=$true)] [ValidateNotNullorEmpty()] [string] $Message ) [SerilogBridge.TelemetryBridge]::WriteDosTelemetry($Message) } # SIG # Begin signature block # MIIaxgYJKoZIhvcNAQcCoIIatzCCGrMCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCAMhAduq01mydQ/ # uc8KU0P1cWI3Wi7faXAbFwv8/lRvbqCCCqMwggUwMIIEGKADAgECAhAECRgbX9W7 # ZnVTQ7VvlVAIMA0GCSqGSIb3DQEBCwUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQK # EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNV # BAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0xMzEwMjIxMjAwMDBa # Fw0yODEwMjIxMjAwMDBaMHIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy # dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xMTAvBgNVBAMTKERpZ2lD # ZXJ0IFNIQTIgQXNzdXJlZCBJRCBDb2RlIFNpZ25pbmcgQ0EwggEiMA0GCSqGSIb3 # DQEBAQUAA4IBDwAwggEKAoIBAQD407Mcfw4Rr2d3B9MLMUkZz9D7RZmxOttE9X/l # qJ3bMtdx6nadBS63j/qSQ8Cl+YnUNxnXtqrwnIal2CWsDnkoOn7p0WfTxvspJ8fT # eyOU5JEjlpB3gvmhhCNmElQzUHSxKCa7JGnCwlLyFGeKiUXULaGj6YgsIJWuHEqH # CN8M9eJNYBi+qsSyrnAxZjNxPqxwoqvOf+l8y5Kh5TsxHM/q8grkV7tKtel05iv+ # bMt+dDk2DZDv5LVOpKnqagqrhPOsZ061xPeM0SAlI+sIZD5SlsHyDxL0xY4PwaLo # LFH3c7y9hbFig3NBggfkOItqcyDQD2RzPJ6fpjOp/RnfJZPRAgMBAAGjggHNMIIB # yTASBgNVHRMBAf8ECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBhjATBgNVHSUEDDAK # BggrBgEFBQcDAzB5BggrBgEFBQcBAQRtMGswJAYIKwYBBQUHMAGGGGh0dHA6Ly9v # Y3NwLmRpZ2ljZXJ0LmNvbTBDBggrBgEFBQcwAoY3aHR0cDovL2NhY2VydHMuZGln # aWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNydDCBgQYDVR0fBHow # eDA6oDigNoY0aHR0cDovL2NybDQuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJl # ZElEUm9vdENBLmNybDA6oDigNoY0aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0Rp # Z2lDZXJ0QXNzdXJlZElEUm9vdENBLmNybDBPBgNVHSAESDBGMDgGCmCGSAGG/WwA # AgQwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAK # BghghkgBhv1sAzAdBgNVHQ4EFgQUWsS5eyoKo6XqcQPAYPkt9mV1DlgwHwYDVR0j # BBgwFoAUReuir/SSy4IxLVGLp6chnfNtyA8wDQYJKoZIhvcNAQELBQADggEBAD7s # DVoks/Mi0RXILHwlKXaoHV0cLToaxO8wYdd+C2D9wz0PxK+L/e8q3yBVN7Dh9tGS # dQ9RtG6ljlriXiSBThCk7j9xjmMOE0ut119EefM2FAaK95xGTlz/kLEbBw6RFfu6 # r7VRwo0kriTGxycqoSkoGjpxKAI8LpGjwCUR4pwUR6F6aGivm6dcIFzZcbEMj7uo # +MUSaJ/PQMtARKUT8OZkDCUIQjKyNookAv4vcn4c10lFluhZHen6dGRrsutmQ9qz # sIzV6Q3d9gEgzpkxYz0IGhizgZtPxpMQBvwHgfqL2vmCSfdibqFT+hKUGIUukpHq # aGxEMrJmoecYpJpkUe8wggVrMIIEU6ADAgECAhAMMCpTLsjxo9FR9hag8ePUMA0G # CSqGSIb3DQEBCwUAMHIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJ # bmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xMTAvBgNVBAMTKERpZ2lDZXJ0 # IFNIQTIgQXNzdXJlZCBJRCBDb2RlIFNpZ25pbmcgQ0EwHhcNMjAwMzMxMDAwMDAw # WhcNMjMwNTEwMTIwMDAwWjCBpzELMAkGA1UEBhMCVVMxDTALBgNVBAgTBFV0YWgx # FzAVBgNVBAcTDlNhbHQgTGFrZSBDaXR5MR4wHAYDVQQKExVIZWFsdGggQ2F0YWx5 # c3QsIEluYy4xHjAcBgNVBAMTFUhlYWx0aCBDYXRhbHlzdCwgSW5jLjEwMC4GCSqG # SIb3DQEJARYhYWRtaW5uaXN0cmF0b3JAaGVhbHRoY2F0YWx5c3QuY29tMIIBIjAN # BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2fY0HWdxDJezDOsbHp7f9u/lrrD5 # nuZ1mENMgvixlrtC/KXgBRXlcWH7ajIOKljKnWCSAZwlZy4nFGbMagKmMzohXUXg # xo94u5nCdiBa/kgPazNGpL0AyGgX2VARMbcpm8Gdy+/uH3Kc7L91lcoGZVVBnVIt # 1oj5iXURqmhL83TrMyYqyj3XOH0So8Y10FVLPSukocMzMqBIRgvn/7EP0iWtOjXx # +o1wB5Ql+z9G3NCqF6CKE/Pn355XYbbmjF7BPzKoOjocHO6VU2uEflJWq1ZFb0QY # /tAosyyLYi9kFfO1damtJfRbbsVqavwg2UeQkzhg9CpB6eSsmBXPlFHudQIDAQAB # o4IBxTCCAcEwHwYDVR0jBBgwFoAUWsS5eyoKo6XqcQPAYPkt9mV1DlgwHQYDVR0O # BBYEFFjfHOOIre2C4m9NCk8TFJlDwMxUMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUE # DDAKBggrBgEFBQcDAzB3BgNVHR8EcDBuMDWgM6Axhi9odHRwOi8vY3JsMy5kaWdp # Y2VydC5jb20vc2hhMi1hc3N1cmVkLWNzLWcxLmNybDA1oDOgMYYvaHR0cDovL2Ny # bDQuZGlnaWNlcnQuY29tL3NoYTItYXNzdXJlZC1jcy1nMS5jcmwwTAYDVR0gBEUw # QzA3BglghkgBhv1sAwEwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNl # cnQuY29tL0NQUzAIBgZngQwBBAEwgYQGCCsGAQUFBwEBBHgwdjAkBggrBgEFBQcw # AYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tME4GCCsGAQUFBzAChkJodHRwOi8v # Y2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRTSEEyQXNzdXJlZElEQ29kZVNp # Z25pbmdDQS5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAQEAsBxn # 9yJAQi+9cJPZpJvOEV6iHaOBGv8898wNJCc4eB5g8WPziEY70GZVeqEdx3z0wS8U # QQIr19Hkju2NFZjDtzB9z1jAc/9EgqFGoCZbPijv1EYAa2oOVAp1BPbLjqBSdXqu # 2mzqo14CJ30oNom9ep9F6LGZ5zEoPsMrJejSbJGr4EacrksX8C8qeFklc7FzwiGk # GX7IQxidrrhOm2fOvGGAAxnvNYAR0FqJK0LiWWPSt5R/j63H/6HQtqD2sLevI3+O # bRP74TPchDobFmWlSogX9oB63E7fsbDAqecY0cRPQ6tVWK53Ke2sB514nahFjZDa # mxsa3/acZWL659ly3jGCD3kwgg91AgEBMIGGMHIxCzAJBgNVBAYTAlVTMRUwEwYD # VQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xMTAv # BgNVBAMTKERpZ2lDZXJ0IFNIQTIgQXNzdXJlZCBJRCBDb2RlIFNpZ25pbmcgQ0EC # EAwwKlMuyPGj0VH2FqDx49QwDQYJYIZIAWUDBAIBBQCgfDAQBgorBgEEAYI3AgEM # MQIwADAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgorBgEEAYI3AgELMQ4w # DAYKKwYBBAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQgQBQCtoMKBzPqp1cyv4Y1Gobc # UbiowchmHeo4wN2zhM0wDQYJKoZIhvcNAQEBBQAEggEAVe7Otg6DCCiJFdZuQf/B # KfuFMcbpkYCO7YCMDST6EPTQ2kDXs7FXJpD2dGvaxaTk3lEBS4wlZwKGhwniZFRK # yzwTNtJQb0QDonEYy/eSOh79su44FVkfy6+cIXliFJN3AxUmVqhrQUiQfUWf/pYX # FMmWsarQwMAg4tZezDp1CS+/VptaH1nazNcADlLzJU1f93fDl9OAptVwh1Z3WMno # CTzPtAar3FMzuOB+ypqYwsK9ctp9SEnQg9j8RU0Q4YGDScFvZl/J0ESslHxkVkqH # KJ4ZyYhrm0rgi7mgs6Sijl8KnVEgYR1cvwKhhEvuothIWKm9yPmpr1mPXmYzABPg # 56GCDUUwgg1BBgorBgEEAYI3AwMBMYINMTCCDS0GCSqGSIb3DQEHAqCCDR4wgg0a # AgEDMQ8wDQYJYIZIAWUDBAIBBQAweAYLKoZIhvcNAQkQAQSgaQRnMGUCAQEGCWCG # SAGG/WwHATAxMA0GCWCGSAFlAwQCAQUABCAdkYsjqLBSOb/boqjgwqwzjctHmmGY # 31+El+z/qgUzawIRAOHa7uHhnncSz1t0pVVokEEYDzIwMjEwMjI0MjE0MjQzWqCC # CjcwggT+MIID5qADAgECAhANQkrgvjqI/2BAIc4UAPDdMA0GCSqGSIb3DQEBCwUA # MHIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsT # EHd3dy5kaWdpY2VydC5jb20xMTAvBgNVBAMTKERpZ2lDZXJ0IFNIQTIgQXNzdXJl # ZCBJRCBUaW1lc3RhbXBpbmcgQ0EwHhcNMjEwMTAxMDAwMDAwWhcNMzEwMTA2MDAw # MDAwWjBIMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xIDAe # BgNVBAMTF0RpZ2lDZXJ0IFRpbWVzdGFtcCAyMDIxMIIBIjANBgkqhkiG9w0BAQEF # AAOCAQ8AMIIBCgKCAQEAwuZhhGfFivUNCKRFymNrUdc6EUK9CnV1TZS0DFC1JhD+ # HchvkWsMlucaXEjvROW/m2HNFZFiWrj/ZwucY/02aoH6KfjdK3CF3gIY83htvH35 # x20JPb5qdofpir34hF0edsnkxnZ2OlPR0dNaNo/Go+EvGzq3YdZz7E5tM4p8XUUt # S7FQ5kE6N1aG3JMjjfdQJehk5t3Tjy9XtYcg6w6OLNUj2vRNeEbjA4MxKUpcDDGK # SoyIxfcwWvkUrxVfbENJCf0mI1P2jWPoGqtbsR0wwptpgrTb/FZUvB+hh6u+elsK # IC9LCcmVp42y+tZji06lchzun3oBc/gZ1v4NSYS9AQIDAQABo4IBuDCCAbQwDgYD # VR0PAQH/BAQDAgeAMAwGA1UdEwEB/wQCMAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUH # AwgwQQYDVR0gBDowODA2BglghkgBhv1sBwEwKTAnBggrBgEFBQcCARYbaHR0cDov # L3d3dy5kaWdpY2VydC5jb20vQ1BTMB8GA1UdIwQYMBaAFPS24SAd/imu0uRhpbKi # JbLIFzVuMB0GA1UdDgQWBBQ2RIaOpLqwZr68KC0dRDbd42p6vDBxBgNVHR8EajBo # MDKgMKAuhixodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vc2hhMi1hc3N1cmVkLXRz # LmNybDAyoDCgLoYsaHR0cDovL2NybDQuZGlnaWNlcnQuY29tL3NoYTItYXNzdXJl # ZC10cy5jcmwwgYUGCCsGAQUFBwEBBHkwdzAkBggrBgEFBQcwAYYYaHR0cDovL29j # c3AuZGlnaWNlcnQuY29tME8GCCsGAQUFBzAChkNodHRwOi8vY2FjZXJ0cy5kaWdp # Y2VydC5jb20vRGlnaUNlcnRTSEEyQXNzdXJlZElEVGltZXN0YW1waW5nQ0EuY3J0 # MA0GCSqGSIb3DQEBCwUAA4IBAQBIHNy16ZojvOca5yAOjmdG/UJyUXQKI0ejq5LS # JcRwWb4UoOUngaVNFBUZB3nw0QTDhtk7vf5EAmZN7WmkD/a4cM9i6PVRSnh5Nnon # t/PnUp+Tp+1DnnvntN1BIon7h6JGA0789P63ZHdjXyNSaYOC+hpT7ZDMjaEXcw30 # 82U5cEvznNZ6e9oMvD0y0BvL9WH8dQgAdryBDvjA4VzPxBFy5xtkSdgimnUVQvUt # MjiB2vRgorq0Uvtc4GEkJU+y38kpqHNDUdq9Y9YfW5v3LhtPEx33Sg1xfpe39D+E # 68Hjo0mh+s6nv1bPull2YYlffqe0jmd4+TaY4cso2luHpoovMIIFMTCCBBmgAwIB # AgIQCqEl1tYyG35B5AXaNpfCFTANBgkqhkiG9w0BAQsFADBlMQswCQYDVQQGEwJV # UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu # Y29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwHhcNMTYw # MTA3MTIwMDAwWhcNMzEwMTA3MTIwMDAwWjByMQswCQYDVQQGEwJVUzEVMBMGA1UE # ChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMTEwLwYD # VQQDEyhEaWdpQ2VydCBTSEEyIEFzc3VyZWQgSUQgVGltZXN0YW1waW5nIENBMIIB # IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvdAy7kvNj3/dqbqCmcU5VChX # tiNKxA4HRTNREH3Q+X1NaH7ntqD0jbOI5Je/YyGQmL8TvFfTw+F+CNZqFAA49y4e # O+7MpvYyWf5fZT/gm+vjRkcGGlV+Cyd+wKL1oODeIj8O/36V+/OjuiI+GKwR5PCZ # A207hXwJ0+5dyJoLVOOoCXFr4M8iEA91z3FyTgqt30A6XLdR4aF5FMZNJCMwXbzs # PGBqrC8HzP3w6kfZiFBe/WZuVmEnKYmEUeaC50ZQ/ZQqLKfkdT66mA+Ef58xFNat # 1fJky3seBdCEGXIX8RcG7z3N1k3vBkL9olMqT4UdxB08r8/arBD13ays6Vb/kwID # AQABo4IBzjCCAcowHQYDVR0OBBYEFPS24SAd/imu0uRhpbKiJbLIFzVuMB8GA1Ud # IwQYMBaAFEXroq/0ksuCMS1Ri6enIZ3zbcgPMBIGA1UdEwEB/wQIMAYBAf8CAQAw # DgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUFBwMIMHkGCCsGAQUFBwEB # BG0wazAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEMGCCsG # AQUFBzAChjdodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRBc3N1 # cmVkSURSb290Q0EuY3J0MIGBBgNVHR8EejB4MDqgOKA2hjRodHRwOi8vY3JsNC5k # aWdpY2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURSb290Q0EuY3JsMDqgOKA2hjRo # dHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURSb290Q0Eu # Y3JsMFAGA1UdIARJMEcwOAYKYIZIAYb9bAACBDAqMCgGCCsGAQUFBwIBFhxodHRw # czovL3d3dy5kaWdpY2VydC5jb20vQ1BTMAsGCWCGSAGG/WwHATANBgkqhkiG9w0B # AQsFAAOCAQEAcZUS6VGHVmnN793afKpjerN4zwY3QITvS4S/ys8DAv3Fp8MOIEIs # r3fzKx8MIVoqtwU0HWqumfgnoma/Capg33akOpMP+LLR2HwZYuhegiUexLoceywh # 4tZbLBQ1QwRostt1AuByx5jWPGTlH0gQGF+JOGFNYkYkh2OMkVIsrymJ5Xgf1gsU # pYDXEkdws3XVk4WTfraSZ/tTYYmo9WuWwPRYaQ18yAGxuSh1t5ljhSKMYcp5lH5Z # /IwP42+1ASa2bKXuh1Eh5Fhgm7oMLSttosR+u8QlK0cCCHxJrhO24XxCQijGGFbP # QTS2Zl22dHv1VjMiLyI2skuiSpXY9aaOUjGCAk0wggJJAgEBMIGGMHIxCzAJBgNV # BAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdp # Y2VydC5jb20xMTAvBgNVBAMTKERpZ2lDZXJ0IFNIQTIgQXNzdXJlZCBJRCBUaW1l # c3RhbXBpbmcgQ0ECEA1CSuC+Ooj/YEAhzhQA8N0wDQYJYIZIAWUDBAIBBQCggZgw # GgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yMTAy # MjQyMTQyNDNaMCsGCyqGSIb3DQEJEAIMMRwwGjAYMBYEFOHXgqjhkb7va8oWkbWq # tJSmJJvzMC8GCSqGSIb3DQEJBDEiBCD6vC15jgmo+c67NXeJR5Fh0OfwuJJcJOJq # FkilgAHYEzANBgkqhkiG9w0BAQEFAASCAQA1QSrvlQXKQzKaf6GlCTsED6DQ/C8K # P164MpS8OnsHNyakO9HFRJwyu+YN443WeX9rkJksSRUEpVYU/AGm3IsH57JW48pU # X6Rf9JtiId86oVgMC/MG3PT5IhLH6wP9ryzszbU+k+l4Zchas8rXWdf57r650V0+ # JVaGLrv+L9I6rYYJcphs/VGx5vY0tcJuXNi6WsAwLhWDlPPinvRewBiZttyTbIBn # rkKjkYCR1Ynz1OMAf3jUiBeG6El+kFJpB7LKztOpo1rGdmU/Viu8DCRom17KhaUg # vjR8zWLzdZscqMdRDAX87S38rWFGLNHU+9h2IJN/etG3rEgRDRohacHe # SIG # End signature block |