diff --git a/Scenarios/AzSHCI and Dell Servers Update/Scenario.ps1 b/Scenarios/AzSHCI and Dell Servers Update/Scenario.ps1 index d5985b37..7fe6f41f 100644 --- a/Scenarios/AzSHCI and Dell Servers Update/Scenario.ps1 +++ b/Scenarios/AzSHCI and Dell Servers Update/Scenario.ps1 @@ -1,6 +1,7 @@ #I created this scenario for updating my Ax6515 based lab. I don't think it's robust enough for production usage. #Ideas grabbed from here https://github.com/DellProSupportGse/Tools/blob/main/DART.ps1 -#todo: update offline from file share +#DSU documentation: https://www.dell.com/support/manuals/en-us/system-update/dsu_2.0.2.0_ug/introduction-to-dell-system-update?guid=guid-3061ce04-4779-4d50-8276-ddf911e3884a&lang=en-us +#you can rewrite code to upload binaries to \\$node\c$ .. but I decided to use pssession. Just make sure maxevenlopesize is big enough. #region Variables #make sure failover clustering management is installed @@ -9,21 +10,27 @@ $ClusterName=(Get-Cluster -Domain $env:USERDOMAIN | Out-GridView -OutputMode Single -Title "Please select failover cluster to patch").Name $Nodes=(Get-ClusterNode -Cluster $ClusterName).Name - $DSUDownloadFolder="$env:USERPROFILE\Downloads\DSU" - $DSUPackageDownloadFolder="$env:USERPROFILE\Downloads\DSUPackage" + $DellToolsDownloadFolder="$Env:UserProfile\Downloads\Dell\" - $Updates="Recommended" #or "All" + $MicrosoftUpdatesDownloadFolder="$Env:UserProfile\Downloads\Microsoft\" + + #folder on servers where all binaries will be staged + $BinariesLocation="c:\Dell\" + + $Offline=$True #assuming nodes are offline. If true, all tools will be downloaded to management machine, all updates will be installed "offline" + + $MSUpdates="Recommended" #or "All" - for online version only $ForceReboot=$false #or $true, to reboot even if there were no updates applied - # Configure Search Criteria for windows update - if ($Updates -eq "Recommended"){ - $SearchCriteriaAllUpdates = "IsInstalled=0 and DeploymentAction='Installation' or + #Configure Search Criteria for windows update when running online + if ($MSUpdates -eq "Recommended"){ + $UpdatesSearchCriteria = "IsInstalled=0 and DeploymentAction='Installation' or IsPresent=1 and DeploymentAction='Uninstallation' or IsInstalled=1 and DeploymentAction='Installation' and RebootRequired=1 or IsInstalled=0 and DeploymentAction='Uninstallation' and RebootRequired=1" - }elseif ($Updates -eq "All"){ - $SearchCriteriaAllUpdates = "IsInstalled=0 and DeploymentAction='Installation' or + }elseif ($MSUpdates -eq "All"){ + $UpdatesSearchCriteria = "IsInstalled=0 and DeploymentAction='Installation' or IsInstalled=0 and DeploymentAction='OptionalInstallation' or IsPresent=1 and DeploymentAction='Uninstallation' or IsInstalled=1 and DeploymentAction='Installation' and RebootRequired=1 or @@ -31,134 +38,248 @@ } #endregion -#region prepare DSU binaries - #Download DSU - #https://github.com/DellProSupportGse/Tools/blob/main/DART.ps1 - +#region download all required Dell binaries + #create downloads folder + if (-not (Test-Path $DellToolsDownloadFolder -ErrorAction Ignore)){New-Item -Path $DellToolsDownloadFolder -ItemType Directory} #grab DSU links from Dell website - $URL="https://dl.dell.com/omimswac/dsu/" - $Results=Invoke-WebRequest $URL -UseDefaultCredentials - $Links=$results.Links.href | Select-Object -Skip 1 - #create PSObject from results - $DSUs=@() - foreach ($Link in $Links){ - $DSUs+=[PSCustomObject]@{ - Link = "https://dl.dell.com$Link" - Version = $link -split "_" | Select-Object -Last 2 | Select-Object -First 1 + $URL="https://dl.dell.com/omimswac/dsu/" + $Results=Invoke-WebRequest $URL -UseDefaultCredentials + $Links=$results.Links.href | Select-Object -Skip 1 + #create PSObject from results + $DSUs=@() + foreach ($Link in $Links){ + $DSUs+=[PSCustomObject]@{ + Link = "https://dl.dell.com$Link" + Version = $link -split "_" | Select-Object -Last 2 | Select-Object -First 1 + } } - } - #download latest to separate folder - $LatestDSU=$DSUs | Sort-Object Version | Select-Object -Last 1 - if (-not (Test-Path $DSUDownloadFolder -ErrorAction Ignore)){New-Item -Path $DSUDownloadFolder -ItemType Directory} - Start-BitsTransfer -Source $LatestDSU.Link -Destination $DSUDownloadFolder\DSU.exe - - #upload DSU to servers - $Sessions=New-PSSession -ComputerName $Nodes - Invoke-Command -Session $Sessions -ScriptBlock { - if (-not (Test-Path $using:DSUDownloadFolder -ErrorAction Ignore)){New-Item -Path $using:DSUDownloadFolder -ItemType Directory} - } + #download latest DSU + $LatestDSU=$DSUs | Sort-Object Version | Select-Object -Last 1 + Start-BitsTransfer -Source $LatestDSU.Link -Destination $DellToolsDownloadFolder\DSU.exe + + #grab IC (inventory collection tool. Required for offline patching) + if ($Offline){ + #grab IC links from Dell website + $URL="https://downloads.dell.com/omimswac/ic/" + $Results=Invoke-WebRequest $URL -UseDefaultCredentials + $Links=$results.Links.href | Select-Object -Skip 1 + #create PSObject from results + $ICs=@() + foreach ($Link in $Links){ + $ICs+=[PSCustomObject]@{ + Link = "https://dl.dell.com$Link" + Version = [int]($link -split "_" | Select-Object -Last 2 | Select-Object -First 1) + } + } + #download latest + $LatestIC=$ICs | Sort-Object Version | Select-Object -Last 1 + Start-BitsTransfer -Source $LatestIC.Link -Destination $DellToolsDownloadFolder\IC.exe + } + + #grab Dell Azure Stack HCI driver catalog https://downloads.dell.com/catalog/ASHCI-Catalog.xml.gz + #Download catalog + Start-BitsTransfer -Source "https://downloads.dell.com/catalog/ASHCI-Catalog.xml.gz" -Destination "$env:UserProfile\Downloads\ASHCI-Catalog.xml.gz" + #unzip gzip to a folder https://scatteredcode.net/download-and-extract-gzip-tar-with-powershell/ + Function Expand-GZipArchive{ + Param( + $infile, + $outfile = ($infile -replace '\.gz$','') + ) + $input = New-Object System.IO.FileStream $inFile, ([IO.FileMode]::Open), ([IO.FileAccess]::Read), ([IO.FileShare]::Read) + $output = New-Object System.IO.FileStream $outFile, ([IO.FileMode]::Create), ([IO.FileAccess]::Write), ([IO.FileShare]::None) + $gzipStream = New-Object System.IO.Compression.GzipStream $input, ([IO.Compression.CompressionMode]::Decompress) + $buffer = New-Object byte[](1024) + while($true){ + $read = $gzipstream.Read($buffer, 0, 1024) + if ($read -le 0){break} + $output.Write($buffer, 0, $read) + } + $gzipStream.Close() + $output.Close() + $input.Close() + } + Expand-GZipArchive "$env:UserProfile\Downloads\ASHCI-Catalog.xml.gz" "$DellToolsDownloadFolder\ASHCI-Catalog.xml" + # +#endregion + +#region download all Microsoft updates + if ($Offline){ + $FileContent = (Invoke-WebRequest -UseBasicParsing -Uri "https://raw.githubusercontent.com/microsoft/MSLab/master/Tools/DownloadLatestCUs.ps1").Content + if (-not (Test-Path $MicrosoftUpdatesDownloadFolder -ErrorAction Ignore)){New-Item -Path $MicrosoftUpdatesDownloadFolder -ItemType Directory} + Set-Content -path "$MicrosoftUpdatesDownloadFolder\DownloadLatestCUs.ps1" -value $FileContent + #run download latest CU (will prompt for what OS you want to download for) + & "$MicrosoftUpdatesDownloadFolder\DownloadLatestCUs.ps1" + } +#endregion + +#region prepare DSU binaries + #upload DSU to servers + $Sessions=New-PSSession -ComputerName $Nodes + Invoke-Command -Session $Sessions -ScriptBlock { + if (-not (Test-Path $using:BinariesLocation -ErrorAction Ignore)){New-Item -Path $using:BinariesLocation -ItemType Directory} + } + foreach ($Session in $Sessions){ + Copy-Item -Path "$DellToolsDownloadFolder\DSU.exe" -Destination "$BinariesLocation" -ToSession $Session -Force -Recurse + } + #install DSU + Invoke-Command -ComputerName $Nodes -ScriptBlock { + Start-Process -FilePath "$using:BinariesLocation\DSU.exe" -ArgumentList "/silent" -Wait + } + + #upload IC.exe to servers + if ($Offline){ foreach ($Session in $Sessions){ - Copy-Item -Path "$DSUDownloadFolder\DSU.exe" -Destination "$DSUDownloadFolder" -ToSession $Session -Force -Recurse + Copy-Item -Path "$DellToolsDownloadFolder\IC.exe" -Destination "$BinariesLocation" -ToSession $Session -Force -Recurse } - $Sessions | Remove-PSSession - #install DSU + } + + #upload catalog + foreach ($Session in $Sessions){ + Copy-Item -Path "$DellToolsDownloadFolder\ASHCI-Catalog.xml" -Destination "$BinariesLocation" -ToSession $Session -Force -Recurse + } + + #close sessions + $Sessions | Remove-PSSession +#endregion + +#region check Dell compliance + #scan for compliance + if ($offline){ + Invoke-Command -ComputerName $Nodes -ScriptBlock { + & "C:\Program Files\Dell\DELL System Update\DSU.exe" --compliance --catalog-location="$using:BinariesLocation\ASHCI-Catalog.xml" --ic-location="$using:BinariesLocation\ic.exe" --output-format="json" --output="$using:BinariesLocation\Compliance.json" + } + }else{ Invoke-Command -ComputerName $Nodes -ScriptBlock { - Start-Process -FilePath "$using:DSUDownloadFolder\DSU.exe" -ArgumentList "/silent" -Wait + & "C:\Program Files\Dell\DELL System Update\DSU.exe" --compliance --catalog-location="$using:BinariesLocation\ASHCI-Catalog.xml" --output-format="json" --output="$using:BinariesLocation\Compliance.json" } + } + #collect results + $Compliance=@() + foreach ($Node in $Nodes){ + $json=Invoke-Command -ComputerName $node -ScriptBlock {Get-Content "$using:BinariesLocation\Compliance.json"} + $object = $json | ConvertFrom-Json + $components=$object.SystemUpdateCompliance.UpdateableComponent + $components | Add-Member -MemberType NoteProperty -Name "ClusterName" -Value $ClusterName + $components | Add-Member -MemberType NoteProperty -Name "NodeName" -Value $Node + $Compliance+=$Components + } - #download catalog and copy DSU Package to servers - #Dell Azure Stack HCI driver catalog https://downloads.dell.com/catalog/ASHCI-Catalog.xml.gz - #Download catalog - Start-BitsTransfer -Source "https://downloads.dell.com/catalog/ASHCI-Catalog.xml.gz" -Destination "$env:UserProfile\Downloads\ASHCI-Catalog.xml.gz" - #unzip gzip to a folder https://scatteredcode.net/download-and-extract-gzip-tar-with-powershell/ - if (-not (Test-Path $DSUPackageDownloadFolder -ErrorAction Ignore)){New-Item -Path $DSUPackageDownloadFolder -ItemType Directory} - Function Expand-GZipArchive{ - Param( - $infile, - $outfile = ($infile -replace '\.gz$','') - ) - $input = New-Object System.IO.FileStream $inFile, ([IO.FileMode]::Open), ([IO.FileAccess]::Read), ([IO.FileShare]::Read) - $output = New-Object System.IO.FileStream $outFile, ([IO.FileMode]::Create), ([IO.FileAccess]::Write), ([IO.FileShare]::None) - $gzipStream = New-Object System.IO.Compression.GzipStream $input, ([IO.Compression.CompressionMode]::Decompress) - $buffer = New-Object byte[](1024) - while($true){ - $read = $gzipstream.Read($buffer, 0, 1024) - if ($read -le 0){break} - $output.Write($buffer, 0, $read) - } - $gzipStream.Close() - $output.Close() - $input.Close() + #display results + $Compliance | Out-GridView + + #you can also select what updates you want to deploy to each node + #$Compliance=$Compliance | Out-GridView -OutputMode Multiple +#endregion + +#region check Dell inventory + <# + #scan for inventory + if ($offline){ + Invoke-Command -ComputerName $Nodes -ScriptBlock { + & "C:\Program Files\Dell\DELL System Update\DSU.exe" --inventory --catalog-location="$using:BinariesLocation\ASHCI-Catalog.xml" --ic-location="$using:BinariesLocation\ic.exe" --output-format="json" --output="$using:BinariesLocation\Inventory.json" } - Expand-GZipArchive "$env:UserProfile\Downloads\ASHCI-Catalog.xml.gz" "$DSUPackageDownloadFolder\ASHCI-Catalog.xml" - #create answerfile for DU - $content='@ - a - c - @' - Set-Content -Path "$DSUPackageDownloadFolder\answer.txt" -Value $content -NoNewline - $content='"C:\Program Files\Dell\DELL System Update\DSU.exe" --catalog-location=ASHCI-Catalog.xml --apply-upgrades #endregion -#region check if there are any updates needed -$ScanResult=Invoke-Command -ComputerName $Nodes -ScriptBlock { - & "C:\Program Files\Dell\DELL System Update\DSU.exe" --catalog-location="$using:DSUPackageDownloadFolder\ASHCI-Catalog.xml" --preview | Out-Null - $Result=(Get-content "C:\ProgramData\Dell\DELL System Update\dell_dup\DSU_STATUS.json" | ConvertFrom-JSon).systemupdatestatus.invokerinfo.statusmessage - if ($Result -like "No Applicable Update*" ){ - $DellUpdateRequired=$false - }else{ - $DellUpdateRequired=$true +#region download updates to $DellToolsDownloadFolder on management machine based on compliance scan if offline + if ($offline){ + $DellUpdatesList=($Compliance | Where-Object ComplianceStatus -eq $False | Group-Object PackageFilePath | ForEach-Object {$_.Group | Select-Object PackageFilePath -First 1}).PackageFilePath + foreach ($Update in $DellUpdatesList){ + #create destination folder + New-Item -Path ("$DellToolsDownloadFolder\Updates\$update" | Split-Path -Parent) -ItemType Directory -Force + Start-BitsTransfer -Source https://dl.dell.com/$Update -Destination "$DellToolsDownloadFolder\Updates\$update" + } } +#endregion - #scan for microsoft updates - $Searcher = New-Object -ComObject Microsoft.Update.Searcher - $SearchResult = $Searcher.Search($using:SearchCriteriaAllUpdates).Updates - if ($SearchResult.Count -gt 0){ - $MicrosoftUpdateRequired=$True - }else{ - $MicrosoftUpdateRequired=$False +#region upload drivers to nodes + if ($offline){ + $Sessions=New-PSSession -ComputerName ($Compliance.NodeName | Select-Object -Unique) + foreach ($session in $sessions){ + Copy-Item -Path "$DellToolsDownloadFolder\Updates\" -Destination $BinariesLocation -Recurse -ToSession $Session + } + #close sessions + $Sessions | Remove-PSSession } +#endregion - #grab windows version - $ComputersInfo = Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\' - - $Output=@() - $Output += [PSCustomObject]@{ - "DellUpdateRequired" = $DellUpdateRequired - "MicrosoftUpdateRequired" = $MicrosoftUpdateRequired - "MicrosoftUpdates" = $SearchResult - "ComputerName" = $env:COMPUTERNAME - "CurrentBuildNumber" = $ComputersInfo.CurrentBuildNumber - "UBR" = $ComputersInfo.UBR +#region upload Microsoft update(s) to nodes +if ($offline){ + #Grab CAB + $MSUs=Get-ChildItem -Path $MicrosoftUpdatesDownloadFolder -Recurse | Where-Object Extension -eq ".msu" + #copy over to nodes + $Sessions=New-PSSession -ComputerName ($Compliance.NodeName | Select-Object -Unique) + foreach ($session in $sessions){ + #copy microsoft update + foreach ($MSU in $MSUs) { + Copy-Item $MSU.FullName -Destination "$BinariesLocation\Updates\" -ToSession $Session -Force + } } - return $Output + #close sessions + $Sessions | Remove-PSSession } -$ScanResult #endregion -#region install DSU and Microsoft updates and restart - #Configure virtual account - Invoke-Command -ComputerName $nodes -ScriptBlock { - New-PSSessionConfigurationFile -RunAsVirtualAccount -Path $env:TEMP\VirtualAccount.pssc - Register-PSSessionConfiguration -Name 'VirtualAccount' -Path $env:TEMP\VirtualAccount.pssc -Force - } -ErrorAction Ignore - +#region check Microsoft Compliance + $ScanResult=Invoke-Command -ComputerName $Nodes -ScriptBlock { + if ($using:Offline){ + $SearchResult=get-childitem -Path $using:BinariesLocation\Updates\ | Where-Object Extension -eq ".msu" + if ($SearchResult){ + $MicrosoftUpdateRequired=$True + }else{ + $MicrosoftUpdateRequired=$False + } + }else{ + #scan for microsoft updates + $Searcher = New-Object -ComObject Microsoft.Update.Searcher + $SearchResult = $Searcher.Search($using:UpdatesSearchCriteria).Updates + if ($SearchResult.Count -gt 0){ + $MicrosoftUpdateRequired=$True + }else{ + $MicrosoftUpdateRequired=$False + } + } + + #grab windows version + $ComputersInfo = Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\' + $Output=@() + $Output += [PSCustomObject]@{ + "MicrosoftUpdateRequired" = $MicrosoftUpdateRequired + "MicrosoftUpdates" = $SearchResult + "ComputerName" = $env:COMPUTERNAME + "CurrentBuildNumber" = $ComputersInfo.CurrentBuildNumber + "UBR" = $ComputersInfo.UBR + } + return $Output + } + $ScanResult +#endregion - #let's update do node by node (in case something goes wrong). It would be definitely faster if we would install both updates at once. But if something would go wrong on one node, it's easier to roll back +#region apply updates on nodes foreach ($Node in $Nodes){ #Check if reboot is required - if (($ScanResult | Where-Object ComputerName -eq $node).DellUpdateRequired -or $MSUpdateInstallResult.RebootRequired -or $ForceReboot){ - Write-Output "$($Node): Reboot is required" + if (($Compliance | Where-Object {$_.NodeName -eq $Node -and $_.rebootrequired -eq $True}) -or ($Scanresult | Where-Object ($_.ComputerName -eq $node -and $_.MicrosoftUpdateRequired -eq $True) -or ($ForceReboot -eq $True))){ + Write-Output "$(get-date -Format 'yyyy/MM/dd hh:mm:ss tt') $($Node): Reboot is required" #check for repair jobs, if found, wait until finished - Write-Output "$($Node): Waiting for Storage jobs to finish" + Write-Output "$(get-date -Format 'yyyy/MM/dd hh:mm:ss tt') $($Node): Waiting for Storage jobs to finish" if ((Get-StorageSubSystem -CimSession $ClusterName -FriendlyName Clus* | Get-StorageJob -CimSession $ClusterName | Where-Object Name -eq Repair) -ne $Null){ do{ $jobs=(Get-StorageSubSystem -CimSession $ClusterName -FriendlyName Clus* | Get-StorageJob -CimSession $ClusterName) @@ -178,87 +299,124 @@ $ScanResult } #Check if all disks are healthy. Wait if not - Write-Output "$($Node): Checking if all disks are healthy" + Write-Output "$(get-date -Format 'yyyy/MM/dd hh:mm:ss tt') $($Node): Checking if all disks are healthy" if (Get-VirtualDisk -CimSession $ClusterName | Where-Object HealthStatus -ne "Healthy"){ - Write-Output "$($Node): Waiting for virtual disks to become healthy" + Write-Output "$(get-date -Format 'yyyy/MM/dd hh:mm:ss tt') $($Node): Waiting for virtual disks to become healthy" do{Start-Sleep 5}while(Get-VirtualDisk -CimSession $ClusterName | Where-Object HealthStatus -ne "Healthy") } + #Check if all fault domains are healthy. Wait if not - Write-Output "$($Node): Checking if all fault domains are healthy" + Write-Output "$(get-date -Format 'yyyy/MM/dd hh:mm:ss tt') $($Node): Checking if all fault domains are healthy" if (Get-StorageFaultDomain -CimSession $ClusterName | Where-Object HealthStatus -ne "Healthy"){ - Write-Output "$($Node): Waiting for fault domains to become healthy" + Write-Output "$(get-date -Format 'yyyy/MM/dd hh:mm:ss tt') $($Node): Waiting for fault domains to become healthy" do{Start-Sleep 5}while(Get-StorageFaultDomain -CimSession $ClusterName | Where-Object HealthStatus -ne "Healthy") } - #install Microsoft updates - if (($ScanResult | Where-Object ComputerName -eq $node).MicrosoftUpdateRequired){ - Write-Output "$($Node): Installing $Updates Microsoft Updates" - $MSUpdateInstallResult=Invoke-Command -ComputerName $Node -ConfigurationName 'VirtualAccount' { - $Searcher = New-Object -ComObject Microsoft.Update.Searcher - $SearchResult = $Searcher.Search($using:SearchCriteriaAllUpdates).Updates - $Session = New-Object -ComObject Microsoft.Update.Session - $Downloader = $Session.CreateUpdateDownloader() - $Downloader.Updates = $SearchResult - $Downloader.Download() - $Installer = New-Object -ComObject Microsoft.Update.Installer - $Installer.Updates = $SearchResult - $Result = $Installer.Install() - $Result + #install microsoft updates (we will do it online to limit when node is suspended) + if ($offline){ + Write-Output "$(get-date -Format 'yyyy/MM/dd hh:mm:ss tt') $($Node): Applying offline MSUs" + #apply all MSU from $BinariesLocation + Invoke-Command -ComputerName $Node -ScriptBlock { + $MSUs=Get-ChildItem -Path $using:BinariesLocation\Updates | Where-Object Extension -eq ".msu" + foreach ($MSU in $MSUs) { + Write-Output "$(get-date -Format 'yyyy/MM/dd hh:mm:ss tt') $($using:Node): Applying MSU $($MSU.FullName)" + <# + $CabName="$($MSU.basename -split "_" | Select-Object -First 1).cab" + expand.exe $MSU.FullName $using:BinariesLocation\Updates\ -f:"$CabName" | Out-Null + Add-WindowsPackage -PackagePath "$using:BinariesLocation\Updates\$CabName" -Online | Out-Null + #> + Start-Process -FilePath "$env:systemroot\System32\wusa.exe" -ArgumentList "$($MSU.FullName) /quiet /norestart" -Wait + } + } + }else{ + #install Microsoft updates + if (($ScanResult | Where-Object ComputerName -eq $node).MicrosoftUpdateRequired){ + Write-Output "$(get-date -Format 'yyyy/MM/dd hh:mm:ss tt') $($Node): Installing $MSUpdates Microsoft Updates online" + #Configure virtual account + Invoke-Command -ComputerName $node -ScriptBlock { + New-PSSessionConfigurationFile -RunAsVirtualAccount -Path $env:TEMP\VirtualAccount.pssc + Register-PSSessionConfiguration -Name 'VirtualAccount' -Path $env:TEMP\VirtualAccount.pssc -Force + } -ErrorAction Ignore + #install update + $MSUpdateInstallResult=Invoke-Command -ComputerName $Node -ConfigurationName 'VirtualAccount' { + $Searcher = New-Object -ComObject Microsoft.Update.Searcher + $SearchResult = $Searcher.Search($using:UpdatesSearchCriteria).Updates + $Session = New-Object -ComObject Microsoft.Update.Session + $Downloader = $Session.CreateUpdateDownloader() + $Downloader.Updates = $SearchResult + $Downloader.Download() + $Installer = New-Object -ComObject Microsoft.Update.Installer + $Installer.Updates = $SearchResult + $Result = $Installer.Install() + $Result + } + #remove temporary virtual account config + Invoke-Command -ComputerName $Node -ScriptBlock { + Unregister-PSSessionConfiguration -Name 'VirtualAccount' + Remove-Item -Path $env:TEMP\VirtualAccount.pssc + } + }else{ + Write-Output "$(get-date -Format 'yyyy/MM/dd hh:mm:ss tt') $($Node): Microsoft Updates not required" + $MSUpdateInstallResult=$Null + } } - }else{ - Write-Output "$($Node): Microsoft Updates not required" - $MSUpdateInstallResult=$Null - } #Suspend node - Write-Output "$($Node): Suspending Cluster Node" + Write-Output "$(get-date -Format 'yyyy/MM/dd hh:mm:ss tt') $($Node): Suspending Cluster Node" Suspend-ClusterNode -Name "$Node" -Cluster $ClusterName -Drain -Wait | Out-Null + if (Get-ClusterResource -Cluster $ClusterName | Where-Object OwnerNode -eq $Node | Where-Object State -eq "Online"){ + Write-Output "$(get-date -Format 'yyyy/MM/dd hh:mm:ss tt') $($Node): Suspending Cluster Node Failed. Resuming and terminating patch run" + Resume-ClusterNode -Name "$Node" -Cluster $ClusterName -Failback Immediate | Out-Null + break + } + #enable storage maintenance mode - Write-Output "$($Node): Enabling Storage Maintenance mode" + Write-Output "$(get-date -Format 'yyyy/MM/dd hh:mm:ss tt') $($Node): Enabling Storage Maintenance mode" Get-StorageFaultDomain -CimSession $ClusterName -FriendlyName $Node | Enable-StorageMaintenanceMode -CimSession $ClusterName #Install Dell updates https://dl.dell.com/content/manual36290092-dell-emc-system-update-version-1-9-3-0-user-s-guide.pdf?language=en-us&ps=true - if (($ScanResult | Where-Object ComputerName -eq $node).DellUpdateRequired){ - Write-Output "$($Node): Installing Dell System Updates" - Invoke-Command -ComputerName $Node -ScriptBlock { - #install DSU updates - Start-Process -FilePath "install.cmd" -Wait -WorkingDirectory $using:DSUPackageDownloadFolder - #display result - Get-Content "C:\ProgramData\Dell\DELL System Update\dell_dup\DSU_STATUS.json" + $UpdateNames=(($Compliance | Where-Object {$_.NodeName -eq $Node -and $_.compliancestatus -eq $false}).PackageFilePath | Split-Path -Leaf) -join "," + if ($UpdateNames){ + if ($offline){ + Write-Output "$(get-date -Format 'yyyy/MM/dd hh:mm:ss tt') $($Node): Installing Dell Updates Offline" + Invoke-Command -ComputerName $node -ScriptBlock { + & "C:\Program Files\Dell\DELL System Update\DSU.exe" --source-location="$using:BinariesLocation\Updates" --source-type="Repository" --catalog-location="$using:BinariesLocation\ASHCI-Catalog.xml" --ic-location="$using:BinariesLocation\IC.exe" --update-list="$using:UpdateNames" --apply-upgrades + } + }else{ + Write-Output "$(get-date -Format 'yyyy/MM/dd hh:mm:ss tt') $($Node): Installing Dell Updates Online (downloading updates from internet)" + Invoke-Command -ComputerName $node -ScriptBlock { + & "C:\Program Files\Dell\DELL System Update\DSU.exe" --catalog-location="$using:BinariesLocation\ASHCI-Catalog.xml" --update-list="$using:UpdateNames" --apply-upgrades + } } - }else{ - Write-Output "$($Node): Dell System Updates not required" } - #restart node and wait for PowerShell to come up - Write-Output "$($Node): Restarting Cluster Node" - Restart-Computer -ComputerName $Node -Protocol WSMan -Wait -For PowerShell + #restart node and wait for PowerShell to come up (with powershell 7 you need to wait for WINRM :) + Write-Output "$(get-date -Format 'yyyy/MM/dd hh:mm:ss tt') $($Node): Restarting Cluster Node" + Restart-Computer -ComputerName $Node -Protocol WSMan -Wait -For PowerShell | Out-Null #disable storage maintenance mode - Write-Output "$($Node): Disabling Storage Maintenance mode" - Get-StorageFaultDomain -CimSession $ClusterName -FriendlyName $Node | Disable-StorageMaintenanceMode -CimSession $ClusterName + Write-Output "$(get-date -Format 'yyyy/MM/dd hh:mm:ss tt') $($Node): Disabling Storage Maintenance mode" + Get-StorageFaultDomain -Type StorageScaleUnit -CimSession $Node | Where-Object FriendlyName -eq $Node | Disable-StorageMaintenanceMode -CimSession $Node #resume cluster node - Write-Output "$($Node): Resuming Cluster Node" + Write-Output "$(get-date -Format 'yyyy/MM/dd hh:mm:ss tt') $($Node): Resuming Cluster Node" Resume-ClusterNode -Name "$Node" -Cluster $ClusterName -Failback Immediate | Out-Null #wait for machines to finish live migration - Write-Output "$($Node): Waiting for Live migrations to finish" + Write-Output "$(get-date -Format 'yyyy/MM/dd hh:mm:ss tt') $($Node): Waiting for Live migrations to finish" do {Start-Sleep 5}while( Get-CimInstance -CimSession $Nodes -Namespace root\virtualization\v2 -ClassName Msvm_MigrationJob | Where-Object StatusDescriptions -eq "Job is running" ) }else{ - Write-Output "$($Node): Reboot is not required" + Write-Output "$(get-date -Format 'yyyy/MM/dd hh:mm:ss tt') $($Node): Reboot is not required" } } - #remove temporary virtual account config + #cleanup updates folder on nodes Invoke-Command -ComputerName $Nodes -ScriptBlock { - Unregister-PSSessionConfiguration -Name 'VirtualAccount' - Remove-Item -Path $env:TEMP\VirtualAccount.pssc + Remove-Item -Path $using:BinariesLocation\Updates -Recurse -Force } - #endregion #region check Microsoft Update Levels @@ -268,5 +426,32 @@ $ScanResult Get-ItemProperty -Path $using:RegistryPath } $ComputersInfo | Select-Object PSComputerName,CurrentBuildNumber,UBR - -#endregion \ No newline at end of file +#endregion + +#region check Dell compliance again + #scan for compliance + if ($offline){ + Invoke-Command -ComputerName $Nodes -ScriptBlock { + & "C:\Program Files\Dell\DELL System Update\DSU.exe" --compliance --catalog-location="$using:BinariesLocation\ASHCI-Catalog.xml" --ic-location="$using:BinariesLocation\ic.exe" --output-format="json" --output="$using:BinariesLocation\Compliance.json" + } + }else{ + Invoke-Command -ComputerName $Nodes -ScriptBlock { + & "C:\Program Files\Dell\DELL System Update\DSU.exe" --compliance --catalog-location="$using:BinariesLocation\ASHCI-Catalog.xml" --output-format="json" --output="$using:BinariesLocation\Compliance.json" + } + } + #collect results + $Compliance=@() + foreach ($Node in $Nodes){ + $json=Invoke-Command -ComputerName $node -ScriptBlock {Get-Content "$using:BinariesLocation\Compliance.json"} + $object = $json | ConvertFrom-Json + $components=$object.SystemUpdateCompliance.UpdateableComponent + $components | Add-Member -MemberType NoteProperty -Name "ClusterName" -Value $ClusterName + $components | Add-Member -MemberType NoteProperty -Name "NodeName" -Value $Node + $Compliance+=$Components + } + + #display results + $Compliance | Out-GridView + +#endregion + diff --git a/Scenarios/AzSHCI and Stretch Cluster/Scenario.ps1 b/Scenarios/AzSHCI and Stretch Cluster/Scenario.ps1 index a4726f9f..83b9a2ce 100644 --- a/Scenarios/AzSHCI and Stretch Cluster/Scenario.ps1 +++ b/Scenarios/AzSHCI and Stretch Cluster/Scenario.ps1 @@ -485,11 +485,12 @@ foreach ($Number in $Numbers){ $r | where Datavolume -like "C:\ClusterStorage*" | select Datavolume,ReplicationStatus #Configure Network Constraints +#Network Constraints network settings shal only contain Source Interafaces from the source site for the -SourceNWInterface paramater, and Destination Interfaces for the other site for the -DestinationNWInterface $Numbers=1..$NumberOfVolumesPerSite foreach ($Number in $Numbers){ $SourceRGName="$SourceRGNamePrefix$Number" $DestinationRGName="$DestinationRGNamePrefix$Number" - Set-SRNetworkConstraint -SourceComputerName $ClusterName -DestinationComputerName $ClusterName -SourceRGName $SourceRGName -DestinationRGName $DestinationRGName -SourceNWInterface "ReplicaNet1","ReplicaNet2" -DestinationNWInterface "ReplicaNet1","ReplicaNet2" -ErrorAction Ignore + Set-SRNetworkConstraint -SourceComputerName $ClusterName -DestinationComputerName $ClusterName -SourceRGName $SourceRGName -DestinationRGName $DestinationRGName -SourceNWInterface "ReplicaNet1" -DestinationNWInterface "ReplicaNet2" -ErrorAction Ignore } #Validate Network Constraints