ProgramUpdater.ps1, Teil 1

Automates upgrading a .NET and ASP.NET web application on IIS with MSSQL backend and .NET client installers. Chooses correct deployment packages, starts data model upgrade, builds a new web.config with the new settings from deployment packages and by selectively copying the installation specific, client side settings from the old files by attrib name.

#Requires -Version 4 #Requires -RunAsAdmin

# Todo: $strNewVersion, zunaechst aus .msi-Props ausgelesen, aus dann entpackter Anwendung -> bin\ -> .dll neu auslesen, falls nicht zueinander passende Deploymentpakete .zip und .msi zusammenkopiert wurden [string]$strNewVersion = ((Get-ItemProperty ($strNewVerFolder + “bin<filename>.dll”)).VersionInfo).ProductVersion fuehrt aktuell zu Mosern ueber mangelnde Zugriffsrechte # Todo: Optische Aufbereitung des Mailbodys oder Attachments ueber CC-Webservice

<# .SYNOPSIS Update <Program> Standard. Automates upgrading a .NET and ASP.NET web application on IIS with MSSQL backend and .NET client installers. Chooses correct deployment packages, starts data model upgrade, builds a new web.config with the new settings from deployment packages and by selectively copying the installation specific, client side settings from the old files by attrib name. .DESCRIPTION Update <Program> Standard Syntax: [D:\Websites\Deploy\UpdaterScript]ProgramUpdaterAsAdmin.lnk (with a script file path in the command line saying D:\Websites\Deploy\UpdaterScript\ProgramUpdater.ps1. The default paths (see below) to both .INIs will usually work. If you need to supply an absolute path to one or boths .INIs, edit ProgramUpdaterAsAdmin.lnk's command line along this syntax note: D:\Websites\Deploy\UpdaterScript\ProgramUpdater.ps1 [[-IniFileUser] <String>] [[-IniFileWebConfig] <String>] [<CommonParameters>] ) Use this internal helper at your own risk. Addressed audience are informed IT workers. The author cannot be held liable for loss of data by the use of this script. * This script requires PowerShell version 4 (in WMF, but if you need update anyway, install WMF 5, https://www.microsoft.com/en-us/download/confirmation.aspx?id=54616&6B49FDFB-8E5B-4B07-BC31-15695C5A2143=1). * This script requires to be run as admin. * You may supply absolute paths inc. file name to the two .ini files as the only parameter. Use “ ” if path contains blanks. * Run without parameter, this script searches its .ini at D:\Websites\Deploy\UpdaterScript\ProgramUpdaterSettingsuser.ini plus D:\Websites\Deploy\UpdaterScript\ProgramUpdaterSettingsweb.config.ini. (In fact, this script retrieves its own full path and replaces “.ps1” with “Settingsuser.ini” / “Settingsweb.config.ini”, and this will regularly lead to the paths mentioned above.) * Your website folder path in Settingsuser.ini must start with “D:\Websites\“. If it does not, the script adds this automatically. * Your website folder path in Settingsuser.ini must end with “\Company.Program.Gui\“. If it does not, the script adds this automatically. * Deployment folder is always D:\websites\deploy. Copy your deployment packages (.ZIPs, .MSIs, UpgradeTool.exe) into this folder. * The current default for the MS SQL version (part of deployment packages' names) is 2008 (means: 2008 and 2008 R2). You can define other versions (allowed: 2012, 2014) by machine name in Settingsuser.ini, and you can change the default in Settingsuser.ini. When the script lists the detected parameters and paths and asks for your permission to go on, check the MS SQL version (check everything). If the version does not match the <Program> instance of SQL Server on the customer's machine, stop the script and edit Settingsuser.ini before you start it again. * To not overwrite user settings during deployment, this script is shipped with ProgramUpdaterSettingsusermaster.ini used for dev tests. You can check this e.g. for version updates. You are warned if script and ini versions (inside) don't fit. * The old version is detected from <WebsiteFolder>\bin&lt;FirstDll>.dll's file properties ('Product version'). * The new version is detected from D:\websites\deploy&lt;FirstMsi>.msi's file properties ('Tags'). * The backup folder is always <WebsiteFolder><OldVersion><DateTime>. * The script log folder is always D:\websites\deploy\UpdateLogs&lt;DateTime>. You will find some explicitly logged results, powershell script errors, and the UpgradeTool log in there. * DevExpress deployment folder is always D:\websites\deploy\DevExpress&lt;Version>. Ensure the DevExpress runtime files are copied to this folder before. * The script stops the IIS website for the duration of its run. .PARAMETER IniFileUser Default: <ThisScriptsPath>&lt;ThisScriptsNameWithoutExtension>Settingsuser.ini (almost always D:\Websites\Deploy\UpdaterScript\ProgramUpdaterSettingsuser.ini). .PARAMETER IniFileWebConfig Default: <ThisScriptsPath>&lt;ThisScriptsNameWithoutExtension>Settingsweb.config.ini (almost always D:\Websites\Deploy\UpdaterScript\ProgramUpdaterSettings_web.config.ini). .EXAMPLE PS C:\util> .\ProgramUpdaterAsAdmin.lnk # This script is shipped with ProgramUpdaterAsAdmin.lnk, you should prefer this. If you need to change the call e.g. to call another .ini, edit the .lnk. .NOTES Author: ps@<mydomain>.de Date: 2 June 2016 to 24 Jan 2019 Todo: $strNewVersion, zunaechst aus .msi-Props ausgelesen, aus dann entpacktem CC -> bin\ -> .dll neu auslesen, falls nicht zueinander passende Deploymentpakete .zip und .msi zusammenkopiert wurden [string]$strNewVersion = ((Get-ItemProperty ($strNewVerFolder + “bin\Company.Program.BusinessLogik.dll”)).VersionInfo).ProductVersion fuehrt aktuell zu Mosern ueber mangelnde Zugriffsrechte Todo: Optische Aufbereitung des Mailbodys oder Attachments ueber Webservice Version: 1.2.0.2 — check with TFS

