|
#Requires -Version 5.1 -RunAsAdministrator |
|
using namespace System.IO |
|
using namespace System.Net |
|
|
|
#NOTE: The core code flow in is in the Main region at the bottom of the script. |
|
|
|
<# |
|
.SYNOPSIS |
|
This is a script that unifies the Azure Arc and AKS Edge Essentials installation process. You can use this script to connect a server to Azure Arc as well as AKS Edge Essentials. |
|
#> |
|
[CmdletBinding(SupportsShouldProcess)] |
|
param( |
|
#Tenant ID to deploy Azure Arc. Can be specified as a GUID or a domain name. |
|
[Parameter(Mandatory)] |
|
[string]$TenantId, |
|
|
|
#Subscription ID where Azure Arc should be deployed. |
|
[Parameter(Mandatory)] |
|
[string]$SubscriptionId, |
|
|
|
#Resource group where Arc and Kubernetes Arc will be created. Default is 'Arc' |
|
[ValidateNotNullOrEmpty()] |
|
[string]$ResourceGroup = 'arc', |
|
|
|
#Resource group to create Kubernetes Arc resources. If not provided will use the same as ResourceGroup |
|
[ValidateNotNullOrEmpty()] |
|
[string]$K3sResourceGroup = $ResourceGroup, |
|
|
|
# Azure Location where Arc resources will be created. Default is 'westus3' |
|
[ValidateNotNullOrEmpty()] |
|
[string]$Location = 'westus3', |
|
|
|
# Azure Cloud to use. Default is 'AzureCloud' |
|
[ValidateNotNullOrEmpty()] |
|
[string]$Cloud = 'AzureCloud', |
|
|
|
# Path where temporary files will be stored. It will be created if not existing |
|
[ValidateNotNullOrEmpty()] |
|
[string]$TempPath = $(Join-Path ([Path]::GetTempPath()) 'Arcify') |
|
) |
|
|
|
#region Variables |
|
|
|
#Azure Connected Machine Agent Path |
|
$SCRIPT:AzCMAgentPath = Join-Path ([Environment]::GetFolderPath('ProgramFiles')) 'AzureConnectedMachineAgent\azcmagent.exe' |
|
|
|
#endregion Variables |
|
|
|
function Assert-IsElevated { |
|
if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { |
|
Write-Error 'This script must be run as an Administrator.' |
|
} |
|
} |
|
|
|
function Initialize-TempPath ($TempPath) { |
|
if (-not (Test-Path $TempPath)) { |
|
Write-Verbose "Creating $TempPath for deployment." |
|
New-Item -ItemType Directory -Path $TempPath -Force | Out-Null |
|
} |
|
|
|
$TempPath = Resolve-Path $TempPath |
|
Write-Verbose "Verified Temporary Storage Path $TempPath exists." |
|
return $TempPath |
|
} |
|
|
|
function Assert-64Bit { |
|
if (-not [Environment]::Is64BitProcess) { |
|
Write-Error 'This script is not supported with 32-bit Windows PowerShell. Please start the 64 bit Windows PowerShell or PowerShell 7+ and try again.' |
|
} |
|
|
|
if (-not [Environment]::Is64BitOperatingSystem) { |
|
Write-Error 'The agent is not supported on 32 bit operating systems. Please run this script on a 64-bit operating system.' |
|
} |
|
} |
|
|
|
function Get-WebFile { |
|
<# |
|
.SYNOPSIS |
|
Download a file from the web |
|
#> |
|
param( |
|
[ValidateNotNullOrEmpty()] |
|
[string]$Uri, |
|
|
|
[ValidateNotNullOrEmpty()] |
|
[string]$OutFile |
|
) |
|
|
|
if (-not $IsCoreCLR) { |
|
#TLS1.2 is not enabled by default on WinPS, enable it |
|
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 |
|
# Disable progress for Invoke-WebRequest, it slows things down on 5.1 |
|
$CurrentProgressPreference = $ProgressPreference |
|
$ProgressPreference = 'SilentlyContinue' |
|
} |
|
|
|
try { |
|
Invoke-WebRequest -UseBasicParsing -Uri $Uri -TimeoutSec 30 -OutFile $OutFile |
|
} finally { |
|
if ($CurrentProgressPreference) { |
|
$ProgressPreference = $CurrentProgressPreference |
|
} |
|
} |
|
} |
|
|
|
<# |
|
.SYNOPSIS |
|
This function will install the Azure Connected Machine Agent if not already installed |
|
.OUTPUTS |
|
It will return the path to the agent executable. If the agent was already installed, it will return $null. If an error occurs, the error will be returned as a terminating error. |
|
#> |
|
function Install-AzConnectedMachineAgent { |
|
[CmdletBinding()] |
|
param( |
|
[ValidateNotNullOrEmpty()] |
|
[string]$AzCMAgentDownloadUri = 'https://aka.ms/azcmagent-windows', |
|
[ValidateNotNullOrEmpty()] |
|
[string]$TempPath = $SCRIPT:TempPath, |
|
#This is currently not supported to change from the default. |
|
[ValidateNotNullOrEmpty()] |
|
[string]$InstallPath = $SCRIPT:AzCMAgentPath, |
|
[string]$InstallLogPath = $(Join-Path $TempPath 'azcmagent-install.log'), |
|
#Specify this to not configure the agent to automatically update via Microsoft Update |
|
[switch]$NoAutoUpdate, |
|
#Dont delete the MSI after installation. Useful for minimizing downloads during testing/troubleshooting |
|
[switch]$NoCleanup |
|
) |
|
#TODO: Add Reinstall Support |
|
|
|
$ErrorActionPreference = 'Stop' |
|
|
|
$AzCMAgentPath = Join-Path $InstallPath 'azcmagent.exe' |
|
|
|
try { |
|
#Will Error if not present |
|
Resolve-Path $AzCMAgentPath |
|
Write-Verbose "Azure Connected Machine Agent already installed at $AzCMAgentPath." |
|
return $AzCMAgentPath |
|
} catch { |
|
Write-Verbose 'Azure Connected Machine Agent not found. Installing...' |
|
} |
|
|
|
$AzCmMsiPath = Join-Path $TempPath 'AzureConnectedMachineAgent.msi' |
|
|
|
try { |
|
# Download the installation package |
|
Write-Debug "Downloading Azure Connected Machine Agent from $AzCMAgentDownloadUri to $InstallPath" |
|
try { |
|
Get-WebFile -Uri $AzCMAgentDownloadUri -OutFile $AzCmMsiPath |
|
} catch { |
|
$PSItem.ErrorDetails = "Error downloading Azure Connected Machine Agent from $AzCMAgentDownloadUri to $AzCmMsiPath`: $PSItem" |
|
Write-Error $PSItem |
|
} |
|
|
|
# Install the Azure Arc Agent |
|
Write-Debug "Installing Azure Connected Machine Agent from $AzCmMsiPath" |
|
|
|
$installArgs = @( |
|
'/i' |
|
$AzCmMsiPath |
|
'/qn' |
|
'/l*v' |
|
$InstallLogPath |
|
'REBOOT=ReallySuppress' |
|
) |
|
$msiOutput = & $AzCmMsiPath @installArgs |
|
$msiResultCode = $LASTEXITCODE |
|
if ($msiOutput) { |
|
Write-Debug "MSI Output: $msiOutput" |
|
} |
|
#Configure Automatic Updates |
|
#Ref: https://learn.microsoft.com/en-us/azure/azure-arc/servers/manage-agent?tabs=windows |
|
if (-not $NoAutoUpdate) { |
|
$ServiceManager = (New-Object -com 'Microsoft.Update.ServiceManager') |
|
$ServiceID = '7971f918-a847-4430-9279-4a52d1efe18d' |
|
$ServiceManager.AddService2($ServiceId, 7, '') |
|
} |
|
|
|
switch ($msiResultCode) { |
|
0 { |
|
try { |
|
Resolve-Path $AzCMAgentPath |
|
} catch { |
|
Write-Error "Error installing Azure Connected Machine Agent from $AzCmMsiPath`: Installer Reported Success but Azure Connected Machine Agent not found at $AzCMAgentPath. See log at $InstallLogPath for details." |
|
} |
|
|
|
Write-Verbose 'Azure Connected Machine Agent installed successfully.' |
|
return $AzCMAgentPath |
|
} |
|
1641 { |
|
Write-Warning 'Azure Connected Machine Agent installation initiated. Reboot required.' |
|
} |
|
3010 { |
|
Write-Warning 'Azure Connected Machine Agent installation initiated. Reboot required.' |
|
} |
|
default { |
|
Write-Error "Error installing Azure Connected Machine Agent from $AzCmMsiPath`: $msiOutput. See log at $InstallLogPath for details." -Category InvalidOperation -TargetObject $AzCmMsiPath -RecommendedAction 'Check the log at $InstallLogPath for more details.' -ErrorId 'AzCmAgentInstallError' - |
|
} |
|
} |
|
} catch { |
|
$PSItem.ErrorDetails = "Error installing Azure Connected Machine Agent from $AzCmMsiPath`: $PSItem" |
|
Write-Error $PSItem |
|
} finally { |
|
#Clean up the MSI |
|
if ((Test-Path $AzCmMsiPath) -and -not $NoCleanup) { |
|
Remove-Item $AzCmMsiPath -Force -Confirm:$False |
|
} |
|
} |
|
} |
|
|
|
function Assert-AzConnectedMachineAgentConnectivity { |
|
param( |
|
[ValidateNotNullOrEmpty()] |
|
[string]$Location = $SCRIPT:Location, |
|
[ValidateNotNullOrEmpty()] |
|
[string]$Cloud = $SCRIPT:Cloud, |
|
[ValidateNotNullOrEmpty()] |
|
[string]$AzCMAgentPath = $SCRIPT:AzCMAgentPath |
|
) |
|
$AzCMParams = @( |
|
'check' |
|
'--json' |
|
'--location' |
|
$Location |
|
'--cloud' |
|
$Cloud |
|
) |
|
|
|
Write-Verbose "Testing Azure Connected Machine Agent connectivity to $Location in $Cloud." |
|
Write-Debug "Executing Command: $AzCMAgentPath $AzCMParams" |
|
$connectivityResult = & $AzCMAgentPath @AzCMParams | ConvertFrom-Json |
|
|
|
foreach ($url in $connectivityResult.PSObject.Properties.Name) { |
|
$urlStatus = $connectivityResult.$url |
|
if ($true -eq $urlStatus.reachable) { |
|
Write-Debug "Azure Connected Machine Agent can reach $url with $($url.tls)" |
|
continue |
|
} |
|
#EA Continue is used so we can report all errors at once |
|
Write-Error -ErrorAction Continue "Azure Connected Machine Agent cannot reach $url with $($url.tls): $($urlStatus | ConvertTo-Json)" |
|
$errorDetected = $true |
|
} |
|
|
|
if ($errorDetected) { Write-Error "Connectivity Errors trying to reach $url" } |
|
} |
|
|
|
function Connect-AzConnectedMachineAgent { |
|
<# |
|
.SYNOPSIS |
|
Connect the Azure Connected Machine Agent to Azure Arc |
|
#> |
|
[CmdletBinding(DefaultParameterSetName = 'Interactive')] |
|
param( |
|
[ValidateNotNullOrEmpty()] |
|
[string]$AzCMAgentPath = $SCRIPT:AzCMAgentPath, |
|
[ValidateNotNullOrEmpty()] |
|
[string]$TenantId, |
|
[ValidateNotNullOrEmpty()] |
|
[string]$SubscriptionId, |
|
[ValidateNotNullOrEmpty()] |
|
[string]$Location, |
|
[ValidateNotNullOrEmpty()] |
|
[string]$ResourceGroupName, |
|
[ValidateNotNullOrEmpty()] |
|
[string]$Cloud = 'AzureCloud', |
|
[ValidateNotNullOrEmpty()] |
|
[string]$CorrelationId, |
|
[ValidateNotNullOrEmpty()] |
|
[Parameter(Mandatory, ParameterSetName = 'DeviceCode')] |
|
[switch]$UseDeviceCode, |
|
[Parameter(Mandatory, ParameterSetName = 'ServicePrincipal')] |
|
[PSCredential]$ServicePrincipalCredential |
|
) |
|
|
|
$ErrorActionPreference = 'Stop' |
|
|
|
$azcmAgentArgs = @( |
|
'connect' |
|
'--tenant-id' |
|
$TenantId |
|
'--subscription-id' |
|
$SubscriptionId |
|
'--location' |
|
$Location |
|
'--resource-group' |
|
$ResourceGroupName |
|
'--cloud' |
|
$Cloud |
|
'--correlation-id' |
|
$CorrelationId |
|
) |
|
|
|
if ($ServicePrincipalCredential) { |
|
$azcmAgentArgs += '--service-principal-id' |
|
$azcmAgentArgs += $ServicePrincipalCredential.UserName |
|
$azcmAgentArgs += '--service-principal-secret' |
|
$azcmAgentArgs += $ServicePrincipalCredential.GetNetworkCredential().Password |
|
} elseif ($UseDeviceCode) { |
|
$azcmAgentArgs += '--use-device-code' |
|
} |
|
|
|
Write-Verbose "Running $AzCMAgentPath $azcmAgentArgs" |
|
|
|
# Run connect command |
|
& $AzCMAgentPath @azcmAgentArgs |
|
} |
|
|
|
|
|
|
|
|
|
#region Main |
|
|
|
$ErrorActionPreference = 'Stop' |
|
|
|
#Override Debug Inquire to avoid prompts (usually 5.1 only) |
|
if ($DebugPreference -eq 'Inquire') {$DebugPreference = 'Continue'} |
|
|
|
Assert-IsElevated |
|
Assert-64Bit |
|
|
|
$SCRIPT:TempPath = Initialize-TempPath $TempPath |
|
|
|
Install-AzConnectedMachineAgent |
|
|
|
Assert-AzConnectedMachineAgentConnectivity |
|
|
|
#endregion Main |