Consolidated ASP Classic MVC framework from best components
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

403 lignes
13KB

  1. <#
  2. .SYNOPSIS
  3. Installs, validates, switches, or rolls back an immutable IIS release.
  4. .DESCRIPTION
  5. Run this script in an elevated Windows PowerShell 5.1 session on the IIS host.
  6. A release contains the full repository, while IIS is pointed only at its public
  7. directory. The current site's public\web.config is captured once in shared\
  8. and copied into every new release so host-specific values are not overwritten.
  9. No database migration runs unless -RunMigrations is explicitly supplied.
  10. #>
  11. [CmdletBinding(DefaultParameterSetName = 'Deploy')]
  12. param(
  13. [Parameter(Mandatory = $true)]
  14. [ValidatePattern('^[A-Za-z0-9_. -]+$')]
  15. [string]$SiteName,
  16. [Parameter(ParameterSetName = 'Deploy')]
  17. [string]$PackagePath = '',
  18. [Parameter(ParameterSetName = 'Deploy')]
  19. [ValidatePattern('^[A-Za-z0-9._-]+$')]
  20. [string]$ReleaseId = (Get-Date -Format 'yyyyMMdd-HHmmss'),
  21. [Parameter(Mandatory = $true, ParameterSetName = 'Rollback')]
  22. [ValidatePattern('^[A-Za-z0-9._-]+$')]
  23. [string]$RollbackTo,
  24. [string]$DeployRoot = '',
  25. [string]$BaseUrl = '',
  26. [ValidateRange(2, 100)]
  27. [int]$KeepReleases = 5,
  28. [string]$ExpectedSha256 = '',
  29. [switch]$RunMigrations,
  30. [switch]$SkipSmokeTest,
  31. [switch]$PreflightOnly,
  32. [switch]$DryRun
  33. )
  34. Set-StrictMode -Version 2.0
  35. $ErrorActionPreference = 'Stop'
  36. function Write-Step {
  37. param([string]$Message)
  38. Write-Host ('==> ' + $Message)
  39. }
  40. function Invoke-Change {
  41. param(
  42. [string]$Description,
  43. [scriptblock]$Action
  44. )
  45. if ($DryRun) {
  46. Write-Host ('DRY-RUN: ' + $Description)
  47. return
  48. }
  49. Write-Step $Description
  50. & $Action
  51. }
  52. function Get-NormalizedPath {
  53. param([string]$Path)
  54. return [System.IO.Path]::GetFullPath([Environment]::ExpandEnvironmentVariables($Path)).TrimEnd('\')
  55. }
  56. function Assert-Administrator {
  57. $identity = [Security.Principal.WindowsIdentity]::GetCurrent()
  58. $principal = New-Object Security.Principal.WindowsPrincipal($identity)
  59. if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
  60. throw 'An elevated Administrator PowerShell session is required.'
  61. }
  62. }
  63. function Assert-ReleaseLayout {
  64. param([string]$ReleasePath)
  65. $required = @(
  66. 'public\Default.asp',
  67. 'public\web.config',
  68. 'core\autoload_core.asp',
  69. 'app\controllers\autoload_controllers.asp'
  70. )
  71. foreach ($relativePath in $required) {
  72. if (-not (Test-Path -LiteralPath (Join-Path $ReleasePath $relativePath) -PathType Leaf)) {
  73. throw "Release is incomplete; missing $relativePath"
  74. }
  75. }
  76. try {
  77. [xml](Get-Content -LiteralPath (Join-Path $ReleasePath 'public\web.config') -Raw) | Out-Null
  78. } catch {
  79. throw "Release public\web.config is not valid XML: $($_.Exception.Message)"
  80. }
  81. }
  82. function Get-LocalBaseUrl {
  83. param($Site)
  84. $binding = $Site.Bindings.Collection |
  85. Where-Object { $_.protocol -eq 'http' } |
  86. Select-Object -First 1
  87. if ($null -eq $binding) {
  88. return ''
  89. }
  90. $parts = $binding.bindingInformation.Split(':')
  91. $port = $parts[1]
  92. if ([string]::IsNullOrWhiteSpace($port)) {
  93. $port = '80'
  94. }
  95. return 'http://127.0.0.1:' + $port
  96. }
  97. function Set-IisRelease {
  98. param(
  99. [string]$PhysicalPath,
  100. [string]$PoolName
  101. )
  102. Set-ItemProperty -Path ('IIS:\Sites\' + $SiteName) -Name physicalPath -Value $PhysicalPath
  103. $poolState = (Get-WebAppPoolState -Name $PoolName).Value
  104. if ($poolState -eq 'Started') {
  105. Restart-WebAppPool -Name $PoolName
  106. } else {
  107. Start-WebAppPool -Name $PoolName
  108. }
  109. }
  110. function Invoke-SmokeTest {
  111. param([string]$Url)
  112. if ($SkipSmokeTest) {
  113. Write-Step 'Smoke test skipped by explicit request'
  114. return
  115. }
  116. if ([string]::IsNullOrWhiteSpace($Url)) {
  117. throw 'No HTTP binding was found. Supply -BaseUrl or use -SkipSmokeTest explicitly.'
  118. }
  119. $target = $Url.TrimEnd('/') + '/'
  120. Write-Step ('Smoke testing ' + $target)
  121. $response = Invoke-WebRequest -UseBasicParsing -Uri $target -TimeoutSec 30
  122. if ($response.StatusCode -lt 200 -or $response.StatusCode -ge 400) {
  123. throw "Smoke test returned HTTP $($response.StatusCode)"
  124. }
  125. Write-Host ('Smoke test returned HTTP ' + $response.StatusCode)
  126. }
  127. if ($env:OS -ne 'Windows_NT') {
  128. throw 'This script must run on Windows.'
  129. }
  130. Assert-Administrator
  131. Import-Module WebAdministration -ErrorAction Stop
  132. $site = Get-Website -Name $SiteName -ErrorAction Stop
  133. if ($null -eq $site) {
  134. throw "IIS site not found: $SiteName"
  135. }
  136. $appPool = $site.applicationPool
  137. if ([string]::IsNullOrWhiteSpace($appPool)) {
  138. throw "IIS site $SiteName has no application pool."
  139. }
  140. if ([string]::IsNullOrWhiteSpace($DeployRoot)) {
  141. $DeployRoot = Join-Path $env:SystemDrive ('inetpub\deployments\' + $SiteName)
  142. }
  143. $DeployRoot = Get-NormalizedPath $DeployRoot
  144. $releasesRoot = Join-Path $DeployRoot 'releases'
  145. $sharedRoot = Join-Path $DeployRoot 'shared'
  146. $sharedConfig = Join-Path $sharedRoot 'public.web.config'
  147. $statePath = Join-Path $DeployRoot 'deployment-state.json'
  148. $currentPhysicalPath = Get-NormalizedPath $site.physicalPath
  149. $currentConfig = Join-Path $currentPhysicalPath 'web.config'
  150. Write-Step "Site: $SiteName"
  151. Write-Host "App pool: $appPool"
  152. Write-Host "Current physicalPath: $currentPhysicalPath"
  153. Write-Host "Deployment root: $DeployRoot"
  154. Write-Host 'Classic ASP parent paths will be set at the site location in applicationHost.config.'
  155. Write-Host 'Database migrations are disabled unless -RunMigrations is supplied.'
  156. if ($PSCmdlet.ParameterSetName -eq 'Deploy' -and
  157. (-not $PreflightOnly) -and
  158. (-not $DryRun) -and
  159. [string]::IsNullOrWhiteSpace($PackagePath)) {
  160. throw '-PackagePath is required for a deployment.'
  161. }
  162. if (-not [string]::IsNullOrWhiteSpace($PackagePath)) {
  163. $PackagePath = Get-NormalizedPath $PackagePath
  164. if (-not (Test-Path -LiteralPath $PackagePath -PathType Leaf)) {
  165. throw "Package not found: $PackagePath"
  166. }
  167. if ([System.IO.Path]::GetExtension($PackagePath) -ne '.zip') {
  168. throw 'PackagePath must name a .zip release package.'
  169. }
  170. if (-not [string]::IsNullOrWhiteSpace($ExpectedSha256)) {
  171. $actualHash = (Get-FileHash -LiteralPath $PackagePath -Algorithm SHA256).Hash
  172. if ($actualHash -ne $ExpectedSha256) {
  173. throw "Package SHA-256 mismatch. Expected $ExpectedSha256; got $actualHash"
  174. }
  175. Write-Host ('Package SHA-256 verified: ' + $actualHash)
  176. }
  177. }
  178. if ((-not (Test-Path -LiteralPath $sharedConfig -PathType Leaf)) -and
  179. (-not (Test-Path -LiteralPath $currentConfig -PathType Leaf))) {
  180. throw "Cannot preserve machine configuration: neither $sharedConfig nor $currentConfig exists."
  181. }
  182. $getWindowsFeature = Get-Command Get-WindowsFeature -ErrorAction SilentlyContinue
  183. $getOptionalFeature = Get-Command Get-WindowsOptionalFeature -ErrorAction SilentlyContinue
  184. if ($null -ne $getWindowsFeature) {
  185. $aspFeature = Get-WindowsFeature -Name Web-ASP
  186. if ($null -eq $aspFeature -or -not $aspFeature.Installed) {
  187. throw 'The IIS Classic ASP feature (Web-ASP) is not installed.'
  188. }
  189. } elseif ($null -ne $getOptionalFeature) {
  190. $aspFeature = Get-WindowsOptionalFeature -Online -FeatureName IIS-ASP -ErrorAction SilentlyContinue
  191. if ($null -ne $aspFeature -and $aspFeature.State -ne 'Enabled') {
  192. throw 'The IIS-ASP Windows feature is not enabled.'
  193. }
  194. } else {
  195. Write-Warning 'No Windows feature-query cmdlet is available; Classic ASP feature state could not be preflighted.'
  196. }
  197. $rewriteModule = Get-WebGlobalModule -Name RewriteModule -ErrorAction SilentlyContinue
  198. if ($null -eq $rewriteModule) {
  199. throw 'IIS URL Rewrite is not installed (RewriteModule was not found).'
  200. }
  201. if ($PreflightOnly -or $DryRun) {
  202. Write-Step 'Preflight passed; no IIS or filesystem changes were made'
  203. exit 0
  204. }
  205. Invoke-Change "Create deployment directories under $DeployRoot" {
  206. New-Item -ItemType Directory -Force -Path $releasesRoot, $sharedRoot | Out-Null
  207. }
  208. if (-not (Test-Path -LiteralPath $sharedConfig -PathType Leaf)) {
  209. Invoke-Change "Capture machine-specific configuration from $currentConfig" {
  210. Copy-Item -LiteralPath $currentConfig -Destination $sharedConfig -Force
  211. }
  212. }
  213. try {
  214. [xml](Get-Content -LiteralPath $sharedConfig -Raw) | Out-Null
  215. } catch {
  216. throw "Preserved configuration is not valid XML: $($_.Exception.Message)"
  217. }
  218. Invoke-Change 'Enable Classic ASP parent paths explicitly for this IIS site' {
  219. Set-WebConfigurationProperty `
  220. -PSPath 'MACHINE/WEBROOT/APPHOST' `
  221. -Location $SiteName `
  222. -Filter 'system.webServer/asp' `
  223. -Name 'enableParentPaths' `
  224. -Value $true
  225. }
  226. if ($PSCmdlet.ParameterSetName -eq 'Rollback') {
  227. $rollbackRoot = Get-NormalizedPath (Join-Path $releasesRoot $RollbackTo)
  228. $expectedPrefix = $releasesRoot.TrimEnd('\') + '\'
  229. if (-not $rollbackRoot.StartsWith($expectedPrefix, [StringComparison]::OrdinalIgnoreCase)) {
  230. throw 'Rollback target escaped the releases directory.'
  231. }
  232. Assert-ReleaseLayout -ReleasePath $rollbackRoot
  233. $rollbackPublic = Join-Path $rollbackRoot 'public'
  234. Invoke-Change "Refresh preserved web.config in rollback release $RollbackTo" {
  235. Copy-Item -LiteralPath $sharedConfig -Destination (Join-Path $rollbackPublic 'web.config') -Force
  236. }
  237. $oldPath = $currentPhysicalPath
  238. try {
  239. Invoke-Change "Switch IIS physicalPath to rollback release $rollbackPublic" {
  240. Set-IisRelease -PhysicalPath $rollbackPublic -PoolName $appPool
  241. }
  242. if ([string]::IsNullOrWhiteSpace($BaseUrl)) {
  243. $BaseUrl = Get-LocalBaseUrl -Site $site
  244. }
  245. Invoke-SmokeTest -Url $BaseUrl
  246. } catch {
  247. Write-Warning "Rollback smoke test failed; restoring $oldPath"
  248. Set-IisRelease -PhysicalPath $oldPath -PoolName $appPool
  249. throw
  250. }
  251. $rollbackState = [ordered]@{
  252. siteName = $SiteName
  253. currentRelease = $RollbackTo
  254. currentPhysicalPath = $rollbackPublic
  255. previousPhysicalPath = $oldPath
  256. switchedAtUtc = (Get-Date).ToUniversalTime().ToString('o')
  257. operation = 'rollback'
  258. }
  259. $rollbackState | ConvertTo-Json | Set-Content -LiteralPath $statePath -Encoding UTF8
  260. Write-Step "Rollback complete: $RollbackTo"
  261. exit 0
  262. }
  263. $releaseRoot = Join-Path $releasesRoot $ReleaseId
  264. $stagingRoot = $releaseRoot + '.staging'
  265. if ((Test-Path -LiteralPath $releaseRoot) -or (Test-Path -LiteralPath $stagingRoot)) {
  266. throw "Release already exists: $ReleaseId"
  267. }
  268. try {
  269. Invoke-Change "Extract package into staging directory $stagingRoot" {
  270. New-Item -ItemType Directory -Force -Path $stagingRoot | Out-Null
  271. Expand-Archive -LiteralPath $PackagePath -DestinationPath $stagingRoot -Force
  272. }
  273. Assert-ReleaseLayout -ReleasePath $stagingRoot
  274. Invoke-Change 'Overlay the preserved machine-specific public\web.config' {
  275. Copy-Item -LiteralPath $sharedConfig -Destination (Join-Path $stagingRoot 'public\web.config') -Force
  276. }
  277. Assert-ReleaseLayout -ReleasePath $stagingRoot
  278. if ($RunMigrations) {
  279. $migrationScript = Join-Path $stagingRoot 'scripts\runMigrations.vbs'
  280. if (-not (Test-Path -LiteralPath $migrationScript -PathType Leaf)) {
  281. throw "Migration script not found: $migrationScript"
  282. }
  283. Write-Warning 'Running production migrations by explicit request. IIS rollback will not undo database changes.'
  284. Push-Location $stagingRoot
  285. try {
  286. & cscript.exe //nologo $migrationScript up
  287. if ($LASTEXITCODE -ne 0) {
  288. throw "Migration command exited with code $LASTEXITCODE"
  289. }
  290. } finally {
  291. Pop-Location
  292. }
  293. }
  294. Invoke-Change "Promote staging directory to immutable release $releaseRoot" {
  295. Move-Item -LiteralPath $stagingRoot -Destination $releaseRoot
  296. }
  297. $newPublic = Join-Path $releaseRoot 'public'
  298. $oldPath = $currentPhysicalPath
  299. try {
  300. Invoke-Change "Atomically switch IIS physicalPath to $newPublic" {
  301. Set-IisRelease -PhysicalPath $newPublic -PoolName $appPool
  302. }
  303. if ([string]::IsNullOrWhiteSpace($BaseUrl)) {
  304. $BaseUrl = Get-LocalBaseUrl -Site $site
  305. }
  306. Invoke-SmokeTest -Url $BaseUrl
  307. } catch {
  308. Write-Warning "Deployment smoke test failed; restoring $oldPath"
  309. Set-IisRelease -PhysicalPath $oldPath -PoolName $appPool
  310. throw
  311. }
  312. $state = [ordered]@{
  313. siteName = $SiteName
  314. currentRelease = $ReleaseId
  315. currentPhysicalPath = $newPublic
  316. previousPhysicalPath = $oldPath
  317. packageSha256 = (Get-FileHash -LiteralPath $PackagePath -Algorithm SHA256).Hash
  318. switchedAtUtc = (Get-Date).ToUniversalTime().ToString('o')
  319. migrationsRun = [bool]$RunMigrations
  320. operation = 'deploy'
  321. }
  322. $state | ConvertTo-Json | Set-Content -LiteralPath $statePath -Encoding UTF8
  323. $protectedPaths = @($newPublic, $oldPath)
  324. $oldReleases = Get-ChildItem -LiteralPath $releasesRoot -Directory |
  325. Where-Object { $_.Name -notlike '*.staging' } |
  326. Sort-Object LastWriteTimeUtc -Descending |
  327. Select-Object -Skip $KeepReleases
  328. foreach ($oldRelease in $oldReleases) {
  329. $oldPublic = Join-Path $oldRelease.FullName 'public'
  330. if ($protectedPaths -notcontains $oldPublic) {
  331. Write-Step ('Retention candidate (not deleted automatically): ' + $oldRelease.FullName)
  332. }
  333. }
  334. Write-Step "Deployment complete: $ReleaseId"
  335. Write-Host "Rollback command: .\install-iis-release.ps1 -SiteName '$SiteName' -RollbackTo '<release-id>'"
  336. } catch {
  337. if (Test-Path -LiteralPath $stagingRoot) {
  338. Write-Warning "Incomplete staging directory retained for inspection: $stagingRoot"
  339. }
  340. throw
  341. }

Powered by TurnKey Linux.