diff --git a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/challenges/challenge-01.md b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/challenges/challenge-01.md index 495bd8d8c..59fc3453e 100644 --- a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/challenges/challenge-01.md +++ b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/challenges/challenge-01.md @@ -19,6 +19,7 @@ - You have understood the concept and architecture for the Hack. - The Bicep deployment command exits successfully. - The *source* and *destination* resource groups are visible in the Azure Portal. +- The Windows/IIS and Ubuntu/Apache demo dashboards return HTTP success and show the correct hostname, platform, and web server. ## Learning resources diff --git a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/challenges/challenge-08.md b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/challenges/challenge-08.md index 818b44322..26bd75ff4 100644 --- a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/challenges/challenge-08.md +++ b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/challenges/challenge-08.md @@ -34,7 +34,7 @@ Teams complete only their selected track. * Inspect the site's content, bindings or virtual hosts, runtime/modules, dependencies, and state requirements. * Confirm the workload consists of portable static HTML, CSS, and image assets. -* Replace the VM-specific hostname displayed in `index.html` with a platform-neutral value. +* Replace the VM-specific hostname, platform, and web-server values in `index.html` with App Service values by using the page's stable metadata attributes. * Create a ZIP package whose root contains `index.html`. * Create a low-cost Windows App Service plan and web app in `destination-rg`. * Deploy with the supported App Service ZIP deployment experience. @@ -62,7 +62,7 @@ Teams complete only their selected track. * The discovery record shows that the selected site has no required server-side runtime, database, machine-local dependency, or session state. * The ZIP root contains `index.html` rather than a nested content directory. * A Windows App Service web app serves the selected site's page and assets over HTTPS. -* The rendered page contains a platform-neutral hosting value. +* The rendered page identifies the hostname as Azure App Service, the platform as managed PaaS, and the web server as Azure App Service. * The App Service endpoint remains healthy after `W3SVC` or `apache2` is stopped. * Track B signs out of Azure CLI after deployment. * No Azure Load Balancer is placed in front of App Service and no GitHub integration is required. diff --git a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/README.md b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/README.md index d1d1c0ba3..a3ccbc5f2 100644 --- a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/README.md +++ b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/README.md @@ -2,6 +2,8 @@ Download the \*.bicep files to your local PC and follow [Solution 1](../walkthrough/challenge-01/solution-01.md). +The deployment defaults to downloading automation and demo-page assets from `microsoft/MicroHack` on the `main` branch. To test a fork before merge, check out that fork branch locally and pass the existing `githubAccount` and `githubBranch` parameters as described in Solution 1. The selected branch must contain the same Hack resource paths. + ## Quota information if multiple users deploy to the same subscription diff --git a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/Bootstrap.ps1 b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/Bootstrap.ps1 index be98e539f..83c14bb24 100644 --- a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/Bootstrap.ps1 +++ b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/Bootstrap.ps1 @@ -1,11 +1,9 @@ param ( [string]$adminUsername, - [string]$spnClientId, [string]$tenantId, [string]$spnAuthority, [string]$subscriptionId, [string]$resourceGroup, - [string]$azdataUsername, [string]$acceptEula, [string]$registryUsername, [string]$azureLocation, @@ -14,9 +12,7 @@ param ( [string]$templateBaseUrl, [string]$flavor, [string]$rdpPort, - [string]$sshPort, [string]$vmAutologon, - [object]$resourceTags, [string]$namingPrefix, [string]$debugEnabled, [string]$sqlServerEdition, @@ -35,8 +31,6 @@ param ( [System.Environment]::SetEnvironmentVariable('githubUser', $githubUser, [System.EnvironmentVariableTarget]::Machine) [System.Environment]::SetEnvironmentVariable('templateBaseUrl', $templateBaseUrl, [System.EnvironmentVariableTarget]::Machine) [System.Environment]::SetEnvironmentVariable('flavor', $flavor, [System.EnvironmentVariableTarget]::Machine) -[System.Environment]::SetEnvironmentVariable('automationTriggerAtLogon', $automationTriggerAtLogon, [System.EnvironmentVariableTarget]::Machine) -[System.Environment]::SetEnvironmentVariable('resourceTags', $resourceTags, [System.EnvironmentVariableTarget]::Machine) [System.Environment]::SetEnvironmentVariable('namingPrefix', $namingPrefix, [System.EnvironmentVariableTarget]::Machine) [System.Environment]::SetEnvironmentVariable('MHBoxDir', 'C:\MHBox', [System.EnvironmentVariableTarget]::Machine) [System.Environment]::SetEnvironmentVariable('sqlServerEdition', $sqlServerEdition, [System.EnvironmentVariableTarget]::Machine) @@ -63,28 +57,14 @@ $Env:MHBoxDir = 'C:\MHBox' $Env:MHBoxDscDir = "$Env:MHBoxDir\DSC" $Env:MHBoxLogsDir = "$Env:MHBoxDir\Logs" $Env:MHBoxVMDir = 'F:\Virtual Machines' -$Env:MHBoxKVDir = "$Env:MHBoxDir\KeyVault" -$Env:MHBoxGitOpsDir = "$Env:MHBoxDir\GitOps" -$Env:MHBoxIconDir = "$Env:MHBoxDir\Icons" -$Env:agentScript = "$Env:MHBoxDir\agentScript" -$Env:MHBoxTestsDir = "$Env:MHBoxDir\Tests" -$Env:ToolsDir = 'C:\Tools' $Env:tempDir = 'C:\Temp' -$Env:MHBoxDataOpsDir = "$Env:MHBoxDir\DataOps" $Env:MHBoxDemoPageDir = "$Env:MHBoxDir\DemoPage" New-Item -Path $Env:MHBoxDir -ItemType Directory -Force | Out-Null New-Item -Path $Env:MHBoxDscDir -ItemType Directory -Force | Out-Null New-Item -Path $Env:MHBoxLogsDir -ItemType Directory -Force | Out-Null New-Item -Path $Env:MHBoxVMDir -ItemType Directory -Force | Out-Null -New-Item -Path $Env:MHBoxKVDir -ItemType Directory -Force | Out-Null -New-Item -Path $Env:MHBoxGitOpsDir -ItemType Directory -Force | Out-Null -New-Item -Path $Env:MHBoxIconDir -ItemType Directory -Force | Out-Null -New-Item -Path $Env:ToolsDir -ItemType Directory -Force | Out-Null New-Item -Path $Env:tempDir -ItemType Directory -Force | Out-Null -New-Item -Path $Env:agentScript -ItemType Directory -Force | Out-Null -New-Item -Path $Env:MHBoxDataOpsDir -ItemType Directory -Force | Out-Null -New-Item -Path $Env:MHBoxTestsDir -ItemType Directory -Force | Out-Null New-Item -Path $Env:MHBoxDemoPageDir -ItemType Directory -Force | Out-Null Start-Transcript -Path "$Env:MHBoxLogsDir\Bootstrap.log" @@ -103,10 +83,6 @@ $PSBoundParameters.GetEnumerator() | Sort-Object Name | ForEach-Object { # Set SyncForegroundPolicy to 1 to ensure that the scheduled task runs after the client VM joins the domain Set-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' 'SyncForegroundPolicy' 1 -# Copy PowerShell Profile and Reload -Invoke-WebRequest ($templateBaseUrl + 'artifacts/PSProfile.ps1') -OutFile $PsHome\Profile.ps1 -. $PsHome\Profile.ps1 - # Extending C:\ partition to the maximum size Write-Host 'Extending C:\ partition to the maximum size' Resize-Partition -DriveLetter C -Size $(Get-PartitionSupportedSize -DriveLetter C).SizeMax @@ -115,11 +91,7 @@ Resize-Partition -DriveLetter C -Size $(Get-PartitionSupportedSize -DriveLetter Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force Install-Module -Name Microsoft.PowerShell.PSResourceGet -Force -$modules = @('Az', 'Azure.Arc.Jumpstart.Common', 'Microsoft.PowerShell.SecretManagement', 'Pester') - -foreach ($module in $modules) { - Install-PSResource -Name $module -Scope AllUsers -Quiet -AcceptLicense -TrustRepository -} +Install-PSResource -Name Az -Scope AllUsers -Quiet -AcceptLicense -TrustRepository # Azure init in isolated child process $azInitScriptPath = "$Env:tempDir\AzBootstrap-Init.ps1" @@ -213,7 +185,7 @@ if ($LASTEXITCODE -ne 0) { } # Installing tools -Write-Header 'Installing PowerShell 7' +Write-Host "`n### Installing PowerShell 7 ###`n" $ProgressPreference = 'SilentlyContinue' $url = 'https://github.com/PowerShell/PowerShell/releases/latest' @@ -227,16 +199,13 @@ Invoke-WebRequest -UseBasicParsing -Uri $downloadUrl -OutFile .\PowerShell7.msi Start-Process msiexec.exe -Wait -ArgumentList '/I PowerShell7.msi /quiet ADD_EXPLORER_CONTEXT_MENU_OPENPOWERSHELL=1 ADD_FILE_CONTEXT_MENU_RUNPOWERSHELL=1 ENABLE_PSREMOTING=1 REGISTER_MANIFEST=1 USE_MU=1 ENABLE_MU=1 ADD_PATH=1' Remove-Item .\PowerShell7.msi -Force -Copy-Item $PsHome\Profile.ps1 -Destination 'C:\Program Files\PowerShell\7\' -Force - -Write-Header 'Fetching GitHub Artifacts' +Write-Host "`n### Fetching GitHub Artifacts ###`n" # All flavors Write-Host 'Fetching Artifacts for All Flavors' Invoke-WebRequest ($templateBaseUrl + 'artifacts/dsc/common.dsc.yml') -OutFile "$Env:MHBoxDscDir\common.dsc.yml" Invoke-WebRequest ($templateBaseUrl + 'artifacts/dsc/virtual_machines_sql.dsc.yml') -OutFile "$Env:MHBoxDscDir\virtual_machines_sql.dsc.yml" Invoke-WebRequest ($templateBaseUrl + 'artifacts/WinGet.ps1') -OutFile "$Env:MHBoxDir\WinGet.ps1" -Invoke-WebRequest ($templateBaseUrl + 'artifacts/MHWallpaper.bmp') -OutFile "$Env:MHBoxDir\MHWallpaper.bmp" Invoke-WebRequest ($templateBaseUrl + 'artifacts/demopage/deploy-webapp.sh') -OutFile "$Env:MHBoxDemoPageDir\deploy-webapp.sh" Invoke-WebRequest ($templateBaseUrl + 'artifacts/demopage/deployWebApp.ps1') -OutFile "$Env:MHBoxDemoPageDir\deployWebApp.ps1" @@ -248,8 +217,6 @@ if ($flavor -eq 'ITPro') { Invoke-WebRequest ($templateBaseUrl + 'artifacts/dsc/virtual_machines_itpro.dsc.yml') -OutFile "$Env:MHBoxDscDir\virtual_machines_itpro.dsc.yml" } -New-Item -Path alias:azdata -Value 'C:\Program Files (x86)\Microsoft SDKs\Azdata\CLI\wbin\azdata.cmd' -Force - # Disable Microsoft Edge sidebar $RegistryPath = 'HKLM:\SOFTWARE\Policies\Microsoft\Edge' $Name = 'HubsSidebarEnabled' @@ -346,7 +313,7 @@ if (-not (Test-Path $registryPath)) { Set-ItemProperty -Path $registryPath -Name $registryName -Value $registryValue -Type DWord Write-Host "Registry setting applied successfully. fClientDisableUDP set to $registryValue" -Write-Header 'Configuring Logon Scripts' +Write-Host "`n### Configuring Logon Scripts ###`n" $ScheduledTaskExecutable = 'pwsh.exe' @@ -426,7 +393,7 @@ if ($flavor -eq 'ITPro') { Get-ScheduledTask -TaskName ServerManager | Disable-ScheduledTask | Out-Null if ($flavor -eq 'ITPro') { - Write-Header 'Installing Hyper-V' + Write-Host "`n### Installing Hyper-V ###`n" # Install Hyper-V and reboot Write-Host 'Installing Hyper-V and restart' diff --git a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/MHServersLogonScript.ps1 b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/MHServersLogonScript.ps1 index a7d3048e3..a7316a790 100644 --- a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/MHServersLogonScript.ps1 +++ b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/MHServersLogonScript.ps1 @@ -3,19 +3,602 @@ $ErrorActionPreference = $env:ErrorActionPreference $Env:MHBoxDir = 'C:\MHBox' $Env:MHBoxLogsDir = "$Env:MHBoxDir\Logs" $Env:MHBoxVMDir = 'F:\Virtual Machines' -$Env:MHBoxIconDir = "$Env:MHBoxDir\Icons" -$Env:MHBoxTestsDir = "$Env:MHBoxDir\Tests" $Env:MHBoxDscDir = "$Env:MHBoxDir\DSC" -$agentScript = "$Env:MHBoxDir\agentScript" # Set variables to execute remote powershell scripts on guest VMs -$nestedVMMHBoxDir = $Env:MHBoxDir $tenantId = $env:tenantId $subscriptionId = $env:subscriptionId -$azureLocation = $env:azureLocation -$resourceGroup = $env:resourceGroup -$resourceTags = $env:resourceTags $namingPrefix = $env:namingPrefix +$templateBaseUrl = [Environment]::GetEnvironmentVariable( + 'templateBaseUrl', + [EnvironmentVariableTarget]::Machine +) + +$templateBaseUri = $null +if ( + [string]::IsNullOrWhiteSpace($templateBaseUrl) -or + -not [Uri]::TryCreate($templateBaseUrl, [UriKind]::Absolute, [ref]$templateBaseUri) -or + $templateBaseUri.Scheme -ne [Uri]::UriSchemeHttps +) { + throw 'The machine-level templateBaseUrl must be an absolute HTTPS URL.' +} + +$demoAssetSourceRoot = "$($templateBaseUrl.Trim().TrimEnd('/'))/artifacts/demopage" + +function Wait-Until { + param( + [Parameter(Mandatory)] + [scriptblock]$Condition, + + [Parameter(Mandatory)] + [string]$Description, + + [ValidateRange(1, 3600)] + [int]$TimeoutSeconds = 600, + + [ValidateRange(1, 60)] + [int]$PollIntervalSeconds = 5 + ) + + $stopwatch = [Diagnostics.Stopwatch]::StartNew() + $lastError = $null + + while ($stopwatch.Elapsed.TotalSeconds -lt $TimeoutSeconds) { + try { + if (& $Condition) { + return + } + } + catch { + $lastError = $_.Exception.Message + } + + Start-Sleep -Seconds $PollIntervalSeconds + } + + $errorDetail = if ($lastError) { + " Last error: $lastError" + } + else { + '' + } + throw "Timed out after $TimeoutSeconds seconds waiting for $Description.$errorDetail" +} + +function Wait-VMCommand { + param( + [Parameter(Mandatory)] + [string]$VMName, + + [Parameter(Mandatory)] + [pscredential]$Credential, + + [scriptblock]$ScriptBlock = { $true }, + + [object[]]$ArgumentList = @(), + + [string]$Description = 'PowerShell Direct readiness', + + [int]$TimeoutSeconds = 600 + ) + + Wait-Until -Description "$Description on '$VMName'" -TimeoutSeconds $TimeoutSeconds -Condition { + $result = Invoke-Command -VMName $VMName ` + -Credential $Credential ` + -ScriptBlock $ScriptBlock ` + -ArgumentList $ArgumentList ` + -ErrorAction Stop + @($result) -contains $true + } +} + +function Invoke-VMCommandWithRetry { + param( + [Parameter(Mandatory)] + [string]$VMName, + + [Parameter(Mandatory)] + [pscredential]$Credential, + + [Parameter(Mandatory)] + [scriptblock]$ScriptBlock, + + [object[]]$ArgumentList = @(), + + [Parameter(Mandatory)] + [string]$Description, + + [int]$TimeoutSeconds = 600 + ) + + $commandResult = $null + $commandResultReference = [ref]$commandResult + Wait-Until -Description "$Description on '$VMName'" -TimeoutSeconds $TimeoutSeconds -Condition { + $commandResultReference.Value = @( + Invoke-Command -VMName $VMName ` + -Credential $Credential ` + -ScriptBlock $ScriptBlock ` + -ArgumentList $ArgumentList ` + -ErrorAction Stop + ) + return $true + } + + return $commandResult +} + +$removeWallpaperScript = { + $desktopKey = 'HKCU:\Control Panel\Desktop' + New-ItemProperty ` + -Path $desktopKey ` + -Name 'Wallpaper' ` + -Value '' ` + -PropertyType String ` + -Force ` + -ErrorAction Stop | + Out-Null + + $desktopProperties = Get-ItemProperty -Path $desktopKey -ErrorAction Stop + foreach ($propertyName in @('WallpaperStyle', 'TileWallpaper')) { + if ($desktopProperties.PSObject.Properties.Name -contains $propertyName) { + Remove-ItemProperty ` + -Path $desktopKey ` + -Name $propertyName ` + -ErrorAction Stop + } + } + + foreach ($policyKey in @( + 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Policies\System', + 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Policies\System' + )) { + if (Test-Path -LiteralPath $policyKey -ErrorAction Stop) { + $policyProperties = Get-ItemProperty -Path $policyKey -ErrorAction Stop + foreach ($propertyName in @('Wallpaper', 'WallpaperStyle')) { + if ($policyProperties.PSObject.Properties.Name -contains $propertyName) { + Remove-ItemProperty ` + -Path $policyKey ` + -Name $propertyName ` + -ErrorAction Stop + } + } + } + } + + $themePath = Join-Path $env:APPDATA 'Microsoft\Windows\Themes' + if (Test-Path -LiteralPath $themePath -ErrorAction Stop) { + Get-ChildItem ` + -Path $themePath ` + -Filter 'Transcoded*' ` + -File ` + -ErrorAction Stop | + Remove-Item -Force -ErrorAction Stop + } + + $cachedFilesPath = Join-Path $themePath 'CachedFiles' + if (Test-Path -LiteralPath $cachedFilesPath -ErrorAction Stop) { + Get-ChildItem ` + -Path $cachedFilesPath ` + -File ` + -ErrorAction Stop | + Remove-Item -Force -ErrorAction Stop + } +} + +function Remove-VMUserWallpaper { + param( + [Parameter(Mandatory)] + [string]$VMName, + + [Parameter(Mandatory)] + [pscredential]$Credential + ) + + Invoke-VMCommandWithRetry ` + -VMName $VMName ` + -Credential $Credential ` + -ScriptBlock $removeWallpaperScript ` + -Description 'Administrator wallpaper removal' | + Out-Null +} + +function Get-VMIPv4AddressWithRetry { + param( + [Parameter(Mandatory)] + [string]$VMName, + + [int]$TimeoutSeconds = 600 + ) + + $address = $null + $addressReference = [ref]$address + + Wait-Until -Description "a usable IPv4 address on '$VMName'" -TimeoutSeconds $TimeoutSeconds -Condition { + $candidates = Get-VM -Name $VMName -ErrorAction Stop | + Select-Object -ExpandProperty NetworkAdapters | + Select-Object -ExpandProperty IPAddresses + + foreach ($candidate in $candidates) { + $parsedAddress = $null + if ( + [IPAddress]::TryParse([string]$candidate, [ref]$parsedAddress) -and + $parsedAddress.AddressFamily -eq [Net.Sockets.AddressFamily]::InterNetwork -and + -not $parsedAddress.IsIPv6LinkLocal -and + $candidate -notlike '127.*' -and + $candidate -notlike '169.254.*' + ) { + $addressReference.Value = [string]$candidate + return $true + } + } + + return $false + } + + return $address +} + +function Wait-TcpPort { + param( + [Parameter(Mandatory)] + [string]$ComputerName, + + [Parameter(Mandatory)] + [int]$Port, + + [int]$TimeoutSeconds = 300 + ) + + Wait-Until -Description "TCP $Port on '$ComputerName'" -TimeoutSeconds $TimeoutSeconds -Condition { + $client = [Net.Sockets.TcpClient]::new() + try { + $connection = $client.BeginConnect($ComputerName, $Port, $null, $null) + $waitHandle = $connection.AsyncWaitHandle + try { + if (-not $waitHandle.WaitOne(3000)) { + return $false + } + } + finally { + $waitHandle.Dispose() + } + + $client.EndConnect($connection) + return $true + } + finally { + $client.Dispose() + } + } +} + +function Restart-VMAndWait { + param( + [Parameter(Mandatory)] + [string]$VMName, + + [Parameter(Mandatory)] + [pscredential]$Credential + ) + + $previousUptime = (Get-VM -Name $VMName -ErrorAction Stop).Uptime + Restart-VM -Name $VMName -Force -ErrorAction Stop + Wait-Until ` + -Description "a completed host-controlled restart of '$VMName'" ` + -TimeoutSeconds 180 ` + -PollIntervalSeconds 1 ` + -Condition { + $vm = Get-VM -Name $VMName -ErrorAction Stop + $vm.State -eq 'Running' -and $vm.Uptime -lt $previousUptime + } + Wait-VMCommand ` + -VMName $VMName ` + -Credential $Credential ` + -Description 'PowerShell Direct readiness after host-controlled restart' +} + +function Restart-VMGracefullyAndWaitForHeartbeat { + param( + [Parameter(Mandatory)] + [string]$VMName + ) + + Stop-VM -Name $VMName -ErrorAction Stop + Wait-Until ` + -Description "a graceful shutdown of '$VMName'" ` + -TimeoutSeconds 300 ` + -Condition { + (Get-VM -Name $VMName -ErrorAction Stop).State -eq 'Off' + } + + Start-VM -Name $VMName -ErrorAction Stop + Wait-Until ` + -Description "a healthy heartbeat after restarting '$VMName'" ` + -TimeoutSeconds 600 ` + -Condition { + $vm = Get-VM -Name $VMName -ErrorAction Stop + $heartbeat = Get-VMIntegrationService ` + -VMName $VMName ` + -Name 'Heartbeat' ` + -ErrorAction Stop + $vm.State -eq 'Running' -and + $heartbeat.Enabled -and + $heartbeat.PrimaryStatusDescription -eq 'OK' + } +} + +function Test-TcpPort { + param( + [Parameter(Mandatory)] + [string]$ComputerName, + + [Parameter(Mandatory)] + [int]$Port, + + [int]$TimeoutMilliseconds = 3000 + ) + + $client = [Net.Sockets.TcpClient]::new() + try { + $connection = $client.BeginConnect($ComputerName, $Port, $null, $null) + $waitHandle = $connection.AsyncWaitHandle + try { + if (-not $waitHandle.WaitOne($TimeoutMilliseconds)) { + return $false + } + } + finally { + $waitHandle.Dispose() + } + + $client.EndConnect($connection) + return $true + } + catch { + return $false + } + finally { + $client.Dispose() + } +} + +function Get-LinuxBootId { + param( + [Parameter(Mandatory)] + [string]$ComputerName, + + [Parameter(Mandatory)] + [string]$UserName, + + [Parameter(Mandatory)] + [string]$KeyFilePath + ) + + $bootIdOutput = @( + Invoke-Command ` + -HostName $ComputerName ` + -KeyFilePath $KeyFilePath ` + -UserName $UserName ` + -ConnectingTimeout 10000 ` + -ErrorAction Stop ` + -ScriptBlock { + $bootId = (Get-Content /proc/sys/kernel/random/boot_id -Raw).Trim() + if ([string]::IsNullOrWhiteSpace($bootId)) { + throw 'Linux boot ID is empty.' + } + return $bootId + } + ) + $bootId = ([string]$bootIdOutput[-1]).Trim() + $parsedBootId = [guid]::Empty + if (-not [guid]::TryParse($bootId, [ref]$parsedBootId)) { + throw "Linux boot ID '$bootId' is not a valid GUID." + } + + return $parsedBootId.ToString() +} + +function Clear-LinuxHostNameRebootMarker { + param( + [Parameter(Mandatory)] + [string]$ComputerName, + + [Parameter(Mandatory)] + [string]$UserName, + + [Parameter(Mandatory)] + [string]$KeyFilePath, + + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string]$ExpectedHostName + ) + + Invoke-Command ` + -HostName $ComputerName ` + -KeyFilePath $KeyFilePath ` + -UserName $UserName ` + -ConnectingTimeout 10000 ` + -ErrorAction Stop ` + -ScriptBlock { + param( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string]$ExpectedHostName + ) + + $markerPath = '/var/lib/mhbox/hostname-reboot-pending' + if (-not (Test-Path -LiteralPath $markerPath)) { + return + } + + $markerLines = @(Get-Content -LiteralPath $markerPath) + if ($markerLines.Count -ne 2) { + throw "Ubuntu hostname reboot marker '$markerPath' is malformed." + } + + $pendingBootId = ([string]$markerLines[0]).Trim() + $pendingHostName = ([string]$markerLines[1]).Trim() + $currentBootId = (Get-Content /proc/sys/kernel/random/boot_id -Raw).Trim() + if ($pendingHostName -cne $ExpectedHostName) { + throw "Ubuntu hostname reboot marker targets '$pendingHostName', expected '$ExpectedHostName'." + } + if ($pendingBootId -ceq $currentBootId) { + throw "Ubuntu hostname reboot marker still belongs to the current boot '$currentBootId'." + } + + $removeOutput = @(& sudo -- rm -f -- $markerPath 2>&1) + $removeExitCode = $LASTEXITCODE + if ($removeExitCode -ne 0) { + throw "Failed to clear Ubuntu hostname reboot marker with exit code $removeExitCode. $($removeOutput -join [Environment]::NewLine)" + } + } ` + -ArgumentList @($ExpectedHostName) | + Out-Null +} + +function Wait-LinuxGuestReboot { + param( + [Parameter(Mandatory)] + [string]$VMName, + + [Parameter(Mandatory)] + [string]$UserName, + + [Parameter(Mandatory)] + [string]$KeyFilePath, + + [Parameter(Mandatory)] + [string]$PreviousBootId, + + [int]$TimeoutSeconds = 600 + ) + + $address = $null + $addressReference = [ref]$address + Wait-Until ` + -Description "a new Linux boot and SSH readiness on '$VMName'" ` + -TimeoutSeconds $TimeoutSeconds ` + -Condition { + $candidates = @( + Get-VM -Name $VMName -ErrorAction Stop | + Select-Object -ExpandProperty NetworkAdapters | + Select-Object -ExpandProperty IPAddresses | + Where-Object { + $parsedAddress = $null + [IPAddress]::TryParse([string]$_, [ref]$parsedAddress) -and + $parsedAddress.AddressFamily -eq [Net.Sockets.AddressFamily]::InterNetwork -and + $_ -notlike '127.*' -and + $_ -notlike '169.254.*' + } | + Select-Object -Unique + ) + if ($candidates.Count -eq 0) { + throw 'The guest is not reporting a usable IPv4 address.' + } + + $lastAttempt = $null + foreach ($candidate in $candidates) { + if (-not (Test-TcpPort -ComputerName $candidate -Port 22)) { + $lastAttempt = "SSH is not yet reachable on '$candidate'." + continue + } + + try { + $currentBootId = Get-LinuxBootId ` + -ComputerName $candidate ` + -UserName $UserName ` + -KeyFilePath $KeyFilePath + if ($currentBootId -cne $PreviousBootId) { + $addressReference.Value = [string]$candidate + return $true + } + $lastAttempt = "SSH is still connected to the previous boot '$PreviousBootId'." + } + catch { + $lastAttempt = $_.Exception.Message + } + } + + throw $lastAttempt + } + + return $address +} + +function ConvertTo-LinuxHostName { + param( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string]$Name + ) + + $normalizedName = $Name.ToLowerInvariant() ` + -replace '[^a-z0-9-]', '-' ` + -replace '-+', '-' + $normalizedName = $normalizedName.Trim('-') + if ($normalizedName.Length -gt 63) { + $normalizedName = $normalizedName.Substring(0, 63).TrimEnd('-') + } + if ( + [string]::IsNullOrWhiteSpace($normalizedName) -or + $normalizedName -notmatch '^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$' + ) { + throw "'$Name' cannot be normalized to a valid Linux hostname." + } + + return $normalizedName +} + +function Wait-LinuxHostName { + param( + [Parameter(Mandatory)] + [string]$ComputerName, + + [Parameter(Mandatory)] + [string]$UserName, + + [Parameter(Mandatory)] + [string]$KeyFilePath, + + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string]$ExpectedHostName, + + [int]$TimeoutSeconds = 300 + ) + + Wait-Until ` + -Description "Linux hostname '$ExpectedHostName' through a fresh SSH connection to '$ComputerName'" ` + -TimeoutSeconds $TimeoutSeconds ` + -Condition { + Invoke-Command ` + -HostName $ComputerName ` + -KeyFilePath $KeyFilePath ` + -UserName $UserName ` + -ErrorAction Stop ` + -ScriptBlock { + param( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string]$ExpectedHostName + ) + + $hostnameOutput = @(& hostname 2>&1) + $hostnameExitCode = $LASTEXITCODE + if ($hostnameExitCode -ne 0) { + throw "hostname failed with exit code $hostnameExitCode." + } + + $effectiveHostName = ([string]$hostnameOutput[-1]).Trim() + if ($effectiveHostName -cne $ExpectedHostName) { + throw "Effective hostname is '$effectiveHostName', expected '$ExpectedHostName'." + } + return $true + } ` + -ArgumentList @($ExpectedHostName) + } +} # Moved VHD storage account details here to keep only in place to prevent duplicates. $vhdSourceFolder = 'https://jumpstartprodsg.blob.core.windows.net/arcbox/prod/*' @@ -67,7 +650,7 @@ if (Test-Path $registryPath) { ################################################ # Setup Hyper-V server before deploying VMs for each flavor ################################################ -if ($Env:flavor -ne 'DevOps') { +if ($Env:flavor -eq 'ITPro') { # Install and configure DHCP service (used by Hyper-V nested VMs) Write-Host 'Configuring DHCP Service' $dnsClient = Get-DnsClient | Where-Object { $_.InterfaceAlias -eq 'Ethernet' } @@ -118,22 +701,12 @@ if ($Env:flavor -ne 'DevOps') { $folder.Attributes += [System.IO.FileAttributes]::Hidden } - # Install Azure CLI extensions - Write-Header 'Az CLI extensions' - - az config set extension.use_dynamic_install=yes_without_prompt --only-show-errors - - @('ssh', 'log-analytics-solution', 'connectedmachine', 'monitor-control-service') | - ForEach-Object -Parallel { - az extension add --name $PSItem --yes --only-show-errors - } - # Required for CLI commands - Write-Header 'Az CLI Login' + Write-Host "`n### Az CLI Login ###`n" az login --identity az account set -s $subscriptionId - Write-Header 'Az PowerShell Login' + Write-Host "`n### Az PowerShell Login ###`n" Connect-AzAccount -Identity -Tenant $tenantId -Subscription $subscriptionId $DeploymentProgressString = 'Started MHServersLogonScript' @@ -193,59 +766,72 @@ if ($Env:flavor -ne 'DevOps') { } # Create the nested VMs if not already created - Write-Header 'Create Hyper-V VMs' + Write-Host "`n### Create Hyper-V VMs ###`n" # Create the nested SQL VMs $sqlDscConfigurationFile = "$Env:MHBoxDscDir\virtual_machines_sql.dsc.yml" (Get-Content -Path $sqlDscConfigurationFile) -replace 'namingPrefixStage', $namingPrefix | Set-Content -Path $sqlDscConfigurationFile winget configure --file C:\MHBox\DSC\virtual_machines_sql.dsc.yml --accept-configuration-agreements --disable-interactivity - # Restarting Windows VM Network Adapters - Write-Host 'Restarting Network Adapters' - Start-Sleep -Seconds 5 - Invoke-Command -VMName $SQLvmName -ScriptBlock { Get-NetAdapter | Restart-NetAdapter } -Credential $winCreds - Start-Sleep -Seconds 20 + # Restart the newly created VM from the host before guest configuration. + Write-Host 'Restarting the nested SQL VM' + Restart-VMAndWait -VMName $SQLvmName -Credential $winCreds # Rename server if hostname is not as MHBox-SQL or doesn't match naming prefix - $hostname = Invoke-Command -VMName $SQLvmName -ScriptBlock { hostname } -Credential $winCreds + $hostname = Invoke-VMCommandWithRetry ` + -VMName $SQLvmName ` + -Credential $winCreds ` + -ScriptBlock { hostname } ` + -Description 'hostname query' if ($hostname -ne $SQLvmName) { - Write-Header 'Renaming the nested SQL VM' - Invoke-Command -VMName $SQLvmName -ScriptBlock { Rename-Computer -NewName $using:SQLvmName -Restart } -Credential $winCreds - - Get-VM *SQL* | Wait-VM -For IPAddress - - Write-Host 'Waiting for the nested Windows SQL VM to come back online...waiting for 30 seconds' - Start-Sleep -Seconds 30 - - # Wait for VM to start again - while ((Get-VM -vmName $SQLvmName).State -ne 'Running') { - Write-Host 'Waiting for VM to start...' - Start-Sleep -Seconds 5 - } - Write-Host 'VM has rebooted successfully!' + Write-Host "`n### Renaming the nested SQL VM ###`n" + Invoke-VMCommandWithRetry ` + -VMName $SQLvmName ` + -Credential $winCreds ` + -ScriptBlock { + param($newName) + if ($env:COMPUTERNAME -cne $newName) { + Rename-Computer -NewName $newName + } + } ` + -ArgumentList $SQLvmName ` + -Description 'computer rename' + Restart-VMAndWait -VMName $SQLvmName -Credential $winCreds } # Enable Windows Firewall rule for SQL Server - Invoke-Command -VMName $SQLvmName -ScriptBlock { New-NetFirewallRule -DisplayName 'Allow SQL Server TCP 1433' -Direction Inbound -Protocol TCP -LocalPort 1433 -Action Allow } -Credential $winCreds + Invoke-VMCommandWithRetry ` + -VMName $SQLvmName ` + -Credential $winCreds ` + -ScriptBlock { + if (-not (Get-NetFirewallRule -DisplayName 'Allow SQL Server TCP 1433' -ErrorAction SilentlyContinue)) { + New-NetFirewallRule ` + -DisplayName 'Allow SQL Server TCP 1433' ` + -Direction Inbound ` + -Protocol TCP ` + -LocalPort 1433 ` + -Action Allow | Out-Null + } + } ` + -Description 'SQL Server firewall configuration' # Onboard nested Windows and Linux VMs to Azure Arc if ($Env:flavor -eq 'ITPro') { - Write-Header 'Fetching Nested VMs' + Write-Host "`n### Fetching Nested VMs ###`n" $Win2k22vmName = "$namingPrefix-Win2K22" $Win2k22vmvhdPath = "${Env:MHBoxVMDir}\$namingPrefix-Win2K22.vhdx" $Ubuntu01vmName = "$namingPrefix-Ubuntu-01" + $Ubuntu01HostName = ConvertTo-LinuxHostName -Name $Ubuntu01vmName + Write-Output "Expected Ubuntu guest hostname: $Ubuntu01HostName" $Ubuntu01vmvhdPath = "${Env:MHBoxVMDir}\$namingPrefix-Ubuntu-01.vhdx" $AzMigSrvvmName = "$namingPrefix-AzMigSrv" $AzMigSrvvmvhdPath = "${Env:MHBoxVMDir}\$namingPrefix-AzMigSrv.vhdx" - #$AzRepSrvvmName = "$namingPrefix-AzRepSrv" - #$AzRepSrvvmvhdPath = "${Env:MHBoxVMDir}\$namingPrefix-AzRepSrv.vhdx" - $files = 'ArcBox-Win2K22.vhdx;ArcBox-Ubuntu-01.vhdx;' $DeploymentProgressString = 'Downloading and configuring nested VMs' @@ -297,14 +883,13 @@ if ($Env:flavor -ne 'DevOps') { Write-Output 'Local Copy of Win2K22 Disk for Azure Migrate Appliance. This can take some time, hold tight...' Copy-Item -Path $Win2k22vmvhdPath -Destination $AzMigSrvvmvhdPath -Force - #Copy-Item -Path $Win2k22vmvhdPath -Destination $AzRepSrvvmvhdPath -Force } # Update disk IOPS and throughput after downloading nested VMs (note: a disk's performance tier can be downgraded only once every 12 hours) az disk update --resource-group $env:resourceGroup --name $existingVMDisk.Name --disk-iops-read-write $existingVMDisk.DiskIOPSReadWrite --disk-mbps-read-write $existingVMDisk.DiskMBpsReadWrite # Create the nested VMs if not already created - Write-Header 'Create Hyper-V VMs' + Write-Host "`n### Create Hyper-V VMs ###`n" $serversDscConfigurationFile = "$Env:MHBoxDscDir\virtual_machines_itpro.dsc.yml" (Get-Content -Path $serversDscConfigurationFile) -replace 'namingPrefixStage', $namingPrefix | Set-Content -Path $serversDscConfigurationFile winget configure --file C:\MHBox\DSC\virtual_machines_itpro.dsc.yml --accept-configuration-agreements --disable-interactivity @@ -316,72 +901,40 @@ if ($Env:flavor -ne 'DevOps') { Set-VM -Name $PSItem.Name -AutomaticStopAction ShutDown -AutomaticStartAction Start Start-VM -Name $PSItem.Name } - - Start-Sleep -Seconds 30 - - Write-Header 'Creating VM Credentials' - # Hard-coded username and password for the nested VMs - $nestedLinuxUsername = 'jumpstart' - $nestedLinuxPassword = 'JS123!!' - # Create Linux credential object - $secLinuxPassword = ConvertTo-SecureString $nestedLinuxPassword -AsPlainText -Force - $linCreds = New-Object System.Management.Automation.PSCredential ($nestedLinuxUsername, $secLinuxPassword) + foreach ($vmName in @($SQLvmName, $Win2k22vmName, $AzMigSrvvmName)) { + Wait-VMCommand -VMName $vmName -Credential $winCreds + } - # Restarting Windows VM Network Adapters - Write-Header 'Restarting Network Adapters' - Start-Sleep -Seconds 5 - Invoke-Command -VMName $Win2k22vmName -ScriptBlock { Get-NetAdapter | Restart-NetAdapter } -Credential $winCreds - Invoke-Command -VMName $AzMigSrvvmName -ScriptBlock { Get-NetAdapter | Restart-NetAdapter } -Credential $winCreds - #Invoke-Command -VMName $AzRepSrvvmName -ScriptBlock { Get-NetAdapter | Restart-NetAdapter } -Credential $winCreds - Start-Sleep -Seconds 10 + Write-Host "`n### Creating VM Credentials ###`n" + # Hard-coded username for the nested Linux VM + $nestedLinuxUsername = 'jumpstart' if ($namingPrefix -ne 'ArcBox') { # Renaming the nested VMs - Write-Header 'Renaming the nested Windows VMs' - Invoke-Command -VMName $Win2k22vmName -ScriptBlock { - - if ($env:computername -cne $using:Win2k22vmName) { - Rename-Computer -NewName $using:Win2k22vmName -Restart - } - - } -Credential $winCreds - - Invoke-Command -VMName $AzMigSrvvmName -ScriptBlock { - - if ($env:computername -cne $using:AzMigSrvvmName) { - Rename-Computer -NewName $using:AzMigSrvvmName -Restart - } - - } -Credential $winCreds -<# - Invoke-Command -VMName $AzRepSrvvmName -ScriptBlock { - - if ($env:computername -cne $using:AzRepSrvvmName) { - Rename-Computer -NewName $using:AzRepSrvvmName -Restart + Write-Host "`n### Renaming the nested Windows VMs ###`n" + foreach ($vmName in @($Win2k22vmName, $AzMigSrvvmName)) { + $restartRequired = Invoke-VMCommandWithRetry ` + -VMName $vmName ` + -Credential $winCreds ` + -ScriptBlock { + param($newName) + if ($env:COMPUTERNAME -ceq $newName) { + return $false + } + Rename-Computer -NewName $newName + return $true + } ` + -ArgumentList $vmName ` + -Description 'computer rename' + + if ($restartRequired) { + Restart-VMGracefullyAndWaitForHeartbeat -VMName $vmName } - - } -Credential $winCreds -#> - Write-Host 'Waiting for the nested Windows VMs to come back online...' - - start-sleep -Seconds 30 - - Get-VM $Win2k22vmName | Restart-VM -Force - Get-VM $Win2k22vmName | Wait-VM -For Heartbeat -Timeout 600 - - Get-VM $AzMigSrvvmName | Restart-VM -Force - Get-VM $AzMigSrvvmName | Wait-VM -For Heartbeat -Timeout 600 - - #Get-VM $AzRepSrvvmName | Restart-VM -Force - #Get-VM $AzRepSrvvmName | Wait-VM -For Heartbeat - + } } - # Getting the Ubuntu nested VM IP address - $Ubuntu01VmIp = Get-VM -Name $Ubuntu01vmName | Select-Object -ExpandProperty NetworkAdapters | Select-Object -ExpandProperty IPAddresses | Select-Object -Index 0 - # Configuring SSH for accessing Linux VMs Write-Output 'Generating SSH key for accessing nested Linux VMs' @@ -393,7 +946,8 @@ if ($Env:flavor -ne 'DevOps') { # Automatically accept unseen keys but will refuse connections for changed or invalid hostkeys. Add-Content -Path "$Env:USERPROFILE\.ssh\config" -Value 'StrictHostKeyChecking=accept-new' - Get-VM *Ubuntu* | Wait-VM -For Heartbeat + $Ubuntu01VmIp = Get-VMIPv4AddressWithRetry -VMName $Ubuntu01vmName + Wait-TcpPort -ComputerName $Ubuntu01VmIp -Port 22 Get-VM *Ubuntu* | Copy-VMFile -SourcePath "$Env:TEMP\authorized_keys" -DestinationPath "/home/$nestedLinuxUsername/.ssh/" -FileSource Host -Force -CreateFullPath if ($namingPrefix -ne 'ArcBox') { @@ -401,88 +955,343 @@ if ($Env:flavor -ne 'DevOps') { # Renaming the nested linux VMs Write-Output 'Renaming the nested Linux VMs' - Invoke-Command -HostName $Ubuntu01VmIp -KeyFilePath "$Env:USERPROFILE\.ssh\id_rsa" -UserName $nestedLinuxUsername -ScriptBlock { - Invoke-Expression "sudo hostnamectl set-hostname $using:ubuntu01vmName;sudo systemctl reboot" + $linuxKeyFilePath = "$Env:USERPROFILE\.ssh\id_rsa" + $previousBootId = Get-LinuxBootId ` + -ComputerName $Ubuntu01VmIp ` + -UserName $nestedLinuxUsername ` + -KeyFilePath $linuxKeyFilePath + + $configurationResult = $null + $configurationError = $null + try { + $configurationResult = Invoke-Command -HostName $Ubuntu01VmIp -KeyFilePath $linuxKeyFilePath -UserName $nestedLinuxUsername -ScriptBlock { + param( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string]$NewHostName + ) + + function Invoke-SudoCommand { + param( + [Parameter(Mandatory)] + [string]$Command, + + [string[]]$Arguments = @() + ) + + $commandOutput = @(& sudo -- $Command @Arguments 2>&1) + $commandExitCode = $LASTEXITCODE + if ($commandExitCode -ne 0) { + $commandDetail = $commandOutput -join [Environment]::NewLine + throw "'$Command' failed with exit code $commandExitCode. $commandDetail" + } + + return $commandOutput + } + + $currentHostNameOutput = @( + Invoke-SudoCommand -Command 'hostname' + ) + $currentHostName = ([string]$currentHostNameOutput[-1]).Trim() + $desiredCloudInitContent = "preserve_hostname: true`n" + $desiredHostnameContent = "$NewHostName`n" + $desiredHostsEntry = "127.0.1.1`t$NewHostName" + $cloudInitTarget = '/etc/cloud/cloud.cfg.d/99-mhbox-preserve-hostname.cfg' + $rebootMarkerTarget = '/var/lib/mhbox/hostname-reboot-pending' + $bootId = (Get-Content /proc/sys/kernel/random/boot_id -Raw).Trim() + $desiredRebootMarkerContent = "$bootId`n$NewHostName`n" + + $currentCloudInitContent = if (Test-Path -LiteralPath $cloudInitTarget) { + [IO.File]::ReadAllText($cloudInitTarget) + } + else { + $null + } + $currentHostnameContent = [IO.File]::ReadAllText('/etc/hostname') + $hostsLines = @( + Invoke-SudoCommand -Command 'cat' -Arguments @('/etc/hosts') + ) + $currentHostsEntries = @( + $hostsLines | + Where-Object { [string]$_ -match '^\s*127\.0\.1\.1(?:\s|$)' } + ) + $currentRebootMarkerContent = if (Test-Path -LiteralPath $rebootMarkerTarget) { + [IO.File]::ReadAllText($rebootMarkerTarget) + } + else { + $null + } + $restartRequired = ( + $currentHostName -cne $NewHostName -or + $currentCloudInitContent -cne $desiredCloudInitContent -or + $currentHostnameContent -cne $desiredHostnameContent -or + $currentHostsEntries.Count -ne 1 -or + [string]$currentHostsEntries[0] -cne $desiredHostsEntry -or + $currentRebootMarkerContent -ceq $desiredRebootMarkerContent + ) + + if (-not $restartRequired) { + return [pscustomobject]@{ + RestartRequired = $false + RebootScheduled = $false + BootId = $bootId + } + } + + $tempRoot = [IO.Path]::Combine( + [IO.Path]::GetTempPath(), + "mhbox-hostname-$([guid]::NewGuid())" + ) + [IO.Directory]::CreateDirectory($tempRoot) | Out-Null + try { + $utf8NoBom = [Text.UTF8Encoding]::new($false) + $cloudInitPath = Join-Path $tempRoot '99-mhbox-preserve-hostname.cfg' + [IO.File]::WriteAllText( + $cloudInitPath, + $desiredCloudInitContent, + $utf8NoBom + ) + + $hostnamePath = Join-Path $tempRoot 'hostname' + [IO.File]::WriteAllText( + $hostnamePath, + $desiredHostnameContent, + $utf8NoBom + ) + + $rebootMarkerPath = Join-Path $tempRoot 'hostname-reboot-pending' + [IO.File]::WriteAllText( + $rebootMarkerPath, + $desiredRebootMarkerContent, + $utf8NoBom + ) + + $localHostEntryFound = $false + $updatedHostsLines = foreach ($hostsLine in $hostsLines) { + if ([string]$hostsLine -match '^\s*127\.0\.1\.1(?:\s|$)') { + if (-not $localHostEntryFound) { + $localHostEntryFound = $true + $desiredHostsEntry + } + } + else { + [string]$hostsLine + } + } + if (-not $localHostEntryFound) { + $updatedHostsLines += $desiredHostsEntry + } + + $hostsPath = Join-Path $tempRoot 'hosts' + [IO.File]::WriteAllText( + $hostsPath, + ($updatedHostsLines -join "`n") + "`n", + $utf8NoBom + ) + + Invoke-SudoCommand ` + -Command 'mkdir' ` + -Arguments @('-p', '/etc/cloud/cloud.cfg.d', '/var/lib/mhbox') | + Out-Null + Invoke-SudoCommand ` + -Command 'install' ` + -Arguments @( + '-m', '0644', '--', + $rebootMarkerPath, + $rebootMarkerTarget + ) | + Out-Null + Invoke-SudoCommand ` + -Command 'install' ` + -Arguments @( + '-m', '0644', '--', + $cloudInitPath, + $cloudInitTarget + ) | + Out-Null + Invoke-SudoCommand ` + -Command 'install' ` + -Arguments @('-m', '0644', '--', $hostnamePath, '/etc/hostname') | + Out-Null + Invoke-SudoCommand ` + -Command 'install' ` + -Arguments @('-m', '0644', '--', $hostsPath, '/etc/hosts') | + Out-Null + Invoke-SudoCommand ` + -Command 'hostnamectl' ` + -Arguments @('set-hostname', '--', $NewHostName) | + Out-Null + Invoke-SudoCommand -Command 'sync' | Out-Null + + $rebootUnitName = "mhbox-hostname-reboot-$([guid]::NewGuid().ToString('N').Substring(0, 12))" + Invoke-SudoCommand ` + -Command 'systemd-run' ` + -Arguments @( + "--unit=$rebootUnitName", + '--on-active=3s', + '--collect', + '/usr/bin/systemctl', + 'reboot' + ) | + Out-Null + } + finally { + Remove-Item -LiteralPath $tempRoot -Recurse -Force -ErrorAction Continue + } + + return [pscustomobject]@{ + RestartRequired = $true + RebootScheduled = $true + BootId = $bootId + } + } -ArgumentList @($Ubuntu01HostName) -ErrorAction Stop + } + catch { + $configurationError = $_ } - Restart-VM -Name $ubuntu01vmName -Force - } - Get-VM *Ubuntu* | Wait-VM -For IPAddress + if ($configurationError) { + $disconnectPattern = 'remote session might have ended|SSH transport|connection.*(?:closed|reset|lost)|broken pipe' + if ($configurationError.Exception.Message -notmatch $disconnectPattern) { + throw "Ubuntu hostname configuration failed before reboot acknowledgement. $($configurationError.Exception.Message)" + } - Write-Host 'Waiting for the nested Linux VMs to come back online...waiting for 10 seconds' + try { + $Ubuntu01VmIp = Wait-LinuxGuestReboot ` + -VMName $Ubuntu01vmName ` + -UserName $nestedLinuxUsername ` + -KeyFilePath $linuxKeyFilePath ` + -PreviousBootId $previousBootId ` + -TimeoutSeconds 300 + } + catch { + throw "Ubuntu reboot was not acknowledged and no new boot was observed. Configuration error: $($configurationError.Exception.Message) Reboot error: $($_.Exception.Message)" + } + } + elseif ($configurationResult.RestartRequired) { + if ( + -not $configurationResult.RebootScheduled -or + $configurationResult.BootId -cne $previousBootId + ) { + throw 'Ubuntu returned an invalid hostname reboot acknowledgement.' + } + $Ubuntu01VmIp = Wait-LinuxGuestReboot ` + -VMName $Ubuntu01vmName ` + -UserName $nestedLinuxUsername ` + -KeyFilePath $linuxKeyFilePath ` + -PreviousBootId $previousBootId + } + elseif ($configurationResult.RebootScheduled) { + throw 'Ubuntu scheduled an unexpected hostname reboot.' + } - Start-Sleep -Seconds 10 + Wait-LinuxHostName ` + -ComputerName $Ubuntu01VmIp ` + -UserName $nestedLinuxUsername ` + -KeyFilePath $linuxKeyFilePath ` + -ExpectedHostName $Ubuntu01HostName + Clear-LinuxHostNameRebootMarker ` + -ComputerName $Ubuntu01VmIp ` + -UserName $nestedLinuxUsername ` + -KeyFilePath $linuxKeyFilePath ` + -ExpectedHostName $Ubuntu01HostName + } + else { + $Ubuntu01VmIp = Get-VMIPv4AddressWithRetry -VMName $Ubuntu01vmName + Wait-TcpPort -ComputerName $Ubuntu01VmIp -Port 22 + } Write-Output 'Activating operating system on Windows VMs...' - Invoke-Command -VMName $Win2k22vmName -ScriptBlock { - - cscript C:\Windows\system32\slmgr.vbs -ipk VDYBN-27WPP-V4HQT-9VMD4-VMK7H - cscript C:\Windows\system32\slmgr.vbs -skms kms.core.windows.net - cscript C:\Windows\system32\slmgr.vbs -ato - cscript C:\Windows\system32\slmgr.vbs -dlv - - } -Credential $winCreds - - Invoke-Command -VMName $AzMigSrvvmName -ScriptBlock { - - cscript C:\Windows\system32\slmgr.vbs -ipk VDYBN-27WPP-V4HQT-9VMD4-VMK7H - cscript C:\Windows\system32\slmgr.vbs -skms kms.core.windows.net - cscript C:\Windows\system32\slmgr.vbs -ato - cscript C:\Windows\system32\slmgr.vbs -dlv - - } -Credential $winCreds - - Invoke-Command -VMName $SQLvmName -ScriptBlock { - + $activationScript = { cscript C:\Windows\system32\slmgr.vbs -ipk VDYBN-27WPP-V4HQT-9VMD4-VMK7H cscript C:\Windows\system32\slmgr.vbs -skms kms.core.windows.net cscript C:\Windows\system32\slmgr.vbs -ato cscript C:\Windows\system32\slmgr.vbs -dlv + } - } -Credential $winCreds + foreach ($vmName in @($Win2k22vmName, $AzMigSrvvmName, $SQLvmName)) { + Remove-VMUserWallpaper -VMName $vmName -Credential $winCreds + Invoke-VMCommandWithRetry ` + -VMName $vmName ` + -Credential $winCreds ` + -ScriptBlock $activationScript ` + -Description 'Windows activation' + } # Install Demo Web App on Windows VM - Write-Header 'Installing Web App on Windows and Linux Server' + Write-Host "`n### Installing Web App on Windows and Linux Servers ###`n" # Copy WebApp to Windows VM Copy-VMFile $Win2k22vmName -SourcePath "$Env:MHBoxDir\DemoPage\deployWebApp.ps1" -DestinationPath "C:\MHDir\DemoPage\deployWebApp.ps1" -CreateFullPath -FileSource Host -Force - # Install IIS and clean default web files on Windows VM + # Install IIS and deploy the demo page on Windows VM Invoke-Command -VMName $Win2k22vmName -ScriptBlock { + param($sourceRoot) + Add-WindowsFeature Web-Server -IncludeManagementTools - Remove-Item -Path "C:\inetpub\wwwroot\*.*" - powershell -ExecutionPolicy Unrestricted -File "C:\MHDir\DemoPage\deployWebApp.ps1" - } -Credential $winCreds + & powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass ` + -File 'C:\MHDir\DemoPage\deployWebApp.ps1' ` + -SourceRoot $sourceRoot + if ($LASTEXITCODE -ne 0) { + throw "deployWebApp.ps1 failed with exit code $LASTEXITCODE." + } + } -ArgumentList $demoAssetSourceRoot -Credential $winCreds -ErrorAction Stop - # Install Apache and clean default web files on Linux VM + # Install Apache and deploy the demo page on Linux VM Write-Output 'Installing Apache Web Server on Linux VM...' - $UbuntuSessions = New-PSSession -HostName $Ubuntu01VmIp -KeyFilePath "$Env:USERPROFILE\.ssh\id_rsa" -UserName $nestedLinuxUsername - Copy-VMFile $Ubuntu01vmName -SourcePath "$Env:MHBoxDir\DemoPage\deploy-webapp.sh" -DestinationPath "/home/$nestedLinuxUsername" -FileSource Host -Force - Invoke-JSSudoCommand -Session $UbuntuSessions -Command "sh /home/$nestedLinuxUsername/deploy-webapp.sh" + $UbuntuSessions = $null + try { + $UbuntuSessions = New-PSSession -HostName $Ubuntu01VmIp -KeyFilePath "$Env:USERPROFILE\.ssh\id_rsa" -UserName $nestedLinuxUsername -ErrorAction Stop + $linuxDeploymentScript = "/home/$nestedLinuxUsername/deploy-webapp.sh" + Copy-VMFile $Ubuntu01vmName -SourcePath "$Env:MHBoxDir\DemoPage\deploy-webapp.sh" -DestinationPath "/home/$nestedLinuxUsername" -FileSource Host -Force + + $sourceRootBase64 = [Convert]::ToBase64String( + [Text.Encoding]::UTF8.GetBytes($demoAssetSourceRoot) + ) + Invoke-Command -Session $UbuntuSessions -ScriptBlock { + param($deploymentScript, $encodedSourceRoot) + + try { + $sourceRoot = [Text.Encoding]::UTF8.GetString( + [Convert]::FromBase64String($encodedSourceRoot) + ) + } + catch { + throw 'The demo asset source URL could not be decoded.' + } + + $deploymentOutput = @(& sudo /bin/sh $deploymentScript $sourceRoot 2>&1) + $deploymentExitCode = $LASTEXITCODE + foreach ($outputLine in $deploymentOutput) { + Write-Output ([string]$outputLine) + } + + if ($deploymentExitCode -ne 0) { + throw "deploy-webapp.sh failed with exit code $deploymentExitCode." + } + } -ArgumentList $linuxDeploymentScript, $sourceRootBase64 -ErrorAction Stop + } + finally { + if ($null -ne $UbuntuSessions) { + Remove-PSSession -Session $UbuntuSessions -ErrorAction Continue + } + } } # Removing the LogonScript Scheduled Task so it won't run on next reboot - Write-Header 'Removing Logon Task' + Write-Host "`n### Removing Logon Task ###`n" if ($null -ne (Get-ScheduledTask -TaskName 'MHServersLogonScript' -ErrorAction SilentlyContinue)) { Unregister-ScheduledTask -TaskName 'MHServersLogonScript' -Confirm:$false } } -#Changing to Jumpstart MHBox wallpaper -Write-Header 'Changing wallpaper' - -# bmp file is required for BGInfo -#Convert-JSImageToBitMap -SourceFilePath "$Env:MHBoxDir\wallpaper.png" -DestinationFilePath "$Env:MHBoxDir\wallpaper.bmp" - -Set-JSDesktopBackground -ImagePath "$Env:MHBoxDir\MHWallpaper.bmp" - # Enabling Azure VM Auto-shutdown if ($env:autoShutdownEnabled -eq "true") { - Write-Header "Enabling Azure VM Auto-shutdown" + Write-Host "`n### Enabling Azure VM Auto-shutdown ###`n" $ScheduleResource = Get-AzResource -ResourceGroup $env:resourceGroup -ResourceType Microsoft.DevTestLab/schedules $Uri = "https://management.azure.com$($ScheduleResource.ResourceId)?api-version=2018-09-15" @@ -496,7 +1305,7 @@ Set-JSDesktopBackground -ImagePath "$Env:MHBoxDir\MHWallpaper.bmp" } else { - Write-Header "Auto-shutdown is not enabled, skipping." + Write-Host "`n### Auto-shutdown is not enabled, skipping. ###`n" } diff --git a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/MHWallpaper.bmp b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/MHWallpaper.bmp deleted file mode 100644 index 2b79b6ef7..000000000 Binary files a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/MHWallpaper.bmp and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/PSProfile.ps1 b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/PSProfile.ps1 deleted file mode 100644 index 28237ba09..000000000 --- a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/PSProfile.ps1 +++ /dev/null @@ -1,48 +0,0 @@ -function Write-Header { - param ( - [string] - $title - ) - - Write-Host - Write-Host ("#" * ($title.Length + 8)) - Write-Host "# - $title" - Write-Host ("#" * ($title.Length + 8)) - Write-Host -} - -function exec -{ - param - ( - [ScriptBlock] $ScriptBlock, - [string] $StderrPrefix = "", - [int[]] $AllowedExitCodes = @(0) - ) - - $backupErrorActionPreference = $script:ErrorActionPreference - - $script:ErrorActionPreference = "Continue" - try - { - & $ScriptBlock 2>&1 | ForEach-Object -Process ` - { - if ($_ -is [System.Management.Automation.ErrorRecord]) - { - "$StderrPrefix$_" - } - else - { - "$_" - } - } - if ($AllowedExitCodes -notcontains $LASTEXITCODE) - { - throw "Execution failed with exit code $LASTEXITCODE" - } - } - finally - { - $script:ErrorActionPreference = $backupErrorActionPreference - } -} diff --git a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/WinGet.ps1 b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/WinGet.ps1 index f4277c28d..7ca337151 100644 --- a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/WinGet.ps1 +++ b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/WinGet.ps1 @@ -3,8 +3,6 @@ $ErrorActionPreference = $env:ErrorActionPreference $Env:MHBoxDir = 'C:\MHBox' $Env:MHBoxLogsDir = "$Env:MHBoxDir\Logs" -# Path to the flag file -$FlagFile = "$Env:MHBoxLogsDir\WINGET_Restart_done.flag" $tenantId = $env:tenantId $subscriptionId = $env:subscriptionId $resourceGroup = $env:resourceGroup @@ -31,25 +29,8 @@ $null = Set-AzResource -ResourceName $env:computername -ResourceGroupName $resou # Pinned to version 1.11.460 to avoid known issue: https://github.com/microsoft/winget-cli/issues/5826 Install-PSResource -Name Microsoft.WinGet.Client -Scope AllUsers -Quiet -AcceptLicense -TrustRepository -Version 1.11.460 Install-PSResource -Name Microsoft.WinGet.DSC -Scope AllUsers -Quiet -AcceptLicense -TrustRepository -Version 1.11.460 - <# # Update WinGet package manager to the latest version (running twice due to a known issue regarding WinAppSDK) - Repair-WinGetPackageManager -AllUsers -Force -Latest -Verbose - # Check if the flag file exists - if (Test-Path $FlagFile) { - Write-Host "Restart already performed previously. Suppressing restart." - } else { - Write-Host "Restart not yet performed. Creating flag and restarting..." - - # Create the flag file - New-Item -ItemType File -Path $FlagFile -Force | Out-Null - - # Restart the computer - start-sleep -Seconds 5 - Restart-Computer -Force - } - # The script will resume here after the restart #> # Install DSC resources required for MHBox -Install-PSResource -Name DSCR_Font -Scope AllUsers -Quiet -AcceptLicense -TrustRepository Install-PSResource -Name HyperVDsc -Scope AllUsers -Quiet -AcceptLicense -TrustRepository -Prerelease Install-PSResource -Name NetworkingDsc -Scope AllUsers -Quiet -AcceptLicense -TrustRepository @@ -59,15 +40,10 @@ Repair-WinGetPackageManager -AllUsers -Force -Latest -Verbose # Apply WinGet Configuration files winget configure --file C:\MHBox\DSC\common.dsc.yml --accept-configuration-agreements --disable-interactivity - -switch ($env:flavor) { - 'DevOps' { winget configure --file C:\MHBox\DSC\devops.dsc.yml --accept-configuration-agreements --disable-interactivity } - 'DataOps' { winget configure --file C:\MHBox\DSC\dataops.dsc.yml --accept-configuration-agreements --disable-interactivity } - 'ITPro' { winget configure --file C:\MHBox\DSC\itpro.dsc.yml --accept-configuration-agreements --disable-interactivity } -} +winget configure --file C:\MHBox\DSC\itpro.dsc.yml --accept-configuration-agreements --disable-interactivity # Start remaining logon scripts -Get-ScheduledTask *LogonScript* | Start-ScheduledTask +Start-ScheduledTask -TaskName 'MHServersLogonScript' #Cleanup Unregister-ScheduledTask -TaskName 'WinGetLogonScript' -Confirm:$false diff --git a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/demopage/deploy-webapp.sh b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/demopage/deploy-webapp.sh index d89f0b34c..01453e1ad 100644 --- a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/demopage/deploy-webapp.sh +++ b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/demopage/deploy-webapp.sh @@ -1,54 +1,67 @@ -#!/bin/bash -# ========================================================= -# Web Server Deployment Script -# Converted from cloud.cfg -# ========================================================= -# This script updates the system, installs required packages, -# deploys a demo web page, and starts the web server. -# ========================================================= - -set -e - -echo "==========================================" -echo "🚀 Starting Web Server Deployment" -echo "==========================================" - -# --- Step 1: Update and upgrade packages --- -#echo "đŸ“Ļ Updating system packages..." -sudo apt update -y -#sudo apt upgrade -y - -# --- Step 2: Install required packages --- -echo "🧰 Installing required packages..." -# Note: 'httpd' is Apache on RHEL; on Ubuntu it's 'apache2' -if ! command -v apache2 >/dev/null 2>&1; then - sudo apt install -y apache2 samba-client wget -else - echo "✅ Apache already installed." +#!/bin/sh +set -eu + +if [ "$#" -ne 1 ] || [ -z "$1" ]; then + echo "Usage: $0 " >&2 + exit 2 +fi + +source_root="$1" +while [ "${source_root%/}" != "$source_root" ]; do + source_root="${source_root%/}" +done + +case "$source_root" in + https://?*) ;; + *) + echo "Source root must be an absolute HTTPS URL." >&2 + exit 2 + ;; +esac + +web_root="/var/www/html" +assets="index.html stylesheet.css GitHub_Logo.png MSLogo.png MSicon.png" +staging_root="$(mktemp -d)" +trap 'rm -rf "$staging_root"' EXIT + +if ! command -v apache2 >/dev/null 2>&1 || + ! command -v wget >/dev/null 2>&1; then + sudo apt-get update + sudo DEBIAN_FRONTEND=noninteractive apt-get install -y apache2 wget +fi + +for asset in $assets; do + wget --quiet --https-only --timeout=30 --tries=3 \ + --output-document "$staging_root/$asset" "$source_root/$asset" + test -s "$staging_root/$asset" || { + echo "Downloaded asset is empty: $asset" >&2 + exit 1 + } +done + +hostname_value="$(hostname)" +sed -i \ + -e "s||$hostname_value|g" \ + -e 's||Ubuntu Linux|g' \ + -e 's||Apache|g' \ + "$staging_root/index.html" + +if grep -Eq '<(HOSTNAME|PLATFORM|WEB_SERVER)>' "$staging_root/index.html"; then + echo "One or more template tokens remain in index.html." >&2 + exit 1 +fi + +sudo mkdir -p "$web_root" +sudo find "$web_root" -mindepth 1 -delete +for asset in $assets; do + sudo install -m 0644 "$staging_root/$asset" "$web_root/$asset" +done + +deployed_count="$(sudo find "$web_root" -mindepth 1 -maxdepth 1 -type f | wc -l)" +if [ "$deployed_count" -ne 5 ]; then + echo "The Apache web root does not contain exactly the expected static assets." >&2 + exit 1 fi -# --- Step 3: Download demo web content --- -echo "🌐 Downloading demo web files..." -sudo mkdir -p /var/www/html - -sudo wget -q https://raw.githubusercontent.com/microsoft/MicroHack/main/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/demopage/index.html -O /var/www/html/index.html -sudo wget -q https://raw.githubusercontent.com/microsoft/MicroHack/main/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/demopage/GitHub_Logo.png -O /var/www/html/GitHub_Logo.png -sudo wget -q https://raw.githubusercontent.com/microsoft/MicroHack/main/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/demopage/MSLogo.png -O /var/www/html/MSLogo.png -sudo wget -q https://raw.githubusercontent.com/microsoft/MicroHack/main/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/demopage/MSicon.png -O /var/www/html/MSicon.png -sudo wget -q https://raw.githubusercontent.com/microsoft/MicroHack/main/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/demopage/github-mark.png -O /var/www/html/github-mark.png -sudo wget -q https://raw.githubusercontent.com/microsoft/MicroHack/main/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/demopage/stylesheet.css -O /var/www/html/stylesheet.css - -# --- Step 4: Replace placeholder --- -echo "đŸ–‹ī¸ Customizing index.html with system hostname..." -sudo sed -i "s//$(hostname)/g" /var/www/html/index.html - -# --- Step 5: Enable and start web server --- -echo "đŸ•šī¸ Enabling and starting Apache..." -sudo systemctl enable apache2 -sudo systemctl start apache2 - -# --- Step 6: Display status and access info --- -echo "==========================================" -echo "✅ Deployment Complete!" -echo "🌍 Access your web page at: http://$(hostname -I | awk '{print $1}')/" -echo "==========================================" +sudo systemctl enable --now apache2 +wget --quiet --spider --timeout=15 http://127.0.0.1/ diff --git a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/demopage/deployWebApp.ps1 b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/demopage/deployWebApp.ps1 index 73999aec8..00227a9b6 100644 --- a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/demopage/deployWebApp.ps1 +++ b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/demopage/deployWebApp.ps1 @@ -1,19 +1,71 @@ -# Setting Variables -$pathToRepo = "https://raw.githubusercontent.com/microsoft/MicroHack/main/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/demopage" -$pathToIndex = "C:\inetpub\wwwroot\index.html" -$replaceText = "" -$hostname = $env:computername - -# Remove Default contents of wwwroot -Remove-Item -Path "C:\inetpub\wwwroot\*.*" - -# Download files from repository -Invoke-WebRequest -Uri "$pathToRepo/index.html" -OutFile C:\inetpub\wwwroot\index.html -Invoke-WebRequest -Uri "$pathToRepo/GitHub_Logo.png" -OutFile C:\inetpub\wwwroot\GitHub_Logo.png -Invoke-WebRequest -Uri "$pathToRepo/MSLogo.png" -OutFile C:\inetpub\wwwroot\MSLogo.png -Invoke-WebRequest -Uri "$pathToRepo/MSicon.png" -OutFile C:\inetpub\wwwroot\MSicon.png -Invoke-WebRequest -Uri "$pathToRepo/github-mark.png" -OutFile C:\inetpub\wwwroot\github-mark.png -Invoke-WebRequest -Uri "$pathToRepo/stylesheet.css" -OutFile C:\inetpub\wwwroot\stylesheet.css - -# Replace Hostname placeholder with actual Hostname -(Get-Content -path $pathToIndex -Raw).replace($replaceText, $hostname) | Set-Content -Path $pathToIndex +param ( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string]$SourceRoot +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$SourceRoot = $SourceRoot.Trim().TrimEnd('/') +$sourceUri = $null +if ( + -not [Uri]::TryCreate($SourceRoot, [UriKind]::Absolute, [ref]$sourceUri) -or + $sourceUri.Scheme -ne [Uri]::UriSchemeHttps +) { + throw 'SourceRoot must be an absolute HTTPS URL.' +} + +$webRoot = 'C:\inetpub\wwwroot' +$assets = @('index.html', 'stylesheet.css', 'GitHub_Logo.png', 'MSLogo.png', 'MSicon.png') +$stagingRoot = Join-Path $env:TEMP "microhack-demopage-$([guid]::NewGuid())" + +New-Item -Path $stagingRoot -ItemType Directory -Force | Out-Null + +try { + foreach ($asset in $assets) { + $destination = Join-Path $stagingRoot $asset + Invoke-WebRequest -Uri "$SourceRoot/$asset" -OutFile $destination -UseBasicParsing + if ((Get-Item -LiteralPath $destination).Length -eq 0) { + throw "Downloaded asset is empty: $asset" + } + } + + $indexPath = Join-Path $stagingRoot 'index.html' + $html = Get-Content -LiteralPath $indexPath -Raw + $values = [ordered]@{ + '' = [Environment]::MachineName + '' = 'Windows Server 2022' + '' = 'IIS' + } + + foreach ($token in $values.Keys) { + $html = $html.Replace($token, $values[$token]) + } + + if ($html -match '<(?:HOSTNAME|PLATFORM|WEB_SERVER)>') { + throw 'One or more template tokens remain in index.html.' + } + + $utf8NoBom = New-Object System.Text.UTF8Encoding($false) + [System.IO.File]::WriteAllText($indexPath, $html, $utf8NoBom) + + New-Item -Path $webRoot -ItemType Directory -Force | Out-Null + Get-ChildItem -LiteralPath $webRoot -Force | Remove-Item -Recurse -Force + Copy-Item -Path (Join-Path $stagingRoot '*') -Destination $webRoot -Force + + $deployedAssets = @(Get-ChildItem -LiteralPath $webRoot -File | Select-Object -ExpandProperty Name) + if ($deployedAssets.Count -ne $assets.Count -or (Compare-Object $assets $deployedAssets)) { + throw 'The IIS web root does not contain exactly the expected static assets.' + } + + Set-Service -Name W3SVC -StartupType Automatic + Start-Service -Name W3SVC + $response = Invoke-WebRequest -Uri 'http://localhost/' -UseBasicParsing -TimeoutSec 15 + if ($response.StatusCode -ne 200) { + throw "Local IIS validation returned HTTP $($response.StatusCode)." + } +} +finally { + Remove-Item -LiteralPath $stagingRoot -Recurse -Force -ErrorAction SilentlyContinue +} diff --git a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/demopage/github-mark.png b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/demopage/github-mark.png deleted file mode 100644 index 6cb3b705d..000000000 Binary files a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/demopage/github-mark.png and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/demopage/index.html b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/demopage/index.html index d60bf02d0..5b0a89876 100644 --- a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/demopage/index.html +++ b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/demopage/index.html @@ -1,37 +1,124 @@ - - - + + - - + + + + Migrate and Secure to be ready for AI Ops + - - MicroHack Demo Page + - -
- - + +
+
+
+ + Workload online +
+

