tasks/PowerShell/AzureDtlCreateVM/task-funcs.ps1 (409 lines of code) (raw):

function Invoke-AzureDtlTask { [CmdletBinding()] param( [string] $DeploymentName, [string] $ResourceGroupName, [string] $TemplateName, $TemplateParameterObject ) $null = @( Write-Host "Preparing deployment parameters" ) $templateFile = Get-TemplateFile -TemplateName $TemplateName $null = @( Write-Host "Invoking deployment with the following parameters:" Write-Host " DeploymentName = $DeploymentName" Write-Host " ResourceGroupName = $ResourceGroupName" Write-Host " TemplateFile = $templateFile" Write-Host (' TemplateParameters = ' + ($TemplateParameterObject.GetEnumerator() | sort -Property Key | % { "-$($_.Key) '$(if ($_.Value.GetType().Name -eq 'Hashtable') { ConvertTo-Json $_.Value -Compress } else { $_.Value })'" })) ) Test-AzureRmResourceGroupDeployment -ResourceGroupName "$ResourceGroupName" -TemplateFile "$templateFile" -TemplateParameterObject $TemplateParameterObject return New-AzureRmResourceGroupDeployment -Name "$DeploymentName" -ResourceGroupName "$ResourceGroupName" -TemplateFile "$templateFile" -TemplateParameterObject $TemplateParameterObject } function ConvertTo-Bool { [CmdletBinding()] param( [string] $Value ) [bool] $boolValue = $false $null = [bool]::TryParse($Value, [ref]$boolValue) return $boolValue } function ConvertTo-Int { [CmdletBinding()] param( [string] $Value ) [int] $intValue = 0 $null = [int]::TryParse($Value, [ref]$intValue) return $intValue } function ConvertTo-MinutesString { [CmdletBinding()] param( [string] $Value ) return "$Value minute$(if ($Value -ne 1){ 's' })" } function ConvertTo-TemplateParameterObject { [CmdletBinding()] Param( [string] $TemplateParameters ) # The following regular expression is used to extract a parameter that is defined following PS rules. # # For example, given the following parameters: # # -newVMName '$(Build.BuildNumber)' -userName '$(User.Name)' -password (ConvertTo-SecureString -String '$(User.Password)' -AsPlainText -Force) # # the regular expression can be used to match newName, userName, password, etc. $pattern = '\-(?<k>\w+)\s+(?<v>[''"].*?[''"]|\(.*?\))' $pattern2 = '\-String\s+(?<v>[''"]?.*?[''"])' $templateParameterObject = @{} $null = @( [regex]::Matches($TemplateParameters, $pattern) | % { $value = $_.Groups[2].Value.Trim("`"'") $m = [regex]::Match($value, $pattern2) if ($m.Success) { $value = $m.Groups[1].Value.Trim("`"'") } $templateParameterObject[$_.Groups[1].Value] = $value } ) return $templateParameterObject } function Get-DeploymentTargetResourceId { [CmdletBinding()] param( [string] $DeploymentName, [string] $ResourceGroupName ) [Array] $operations = Get-AzureRmResourceGroupDeploymentOperation -DeploymentName $DeploymentName -ResourceGroupName $ResourceGroupName foreach ($op in $operations) { if ($null -ne $op.properties.targetResource) { $targetResource = $op.properties.targetResource break } } if ([string]::IsNullOrEmpty($targetResource.id)) { $null = @( Write-Host "##vso[task.logissue type=warning;]Dumping resource group deployment operation details for deployment '$DeploymentName' in resource group name '$ResourceGroupName'`:" Write-Host (ConvertTo-Json $operations) ) throw "Unable to extract the target resource from operations for deployment '$DeploymentName' in resource group name '$ResourceGroupName'." } return $targetResource.id } function Get-DtlLab { [CmdletBinding()] param( [string] $LabId ) $null = @( $labParts = $LabId.Split('/') $labName = $labParts.Get($labParts.Length - 1) Write-Host "Fetching lab '$labName'" ) return Get-AzureRmResource -ResourceId "$LabId" } function Get-DtlLabVm { [CmdletBinding()] param( [string] $ResourceId ) $vm = Get-AzureRmResource -ResourceId "$ResourceId" if (-not $vm) { throw "Unable to find VM with resource ID '$ResourceId'." } $vmName = $vm.Name if ($vm.ResourceName) { $vmLabName = $vm.ResourceName.Split('/')[0] $vmFullName = $vm.ResourceName } else { $vmLabName = $(if ($vm.ParentResource){ $vm.ParentResource.Split('/')[-1] } else { $null }) $vmFullName = $(if ($vmLabName){ "$vmLabName/$vmName" } else { $vmName }) } $vmResourceGroupName = $vm.ResourceGroupName $vmResourceType = $vm.ResourceType $vmDetails = Get-AzureRmResource -ApiVersion '2018-10-15-preview' -Name $vmFullName -ResourceGroupName $vmResourceGroupName -ResourceType $vmResourceType -ODataQuery '$expand=Properties($expand=Artifacts)' if (-not $vmDetails) { throw "Unable to get details for VM '$vmName' under lab '$vmLabName' and resource group '$vmResourceGroupName'." } return $vmDetails } function Get-ExpectedArtifactsCount { [CmdletBinding()] param( [string] $ArmTemplateJson ) $armTemplateObject = ConvertFrom-Json $ArmTemplateJson $vmTemplate = $armTemplateObject.resources | ? { $_.type -eq 'Microsoft.DevTestLab/labs/virtualmachines' } | Select-Object -First 1 return $vmTemplate.properties.artifacts.Count } function Get-TemplateFile { [CmdletBinding()] param( [string] $TemplateName ) $templateFile = $TemplateName if (-not [IO.Path]::IsPathRooted($TemplateName)) { $templateFile = Join-Path "$PSScriptRoot" "$TemplateName" } if (-not (Test-Path "$templateFile")) { throw "Unable to locate template file '$TemplateName'. Make sure the template file exists or the path is correctly specified." } return $templateFile } function Remove-FailedResourcesBeforeRetry { [CmdletBinding()] param( [string] $DeploymentName, [string] $ResourceGroupName, [string] $DeleteLabVM, [string] $DeleteDeployment ) try { # Delete the failed lab VM. if (ConvertTo-Bool -Value $DeleteLabVM) { $resourceId = Get-DeploymentTargetResourceId -DeploymentName $DeploymentName -ResourceGroupName $ResourceGroupName if ($resourceId) { Write-Host "Removing previously created lab virtual machine with resource ID '$resourceId'." Remove-AzureRmResource -ResourceId $resourceId -Force | Out-Null } else { Write-Host "Resource identifier is not available, will not attempt to remove corresponding resouce before retrying." } } # Delete the failed deployment. if (ConvertTo-Bool -Value $DeleteDeployment) { Write-Host "Removing previously created deployment '$DeploymentName' in resource group '$ResourceGroupName'." Remove-AzureRmResourceGroupDeployment -Name $DeploymentName -ResourceGroupName $ResourceGroupName | Out-Null } } catch { Write-Host "##vso[task.logissue type=warning;]Unable to clean-up failed resources. Operation failed with $($Error[0].Exception.Message)" } } function Show-InputParameters { [CmdletBinding()] param( ) Write-Host "Task called with the following parameters:" Write-Host " ConnectedServiceName = $ConnectedServiceName" Write-Host " LabId = $LabId" Write-Host " TemplateName = $TemplateName" Write-Host " TemplateParameters = $TemplateParameters" Write-Host " OutputResourceId = $OutputResourceId" Write-Host " FailOnArtifactError = $FailOnArtifactError" Write-Host " RetryOnFailure = $RetryOnFailure" Write-Host " RetryCount = $RetryCount" Write-Host " DeleteFailedLabVMBeforeRetry = $DeleteFailedLabVMBeforeRetry" Write-Host " DeleteFailedDeploymentBeforeRetry = $DeleteFailedDeploymentBeforeRetry" Write-Host " AppendRetryNumberToVMName = $AppendRetryNumberToVMName" Write-Host " WaitMinutesForApplyArtifacts = $WaitMinutesForApplyArtifacts" } function Test-ArtifactsInstalling { [CmdletBinding()] param( [array] $Artifacts ) [array]$installingArtifacts = $artifacts | ? { $_.status -eq 'Installing' } [array]$pendingArtifacts = $artifacts | ? { $_.status -eq 'Pending' } return $installingArtifacts.Count -gt 0 -or $pendingArtifacts.Count -gt 0 } function Test-ArtifactStatus { [CmdletBinding()] param( [string] $ResourceId, [string] $TemplateName, [string] $Fail ) $checkForFailedArtifacts = ConvertTo-Bool -Value $Fail if ($checkForFailedArtifacts) { $templateFile = Get-TemplateFile -TemplateName $TemplateName # Read the contents of the ARM template and remove any comments of the form /* ... */, # since these cause the call to ConvertFrom-Json, later on, to fail. $armTemplateJson = [IO.File]::ReadAllText($templateFile) -replace '/\*(.|[\r\n])*?\*/','' $expectedArtifactsCount = Get-ExpectedArtifactsCount -ArmTemplateJson $armTemplateJson if ($expectedArtifactsCount -gt 0) { $vm = Get-DtlLabVm -ResourceId $ResourceId [array]$artifacts = $vm.Properties.artifacts [array]$failedArtifacts = $artifacts | ? { $_.status -eq 'Failed' } [array]$succeededArtifacts = $artifacts | ? { $_.status -eq 'Succeeded' } Write-Host "Number of Artifacts Expected: $expectedArtifactsCount, Reported: $($artifacts.Count), Succeeded: $($succeededArtifacts.Count), Failed: $($failedArtifacts.Count)" if ($failedArtifacts.Count -gt 0 -or $succeededArtifacts.Count -lt $expectedArtifactsCount) { foreach ($failedArtifact in $failedArtifacts) { $failedArtifactName = $failedArtifact.artifactId.split('/')[-1] Write-Host "##vso[task.logissue type=warning;]Failed to apply artifact '$failedArtifactName'." if (-not [string]::IsNullOrEmpty($failedArtifact.deploymentStatusMessage)) { # Using a try/catch when converting from JSON, as the returned text may be plain. try { $deploymentStatusMessage = (ConvertFrom-Json $failedArtifact.deploymentStatusMessage).error.details.message } catch { $deploymentStatusMessage = $failedArtifact.deploymentStatusMessage } Write-Host "deploymentStatusMessage = $deploymentStatusMessage" } if (-not [string]::IsNullOrEmpty($failedArtifact.vmExtensionStatusMessage)) { # Using a try/catch when converting from JSON, as the returned text may be plain. try { $vmExtensionStatusMessage = (ConvertFrom-Json $failedArtifact.vmExtensionStatusMessage)[1].message } catch { $vmExtensionStatusMessage = $failedArtifact.vmExtensionStatusMessage } Write-Host "vmExtensionStatusMessage = $($vmExtensionStatusMessage -replace '\\n','')" } } throw 'At least one artifact failed to apply. Review the lab virtual machine artifact results blade for full details.' } } } } function Test-InputParameters { [CmdletBinding()] Param( $TemplateParameterObject ) Write-Host 'Validating input parameters' # Only required for backward compatibility with earlier versions of the task. Test-TemplateParameters -TemplateParameterObject $TemplateParameterObject $vmName = $TemplateParameterObject.Item('newVMName') Test-VirtualMachineName -Name "$vmName" } function Test-TemplateParameters { [CmdletBinding()] Param( $TemplateParameterObject ) $defaultValues = @{ NewVMName = '<Enter VM Name>' UserName = '<Enter User Name>' Password = '<Enter User Password>' } $vmName = $TemplateParameterObject.Item('newVMName') $userName = $TemplateParameterObject.Item('userName') $password = $TemplateParameterObject.Item('password') $mustReplaceDefaults = $false if ($vmName -and $vmName.Contains($defaultValues.NewVMName)) { Write-Host "##vso[task.logissue type=warning;]-newVMName value should be replaced with non-default." $mustReplaceDefaults = $true } if ($userName -and $userName.Contains($defaultValues.UserName)) { Write-Host "##vso[task.logissue type=warning;]-userName value should be replaced with non-default." $mustReplaceDefaults = $true } if ($password -and $password.Contains($defaultValues.Password)) { Write-Host "##vso[task.logissue type=warning;]-password value should be replaced with non-default." $mustReplaceDefaults = $true } if ($mustReplaceDefaults) { throw 'Default values must be replaced. Please review the Template Parameters and modify as needed.' } } function Test-VirtualMachineName { [CmdletBinding()] Param( [string] $Name, [int] $MaxNameLength = 15 ) if ([string]::IsNullOrWhiteSpace($Name)) { throw "Invalid VM name '$Name'. Name must be specified." } if ($Name.Length -gt $MaxNameLength) { throw "Invalid VM name '$Name'. Name must be between 1 and $MaxNameLength characters." } $regex = [regex]'^(?=.*[a-zA-Z/-]+)[0-9a-zA-Z/-]*$' if (-not $regex.Match($Name).Success) { throw "Invalid VM name '$Name'. Name cannot be entirely numeric and cannot contain most special characters." } } function Wait-ApplyArtifacts { [CmdletBinding()] param( [string] $ResourceId, [string] $WaitMinutes ) $maxWaitMinutes = ConvertTo-Int -Value $WaitMinutes if ($maxWaitMinutes -gt 0) { Write-Host "Waiting for a maximum of $(ConvertTo-MinutesString $maxWaitMinutes) for apply artifacts operation to complete." $totalWaitMinutes = 0 [string] $provisioningState $startWait = [DateTime]::Now $continueWaiting = $true do { $waitspan = New-TimeSpan -Start $startWait -End ([DateTime]::Now) $totalWaitMinutes = [Math]::Round($waitspan.TotalMinutes) $expired = $waitspan.TotalMinutes -ge $maxWaitMinutes if ($expired) { throw "Waited for more than $(ConvertTo-MinutesString $totalWaitMinutes). Failing the task." } $vm = Get-DtlLabVm -ResourceId $ResourceId $provisioningState = $vm.Properties.provisioningState $continueWaiting = Test-ArtifactsInstalling -Artifacts $vm.Properties.artifacts if ($continueWaiting) { # The only time we have seen we possibly need to wait is if the ARM deployment completed prematurely, # for some unknown error, and the virtual machine is still applying artifacts. So, it is reasonable to # recheck every 5 minutes. Start-Sleep -Seconds 300 } } while ($continueWaiting) Write-Host "Waited for a total of $(ConvertTo-MinutesString $totalWaitMinutes). Latest provisioning state is $provisioningState." } }