tools/scripts/AksEdgeAzureSetup/AksEdgeAzureSetup.ps1 (357 lines of code) (raw):
<#
Sample script to setup Azure subscription for Arc for Kubernetes Connection
#>
Param(
[String]$jsonFile,
[switch]$spContributorRole,
[switch]$spCredReset
)
#Requires -RunAsAdministrator
New-Variable -Name gAksEdgeAzureSetup -Value "1.0.030325.1100" -Option Constant -ErrorAction SilentlyContinue
New-Variable -Option Constant -ErrorAction SilentlyContinue -Name cliMinVersions -Value @{
"azure-cli" = "2.41.0"
"azure-cli-core" = "2.41.0"
}
New-Variable -Option Constant -ErrorAction SilentlyContinue -Name arcLocations -Value @(
"australiaeast","brazilsouth","canadacentral","canadaeast","centralindia","centralus","centraluseuap",
"eastasia","eastus","eastus2","eastus2euap","francecentral","germanywestcentral","israelcentral",
"italynorth","japaneast","koreacentral","northcentralus","northeurope","norwayeast","southafricanorth",
"southcentralus","southeastasia","southindia","swedencentral","switzerlandnorth","uaenorth","uksouth",
"ukwest","westcentralus","westeurope","westus","westus2","westus3"
)
function Test-AzVersions {
#Function to check if the installed az versions are greater or equal to minVersions
$retval = $true
$curVersion = (az version) | ConvertFrom-Json
if (-not $curVersion) { return $false }
foreach ($item in $cliMinVersions.Keys ) {
Write-Host " Checking $item minVersion $($cliMinVersions.$item).." -NoNewline
$fgcolor = 'Green'
if ($curVersion.$item) {
Write-Verbose " Comparing $($curVersion.$item) -lt $($cliMinVersions.$item)."
if ([version]$($curVersion.$item) -lt [version]$($cliMinVersions.$item)) {
$retval = $false
$fgcolor = 'Red'
}
Write-Host "found $($curVersion.$item)" -ForegroundColor $fgcolor
}
}
return $retval
}
function Install-AzCli {
#Check if Az CLI is installed. If not install it.
$AzCommand = Get-Command -Name az -ErrorAction SilentlyContinue
if (!$AzCommand) {
$CLIPath = "C:\Program Files (x86)\Microsoft SDKs\Azure\CLI2\wbin"
Write-Host "> Installing AzCLI..."
Push-Location $env:TEMP
$progressPreference = 'silentlyContinue'
Invoke-WebRequest -Uri https://aka.ms/installazurecliwindows -OutFile .\AzureCLI.msi -UseBasicParsing
$progressPreference = 'Continue'
Start-Process msiexec.exe -Wait -ArgumentList '/I AzureCLI.msi /passive'
Remove-Item .\AzureCLI.msi
Pop-Location
[System.Environment]::SetEnvironmentVariable("Path", "$($CLIPath);$env:Path")
az config set core.disable_confirm_prompt=yes
az config set core.only_show_errors=yes
#az config set auto-upgrade.enable=yes
}
Write-Host "> Azure CLI installed" -ForegroundColor Green
if (-not (Test-AzVersions)) {
Write-Host "> Required Az versions are not installed. Attempting az upgrade. This may take a while."
az upgrade --all --yes
if (-not (Test-AzVersions)) {
Write-Host "Error: Required versions not found after az upgrade. Please try uninstalling and reinstalling" -ForegroundColor Red
}
}
}
# Formats JSON in a nicer format than the built-in ConvertTo-Json does.
# https://github.com/PowerShell/PowerShell/issues/2736
function Format-Json([Parameter(Mandatory, ValueFromPipeline)][String] $json) {
$indent = 0;
($json -Split '\n' |
ForEach-Object {
if ($_ -match '[\}\]]') {
# This line contains ] or }, decrement the indentation level
$indent--
}
$line = (' ' * $indent * 2) + $_.TrimStart().Replace(': ', ': ')
if ($_ -match '[\{\[]') {
# This line contains [ or {, increment the indentation level
$indent++
}
$line
}) -Join "`n"
}
function AssignRole([String] $roleToAssign) {
#NOTE: using global values here for rest of the parameters.
$roleparams = @(
"--assignee", "$($servicePrincipal.appId)",
"--role", "$roleToAssign",
"--scope", "$rguri"
)
Write-Host "Creating $roleToAssign role assignment"
$res = (az role assignment create @roleparams ) | ConvertFrom-Json
if (!$res) { Write-Host " Error in assigning $roleToAssign role " -ForegroundColor Red }
}
###
# Main
###
Write-Host "AksEdgeAzureSetup version `t: $gAksEdgeAzureSetup"
if (-not $jsonFile) {
$jsonFile = "$PSScriptRoot\AzureConfig.json"
}
if (-not(Test-Path -Path "$jsonFile" -PathType Leaf)) {
Write-Host "Error: Incorrect input. Enter valid jsonFile path" -ForegroundColor Red
exit -1
}
Write-Verbose "Loading $jsonFile.."
$jsonContent = Get-Content "$jsonFile" | ConvertFrom-Json
if ($jsonContent.Azure) {
$aicfg = $jsonContent.Azure
} elseif ($jsonContent.SubscriptionId) {
$aicfg = $jsonContent
} else {
Write-Host "Error: Incorrect json content" -ForegroundColor Red
exit -1
}
if ($arcLocations -inotcontains $($aicfg.Location)) {
Write-Host "Error: Location $($aicfg.Location) is not supported for Azure Arc" -ForegroundColor Red
Write-Host "Supported Locations : $arcLocations"
exit -1
}
# Install Cli
Install-AzCli
Write-Host "$aicfg"
Write-Host "> az login to create/update service principal" -ForegroundColor Cyan
$loginparams = @("--scope", "https://graph.microsoft.com//.default" )
if ($($aicfg.TenantId)) {
$loginparams += @("--tenant", $($aicfg.TenantId))
}
$session = (az login @loginparams) | ConvertFrom-Json
if (-not $session) {
Write-Host "Error: Login failed. See error above and if required specify the tenantId in the input json file." -ForegroundColor Red
exit -1
}
if ($($aicfg.SubscriptionId)) {
#If SubscriptionId is specified, look for that in the session
$reqSession = $session | Where-Object { ($_.id -eq $aicfg.SubscriptionId) -and ($_.state -eq 'Enabled') }
if (!$reqSession) {
Write-Host "Error: [$($aicfg.SubscriptionId)] not found or not enabled." -ForegroundColor Red
Write-Host "Available subscription ids with state :" -ForegroundColor Cyan
$subinfo = $session | Select-Object name, id, state
Write-Host ($subinfo | Out-String)
#Write-Host ($($session.id) -join "`n") -ForegroundColor Cyan
az logout
exit -1
}
(az account set --subscription $($aicfg.SubscriptionId)) | Out-Null
} elseif ($($aicfg.SubscriptionName)) {
#If SubscriptionName is specified, look for that in the session
$reqSession = $session | Where-Object { ($_.name -eq $aicfg.SubscriptionName) -and ($_.state -eq 'Enabled') }
if (!$reqSession) {
Write-Host "Error: [$($aicfg.SubscriptionName)] not found or not enabled." -ForegroundColor Red
Write-Host "Available subscription names with state :" -ForegroundColor Cyan
$subinfo = $session | Select-Object name, id, state
Write-Host ($subinfo | Out-String)
az logout
exit -1
}
(az account set --subscription $($reqSession.id)) | Out-Null
} else {
#nothing specified. So use the default subscription and continue
if ($session.Count -gt 1) {
Write-Host ">>> Multiple subscriptions found :"
$subinfo = $session | Select-Object name, id , state
Write-Host ($subinfo | Out-String)
$sub = $session | Where-Object { $_.IsDefault -eq $true }
} else { $sub = $session }
Write-Host ">>> Default subscription is $($sub.name)[$($sub.id)]" -ForegroundColor Cyan
}
$session = (az account show | ConvertFrom-Json -ErrorAction SilentlyContinue)
$aicfg.SubscriptionId = $session.id
$aicfg.SubscriptionName = $session.name
$aicfg.TenantId = $session.tenantId
Write-Host "Logged in $($session.name) subscription as $($session.user.name) ($($session.user.type))" -ForegroundColor Cyan
Write-Host "TenantID : $($aicfg.TenantId)" -ForegroundColor Cyan
Write-Host "SubscriptionId : $($aicfg.SubscriptionId)" -ForegroundColor Cyan
$hasRights = $false
$userinfo = (az ad signed-in-user show) | ConvertFrom-Json
Write-Host "User Principal Name : $($userinfo.userPrincipalName)"
Write-Host "Looking for Azure RBAC roles"
$adminroles = (az role assignment list --all --assignee $userinfo.userPrincipalName --include-inherited) | ConvertFrom-Json
if ($adminroles) {
Write-Host "Roles enabled for this account are:" -ForegroundColor Cyan
foreach ($role in $adminroles) {
Write-Host "$($role.roleDefinitionName) for scope $($role.scope)" -ForegroundColor Cyan
if (($($role.scope) -eq "/subscriptions/$($aicfg.SubscriptionId)") -and ($role.roleDefinitionName -match 'Owner')) {
Write-Host "* You have sufficient privileges" -ForegroundColor Green
$hasRights = $true
}
}
}
if (-not $hasRights) {
# two stage call to work around issue reported here : https://github.com/Azure/azure-powershell/issues/15261 which occurs for CSP subscriptions
# look for classic administrators only when there is no Azure RBAC roles defined
Write-Host "Looking for classic administrator roles"
$adminroles = (az role assignment list --include-classic-administrators) | ConvertFrom-Json
$adminrole = $adminroles | Where-Object { $_.principalName -ieq $($session.user.name) }
if ($adminrole) {
Write-Host "Roles enabled for this account are:" -ForegroundColor Cyan
foreach ($role in $adminrole) {
Write-Host "$($role.roleDefinitionName) for scope $($role.scope)" -ForegroundColor Cyan
if (($($role.scope) -eq "/subscriptions/$($aicfg.SubscriptionId)") -and (( $role.roleDefinitionName -match 'Administrator'))) {
Write-Host "* You have sufficient privileges" -ForegroundColor Green
$hasRights = $true
}
}
}
}
if (-not $hasRights) {
Write-Host "Error: You do not have sufficient privileges for this subscription $($aicfg.SubscriptionId). Please refer to 'https://learn.microsoft.com/en-us/azure/role-based-access-control/role-assignments-steps#privileged-administrator-roles' for more details." -ForegroundColor Red
az logout
exit -1
}
# Resource group
$rgname = $aicfg.ResourceGroupName
$rguri = "/subscriptions/$($aicfg.SubscriptionId)/resourceGroups/$rgname"
Write-Host "Checking $rgname..."
$rgexists = az group exists --name $rgname
if ($rgexists -ieq 'true') {
Write-Host "* $rgname exists" -ForegroundColor Green
} else {
Write-Host "Creating $rgname resource group"
$rg = (az group create --resource-group $rgname -l $aicfg.Location | ConvertFrom-Json -ErrorAction SilentlyContinue)
if ($rg) {
Write-Host "$($rg.name) resource group created" -ForegroundColor Green
} else {
Write-Host "Error: Failed to create $rgname resource group" -ForegroundColor Red
az logout
exit -1
}
}
# Check and enable namespaces
$namespaces = @("Microsoft.HybridCompute", "Microsoft.GuestConfiguration", "Microsoft.HybridConnectivity",
"Microsoft.Kubernetes", "Microsoft.KubernetesConfiguration", "Microsoft.ExtendedLocation")
foreach ($namespace in $namespaces) {
Write-Host "Checking $namespace..."
$provider = (az provider show -n $namespace | ConvertFrom-Json -ErrorAction SilentlyContinue)
if ($provider.registrationState -ieq "Registered") {
Write-Host "* $namespace provider registered" -ForegroundColor Green
} else {
Write-Host "Registering $namespace provider. This can take some time. Please wait..." -ForegroundColor Yellow
$provider = (az provider register -n $namespace --wait | ConvertFrom-Json -ErrorAction SilentlyContinue)
Write-Host "$namespace provider registered successfully." -ForegroundColor Green
}
}
# Create Service Principal
$spName = $aicfg.ServicePrincipalName
$spApp = (az ad sp list --display-name $spName | ConvertFrom-Json -ErrorAction SilentlyContinue)
$servicePrincipal = $null
$enableContributor = $spContributorRole.IsPresent
$enableKcOnboarding = (!$spContributorRole.IsPresent)
$enableAcmOnboarding = (!$spContributorRole.IsPresent)
$savePassword = $false
if ($spApp -is [Array]) {$spApp = $spApp | Where-Object {$_.displayName -ieq $spName}; }
if ($spApp) {
# service principal found. Check roles required
$servicePrincipal = $spApp
Write-Host "$spName is already present."
$spRoles = (az role assignment list --all --assignee $($spApp.appId)) | ConvertFrom-Json
if ($spRoles) {
$spRolesRgScope = $spRoles | Where-Object {$_.scope -eq $rguri } # resource group scope
if ($spRolesRgScope) {
if ($spRolesRgScope.roleDefinitionName -contains 'Contributor') {
Write-Host "* Contributor role enabled" -ForegroundColor Green
$enableContributor = $false
$enableKcOnboarding = $false
$enableAcmOnboarding = $false
}
if ($spRolesRgScope.roleDefinitionName -contains 'Azure Connected Machine Onboarding') {
Write-Host "* Azure Connected Machine Onboarding role enabled" -ForegroundColor Green
$enableAcmOnboarding = $false
}
if ($spRolesRgScope.roleDefinitionName -contains 'Kubernetes Cluster - Azure Arc Onboarding') {
Write-Host "* Kubernetes Cluster - Azure Arc Onboarding role enabled" -ForegroundColor Green
$enableKcOnboarding = $false
}
}
}
#TODO : Check assigning multiple roles in one go.
if ($enableContributor) {
AssignRole -roleToAssign "Contributor"
} elseif ($enableAcmOnboarding) {
#Check and assign the connected machine onboarding role. the kuberenetes role is assigned later.
AssignRole -roleToAssign "Azure Connected Machine Onboarding"
}
if ($spCredReset) {
Write-Host "Resetting credentials.."
$servicePrincipal = (az ad sp credential reset --id $spApp.appId | ConvertFrom-Json)
if ($servicePrincipal) {
Write-Host "ServicePrincipal credentials reset successfully"
$savePassword = $true
} else {
Write-Host "ServicePrincipal reset failed"
az logout
exit -1
}
}
} else {
Write-Host "$spName not found. Creating.."
$spparams = @(
"--name", "$spName",
"--scopes", "$rguri"
)
if ($spContributorRole) {
$spparams += @("--role", "Contributor")
} else {
$spparams += @("--role", "Azure Connected Machine Onboarding")
}
$servicePrincipal = (az ad sp create-for-RBAC @spparams | ConvertFrom-Json)
if (!$servicePrincipal) {
Write-Host "Error: ServicePrincipal creation failed" -ForegroundColor Red
az logout
exit -1
}
$savePassword = $true
}
if ($enableKcOnboarding) {
#Assign the Kubernetes Cluster - Azure Arc Onboarding role to serviceprincipal too
AssignRole -roleToAssign "Kubernetes Cluster - Azure Arc Onboarding"
}
if ($savePassword) {
$aicfg | Add-Member -MemberType NoteProperty -Name 'Auth' -Value @{"ServicePrincipalId" = "$($servicePrincipal.appId)"; "Password" = "$($servicePrincipal.password)"} -Force
Write-Host "WARNING: The Service Principal password is stored in clear at $jsonFile" -ForegroundColor Yellow
}
$customLocationRPOID = (az ad sp list --filter "displayname eq 'Custom Locations RP'" --query "[?appDisplayName=='Custom Locations RP'].id" -o tsv)
$jsonContent.Azure | Add-Member -MemberType NoteProperty -Name 'CustomLocationOID' -Value $customLocationRPOID -Force
#Adding Arc config as per AKSEdge schema
$arcdata = @{
Location = $aicfg.Location
ResourceGroupName = $aicfg.ResourceGroupName
SubscriptionId = $aicfg.SubscriptionId
TenantId = $aicfg.TenantId
ClientId = $aicfg.Auth.ServicePrincipalId
ClientSecret = $aicfg.Auth.Password
ClusterName = ""
}
$ecFile = $jsonContent.AksEdgeConfigFile
if ($ecFile) {
$parentpath = Split-Path -Path $jsonFile -Parent
if ($ecFile.Contains("\")) {
$ecFile = Resolve-Path -Path $ecFile
} else {
$ecFile = Join-Path -Path $parentpath -ChildPath $ecFile
}
Write-Host "Updating $ecFile with Arc information"
if (Test-Path -Path $ecFile) {
$edgeCfg = Get-Content $ecFile | ConvertFrom-Json
$edgeCfg | Add-Member -MemberType NoteProperty -Name 'Arc' -Value $arcdata -Force
$edgeCfg | ConvertTo-Json -Depth 6 | Format-Json | Set-Content -Path "$ecFile" -Force
}
} else {
$jsonContent | Add-Member -MemberType NoteProperty -Name 'Arc' -Value $arcdata -Force
}
$jsonContent | ConvertTo-Json -Depth 6 | Format-Json | Set-Content -Path "$jsonFile" -Force
az logout
exit 0