Secure Migration Hack

+

Migrate and Secure to be ready for AI Ops

+

From secure migration to agent-assisted cloud operations.

+
-
-
-

MicroHack
Demo Page

-

Current Hostname:

+
+
+

End-to-end journey

+

Modernize with a clear path forward

+
+
    +
  1. + +

    Discover

    +

    Inventory the estate

    +
  2. +
  3. + +

    Assess

    +

    Build the business case

    +
  4. +
  5. + +

    Migrate

    +

    Move with confidence

    +
  6. +
  7. + +

    Secure

    +

    Harden the landing zone

    +
  8. +
  9. + +

    Operate

    +

    Assist cloud operations

    +
  10. +
+
-
-
+
+
+

Portable by design

+

One workload, two platforms

+

+ The same dependency-free static workload runs on Windows Server with IIS and + Ubuntu Linux with Apache. Its hosting details change, but the application stays portable. +

+
- +
+
+
+

Live environment

+

Workload details

+
+ Ready +
+ +
+
+
+
+

© Microsoft 2026

+

Built for the Microsoft MicroHack learning experience.

+
- diff --git a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/demopage/stylesheet.css b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/demopage/stylesheet.css index 57b3258db..ddc735088 100644 --- a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/demopage/stylesheet.css +++ b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/demopage/stylesheet.css @@ -1,81 +1,495 @@ -body, html { - height: 100%; +:root { + color-scheme: light; + --cp-bg: #f7f4ef; + --cp-bg-elevated: #fcfbf8; + --cp-surface: #ffffff; + --cp-surface-soft: #f5f5f5; + --cp-border: #dedede; + --cp-border-strong: #919191; + --cp-text: #242424; + --cp-text-muted: #5c5c5c; + --cp-text-soft: #6f6f6f; + --cp-accent: #b11f4b; + --cp-accent-hover: #9a1a41; + --cp-accent-soft: rgba(177, 31, 75, 0.08); + --cp-accent-fg: #ffffff; + --cp-success: #16a34a; + --cp-danger: #dc2626; + --cp-warning: #f59e0b; + --cp-link: #0078d4; + --cp-shadow: 0 18px 48px rgba(0, 0, 0, 0.12); + --cp-overlay: rgba(255, 255, 255, 0.8); + --cp-panel: rgba(255, 255, 255, 0.86); + --cp-panel-strong: rgba(255, 255, 255, 0.96); + --cp-sheen: rgba(255, 255, 255, 0.55); + --cp-highlight: rgba(177, 31, 75, 0.12); +} +html[data-theme="dark"] { + color-scheme: dark; + --cp-bg: #3d3b3a; + --cp-bg-elevated: #343231; + --cp-surface: #292929; + --cp-surface-soft: #2e2e2e; + --cp-border: #474747; + --cp-border-strong: #5f5f5f; + --cp-text: #dedede; + --cp-text-muted: #919191; + --cp-text-soft: #b0b0b0; + --cp-accent: #fd8ea1; + --cp-accent-hover: #fb7b91; + --cp-accent-soft: rgba(253, 142, 161, 0.14); + --cp-accent-fg: #1a1a1a; + --cp-success: #4ade80; + --cp-danger: #f87171; + --cp-warning: #fbbf24; + --cp-link: #4da6ff; + --cp-shadow: 0 18px 48px rgba(0, 0, 0, 0.32); + --cp-overlay: rgba(41, 41, 41, 0.88); + --cp-panel: rgba(41, 41, 41, 0.72); + --cp-panel-strong: rgba(41, 41, 41, 0.96); + --cp-sheen: rgba(255, 255, 255, 0.04); + --cp-highlight: rgba(253, 142, 161, 0.12); +} + +* { + box-sizing: border-box; +} + +html { + min-width: 20rem; + scroll-behavior: smooth; +} + +body { + min-height: 100vh; + margin: 0; + background: var(--cp-bg); + color: var(--cp-text); + font-family: "Segoe UI", Aptos, Calibri, -apple-system, BlinkMacSystemFont, sans-serif; + line-height: 1.5; +} + +img { + display: block; + max-width: 100%; +} + +a { + color: var(--cp-link); +} + +a:focus-visible { + outline: 3px solid var(--cp-accent); + outline-offset: 4px; +} + +.skip-link { + position: fixed; + top: 0.75rem; + left: 0.75rem; + z-index: 100; + padding: 0.625rem 0.875rem; + border-radius: 0.625rem; + background: var(--cp-accent); + color: var(--cp-accent-fg); + font-weight: 700; + transform: translateY(-200%); + transition: transform 160ms ease; +} + +.skip-link:focus { + transform: translateY(0); +} + +.site-header { + position: sticky; + top: 0; + z-index: 20; + border-bottom: 1px solid var(--cp-border); + background: var(--cp-panel-strong); +} + +.header-inner, +main, +.site-footer { + width: min(72rem, calc(100% - 2.5rem)); + margin-inline: auto; +} + +.header-inner { + display: flex; + min-height: 4.75rem; + align-items: center; + justify-content: space-between; + gap: 1.5rem; +} + +.microsoft-logo { + width: auto; + height: 1.625rem; +} + +.repository-link { + display: inline-flex; + align-items: center; + gap: 0.75rem; + min-height: 2.75rem; + padding: 0.5rem 0.75rem; + border: 1px solid var(--cp-border); + border-radius: 0.625rem; + background: var(--cp-surface); + color: var(--cp-text); + font-weight: 700; + text-decoration: none; + transition: border-color 160ms ease, background-color 160ms ease; +} + +.repository-link:hover { + border-color: var(--cp-border-strong); + background: var(--cp-surface-soft); +} + +.github-logo { + width: auto; + height: 1.375rem; +} + +html[data-theme="dark"] .microsoft-logo, +html[data-theme="dark"] .github-logo { + filter: brightness(0) invert(1); +} + +main { + padding-block: clamp(3rem, 7vw, 6.5rem); +} + +.hero { + max-width: 62rem; + padding-bottom: clamp(3.5rem, 8vw, 6.5rem); +} + +.status-badge, +.card-state { + display: inline-flex; + align-items: center; + gap: 0.5rem; + width: fit-content; + border: 1px solid var(--cp-border); + border-radius: 999px; + background: var(--cp-surface); + color: var(--cp-text); + font-size: 0.875rem; + font-weight: 700; +} + +.status-badge { + margin-bottom: 2rem; + padding: 0.5rem 0.75rem; +} + +.status-dot { + width: 0.625rem; + height: 0.625rem; + border-radius: 50%; + background: var(--cp-success); + box-shadow: 0 0 0 4px var(--cp-highlight); +} + +.eyebrow { + margin: 0 0 0.75rem; + color: var(--cp-accent); + font-size: 0.8125rem; + font-weight: 800; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +h1, +h2, +h3, +p { + overflow-wrap: anywhere; +} + +h1, +h2, +h3 { + margin-top: 0; + color: var(--cp-text); + line-height: 1.12; +} + +h1 { + max-width: 58rem; + margin-bottom: 1.25rem; + font-size: clamp(2.75rem, 7vw, 5.75rem); + font-weight: 720; + letter-spacing: -0.045em; +} + +.hero-subtitle { + max-width: 46rem; + margin: 0; + color: var(--cp-text-soft); + font-size: clamp(1.25rem, 2.5vw, 1.75rem); +} + +.journey-section { + padding-block: clamp(2.5rem, 6vw, 4.5rem); + border-block: 1px solid var(--cp-border); +} + +.section-heading { + display: flex; + align-items: end; + justify-content: space-between; + gap: 2rem; + margin-bottom: 2.25rem; +} + +.section-heading h2, +.workload-copy h2, +.card-heading h2 { + margin-bottom: 0; +} + +.section-heading h2, +.workload-copy h2 { + max-width: 40rem; + font-size: clamp(2rem, 4vw, 3.25rem); + letter-spacing: -0.03em; +} + +.journey { + position: relative; + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 1rem; margin: 0; padding: 0; + list-style: none; } -.bg { - background-image: linear-gradient(-70deg, #0976D5, #9E3DB0, #6F3F95, #2C4470); - background-position: center; - background-repeat: no-repeat; - background-size: cover; +.journey::before { + position: absolute; + top: 1.5rem; + right: 8%; + left: 8%; + height: 1px; + background: var(--cp-border-strong); + content: ""; +} + +.journey-step { position: relative; - background-attachment: fixed; + min-width: 0; + padding: 0 0.75rem 0.75rem 0; + background: var(--cp-bg); } -.footer { - height: 50px; /* Adjust the height as needed */ - width: 100%; - background-color: #f2f2f2; - overflow: hidden; +.step-number { + position: relative; z-index: 1; - position: absolute; - bottom: 0; - text-align: center; + display: grid; + width: 3rem; + height: 3rem; + margin-bottom: 1.25rem; + place-items: center; + border: 1px solid var(--cp-accent); + border-radius: 50%; + background: var(--cp-bg); + color: var(--cp-accent); + font-family: Consolas, "Courier New", Courier, monospace; + font-size: 0.8125rem; + font-weight: 700; +} + +.journey-step h3 { + margin-bottom: 0.375rem; + font-size: 1.125rem; } -.topnav { - background-color: white; +.journey-step p { + margin: 0; + color: var(--cp-text-soft); + font-size: 0.9375rem; +} + +.workload-section { + display: grid; + grid-template-columns: minmax(0, 0.85fr) minmax(28rem, 1.15fr); + gap: clamp(2.5rem, 8vw, 7rem); + align-items: center; + padding-top: clamp(4rem, 9vw, 7rem); +} + +.workload-copy p:last-child { + max-width: 35rem; + margin: 1.5rem 0 0; + color: var(--cp-text-soft); + font-size: 1.125rem; +} + +.workload-card { overflow: hidden; - z-index: 1; - position: fixed; - width: 100%; - top: 0 + border: 1px solid var(--cp-border); + border-top: 4px solid var(--cp-accent); + border-radius: 16px; + background: var(--cp-surface); + box-shadow: var(--cp-shadow); } -.topnav .logo { - float: left; - padding: 25px; +.card-heading { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1.5rem; + padding: 1.75rem; + border-bottom: 1px solid var(--cp-border); } -.topnav .menu { - float: right; - padding: 25px; +.card-heading .eyebrow { + margin-bottom: 0.375rem; } -img { - max-height: 30px; - width: auto; - object-fit:contain; - background-color: white; - vertical-align:middle +.card-heading h2 { + font-size: 1.5rem; } -img:hover { - background-color: #ddd; - color: black; +.card-state { + flex: 0 0 auto; + padding: 0.375rem 0.625rem; + color: var(--cp-text); } -.heading { - font-size: 80px; +.metadata-list { + margin: 0; } -.heading2 { - font-size: 40px; - font-weight: bold; +.metadata-row { + display: grid; + grid-template-columns: minmax(7rem, 0.65fr) minmax(0, 1.35fr); + gap: 1.5rem; + padding: 1.375rem 1.75rem; + border-bottom: 1px solid var(--cp-border); } -.heading, .heading2 { - color: white; - opacity: 90%; - font-family: 'Segoe UI', Tahoma, Verdana, sans-serif; +.metadata-row:last-child { + border-bottom: 0; } -.center { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - text-align: center; - padding: 20px; +.metadata-row dt { + color: var(--cp-text-soft); + font-weight: 600; +} + +.metadata-row dd { + min-width: 0; + margin: 0; + color: var(--cp-text); + font-family: Consolas, "Courier New", Courier, monospace; + font-weight: 700; + text-align: right; +} + +.site-footer { + display: flex; + justify-content: space-between; + gap: 2rem; + margin-top: clamp(4rem, 9vw, 7rem); + padding-block: 1.75rem; + border-top: 1px solid var(--cp-border); + color: var(--cp-text-soft); + font-size: 0.875rem; +} + +.site-footer p { + margin: 0; +} + +@media (max-width: 56rem) { + .journey { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .journey::before { + display: none; + } + + .journey-step { + padding: 1.25rem; + border: 1px solid var(--cp-border); + border-radius: 0.625rem; + background: var(--cp-surface); + } + + .workload-section { + grid-template-columns: 1fr; + } +} + +@media (max-width: 38rem) { + .header-inner, + main, + .site-footer { + width: min(100% - 1.5rem, 72rem); + } + + .header-inner { + min-height: 4.25rem; + } + + .repository-link span { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip-path: inset(50%); + } + + .section-heading { + display: block; + } + + .journey { + grid-template-columns: 1fr; + } + + .journey-step { + display: grid; + grid-template-columns: 3rem minmax(0, 1fr); + column-gap: 1rem; + } + + .step-number { + grid-row: span 2; + margin-bottom: 0; + } + + .metadata-row { + grid-template-columns: 1fr; + gap: 0.25rem; + } + + .metadata-row dd { + text-align: left; + } + + .site-footer { + flex-direction: column; + gap: 0.5rem; + } +} + +@media (prefers-reduced-motion: reduce) { + html { + scroll-behavior: auto; + } + + *, + *::before, + *::after { + transition-duration: 0.01ms !important; + transition-delay: 0ms !important; + } } diff --git a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/dsc/common.dsc.yml b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/dsc/common.dsc.yml index 3521c58ca..9fbe6177c 100644 --- a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/dsc/common.dsc.yml +++ b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/dsc/common.dsc.yml @@ -2,20 +2,6 @@ properties: resources: - - resource: Microsoft.WinGet.DSC/WinGetPackage - id: git - directives: - description: Install Git - settings: - id: Git.Git - source: winget - - resource: Microsoft.WinGet.DSC/WinGetPackage - id: vscode - directives: - description: Install Visual Studio Code - settings: - id: Microsoft.VisualStudioCode - source: winget - resource: Microsoft.WinGet.DSC/WinGetPackage id: AzureCLI directives: @@ -23,20 +9,6 @@ properties: settings: id: Microsoft.AzureCLI source: winget - - resource: Microsoft.WinGet.DSC/WinGetPackage - id: PowerShell7 - directives: - description: Install PowerShell 7 - settings: - id: Microsoft.PowerShell - source: winget - - resource: Microsoft.WinGet.DSC/WinGetPackage - id: kubectl - directives: - description: Install kubectl - settings: - id: Kubernetes.kubectl - source: winget - resource: Microsoft.WinGet.DSC/WinGetPackage id: edge directives: @@ -51,34 +23,6 @@ properties: settings: id: Microsoft.Azure.AZCopy.10 source: winget - - resource: Microsoft.WinGet.DSC/WinGetPackage - id: DotNetSDK8 - directives: - description: Install Microsoft DotNet SDK 8 - settings: - id: Microsoft.DotNet.SDK.8 - source: winget - - resource: Microsoft.WinGet.DSC/WinGetPackage - id: helm - directives: - description: Install Helm - settings: - id: Helm.Helm - source: winget - - resource: Microsoft.WinGet.DSC/WinGetPackage - id: Microsoft.Sysinternals.BGInfo - directives: - description: Install Sysinternals BGInfo - settings: - id: Microsoft.Sysinternals.BGInfo - source: winget - - resource: Microsoft.WinGet.DSC/WinGetPackage - id: OpenSSL - directives: - description: Install OpenSSL - settings: - id: FireDaemon.OpenSSL - source: winget - resource: PSDesiredStateConfiguration/WindowsFeature id: DHCP directives: diff --git a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/dsc/itpro.dsc.yml b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/dsc/itpro.dsc.yml index 616c6d817..e2c05473f 100644 --- a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/dsc/itpro.dsc.yml +++ b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/dsc/itpro.dsc.yml @@ -2,20 +2,6 @@ properties: resources: - - resource: Microsoft.WinGet.DSC/WinGetPackage - id: 7zip - directives: - description: Install 7zip - settings: - id: 7zip.7zip - source: winget - - resource: PSDscResources/WindowsFeature - id: Hyper-V - directives: - description: Install Hyper-V - settings: - Name: Hyper-V - Ensure: Present - resource: HyperVDsc/VMHost id: VMHost directives: diff --git a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/icons/arc.ico b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/icons/arc.ico deleted file mode 100644 index ae32a6486..000000000 Binary files a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/icons/arc.ico and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/icons/arcsql.ico b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/icons/arcsql.ico deleted file mode 100644 index 78bcf5631..000000000 Binary files a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/icons/arcsql.ico and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/icons/bookstore.ico b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/icons/bookstore.ico deleted file mode 100644 index d66629958..000000000 Binary files a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/icons/bookstore.ico and /dev/null differ diff --git a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/powershell/modules/Azure.Arc.Jumpstart.Common/Azure.Arc.Jumpstart.Common.psd1 b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/powershell/modules/Azure.Arc.Jumpstart.Common/Azure.Arc.Jumpstart.Common.psd1 deleted file mode 100644 index 745d3dc77..000000000 --- a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/powershell/modules/Azure.Arc.Jumpstart.Common/Azure.Arc.Jumpstart.Common.psd1 +++ /dev/null @@ -1,131 +0,0 @@ -# -# Module manifest for module 'Azure.Arc.Jumpstart.Common' -# -# Generated by: Azure Arc Jumpstart -# -# Generated on: 22.12.2023 -# - -@{ - -# Script module or binary module file associated with this manifest. -RootModule = 'Azure.Arc.Jumpstart.Common.psm1' - -# Version number of this module. -ModuleVersion = '0.1.0' - -# Supported PSEditions -# CompatiblePSEditions = @() - -# ID used to uniquely identify this module -GUID = 'a0b1c2d3-e4f5-6a7b-8c9d-0e1f2a3b4c5d' - -# Author of this module -Author = 'Azure Arc Jumpstart' - -# Company or vendor of this module -CompanyName = 'Microsoft' - -# Copyright statement for this module -Copyright = '(c) Azure Arc Jumpstart. All rights reserved.' - -# Description of the functionality provided by this module -Description = 'Common functions for Azure Arc Jumpstart' - -# Minimum version of the PowerShell engine required by this module -# PowerShellVersion = '' - -# Name of the PowerShell host required by this module -# PowerShellHostName = '' - -# Minimum version of the PowerShell host required by this module -# PowerShellHostVersion = '' - -# Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. -# DotNetFrameworkVersion = '' - -# Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. -# ClrVersion = '' - -# Processor architecture (None, X86, Amd64) required by this module -# ProcessorArchitecture = '' - -# Modules that must be imported into the global environment prior to importing this module -# RequiredModules = @() - -# Assemblies that must be loaded prior to importing this module -# RequiredAssemblies = @() - -# Script files (.ps1) that are run in the caller's environment prior to importing this module. -# ScriptsToProcess = @() - -# Type files (.ps1xml) to be loaded when importing this module -# TypesToProcess = @() - -# Format files (.ps1xml) to be loaded when importing this module -# FormatsToProcess = @() - -# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess -# NestedModules = @() - -# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. -FunctionsToExport = 'Set-JSDesktopBackground','Convert-JSImageToBitMap','Show-K8sPodStatus','Deploy-Workbook','Invoke-JSSudoCommand' - -# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. -CmdletsToExport = '' - -# Variables to export from this module -VariablesToExport = '' - -# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. -AliasesToExport = '' - -# DSC resources to export from this module -# DscResourcesToExport = @() - -# List of all modules packaged with this module -# ModuleList = @() - -# List of all files packaged with this module -# FileList = @() - -# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. -PrivateData = @{ - - PSData = @{ - - # Tags applied to this module. These help with module discovery in online galleries. - # Tags = @() - - # A URL to the license for this module. - # LicenseUri = '' - - # A URL to the main website for this project. - # ProjectUri = '' - - # A URL to an icon representing this module. - # IconUri = '' - - # ReleaseNotes of this module - # ReleaseNotes = '' - - # Prerelease string of this module - # Prerelease = '' - - # Flag to indicate whether the module requires explicit user acceptance for install/update/save - # RequireLicenseAcceptance = $false - - # External dependent modules of this module - # ExternalModuleDependencies = @() - - } # End of PSData hashtable - -} # End of PrivateData hashtable - -# HelpInfo URI of this module -# HelpInfoURI = '' - -# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. -# DefaultCommandPrefix = '' - -} diff --git a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/powershell/modules/Azure.Arc.Jumpstart.Common/Azure.Arc.Jumpstart.Common.psm1 b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/powershell/modules/Azure.Arc.Jumpstart.Common/Azure.Arc.Jumpstart.Common.psm1 deleted file mode 100644 index cd5bbdda4..000000000 --- a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/powershell/modules/Azure.Arc.Jumpstart.Common/Azure.Arc.Jumpstart.Common.psm1 +++ /dev/null @@ -1,12 +0,0 @@ -# Load functions from module subfolder -$ModuleRoot = Split-Path -Path $MyInvocation.MyCommand.Path - -Resolve-Path "$ModuleRoot\Functions\Public\*.ps1" | ForEach-Object -Process { - . $_.ProviderPath -} - -<# -Resolve-Path "$ModuleRoot\Functions\Private\*.ps1" | ForEach-Object -Process { - . $_.ProviderPath -} -#> \ No newline at end of file diff --git a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/powershell/modules/Azure.Arc.Jumpstart.Common/Functions/Public/Convert-JSImageToBitMap.ps1 b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/powershell/modules/Azure.Arc.Jumpstart.Common/Functions/Public/Convert-JSImageToBitMap.ps1 deleted file mode 100644 index bbcf65126..000000000 --- a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/powershell/modules/Azure.Arc.Jumpstart.Common/Functions/Public/Convert-JSImageToBitMap.ps1 +++ /dev/null @@ -1,10 +0,0 @@ -function Convert-JSImageToBitMap { - param ( - $SourceFilePath, - $DestinationFilePath - ) - [Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") | Out-Null - $file = Get-Item $SourceFilePath - $convertfile = new-object System.Drawing.Bitmap($file.Fullname) - $convertfile.Save($DestinationFilePath, "bmp") -} \ No newline at end of file diff --git a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/powershell/modules/Azure.Arc.Jumpstart.Common/Functions/Public/Deploy-Workbook.ps1 b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/powershell/modules/Azure.Arc.Jumpstart.Common/Functions/Public/Deploy-Workbook.ps1 deleted file mode 100644 index 2ee27e66b..000000000 --- a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/powershell/modules/Azure.Arc.Jumpstart.Common/Functions/Public/Deploy-Workbook.ps1 +++ /dev/null @@ -1,27 +0,0 @@ -function Deploy-Workbook { - param( - [string]$MonitoringDir, - [string]$workbookFileName - ) - - Write-Host "[$(Get-Date -Format t)] INFO: Deploying Azure Workbook $workbookFileName." - Write-Host "`n" - $workbookTemplateFilePath = "$MonitoringDir\$workbookFileName" - # Read the content of the workbook template-file - $content = Get-Content -Path $workbookTemplateFilePath -Raw - # Replace placeholders with actual values - $updatedContent = $content -replace 'rg-placeholder', $env:resourceGroup - $updatedContent = $updatedContent -replace '/subscriptions/00000000-0000-0000-0000-000000000000', "/subscriptions/$($env:subscriptionId)" - $updatedContent = $updatedContent -replace "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/$env:resourceGroup/providers/Microsoft.OperationalInsights/workspaces/xxxx", "/subscriptions/$($env:subscriptionId)/resourceGroups/$($env:resourceGroup)/providers/Microsoft.OperationalInsights/workspaces/$($env:workspaceName)" - - # Write the updated content back to the file - Set-Content -Path $workbookTemplateFilePath -Value $updatedContent - - # Deploy the workbook - try { - New-AzResourceGroupDeployment -ResourceGroupName $Env:resourceGroup -TemplateFile $workbookTemplateFilePath -ErrorAction Stop - Write-Host "[$(Get-Date -Format t)] INFO: Deployment of template-file $workbookTemplateFilePath succeeded." - } catch { - Write-Error "[$(Get-Date -Format t)] ERROR: Deployment of template-file $workbookTemplateFilePath failed. Error details: $PSItem.Exception.Message" - } -} \ No newline at end of file diff --git a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/powershell/modules/Azure.Arc.Jumpstart.Common/Functions/Public/Invoke-JSSudoCommand.ps1 b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/powershell/modules/Azure.Arc.Jumpstart.Common/Functions/Public/Invoke-JSSudoCommand.ps1 deleted file mode 100644 index 4e34ea7b2..000000000 --- a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/powershell/modules/Azure.Arc.Jumpstart.Common/Functions/Public/Invoke-JSSudoCommand.ps1 +++ /dev/null @@ -1,24 +0,0 @@ -function Invoke-JSSudoCommand { - <# - .SYNOPSIS - Invokes sudo command in a remote session to Linux - #> - param ( - [Parameter(Mandatory=$true)] - $Session, - - [Parameter(Mandatory=$true)] - [String] - $Command - ) - Invoke-Command -Session $Session { - $errFile = "/tmp/$($(New-Guid).Guid).err" - Invoke-Expression "sudo ${using:Command} 2>${errFile}" - $err = Get-Content $errFile -ErrorAction SilentlyContinue - Remove-Item $errFile -ErrorAction SilentlyContinue - If (-Not $null -eq $err) - { - $err | Out-String | Write-Warning - } - } - } \ No newline at end of file diff --git a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/powershell/modules/Azure.Arc.Jumpstart.Common/Functions/Public/Set-JSDesktopBackground.ps1 b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/powershell/modules/Azure.Arc.Jumpstart.Common/Functions/Public/Set-JSDesktopBackground.ps1 deleted file mode 100644 index 82009e48b..000000000 --- a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/powershell/modules/Azure.Arc.Jumpstart.Common/Functions/Public/Set-JSDesktopBackground.ps1 +++ /dev/null @@ -1,26 +0,0 @@ -function Set-JSDesktopBackground { - [CmdletBinding()] - param ( - [Parameter(Mandatory = $true)] - [string]$ImagePath - ) - -$code = @' - using System.Runtime.InteropServices; - namespace Win32{ - - public class Wallpaper{ - [DllImport("user32.dll", CharSet=CharSet.Auto)] - static extern int SystemParametersInfo (int uAction , int uParam , string lpvParam , int fuWinIni) ; - - public static void SetWallpaper(string thePath){ - SystemParametersInfo(20,0,thePath,3); - } - } - } -'@ - -Add-Type $code -[Win32.Wallpaper]::SetWallpaper($ImagePath) - -} \ No newline at end of file diff --git a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/powershell/modules/Azure.Arc.Jumpstart.Common/Functions/Public/Show-K8sPodStatus.ps1 b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/powershell/modules/Azure.Arc.Jumpstart.Common/Functions/Public/Show-K8sPodStatus.ps1 deleted file mode 100644 index 28aa2ee87..000000000 --- a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/artifacts/powershell/modules/Azure.Arc.Jumpstart.Common/Functions/Public/Show-K8sPodStatus.ps1 +++ /dev/null @@ -1,13 +0,0 @@ -function Show-K8sPodStatus { -param ( - [string]$kubeconfig, - [string]$clusterName -) - -while ($true) { - Write-Host "Status for $clusterName at $(Get-Date)" -ForegroundColor Green - kubectl get pods -n arc --kubeconfig $kubeconfig - Start-Sleep -Seconds 5 - Clear-Host -} -} \ No newline at end of file diff --git a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/bicep/clientVm/clientVm.bicep b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/bicep/clientVm/clientVm.bicep index c72637712..c59f3489d 100644 --- a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/bicep/clientVm/clientVm.bicep +++ b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/bicep/clientVm/clientVm.bicep @@ -225,8 +225,8 @@ resource vm 'Microsoft.Compute/virtualMachines@2024-07-01' = { fileUris: [ uri(templateBaseUrl, 'artifacts/Bootstrap.ps1') ] - //commandToExecute: 'powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File Bootstrap.ps1 -adminUsername ${windowsAdminUsername} -tenantId ${tenantId} -spnAuthority ${spnAuthority} -subscriptionId ${subscription().subscriptionId} -resourceGroup ${resourceGroup().name} -acceptEula ${acceptEula} -registryUsername ${registryUsername} -azureLocation ${location} -templateBaseUrl ${templateBaseUrl} -flavor ${flavor} -githubUser ${githubUser} -githubBranch ${githubBranch} -vmAutologon ${vmAutologon} -rdpPort ${rdpPort} -namingPrefix ${namingPrefix} -debugEnabled ${debugEnabled} -sqlServerEdition ${sqlServerEdition} -autoShutdownEnabled ${autoShutdownEnabled}' - commandToExecute: 'powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File Bootstrap.ps1 -adminUsername ${windowsAdminUsername} -tenantId ${tenantId} -spnAuthority ${spnAuthority} -subscriptionId ${subscription().subscriptionId} -resourceGroup ${resourceGroup().name} -acceptEula ${acceptEula} -registryUsername ${registryUsername} -azureLocation ${location} -templateBaseUrl ${templateBaseUrl} -flavor ${flavor} -githubUser ${githubUser} -githubBranch ${githubBranch} -vmAutologon ${vmAutologon} -rdpPort ${rdpPort} -namingPrefix "MHBox" -debugEnabled ${debugEnabled} -sqlServerEdition ${sqlServerEdition} -autoShutdownEnabled ${autoShutdownEnabled}' + //commandToExecute: 'powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File Bootstrap.ps1 -adminUsername ${windowsAdminUsername} -tenantId ${tenantId} -spnAuthority ${spnAuthority} -subscriptionId ${subscription().subscriptionId} -resourceGroup ${resourceGroup().name} -acceptEula ${acceptEula} -registryUsername ${registryUsername} -azureLocation ${location} -templateBaseUrl "${templateBaseUrl}" -flavor ${flavor} -githubUser ${githubUser} -githubBranch "${githubBranch}" -vmAutologon ${vmAutologon} -rdpPort ${rdpPort} -namingPrefix ${namingPrefix} -debugEnabled ${debugEnabled} -sqlServerEdition ${sqlServerEdition} -autoShutdownEnabled ${autoShutdownEnabled}' + commandToExecute: 'powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File Bootstrap.ps1 -adminUsername ${windowsAdminUsername} -tenantId ${tenantId} -spnAuthority ${spnAuthority} -subscriptionId ${subscription().subscriptionId} -resourceGroup ${resourceGroup().name} -acceptEula ${acceptEula} -registryUsername ${registryUsername} -azureLocation ${location} -templateBaseUrl "${templateBaseUrl}" -flavor ${flavor} -githubUser ${githubUser} -githubBranch "${githubBranch}" -vmAutologon ${vmAutologon} -rdpPort ${rdpPort} -namingPrefix "MHBox" -debugEnabled ${debugEnabled} -sqlServerEdition ${sqlServerEdition} -autoShutdownEnabled ${autoShutdownEnabled}' } } } diff --git a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/bicep/main.bicep b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/bicep/main.bicep index 455173f09..60c446419 100644 --- a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/bicep/main.bicep +++ b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/bicep/main.bicep @@ -80,7 +80,7 @@ param enableAzureSpotPricing bool = false ]) param zones string = '1' -var templateBaseUrl = 'https://raw.githubusercontent.com/${githubAccount}/microhack/${githubBranch}/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/' +var templateBaseUrl = 'https://raw.githubusercontent.com/${githubAccount}/MicroHack/${githubBranch}/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources/' var userName = contains(deployer().userPrincipalName, '@') ? substring(deployer().userPrincipalName, 0, indexOf(deployer().userPrincipalName, '@')) : deployer().userPrincipalName diff --git a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-01/img/CS13.png b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-01/img/CS13.png index 909a8b727..63d633efe 100644 Binary files a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-01/img/CS13.png and b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-01/img/CS13.png differ diff --git a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-01/solution-01.md b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-01/solution-01.md index e2d966680..f81c7fff4 100644 --- a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-01/solution-01.md +++ b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-01/solution-01.md @@ -38,6 +38,9 @@ Duration: 30 minutes git clone https://github.com/microsoft/MicroHack.git ``` +> [!NOTE] +> Contributors testing before merge must clone or check out their fork's feature branch locally so the Bicep files match the automation source selected below. + ![image](./img/CS4.png) - Change into the Hack's resources directory in the cloned repository using the command: @@ -47,13 +50,39 @@ cd MicroHack/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resource ``` > [!IMPORTANT] -> **Please update the REGION variable with your desired region - defaulting to swedencentral. Please update the USERPASSWORD parameter with your desired password.** +> **Update the `REGION` variable with your desired region (the example defaults to `swedencentral`) and set `USERPASSWORD` to your desired password. The GitHub owner and branch default to the official `microsoft/MicroHack` `main` branch.** + +- Set the deployment values. The defaults use the official repository. To test before merge, replace `microsoft` with your `` and `main` with your ``. -- Execute the following commands to set the target region and your desired password and start the deployment: ```bash REGION="swedencentral" USERPASSWORD="" -az deployment sub create --name "$(az ad signed-in-user show --query userPrincipalName -o tsv | cut -d "@" -f 1 | tr '[:upper:]' '[:lower:]')-MHBox" --location $REGION --template-file ./main.bicep --parameters windowsAdminPassword=$USERPASSWORD +GITHUB_ACCOUNT="microsoft" +GITHUB_BRANCH="main" +``` + +> [!NOTE] +> The local `main.bicep` comes from your checked-out clone. `GITHUB_ACCOUNT` and `GITHUB_BRANCH` select the repository version used for all automation and demo-page assets downloaded during deployment. + +- Verify that the selected repository branch contains a required deployment artifact before starting the costly Azure deployment: + +```bash +ARTIFACT_BASE_URL="https://raw.githubusercontent.com/${GITHUB_ACCOUNT}/MicroHack/${GITHUB_BRANCH}/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/resources" +curl --fail --silent --show-error --head \ + "${ARTIFACT_BASE_URL}/artifacts/Bootstrap.ps1" >/dev/null +``` + +- Start the deployment: + +```bash +az deployment sub create \ + --name "$(az ad signed-in-user show --query userPrincipalName -o tsv | cut -d "@" -f 1 | tr '[:upper:]' '[:lower:]')-MHBox" \ + --location "$REGION" \ + --template-file ./main.bicep \ + --parameters \ + windowsAdminPassword="$USERPASSWORD" \ + githubAccount="$GITHUB_ACCOUNT" \ + githubBranch="$GITHUB_BRANCH" ``` ![image](./img/CS5.png) @@ -105,7 +134,14 @@ Password: JS123!! ![image](./img/CS12.png) -You can also open Microsoft Edge and navigate to the IP address of the *MHBox-Win2K22* or *MHBox-Ubuntu-01* system to view the deployed Demo Web App. +Open Microsoft Edge and navigate to the IP address of each web VM. Confirm that the migration journey dashboard returns HTTP success and shows the correct details: + +| VM | Platform | Web server | +| --- | --- | --- | +| *MHBox-Win2K22* | `Windows Server 2022` | `IIS` | +| *MHBox-Ubuntu-01* | `Ubuntu Linux` | `Apache` | + +The **Hostname** value must match the VM you opened. ![image](./img/CS13.png) diff --git a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-05/solution-05.md b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-05/solution-05.md index dcaeeeec9..3df186648 100644 --- a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-05/solution-05.md +++ b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-05/solution-05.md @@ -152,7 +152,7 @@ Connect to each test VM via Azure Bastion. ![image](./img/TestMig5.png) -On the Windows VM, open a browser and navigate to *http://localhost*. On the Ubuntu VM, run `curl -I http://localhost`. Confirm that the Hack Demo Web App responds successfully on both test VMs. +On the Windows VM, open a browser and navigate to *http://localhost*. Confirm that the dashboard shows the VM hostname, `Windows Server 2022`, and `IIS`. On the Ubuntu VM, run `curl -I http://localhost`, then open the page and confirm that it shows the VM hostname, `Ubuntu Linux`, and `Apache`. Both requests must return HTTP success. ![image](./img/TestMig6.png) diff --git a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-08/solution-08.md b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-08/solution-08.md index ab44bf714..4d69b0ae9 100644 --- a/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-08/solution-08.md +++ b/03-Azure/01-03-Infrastructure/06_Migration_Secure_AI_Ready/walkthrough/challenge-08/solution-08.md @@ -77,7 +77,7 @@ Review enabled virtual hosts/modules and any dependency matches. A text match is | Content | Static HTML, CSS, and images | Static HTML, CSS, and images | | External dependencies | None required to render the page | None required to render the page | | Persistent/session state | None | None | -| Machine-specific value | VM hostname in `index.html` | VM hostname in `index.html` | +| Machine-specific values | Hostname, platform, and web server in stable `data-workload-value` fields | Hostname, platform, and web server in stable `data-workload-value` fields | The source is a static artifact. A Windows App Service worker can host it regardless of whether it came from Windows/IIS or Linux/Apache. @@ -89,15 +89,23 @@ The source is a static artifact. A Windows App Service worker can host it regard $siteRoot = 'C:\inetpub\wwwroot' $indexPath = Join-Path $siteRoot 'index.html' $html = Get-Content -Path $indexPath -Raw -$html = $html -replace 'Current Hostname:\s*[^<]+', 'Hosting platform: Azure App Service' +$html = $html -replace '(]*\bdata-workload-value="hostname"[^>]*>)[^<]*()', '${1}Azure App Service${2}' +$html = $html -replace '(]*\bdata-workload-value="platform"[^>]*>)[^<]*()', '${1}Managed PaaS${2}' +$html = $html -replace '(]*\bdata-workload-value="web-server"[^>]*>)[^<]*()', '${1}Azure App Service${2}' Set-Content -Path $indexPath -Value $html -Encoding UTF8 $updatedHtml = Get-Content -Path $indexPath -Raw -if ($updatedHtml -match [regex]::Escape($env:COMPUTERNAME)) { - throw 'The VM-specific hostname is still present in index.html.' +$expectedMetadata = [ordered]@{ + hostname = 'Azure App Service' + platform = 'Managed PaaS' + 'web-server' = 'Azure App Service' } -if ($updatedHtml -notmatch 'Hosting platform: Azure App Service') { - throw 'The platform-neutral hosting value was not written.' +foreach ($field in $expectedMetadata.Keys) { + $pattern = 'data-workload-value="' + [regex]::Escape($field) + + '"[^>]*>\s*' + [regex]::Escape($expectedMetadata[$field]) + '\s*' + if ($updatedHtml -notmatch $pattern) { + throw "Expected App Service metadata was not written: $field" + } } $packageDirectory = 'C:\temp' @@ -136,17 +144,24 @@ sudo cp -a "${site_root}/." "${staging_dir}/" sudo chown -R "$(id -u):$(id -g)" "${staging_dir}" sed -E -i \ - 's/Current Hostname:[^<]*/Hosting platform: Azure App Service/' \ + -e 's#(]*data-workload-value="hostname"[^>]*>)[^<]*()#\1Azure App Service\2#' \ + -e 's#(]*data-workload-value="platform"[^>]*>)[^<]*()#\1Managed PaaS\2#' \ + -e 's#(]*data-workload-value="web-server"[^>]*>)[^<]*()#\1Azure App Service\2#' \ "${staging_dir}/index.html" -if grep -Fq "$(hostname)" "${staging_dir}/index.html"; then - echo "The VM-specific hostname is still present in index.html." >&2 - rm -rf "${staging_dir}" - exit 1 -fi - -grep -F "Hosting platform: Azure App Service" \ - "${staging_dir}/index.html" +while IFS='|' read -r field expected_value; do + grep -Eq \ + "data-workload-value=\"${field}\"[^>]*>${expected_value}" \ + "${staging_dir}/index.html" || { + echo "Expected App Service metadata was not written: ${field}" >&2 + rm -rf "${staging_dir}" + exit 1 + } +done <<'EOF' +hostname|Azure App Service +platform|Managed PaaS +web-server|Azure App Service +EOF if ! command -v zip >/dev/null 2>&1 || ! command -v unzip >/dev/null 2>&1; then @@ -342,9 +357,9 @@ $appName = '' $appUrl = "https://$appName.azurewebsites.net" $home = Invoke-WebRequest -Uri "$appUrl/" -UseBasicParsing -TimeoutSec 30 $home.StatusCode -$home.Content -match 'Hosting platform: Azure App Service' +$home.Content -match 'data-workload-value="platform"[^>]*>Managed PaaS' -$assets = @('stylesheet.css', 'MSLogo.png', 'MSicon.png') +$assets = @('stylesheet.css', 'GitHub_Logo.png', 'MSLogo.png', 'MSicon.png') foreach ($asset in $assets) { $response = Invoke-WebRequest -Uri "$appUrl/$asset" -UseBasicParsing -TimeoutSec 30 [pscustomobject]@{ @@ -371,9 +386,9 @@ Start-Service -Name W3SVC app_url="https://${app_name}.azurewebsites.net" curl --fail --silent --show-error "${app_url}/" | - grep -F "Hosting platform: Azure App Service" + grep -E 'data-workload-value="platform"[^>]*>Managed PaaS' -for asset in stylesheet.css MSLogo.png MSicon.png; do +for asset in stylesheet.css GitHub_Logo.png MSLogo.png MSicon.png; do curl --fail --silent --show-error --output /dev/null \ --write-out "${asset}: HTTP %{http_code}, %{size_download} bytes\n" \ "${app_url}/${asset}"