deploy/scripts/deploy.ps1 (1,165 lines of code) (raw):

<# .SYNOPSIS Deploys Industrial IoT services to Azure. .DESCRIPTION Deploys the Industrial IoT services and dependencies on Azure. .PARAMETER type The type of deployment (local, services, simulation, all), defaults to all. .PARAMETER version Set to mcr image tag to deploy - if not set and version can not be parsed from branch name will deploy "latest". .PARAMETER branchName The branch name where to find the deployment templates - if not set, will try to use git. .PARAMETER repo The repository to find the deployment templates in - if not set will try to use git or set default. .PARAMETER resourceGroupName Can be the name of an existing or new resource group. .PARAMETER resourceGroupLocation Optional, a resource group location. If specified, will try to create a new resource group in this location. .PARAMETER subscriptionId Optional, the subscription id where resources will be deployed. .PARAMETER subscriptionName Or alternatively the subscription name. .PARAMETER tenantId The Azure Active Directory tenant tied to the subscription(s) that should be listed as options. .PARAMETER applicationName The name of the application, if not local deployment. .PARAMETER context A previously created az context to be used for authentication. .PARAMETER containerRegistryServer The container registry server to use to pull images .PARAMETER containerRegistryUsername The user name to use to pull images .PARAMETER containerRegistryPassword The password to use to pull images .PARAMETER imageNamespace Override the automatically determined namespace of the container images .PARAMETER acrRegistryName An optional name of an Azure container registry to deploy containers from. .PARAMETER acrSubscriptionName The subscription of the container registry, if different from the specified subscription. .PARAMETER acrTenantId The tenant where the container registry resides. If not provided uses all. .PARAMETER environmentName The cloud environment to use, defaults to AzureCloud. .PARAMETER simulationProfile If you are deploying a simulation, the simulation profile to use, if not default. .PARAMETER numberOfSimulationsPerEdge Number of simulations to deploy per edge. .PARAMETER numberOfLinuxGateways Number of Linux gateways to deploy into the simulation. .PARAMETER numberOfWindowsGateways Number of Windows gateways to deploy into the simulation. .PARAMETER gatewayVmSku Virtual machine SKU size that hosts simulated edge gateway. Suggestion: use VM with at least 2 cores and 8 GB of memory. Must Support Generation 1. .PARAMETER opcPlcVmSku Virtual machine SKU size that hosts simulated OPC UA PLC. Suggestion: use VM with at least 1 core and 2 GB of memory. Must Support Generation 1. .PARAMETER noAadAppRegistration Do not deploy service with Azure Active Directory authentication support. Do not use in production!. .PARAMETER authTenantId Specifies an Azure Active Directory tenant for authentication that is different from the one tied to the subscription. .PARAMETER aadConfig The aad configuration object (use aad-register.ps1 to create object). If not provided, calls aad-register.ps1. .PARAMETER aadApplicationName The application name to use when registering aad application. If not set, uses applicationName. .PARAMETER credentials Use these credentials to log in. If not provided you are prompted to provide credentials .PARAMETER disableRbacAuthorization Disable using Azure RBAC authorization using role assignments to the managed identity and use legacy style keys and shared access tokens to access services. .PARAMETER isServicePrincipal The credentials provided are service principal credentials. .PARAMETER whatIfDeployment Create everything but run the deployment as what-if then exit. .PARAMETER verboseDeployment Show verbose progress of the deployment step. #> param( [ValidateSet("local", "services", "simulation", "all")] [string] $type = "all", [string] $version, [string] $repo, [string] $branchName, [string] $applicationName, [string] $resourceGroupName, [string] $resourceGroupLocation, [string] $subscriptionName, [string] $subscriptionId, [string] $tenantId, [string] $containerRegistryServer, [string] $containerRegistryUsername, [securestring] $containerRegistryPassword, [string] $imageNamespace, [string] $acrRegistryName, [string] $acrSubscriptionName, [string] $acrTenantId, [string] $simulationProfile, [string] $gatewayVmSku, [string] $opcPlcVmSku, [int] $numberOfLinuxGateways = 1, [int] $numberOfWindowsGateways = 1, [int] $numberOfSimulationsPerEdge = 1, [pscredential] $credentials, [secureString] $accessToken, [switch] $isServicePrincipal, [switch] $noAadAppRegistration, [switch] $disableRbacAuthorization, [string] $authTenantId, [string] $aadApplicationName, [object] $aadConfig, [object] $context, [string] $environmentName = "AzureCloud", [switch] $whatIfDeployment, [switch] $verboseDeployment ) #******************************************************************************************************* # Login and select subscription to deploy into #******************************************************************************************************* Function Select-Context() { [OutputType([Microsoft.Azure.Commands.Profile.Models.Core.PSAzureContext])] Param( $environment, [Microsoft.Azure.Commands.Profile.Models.Core.PSAzureContext] $context ) $tenantArg = @{} if (![string]::IsNullOrEmpty($script:tenantId)) { $tenantArg = @{ Tenant = $script:tenantId } } $rootDir = Get-RootFolder $script:ScriptDir $contextFile = Join-Path $rootDir ".user" if ($context) { Write-Host "Using provided context (Account $($script:Context.Account), Tenant $($script:Context.Tenant.Id))" $script:subscriptionId = $context.Subscription.Id } else { if (!$context) { # Migrate .user file into root (next to .env) if (!(Test-Path $contextFile)) { $oldFile = Join-Path $script:ScriptDir ".user" if (Test-Path $oldFile) { Move-Item -Path $oldFile -Destination $contextFile } } if (Test-Path $contextFile) { $connection = Import-AzContext -Path $contextFile if (($null -ne $connection) ` -and ($null -ne $connection.Context) ` -and ($null -ne (Get-AzSubscription))) { $context = $connection.Context } } } if (!$context) { try { if ($script:accessToken) { Write-Host "Signing into $($environment.Name) using the provided access token..." $connection = Connect-AzAccount -Environment $environment.Name ` -AccessToken $script:accessToken ` -SkipContextPopulation @tenantArg -ErrorAction Stop } elseif ($script:credentials) { Write-Host "Signing into $($environment.Name) using the provided credentials..." $connection = Connect-AzAccount -Environment $environment.Name ` -Credential $script:credentials ` -ServicePrincipal:$script:isServicePrincipal.IsPresent ` -SkipContextPopulation @tenantArg -ErrorAction Stop } else { Write-Host "Signing into $($environment.Name) ..." $connection = Connect-AzAccount -Environment $environment.Name ` -SkipContextPopulation @tenantArg -ErrorAction Stop } Write-Host "Signed in." Write-Host $context = $connection.Context } catch { $connection | Out-Host $context = Get-AzContext if ($context) { Write-Host "Failed to log in. Using existing context $($context)..." Write-Host } } } if (!$context) { throw "The login to the Azure account was not successful." } } $tenantIdArg = @{} if (![string]::IsNullOrEmpty($script:tenantId)) { $tenantIdArg = @{ TenantId = $script:tenantId } } $subscriptionDetails = $null if (![string]::IsNullOrEmpty($script:subscriptionName)) { $subscriptionDetails = Get-AzSubscription -SubscriptionName $script:subscriptionName @tenantIdArg if (!$subscriptionDetails -and !$script:interactive) { throw "Invalid subscription provided with -subscriptionName" } } if (!$subscriptionDetails -and ![string]::IsNullOrEmpty($script:subscriptionId)) { $subscriptionDetails = Get-AzSubscription -SubscriptionId $script:subscriptionId @tenantIdArg if (!$subscriptionDetails -and !$script:interactive) { throw "Invalid subscription provided with -subscriptionId" } } if (!$subscriptionDetails) { $subscriptions = Get-AzSubscription @tenantIdArg | Where-Object { $_.State -eq "Enabled" } if ($subscriptions.Count -eq 0) { throw "No active subscriptions found - exiting." } elseif ($subscriptions.Count -eq 1) { $subscriptionId = $subscriptions[0].Id } else { if (!$script:interactive) { throw "Provide a subscription to use using -subscriptionId or -subscriptionName" } Write-Host "Please choose a subscription from this list (using its index):" $script:index = 0 $subscriptions | Format-Table -AutoSize -Property ` @{Name="Index"; Expression = {($script:index++)}},` @{Name="Subscription"; Expression = {$_.Name}},` @{Name="Id"; Expression = {$_.SubscriptionId}}` | Out-Host while ($true) { $option = Read-Host ">" try { if ([int]$option -ge 1 -and [int]$option -le $subscriptions.Count) { break } } catch { Write-Host "Invalid index '$($option)' provided." } Write-Host "Choose from the list using an index between 1 and $($subscriptions.Count)." } $subscriptionId = $subscriptions[$option - 1].Id } $subscriptionDetails = Get-AzSubscription -SubscriptionId $subscriptionId @tenantIdArg if (!$subscriptionDetails) { throw "Failed to get details for subscription $($subscriptionId)" } } # Update context $writeProfile = $false if ($context.Subscription.Id -ne $subscriptionDetails.Id) { $context = ($subscriptionDetails | Set-AzContext) # If file exists - silently update profile $writeProfile = Test-Path $contextFile } # If file does not exist yet - ask if (!(Test-Path $contextFile) -and $script:interactive) { $reply = Read-Host -Prompt "To avoid logging in again next time, would you like to save your credentials? [y/n]" if ($reply -match "[yY]") { Write-Host "Your Azure login context will be saved into a .user file in the root of the local repo." Write-Host "Make sure you do not share it and delete it when no longer needed." $writeProfile = $true } } if ($writeProfile) { Save-AzContext -Path $contextFile } # Seed aad token in token cache Write-Host "Azure subscription $($context.Subscription.Name) ($($context.Subscription.Id)) selected." return $context } #******************************************************************************************************* # Select repository and branch #******************************************************************************************************* Function Select-RepositoryAndBranch() { if ([string]::IsNullOrEmpty($script:branchName)) { try { $argumentList = @("rev-parse", "--abbrev-ref", "@{upstream}") $symbolic = (& "git" @argumentList 2>&1 | ForEach-Object { "$_" }); if ($LastExitCode -ne 0) { throw "git $($argumentList) failed with $($LastExitCode)." } $remote = $symbolic.Split('/')[0] $argumentList = @("remote", "get-url", $remote) $giturl = (& "git" @argumentList 2>&1 | ForEach-Object { "$_" }); if ($LastExitCode -ne 0) { throw "git $($argumentList) failed with $($LastExitCode)." } if ([string]::IsNullOrEmpty($script:repo)) { $script:repo = $giturl.Replace(".git", "") } $script:branchName = $symbolic.Replace("$($remote)/", "") if ($script:branchName -eq "HEAD") { Write-Warning "$($symbolic) is not a branch - using main." $script:branchName = "main" } } catch { # Try get branch name from build $script:branchName = $env:BUILD_SOURCEBRANCH if (![string]::IsNullOrEmpty($script:branchName)) { if ($script:branchName.StartsWith("refs/heads/")) { $script:branchName = $script:branchName.Replace("refs/heads/", "") } else { $script:branchName = $null } } elseif (![string]::IsNullOrEmpty($script:version)) { $script:branchName = "release/$script:version" } else { Write-Warning "Cannot determine branch - using main." $script:branchName = "main" } } } if ([string]::IsNullOrEmpty($script:repo)) { # Try get repo name / TODO $script:repo = "https://github.com/Azure/Industrial-IoT" } } #******************************************************************************************************* # Get private registry credentials #******************************************************************************************************* Function Select-RegistryCredentials() { # set private container registry source if provided at command line if ([string]::IsNullOrEmpty($script:acrRegistryName) ` -and [string]::IsNullOrEmpty($script:acrSubscriptionName)) { return $null } if (![string]::IsNullOrEmpty($script:acrSubscriptionName) ` -and ($context.Subscription.Name -ne $script:acrSubscriptionName)) { $tenantIdArg = @{} if (![string]::IsNullOrEmpty($script:acrTenantId)) { $tenantIdArg = @{ TenantId = $script:acrTenantId } } $acrSubscription = Get-AzSubscription -SubscriptionName $script:acrSubscriptionName @tenantIdArg if (!$acrSubscription) { Write-Warning "Specified container registry subscription $($script:acrSubscriptionName) not found." } $containerContext = Get-AzContext -ListAvailable | Where-Object { $_.Subscription.Name -eq $script:acrSubscriptionName } } if (!$containerContext) { # use current context $containerContext = $context Write-Host "Try using current authentication context to access container registry." } elseif ($containerContext.Length -gt 1) { $containerContext = $containerContext[0] } Write-Host "Looking up credentials for $($script:acrRegistryName) registry." try { $registry = Get-AzContainerRegistry -DefaultProfile $containerContext ` | Where-Object { $_.Name -eq $script:acrRegistryName } } catch { $registry = $null } if (!$registry) { Write-Warning "$($script:acrRegistryName) registry not found." return $null } try { $creds = Get-AzContainerRegistryCredential -Registry $registry ` -DefaultProfile $containerContext } catch { $creds = $null } if (!$creds) { Write-Warning "Failed to get credentials for $($script:acrRegistryName)." return $null } return @{ dockerServer = $registry.LoginServer dockerUser = $creds.Username dockerPassword = $creds.Password } } #******************************************************************************************************* # Filter locations for provider and resource type #******************************************************************************************************* Function Select-ResourceGroupLocations() { param ( $locations, $provider, $typeName ) $regions = @() foreach ($item in $(Get-AzResourceProvider -ProviderNamespace $provider)) { foreach ($resourceType in $item.ResourceTypes) { if ($resourceType.ResourceTypeName -eq $typeName) { foreach ($region in $resourceType.Locations) { $regions += $region } } } } if ($regions.Count -gt 0) { $locations = $locations | Where-Object { return $_.DisplayName -in $regions } } return $locations } #******************************************************************************************************* # Get locations #******************************************************************************************************* Function Get-ResourceGroupLocations() { # Filter resource namespaces $locations = Get-AzLocation | Where-Object { foreach ($provider in $script:requiredProviders) { if ($_.Providers -notcontains $provider) { return $false } } return $true } # Filter resource types - TODO read parameters from table $locations = Select-ResourceGroupLocations -locations $locations ` -provider "Microsoft.Devices" -typeName "ProvisioningServices" return $locations } #******************************************************************************************************* # Select location #******************************************************************************************************* Function Select-ResourceGroupLocation() { $locations = Get-ResourceGroupLocations if (![string]::IsNullOrEmpty($script:resourceGroupLocation)) { foreach ($location in $locations) { if ($location.Location -eq $script:resourceGroupLocation -or ` $location.DisplayName -eq $script:resourceGroupLocation) { $script:resourceGroupLocation = $location.Location return } } if ($interactive) { throw "Location '$script:resourceGroupLocation' is not a valid location." } } Write-Host "Please choose a location for your deployment from this list (using its Index):" $script:index = 0 $locations | Format-Table -AutoSize -property ` @{Name="Index"; Expression = {($script:index++)}},` @{Name="Location"; Expression = {$_.DisplayName}} ` | Out-Host while ($true) { $option = Read-Host -Prompt ">" try { if ([int]$option -ge 1 -and [int]$option -le $locations.Count) { break } } catch { Write-Host "Invalid index '$($option)' provided." } Write-Host "Choose from the list using an index between 1 and $($locations.Count)." } $script:resourceGroupLocation = $locations[$option - 1].Location } #******************************************************************************************************* # Update resource group tags #******************************************************************************************************* Function Set-ResourceGroupTags() { Param( [string] $state, [string] $version ) $resourceGroup = Get-AzResourceGroup -ResourceGroupName $script:resourceGroupName if (!$resourceGroup) { return } $tags = $resourceGroup.Tags if (!$tags) { $tags = @{} } $update = $false if (![string]::IsNullOrEmpty($state)) { if ($tags.ContainsKey("IoTSuiteState")) { if ($tags.IoTSuiteState -ne $state) { $tags.IoTSuiteState = $state $update = $true } } else { $tags += @{ "IoTSuiteState" = $state } $update = $true } } if (![string]::IsNullOrEmpty($version)) { if ($tags.ContainsKey("IoTSuiteVersion")) { if ($tags.IoTSuiteVersion -ne $version) { $tags.IoTSuiteVersion = $version $update = $true } } else { $tags += @{ "IoTSuiteVersion" = $version } $update = $true } } $type = "AzureIndustrialIoT" if ($tags.ContainsKey("IoTSuiteType")) { if ($tags.IoTSuiteType -ne $type) { $tags.IoTSuiteType = $type $update = $true } } else { $tags += @{ "IoTSuiteType" = $type } $update = $true } if (!$update) { return } $resourceGroup = Set-AzResourceGroup -Name $script:resourceGroupName -Tag $tags } #******************************************************************************************************* # Get or create new resource group for deployment #******************************************************************************************************* Function Select-ResourceGroup() { $first = $true while ([string]::IsNullOrEmpty($script:resourceGroupName) ` -or ($script:resourceGroupName -notmatch "^[a-z0-9-_]*$")) { if (!$script:interactive) { throw "Invalid resource group name specified which is mandatory for non-interactive script use." } if ($first -eq $false) { Write-Host "Use alphanumeric characters as well as '-' or '_'." } else { Write-Host Write-Host "Please provide a name for the resource group." $first = $false } $script:resourceGroupName = Read-Host -Prompt ">" } $resourceGroup = Get-AzResourceGroup -Name $script:resourceGroupName ` -ErrorAction SilentlyContinue if (!$resourceGroup) { Write-Host "Resource group '$script:resourceGroupName' does not exist." Select-ResourceGroupLocation $resourceGroup = New-AzResourceGroup -Name $script:resourceGroupName ` -Location $script:resourceGroupLocation Write-Host "Created new resource group $($script:resourceGroupName) in $($resourceGroup.Location)." Set-ResourceGroupTags -state "Created" return $True } else { Set-ResourceGroupTags -state "Updating" $script:resourceGroupLocation = $resourceGroup.Location Write-Host "Using existing resource group $($script:resourceGroupName)..." return $False } } #****************************************************************************** # Generate a random password #****************************************************************************** Function New-Password() { param( $length = 15 ) $punc = 46..46 $digits = 48..57 $lcLetters = 65..90 $ucLetters = 97..122 $password = ` [char](Get-Random -Count 1 -InputObject ($lcLetters)) + ` [char](Get-Random -Count 1 -InputObject ($ucLetters)) + ` [char](Get-Random -Count 1 -InputObject ($digits)) + ` [char](Get-Random -Count 1 -InputObject ($punc)) $password += get-random -Count ($length - 4) ` -InputObject ($punc + $digits + $lcLetters + $ucLetters) |` ForEach-Object -begin { $aa = $null } -process { $aa += [char]$_ } -end { $aa } return $password } #****************************************************************************** # Get env file content from deployment #****************************************************************************** Function Get-EnvironmentVariables() { Param( $deployment ) if (![string]::IsNullOrEmpty($script:resourceGroupName)) { Write-Output "PCS_RESOURCE_GROUP=$($script:resourceGroupName)" } $var = $deployment.Outputs["keyVaultUri"].Value if (![string]::IsNullOrEmpty($var)) { Write-Output "PCS_KEYVAULT_URL=$($var)" } $var = $script:aadConfig.ClientId if (![string]::IsNullOrEmpty($var)) { Write-Output "PCS_AUTH_PUBLIC_CLIENT_APPID=$($var)" } $var = $deployment.Outputs["tenantId"].Value $authTenantId = $script:aadConfig.TenantId if($var -ne $authTenantId) { if (![string]::IsNullOrEmpty($var)) { Write-Output "PCS_MSI_TENANT=$($var)" } $var = $authTenantId } if (![string]::IsNullOrEmpty($var)) { Write-Output "PCS_AUTH_TENANT=$($var)" } $var = $deployment.Outputs["serviceUrl"].Value if (![string]::IsNullOrEmpty($var)) { Write-Output "PCS_SERVICE_URL=$($var)" } if (![string]::IsNullOrEmpty($script:version)) { Write-Output "PCS_IMAGES_TAG=$($script:version)" } } #****************************************************************************** # find the top most folder with solution in it #****************************************************************************** Function Get-RootFolder() { param( $startDir ) $cur = $startDir while (![string]::IsNullOrEmpty($cur)) { if (Test-Path -Path (Join-Path $cur "Industrial-IoT.sln") -PathType Leaf) { return $cur } $cur = Split-Path $cur } return $startDir } #****************************************************************************** # Write or output .env file #****************************************************************************** Function Write-EnvironmentVariables() { Param( $deployment ) # find the top most folder $rootDir = Get-RootFolder $script:ScriptDir $writeFile = $false if ($script:interactive) { $ENVVARS = Join-Path $rootDir ".env" $prompt = "Save environment as $ENVVARS for local development? [y/n]" $reply = Read-Host -Prompt $prompt if ($reply -match "[yY]") { $writeFile = $true } if ($writeFile) { if (Test-Path $ENVVARS) { $prompt = "Overwrite existing .env file in $rootDir? [y/n]" if ($reply -match "[yY]") { Remove-Item $ENVVARS -Force } else { $writeFile = $false } } } } if ($writeFile) { Get-EnvironmentVariables $deployment | Out-File -Encoding ascii ` -FilePath $ENVVARS Write-Host Write-Host ".env file created in $rootDir." Write-Host Write-Warning "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" Write-Warning "!The file contains security keys to your Azure resources!" Write-Warning "! Safeguard the contents of this file, or delete it now !" Write-Warning "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" Write-Host } else { Get-EnvironmentVariables $deployment | Out-Default } } #******************************************************************************************************* # Deploy azuredeploy.json #******************************************************************************************************* Function New-Deployment() { Param( $context ) $templateParameters = @{ } Set-ResourceGroupTags -state "Deploying" -version $script:branchName Write-Host "Deployment will use '$($script:branchName)' branch in '$($script:repo)'." $templateParameters.Add("branchName", $script:branchName) # support forks on github by switching the template url if ($script:repo.ToLower().Contains("github.com")) { $templateUrl = $script:repo.ToLower().Replace("github.com", "raw.githubusercontent.com") Write-Host "$repo -> $templateUrl" $templateParameters.Add("templateUrl", $templateUrl) } if ($script:disableRbacAuthorization.IsPresent) { Write-Host "Deploying without Azure RBAC role based authorization." $templateParameters.Add("enableRbacAuthorization", $false) } else { $templateParameters.Add("enableRbacAuthorization", $true) } # Select an application name if (($script:type -eq "local") -or ($script:type -eq "simulation")) { if ([string]::IsNullOrEmpty($script:applicationName) ` -or ($script:applicationName -notmatch "^[a-z0-9-]*$")) { $script:applicationName = $script:resourceGroupName.Replace('_', '-') } } else { $first = $true while ([string]::IsNullOrEmpty($script:applicationName) ` -or ($script:applicationName -notmatch "^[a-z0-9-]*$")) { if (!$script:interactive) { throw "Invalid service name specified which is mandatory for non-interactive script use." } if ($first -eq $false) { Write-Host "You can only use alphanumeric characters as well as '-'." } else { Write-Host Write-Host "Please specify a name for the web-api service." $first = $false } if ($script:resourceGroupName -match "^[a-z0-9-]*$") { Write-Host "Hit enter to use $($script:resourceGroupName)." } $script:applicationName = Read-Host -Prompt ">" if ([string]::IsNullOrEmpty($script:applicationName)) { $script:applicationName = $script:resourceGroupName } } if (($script:type -eq "all") -or ($script:type -eq "services")) { $templateParameters.Add("siteName", $script:applicationName) } } $StartTime = $(get-date) write-host "Start time: $($StartTime.ToShortTimeString())" if ([string]::IsNullOrEmpty($script:version)) { if ($script:branchName.StartsWith("release/")) { $script:version = $script:branchName.Replace("release/", "") } else { $script:version = "latest" } } # Select docker images to use if (-not ($script:type -eq "local")) { $namespace = "" if (-not [string]::IsNullOrEmpty($script:imageNamespace)) { $namespace = $script:imageNamespace } else { if ($script:acrSubscriptionName -eq "IOT_GERMANY") { if ($script:acrRegistryName -eq "industrialiotdev") { $namespace = $script:branchName if ($script:branchName.StartsWith("feature/")) { $namespace = $namespace.Replace("feature/", "") } $namespace = $namespace.Replace("_", "/") $namespace = $namespace.Substring(0, [Math]::Min($namespace.Length, 24)) } elseif (($script:acrRegistryName -eq "industrialiot") -or ` ($script:acrRegistryName -eq "industrialiotprod")) { $namespace = "public" } } } if ([string]::IsNullOrEmpty($script:containerRegistryServer)) { # Try and get registry credentials try { $creds = Select-RegistryCredentials } catch { Write-Warning $_.Exception.Message $creds = $null } # Configure registry if ($creds) { $templateParameters.Add("dockerServer", $creds.dockerServer) $templateParameters.Add("dockerUser", $creds.dockerUser) $templateParameters.Add("dockerPassword", $creds.dockerPassword) $templateParameters.Add("imagesNamespace", $namespace) Write-Host "Using $($script:version) $($namespace) images from private registry $($creds.dockerServer)." } elseif ([string]::IsNullOrEmpty($script:acrRegistryName)) { $templateParameters.Add("dockerServer", "mcr.microsoft.com") Write-Host "Using released $($script:version) images from mcr.microsoft.com." } else { $templateParameters.Add("dockerServer", "$($script:acrRegistryName).azurecr.io") $templateParameters.Add("imagesNamespace", $namespace) Write-Host "Using $($script:version) $($namespace) images from $($script:acrRegistryName).azurecr.io." } } else { Write-Host "Using $($script:version) $($namespace) images from private registry $($script:containerRegistryServer)." $templateParameters.Add("dockerServer", $script:containerRegistryServer) $templateParameters.Add("imagesNamespace", $namespace) if (-not [string]::IsNullOrEmpty($script:containerRegistryUsername)) { $templateParameters.Add("dockerUser", $script:containerRegistryUsername) $plainTextPassword = [Net.NetworkCredential]::new('', $script:containerRegistryPassword).Password $templateParameters.Add("dockerPassword", $plainTextPassword) } } $templateParameters.Add("imagesTag", $script:version) } # Configure simulation if (($script:type -eq "all") -or ($script:type -eq "simulation")) { if ([string]::IsNullOrEmpty($script:simulationProfile)) { $templateParameters.Add("simulationProfile", "default") } else { $templateParameters.Add("simulationProfile", $script:simulationProfile) } if ((-not $script:numberOfSimulationsPerEdge) -or ($script:numberOfSimulationsPerEdge -eq 0)) { $templateParameters.Add("numberOfSimulations", 1) } else { $templateParameters.Add("numberOfSimulations", $script:numberOfSimulationsPerEdge) } # To be refactored: it's necessary to filter out the unsupported SKU sizes. # It still there isn't a API to identify the generations supported by the SKU sizes. if ([string]::IsNullOrEmpty($script:gatewayVmSku)) { # Get all vm skus available in the location and in the account Write-Host "Determining VM sizes for Linux IoT Edge gateway simulations..." $availableVms = Get-AzComputeResourceSku | Where-Object { ($_.ResourceType.Contains("virtualMachines")) -and ` ($_.Locations -icontains $script:resourceGroupLocation) -and ` ($_.Restrictions.Count -eq 0) } # Sort based on sizes and filter minimum requirements $availableVmNames = $availableVms ` | Select-Object -ExpandProperty Name -Unique if (($script:numberOfWindowsGateways -gt 0) -and ($availableVmNames -inotcontains "Standard_D4s_v4")) { Write-Warning "Standard_D4s_v4 VM with Nested virtualization for IoT Edge Eflow simulation not available in selected region or your subscription." $script:numberOfWindowsGateways = 0 } # We will use VM with at least 2 cores and 8 GB of memory as gateway host. $edgeVmSizes = Get-AzVMSize $script:resourceGroupLocation ` | Where-Object { $availableVmNames -icontains $_.Name } ` | Where-Object { ($_.NumberOfCores -ge 2) -and ` ($_.MemoryInMB -ge 8192) -and ` ($_.OSDiskSizeInMB -ge 1047552) -and ` ($_.ResourceDiskSizeInMB -gt 8192) } ` | Sort-Object -Property ` NumberOfCores,MemoryInMB,ResourceDiskSizeInMB,Name # Pick top if ($edgeVmSizes.Count -ne 0) { $edgeVmSize = $edgeVmSizes[0].Name Write-Host "Using $($edgeVmSize) as VM size for Linux IoT Edge gateway simulations..." $templateParameters.Add("edgeVmSize", $edgeVmSize) } } else { $templateParameters.Add("edgeVmSize", $script:gatewayVmSku) } if ((-not $script:numberOfLinuxGateways) -or ($script:numberOfLinuxGateways -eq 0)) { $templateParameters.Add("numberOfLinuxGateways", 1) } else { $templateParameters.Add("numberOfLinuxGateways", $script:numberOfLinuxGateways) } if (-not $script:numberOfWindowsGateways) { $templateParameters.Add("numberOfWindowsGateways", 0) } else { $templateParameters.Add("numberOfWindowsGateways", $script:numberOfWindowsGateways) } if ([string]::IsNullOrEmpty($script:opcPlcVmSku)) { # We will use VM with at least 1 core and 2 GB of memory for hosting OPC PLC simulation containers. Write-Host "Determining VM sizes for simulation containers..." $simulationVmSizes = Get-AzVMSize $script:resourceGroupLocation ` | Where-Object { $availableVmNames -icontains $_.Name } ` | Where-Object { ($_.NumberOfCores -ge 1) -and ` ($_.MemoryInMB -ge 2048) -and ` ($_.OSDiskSizeInMB -ge 1047552) -and ` ($_.ResourceDiskSizeInMB -ge 4096) } ` | Sort-Object -Property ` NumberOfCores,MemoryInMB,ResourceDiskSizeInMB,Name # Pick top if ($simulationVmSizes.Count -ne 0) { $simulationVmSize = $simulationVmSizes[0].Name Write-Host "Using $($simulationVmSize) as VM size for all OPC PLC simulation host machines..." $templateParameters.Add("simulationVmSize", $simulationVmSize) } } else { $templateParameters.Add("simulationVmSize", $script:opcPlcVmSku) } $adminUser = "sandboxuser" $adminPassword = New-Password $templateParameters.Add("edgePassword", $adminPassword) $templateParameters.Add("edgeUserName", $adminUser) } $aadAddReplyUrls = $false if (!$script:aadConfig) { if (!$script:noAadAppRegistration.IsPresent) { if ([string]::IsNullOrEmpty($script:aadApplicationName)) { $script:aadApplicationName = $script:applicationName } # register aad application Write-Host Write-Host "Registering client and services AAD applications in your tenant..." $aadRegisterContext = $context # Use context of auth tenant if (![string]::IsNullOrEmpty($authTenantId)) { Write-Host "Connecting to AAD tenant $($authTenantId)..." Connect-AzAccount -Tenant $authTenantId -ContextName AuthTenantId -Force $aadRegisterContext = Select-AzContext AuthTenantId } $script:aadConfig = & (Join-Path $script:ScriptDir "aad-register.ps1") ` -Context $aadRegisterContext -Name $script:aadApplicationName Write-Host "Client and services AAD applications registered..." Write-Host $aadAddReplyUrls = $true # Restore AD context if (![string]::IsNullOrEmpty($authTenantId)) { Write-Host "Switching to AAD tenant $($context.Tenant)..." Set-AzContext -Context $context } } else { Write-Host "Not registering AAD application!" } } elseif (($script:aadConfig -is [string]) -and (Test-Path $script:aadConfig)) { # read configuration from file $script:aadConfig = Get-Content -Raw -Path $script:aadConfig | ConvertFrom-Json } # Register registered aad applications if (![string]::IsNullOrEmpty($script:aadConfig.ServiceId)) { $templateParameters.Add("serviceAppId", $script:aadConfig.ServiceId) } if (![string]::IsNullOrEmpty($script:aadConfig.ServiceSecret)) { $templateParameters.Add("serviceAppSecret", $script:aadConfig.ServiceSecret) } if (![string]::IsNullOrEmpty($script:aadConfig.Audience)) { $templateParameters.Add("serviceAudience", $script:aadConfig.Audience) } if (![string]::IsNullOrEmpty($script:aadConfig.ClientId)) { $templateParameters.Add("publicClientAppId", $script:aadConfig.ClientId) } if (![string]::IsNullOrEmpty($script:aadConfig.WebAppId)) { $templateParameters.Add("clientAppId", $script:aadConfig.WebAppId) } if (![string]::IsNullOrEmpty($script:aadConfig.WebAppSecret)) { $templateParameters.Add("clientAppSecret", $script:aadConfig.WebAppSecret) } if (![string]::IsNullOrEmpty($script:aadConfig.Authority)) { $templateParameters.Add("authorityUri", $script:aadConfig.Authority) } if (![string]::IsNullOrEmpty($script:aadConfig.tenantId)) { $templateParameters.Add("authTenantId", $script:aadConfig.tenantId) } # Register current aad user to access keyvault $userPrincipalId = $script:aadConfig.UserPrincipalId if (![string]::IsNullOrWhiteSpace($userPrincipalId)) { Write-Warning "Deployment will add access to keyvault for user $userPrincipalId..." } else { $ctx = Get-AzContext if ($ctx.Account.Type -eq "User") { $userPrincipalId = (Get-AzADUser -UserPrincipalName $ctx.Account.Id).Id Write-Warning "Deployment will add access to keyvault for current user..." } else { $userPrincipalId = (Get-AzADServicePrincipal -ApplicationId $ctx.Account.Id).Id Write-Warning "Deployment will add access to keyvault for service principal id $userPrincipalId..." } } if ([string]::IsNullOrWhiteSpace($userPrincipalId)) { $userPrincipalId = $script:aadConfig.FallBackPrincipalId if ([string]::IsNullOrWhiteSpace($userPrincipalId)) { Write-Host "User principal could not be determined." Write-Host "Access to deployed key vault must be configured manually..." } else { Write-Warning "Deployment will add access to keyvault for user $userPrincipalId (Fallback)..." } } $templateParameters.Add("userPrincipalId", $userPrincipalId) # Add IoTSuiteType tag. This tag will be applied for all resources. $tags = @{"IoTSuiteType" = "AzureIndustrialIoT-$($script:type)-$($script:version)-PS1"} $templateParameters.Add("tags", $tags) $deploymentName = $script:version # register providers Write-Host "Registering providers..." $script:requiredProviders | ForEach-Object { Register-AzResourceProvider -ProviderNamespace $_ } | Out-Null if ($script:whatIfDeployment.IsPresent) { Write-Host "Starting what-if deployment..." $templateFilePath = Join-Path (Join-Path (Split-Path $ScriptDir) "templates") "azuredeploy.json" New-AzResourceGroupDeployment -ResourceGroupName $resourceGroupName ` -TemplateFile $templateFilePath -TemplateParameterObject $templateParameters ` -WhatIf -WhatIfResultFormat FullResourcePayloads return } while ($true) { try { if (![string]::IsNullOrEmpty($adminUser) -and ![string]::IsNullOrEmpty($adminPassword)) { Write-Host Write-Host "The following username and password can be used to log into the deployed VMs:" Write-Host $adminUser Write-Host $adminPassword Write-Host } # Start the deployment Write-Host "Starting deployment '$($deploymentName)'..." $templateFilePath = Join-Path (Join-Path (Split-Path $ScriptDir) "templates") "azuredeploy.json" $deployment = New-AzResourceGroupDeployment -ResourceGroupName $resourceGroupName ` -TemplateFile $templateFilePath -TemplateParameterObject $templateParameters ` -Name $deploymentName -Verbose:$script:verboseDeployment if ($deployment.ProvisioningState -ne "Succeeded") { Set-ResourceGroupTags -state "Failed" throw "Deployment '$($deploymentName)' $($deployment.ProvisioningState)." } Set-ResourceGroupTags -state "Complete" Write-Host "Deployment '$($deploymentName)' succeeded." # Use context of auth tenant if (![string]::IsNullOrEmpty($authTenantId)) { Write-Host "Switching to AAD tenant $($authTenantId)..." Select-AzContext AuthTenantId } # # Add reply urls # if ($aadAddReplyUrls -and ![string]::IsNullOrEmpty($script:aadConfig.WebAppId)) { $replyUrls = New-Object System.Collections.Generic.List[System.String] # retrieve existing urls $app = Get-AzADApplication -ApplicationId $script:aadConfig.WebAppId if ($app.ReplyUrls -and ($app.ReplyUrls.Count -ne 0)) { $replyUrls = $app.ReplyUrls; } $serviceUri = $deployment.Outputs["serviceUrl"].Value if (![string]::IsNullOrEmpty($serviceUri)) { $replyUrls.Add($serviceUri + "/swagger/oauth2-redirect.html") } $replyUrls.Add("http://localhost:9080/swagger/oauth2-redirect.html") $replyUrls.Add("http://localhost:5000/signin-oidc") $replyUrls.Add("https://localhost:5001/signin-oidc") # register reply urls in web application registration Write-Host Write-Host "Registering reply urls for $($script:aadConfig.WebAppId)..." try { # assumes we are still connected $replyUrls.Add("urn:ietf:wg:oauth:2.0:oob") $replyUrls = ($replyUrls | sort-object –Unique) # TODO # & (Join-Path $script:ScriptDir "aad-update.ps1") ` # $context ` # -ObjectId $script:aadConfig.WebAppPrincipalId -ReplyUrls $replyUrls Update-AzADApplication -ApplicationId $script:aadConfig.WebAppId -ReplyUrl $replyUrls ` | Out-Null Write-Host "Reply urls registered in web app $($script:aadConfig.WebAppId)..." Write-Host } catch { Write-Host $_.Exception.Message Write-Host Write-Host "Registering reply urls failed. Please add the following urls to" Write-Host "the web app '$($script:aadConfig.WebAppId)' manually:" $replyUrls | ForEach-Object { Write-Host $_ } } } $elapsedTime = $(get-date) - $StartTime write-host "Elapsed time (hh:mm:ss): $($elapsedTime.ToString("hh\:mm\:ss"))" # # Create environment file # Write-EnvironmentVariables -deployment $deployment # Try to open $website in a web browser. try { if (![string]::IsNullOrEmpty($website)) { # Try open application Start-Process $website -ErrorAction SilentlyContinue | Out-Null } } catch { # Ignore if there is no web browser available. } return } catch { $ex = $_ Write-Host $_.Exception.Message Write-Host "Deployment failed." $deleteResourceGroup = $false if (!$script:interactive) { $deleteResourceGroup = $script:deleteOnErrorPrompt } else { $retry = Read-Host -Prompt "Try again? [y/n]" if ($retry -match "[yY]") { continue } if ($script:deleteOnErrorPrompt) { $reply = Read-Host -Prompt "Delete resource group? [y/n]" $deleteResourceGroup = ($reply -match "[yY]") } } if ($deleteResourceGroup) { try { Write-Host "Removing resource group $($script:resourceGroupName)..." Remove-AzResourceGroup -ResourceGroupName $script:resourceGroupName -Force } catch { Write-Warning $_.Exception.Message } } throw $ex } } } #******************************************************************************************************* # Script body #******************************************************************************************************* $ErrorActionPreference = "Stop" $script:ScriptDir = Split-Path $script:MyInvocation.MyCommand.Path $script:interactive = !$script:context $script:requiredProviders = @( "microsoft.devices", "microsoft.storage", "microsoft.keyvault", "microsoft.managedidentity", "microsoft.web", "microsoft.compute" ) # Import-Module Az Import-Module Az.Accounts Import-Module Az.Resources Import-Module Az.Compute Import-Module Az.ContainerRegistry Select-RepositoryAndBranch $script:context = Select-Context -context $script:context ` -environment (Get-AzEnvironment -Name $script:environmentName) $script:deleteOnErrorPrompt = Select-ResourceGroup New-Deployment -context $script:context