#>

param ( [Parameter(Position=0)] # [string]$strWebsiteFolder = “D:\Websites\Company\Company.Program.Gui\” [string]$IniFileUser = $PSCommandPath -replace(”.ps1”,“Settingsuser.ini”), [Parameter(Position=1)] # [string]$strWebsiteFolder = “D:\Websites\Company\Company.Program.Gui\” [string]$IniFileWebConfig = $PSCommandPath -replace(”.ps1”,“Settingsweb.config.ini”)

)

PROCESS {

$Error.Clear()

if ($IniFileUser -in ($null, ””)) { [string]$IniFileUser = $PSCommandPath -replace(”.ps1”,“Settingsuser.ini”) } if ($IniFileUserWebConfig -in ($null, ””)) { [string]$IniFileWebConfig = $PSCommandPath -replace(”.ps1”,“Settingsweb.config.ini”) }

[string]$strWebConfigMsiContent = @” <?xml version=“1.0” encoding=“UTF-8”?> <configuration> <system.webServer> <directoryBrowse enabled=“true” showFlags=“Date, Time, Size, Extension, LongDate” /> </system.webServer> </configuration> ”@

[string]$strIniVersion = “1.5_20181019”

# if ($psise -ne $null) { $strWebsiteFolder = “Company” } # Edit the $strWebsiteFolder part of this line to run this script in ISE.

Add-Type -AssemblyName System.Windows.Forms Import-Module WebAdministration

#region Functions

Function Get-IniContent {
<# .Synopsis Gets the content of an INI file .Description Gets the content of an INI file and returns it as a hashtable .Notes Author : Oliver Lipkau <oliver@lipkau.net> Blog : http://oliver.lipkau.net/blog/ Source : https://github.com/lipkau/PsIni http://gallery.technet.microsoft.com/scriptcenter/ea40c1ef-c856-434b-b8fb-ebd7a76e8d91 Version : 1.0 – 2010/03/12 – Initial release 1.1 – 2014/12/11 – Typo (Thx SLDR) Typo (Thx Dave Stiff) #Requires -Version 2.0 .Inputs System.String .Outputs System.Collections.Hashtable .Parameter FilePath Specifies the path to the input file. .Example $FileContent = Get-IniContent “C:\myinifile.ini” —————– Description Saves the content of the c:\myinifile.ini in a hashtable called $FileContent .Example $inifilepath | $FileContent = Get-IniContent —————– Description Gets the content of the ini file passed through the pipe into a hashtable called $FileContent .Example C:\PS>$FileContent = Get-IniContent “c:\settings.ini” C:\PS>$FileContent[“Section”][“Key”] —————– Description Returns the key “Key” of the section “Section” from the C:\settings.ini file .Link Out-IniFile #>

Param(
[ValidateNotNullOrEmpty()]
[ValidateScript({(Test-Path $) -and ((Get-Item $).Extension -eq ”.ini”)})]
[Parameter(ValueFromPipeline=$True,Mandatory=$True)]
[string]$FilePath
)

Write-Verbose ”$($MyInvocation.MyCommand.Name):: Function started”

Write-Verbose ”$($MyInvocation.MyCommand.Name):: Processing file: $Filepath”

$ini = @{}
switch -regex -file $FilePath
{
”^[(.+)]$” # Section {
$section = $matches[1]
$ini[$section] = @{}
$CommentCount = 0
}
”^(;.)$” # Comment {
if (!($section))
{
$section = “No-Section”
$ini[$section] = @{}
}
$value = $matches[1]
$CommentCount = $CommentCount + 1
$name = “Comment” + $CommentCount
$ini[$section][$name] = $value
}
”(.+?)\s
=\s(.)” # Key {
if (!($section))
{
$section = “No-Section”
$ini[$section] = @{}
}
$name,$value = $matches[1..2]
$ini[$section][$name] = $value
}
}

Write-Verbose ”$($MyInvocation.MyCommand.Name):: Finished Processing file: $FilePath”
Return $ini

Write-Verbose ”$($MyInvocation.MyCommand.Name):: Function ended” } # Function Get-IniContent

function CopyWebConfigAttrValue($rootNew, $rootOld, $strSection, $strKeyName, $strLog) { [string]$strKeyValueRead = $rootOld.SelectSingleNode(”//$strSection”).SelectSingleNode(“add[@key='$strKeyName']/@value”).'#text' [string]$strKeyValueReadNew = $rootNew.SelectSingleNode(”//$strSection”).SelectSingleNode(“add[@key='$strKeyName']/@value”).'#text' if ( $strKeyValueReadNew -in ($null, ””) ) { $newItem = $rootNew.SelectSingleNode(”//$strSection”).SelectSingleNode(“add[1]”).Clone() $newItem.SetAttribute(“key”,$strKeyName) $newItem.SetAttribute(“value”,$strKeyValueRead) $rootNew.SelectSingleNode(”//$strSection”).AppendChild($newItem) [string]$strMsg = “web.config: Setting '$strSection.$strKeyName' did not exist in webref.config (you should maintain webref.config) and has been automatically created by this script in target web.config, value has been set to '$strKeyValueRead', from former version's web.config. “ + (Get-Date -format “yyyyMMddHHmm”) Add-Type -AssemblyName PresentationFramework [System.Windows.MessageBox]::Show($strMsg,'<Program> Updater: New Setting','OK','Error') Add-Content -Value $strMsg -PassThru -Path $strLog } else { $rootNew.SelectSingleNode(”//$strSection”).SelectSingleNode(“add[@key='$strKeyName']/@value”).'#text' = $strKeyValueRead } [string]$strKeyValueWritten = $rootNew.SelectSingleNode(”//$strSection”).SelectSingleNode(“add[@key='$strKeyName']/@value”).'#text' switch ($strKeyName) { 'DbVerbindungString' { Add-Content -Value ($strKeyName + “: “ + $strKeyValueWritten + (Get-Date -format “yyyyMMddHHmm”)) -PassThru -Path $strLog } { $ -in 'SystemAccountPw', 'SmtpPw' } { $strKeyValueRead = ”(confidential)” $strKeyValueWritten = ”(confidential)” } } Add-Content -Value (“web.config: Setting '$strSection.$strKeyName': read '$strKeyValueRead', written '$strKeyValueWritten' “ + (Get-Date -format “yyyyMMddHHmm”)) -PassThru -Path $strLog } # function CopyWebConfigAttrValue

function SetWebConfigAttrValue($root, $strSection, $strKeyName, $strKeyValue, $strLog) { $strKeyValueRead = $root.SelectSingleNode(”//$strSection”).SelectSingleNode(“add[@key='$strKeyName']/@value”).'#text' if ( $strKeyValueRead -in ($null, ””) ) { $newItem = $root.SelectSingleNode(”//$strSection”).SelectSingleNode(“add[1]”).Clone() $newItem.SetAttribute(“key”,$strKeyName) $newItem.SetAttribute(“value”,$strKeyValue) $rootNew.SelectSingleNode(”//$strSection”).AppendChild($newItem) [string]$strMsg = “web.config: Setting '$strSection.$strKeyName' did not exist in webref.config (you should maintain webref.config) and has been automatically created by this script in target web.config, value has been set to '$strKeyValueRead', from former version's web.config. “ + (Get-Date -format “yyyyMMdd_HHmm”) Add-Type -AssemblyName PresentationFramework [System.Windows.MessageBox]::Show($strMsg,'<Program> Updater: New Setting','OK','Error') Add-Content -Value $strMsg -PassThru -Path $strLog } else { $root.SelectSingleNode(”//$strSection”).SelectSingleNode(“add[@key='$strKeyName']/@value”).'#text' = $strKeyValue }

[string]$strKeyValueWritten = $root.SelectSingleNode(”//$strSection”).SelectSingleNode(“add[@key='$strKeyName']/@value”).'#text' switch ($strKeyName) { 'DbVerbindungString' { Add-Content -Value ($strKeyName + “: “ + $strKeyValueWritten + (Get-Date -format “yyyyMMddHHmm”)) -PassThru -Path $strLog } { $ -in 'SystemAccountPw', 'SmtpPw' } { $strKeyValueRead = ”(confidential)” $strKeyValueWritten = ”(confidential)” } } Add-Content -Value (“web.config: Setting '$strSection.$strKeyName' was set to '$strKeyValueWritten' “ + (Get-Date -format “yyyyMMdd_HHmm”)) -PassThru -Path $strLog } # function SetWebConfigAttrValue

# copy authorization keys – http://michlstechblog.info/blog/powershell-some-basic-xml-handling-with-powershell-and-net/ function CopyWebConfigAuthorizationNode($rootNew, $rootOld, $xmlConfigNew, $strLog) {

$rootNew.'system.web'.RemoveChild($rootNew.'system.web'.SelectSingleNode(“authorization”)) | Out-Null $newNode = $xmlConfigNew.ImportNode($rootOld.'system.web'.SelectSingleNode(“authorization”), $true) $rootNew.'system.web'.AppendChild($newNode) | Out-Null Add-Content -Value (“web.config: Copied 'authorization' section. “ + (Get-Date -format “yyyyMMdd_HHmm”)) -PassThru -Path $strLog

}

function AdaptWebConfig([string]$strNewVerFolder, [string]$strOldVerFolder, [string]$strNewVersion, [string]$strLog, $hshSettings) {

Rename-Item ($strNewVerFolder + “web_ref.config”) “web.config” [string]$strOldVerConfig = $strOldVerFolder + “web.config” [string]$strNewVerConfig = $strNewVerFolder + “web.config”

Add-Content -Value (“Old web.config: “ + $strOldVerConfig) -Path $strLog -PassThru Add-Content -Value (“New web.config: “ + $strNewVerConfig) -Path $strLog -PassThru

“Adapting $strNewVerConfig”

[xml]$xmlConfigOld = Get-Content $strOldVerConfig [xml]$xmlConfigNew = Get-Content $strNewVerConfig $rootOld = $xmlConfigOld.getDocumentElement() $rootNew = $xmlConfigNew.getDocumentElement()

[int]$intProgMainVersion = ($strNewVersion.Split(”.”))[0]

switch ($intProgMainVersion) { 2 { # NotebookVersion SetWebConfigAttrValue $rootNew “appSettingsNotebook” “NotebookVersion” $strNewVersion $strLog

# <authorization> CopyWebConfigAuthorizationNode $rootNew $rootOld $xmlConfigNew $strLog

# appSettings foreach ($key in $hshSettings[“web.config v2 appSettings”].Keys) { CopyWebConfigAttrValue $rootNew $rootOld “appSettings” $hshSettings[“web.config v2 appSettings”][$key] $strLog }

# appSettingsNotebook foreach ($key in $hshSettings[“web.config v2 appSettingsNotebook”].Keys) { CopyWebConfigAttrValue $rootNew $rootOld “appSettingsNotebook” $hshSettings[“web.config v2 appSettingsNotebook”][$key] $strLog }

} # switch 2

3 { # appSettings foreach ($key in $hshSettings[“web.config v3 appSettings”].Keys) { CopyWebConfigAttrValue $rootNew $rootOld “appSettings” $hshSettings[“web.config v3 appSettings”][$key] $strLog }

# appSettingsNotebook foreach ($key in $hshSettings[“web.config v3 appSettingsNotebook”].Keys) { CopyWebConfigAttrValue $rootNew $rootOld “appSettingsNotebook” $hshSettings[“web.config v3 appSettingsNotebook”][$key] $strLog }

} # switch 3 } # switch

$xmlConfigNew.Save($strNewVerConfig) | Out-Null

[string]$strNewVerConfigBak = $strNewVerFolder + “BakCfg\” if ((Test-Path $strNewVerConfigBak) -eq $false) { New-Item -Path (Split-Path -Path $strNewVerConfigBak -Parent) -Name (Split-Path -Path $strNewVerConfigBak -Leaf) -ItemType directory | Out-Null } $strNewVerConfigBak += (“web.config.” + (Get-Date -format “yyyyMMdd.HHmm”) + “h”) Copy-Item $strNewVerConfig $strNewVerConfigBak

} # function AdaptWebConfig

function MailResult ($strLogFolder, $hshSettings) { [string]$ThisScriptCopy = $PSCommandPath + ”.txt” Copy-Item $PSCommandPath $ThisScriptCopy -Force $dirLogs = Get-ChildItem ($strLogFolder + “*”)

[string[]]$astrAttachments = @() $astrAttachments += $ThisScriptCopy foreach ($strLogFile in $dirLogs) { $astrAttachments += $strLogFile.FullName }

[string]$strBody = “ProgramUpdater was started by “ + $env:USERDOMAIN + “\” + $env:USERNAME $strBody += ” on “ + [System.Net.Dns]::GetHostByName((hostname)).HostName

foreach ($strAttachment in $astrAttachments) { $strBody += nn ====================” $strBody += nn “ + $strAttachment $strBody += Get-Content $strAttachment }

[string[]]$astrMailRecipients = @() foreach ($key in $hshSettings[“Result mail”].Keys) { if ($key -like “To*”) { $astrMailRecipients += $hshSettings[“Result mail”][$key] } }

if ($hshSettings[“Result mail”][“Use”] -eq “1”) { Send-MailMessage -To $astrMailRecipients -From <span style="color:#19177c">$hshSettings</span>[<span style="color:#4070a0">"Result mail"</span>][<span style="color:#4070a0">"From"</span>] -Subject “ProgramUpdater” -Body <span style="color:#19177c">$strBody</span> -SmtpServer $hshSettings[“Result mail”][“SmtpServer”] -Attachments <span style="color:#19177c">$astrAttachments</span> -ErrorAction SilentlyContinue }

if ($hshSettings[“Result mail WS”][“Use”] -eq “1”) { $pw = ConvertTo-SecureString -String $hshSettings[“Result mail WS”][“WsUserPw”] -AsPlainText -Force $creds = New-Object System.Management.Automation.PSCredential $hshSettings[“Result mail WS”][“WsUserName”], $pw $wsSmtp = New-WebServiceProxy $hshSettings[“Result mail WS”][“WsUrl”] -Credential $creds foreach ($Addr in $astrMailRecipients) { $wsSmtp.SendeEmailMitStandardAbsender($Addr,””,””,“ProgramUpdater”,$strBody) } } } # function MailResult

Function BackslashTrailToFolder ($strFolder) { if ( ($strFolder.Substring($strFolder.Length – 1)) -ne “\” ) { $strFolder += “\” return $strFolder } }

Function FolderNameWoTrailingBackslash ($strFolder) { if ( ($strFolder.Substring($strFolder.Length – 1)) -eq “\” ) { $strFolder = $strFolder.Substring(0, $strFolder.Length – 1) return $strFolder } }