tools/modules/AksEdgeDeploy/AksEdgeDeploy.psm1 (1,084 lines of code) (raw):
<#
.DESCRIPTION
This module contains the functions related to AksEdge setup on a PC
#>
#Requires -RunAsAdministrator
if (! [Environment]::Is64BitProcess) {
Write-Host "Error: Run this in 64bit Powershell session" -ForegroundColor Red
exit -1
}
# dot source the arc module
. $PSScriptRoot\AksEdgeDeploy-AES.ps1
. $PSScriptRoot\AksEdgeDeploy-AEC.ps1
#Hashtable to store session information
$aideSession = @{
HostPC = @{"FreeMem" = 0; "TotalMem" = 0; "FreeDisk" = 0; "TotalDisk" = 0; "TotalCPU" = 0; "Name" = $null }
HostOS = @{"OSName" = $null; "Name" = $null; "BuildNr" = $null; "Version" = $null; "IsServerSKU" = $false; "IsVM" = $false; "IsAzureVM" = $false }
AKSEdge = @{"Product" = $null; "Version" = $null }
UserConfig = $null
UserConfigFile = $null
ReadFromFile = $false
}
New-Variable -Option Constant -ErrorAction SilentlyContinue -Name aksedgeProductPrefix -Value "AKS Edge Essentials"
New-Variable -Option Constant -ErrorAction SilentlyContinue -Name aksedgeProducts -Value @{
"AKS Edge Essentials - K8s" = "https://aka.ms/aks-edge/k8s-msi"
"AKS Edge Essentials - K3s" = "https://aka.ms/aks-edge/k3s-msi"
}
New-Variable -Option Constant -ErrorAction SilentlyContinue -Name WindowsInstallUrl -Value "https://aka.ms/aks-edge/windows-node-zip"
New-Variable -Option Constant -ErrorAction SilentlyContinue -Name WindowsInstallFiles -Value @("AksEdgeWindows-v1.7z.001", "AksEdgeWindows-v1.7z.002", "AksEdgeWindows-v1.7z.003","AksEdgeWindows-v1.7z.004", "AksEdgeWindows-v1.exe")
function Get-AideHostPcInfo {
<#
.SYNOPSIS
Prints the relevant HostPC information on the console output.
.DESCRIPTION
Prints the relevant HostPC information such as OS information, available Free/Total CPU/Memory on the console output.
.OUTPUTS
None
.EXAMPLE
Get-AideHostPcInfo
#>
$pOS = Get-CimInstance Win32_OperatingSystem
$UBR = (Get-ItemPropertyValue "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -Name UBR)
$aideSession.HostOS.OSName = $pOS.Caption
$aideSession.HostOS.BuildNr = $pOS.BuildNumber
$aideSession.HostOS.Version = "$($pOS.Version).$UBR"
Write-Host "HostOS`t: $($pOS.Caption)($($pOS.OperatingSystemSKU)) `nVersion`t: $($aideSession.HostOS.Version) `nLang`t: $($pOS.MUILanguages) `nName`t: $($pOS.CSName)"
#ProductTypeDomainController -Value 2 , #ProductTypeServer -Value 3
$aideSession.HostPC.Name = $pOS.CSName
$aideSession.HostOS.IsServerSKU = ($pOS.ProductType -eq 2 -or $pOS.ProductType -eq 3)
$aideSession.HostPC.FreeMem = [Math]::Round($pOS.FreePhysicalMemory / 1MB) # convert kilo bytes to GB
$pCS = Get-CimInstance -class Win32_ComputerSystem
$aideSession.HostPC.TotalMem = [Math]::Round($pCS.TotalPhysicalMemory / 1GB)
$aideSession.HostPC.TotalCPU = $pCS.numberoflogicalprocessors
Write-Host "Total CPUs`t`t: $($aideSession.HostPC.TotalCPU)"
Write-Host "Free RAM / Total RAM`t: $($aideSession.HostPC.FreeMem) GB / $($aideSession.HostPC.TotalMem) GB"
$disk = Get-CimInstance Win32_LogicalDisk -Filter $("DeviceID='C:'") | Select-Object Size, FreeSpace
$aideSession.HostPC.FreeDisk = [Math]::Round($disk.Freespace / 1GB) # convert bytes into GB
$aideSession.HostPC.TotalDisk = [Math]::Round($disk.Size / 1GB) # convert bytes into GB
Write-Host "Free Disk / Total Disk`t: $($aideSession.HostPC.FreeDisk) GB / $($aideSession.HostPC.TotalDisk) GB"
if ((Get-CimInstance Win32_BaseBoard).Product -eq 'Virtual Machine') {
$aideSession.HostOS.IsVM = $true
Write-Host "Running as a virtual machine " -NoNewline
$guestAgent = Get-Service WindowsAzureGuestAgent -ErrorAction SilentlyContinue
if ($guestAgent) {
$aideSession.HostOS.IsAzureVM = $true
Write-Host "in Azure environment " -NoNewline
if ($guestAgent.Status -eq 'Running') {
try {
$vmInfo = Get-AzureVMInfo
Write-Host "(Name= $($vmInfo.name)" -NoNewline
Write-Host "vmSize= $($vmInfo.vmSize)" -NoNewline
Write-Host "offer= $($vmInfo.offer)" -NoNewline
Write-Host "sku= $($vmInfo.sku) )" -NoNewline
}
catch {
# Ignore exception
Write-Host $_
}
}
}
if ($pCS.HypervisorPresent) {
Write-Host "with Nested Hyper-V enabled"
#(Get-VMProcessor -VM $vm).ExposeVirtualizationExtensions
} else {
Write-Host "without Nested Hyper-V" -ForegroundColor Red
}
}
$nwadapters = Get-NetAdapter -Physical
if ($nwadapters) {
Write-Host "Network Adapters`t: " -NoNewline
foreach ($nw in $nwadapters) { Write-Host "$($nw.Name)($($nw.Status))," -NoNewline }
Write-Host ""
} else { Write-Host "Network Adapters`t: None" -ForegroundColor Red }
$vpn = Get-VpnConnection
if ($vpn) {
Write-Host "VPN Profile`t`t: $($vpn.ConnectionStatus)"
} else { Write-Host "VPN Profile`t`t: None" }
}
function Get-AideInfra {
<#
.SYNOPSIS
Returns a coded string for the host OS information.
.DESCRIPTION
Returns a coded string for the host OS information including whether its Azure VM or regular VM
.OUTPUTS
String
.EXAMPLE
Get-AideInfra
#>
$replacements = [ordered]@{
"Microsoft Windows" = "Win"
"IoT Enterprise" = "IoT"
"Enterprise" = "Ent"
"Server" = "Ser"
"Datacenter" = "DC"
"Standard" = ""
"Evaluation" = ""
"2019" = ""
"2022" = ""
" " = ""
}
$Name = $aideSession.HostOS.OSName
foreach ($key in $replacements.Keys) {
$Name = $Name -replace $key, $replacements[$key]
}
$Name += "-$($aideSession.HostOS.BuildNr)"
if ($aideSession.HostOS.IsAzureVM) {
$Name += "-AVM"
} elseif ($aideSession.HostOS.IsVM) {
$Name += "-VM"
}
return $Name
}
function Get-AideUserConfig {
<#
.SYNOPSIS
Returns the PSCustomObject of the UserConfig Json.
.DESCRIPTION
Returns the PSCustomObject of the UserConfig Json including the embedded AksEdge config data.
.OUTPUTS
PSCustomObject
.EXAMPLE
Get-AideUserConfig
#>
if ($null -eq $aideSession.UserConfig) {
Write-Host "Error: Aide UserConfig is not set." -ForegroundColor Red
}
return $aideSession.UserConfig
}
function Get-AideAksEdgeConfig {
if ($null -eq $aideSession.UserConfig) {
Write-Host "Error: Aide UserConfig is not set." -ForegroundColor Red
}
return $aideSession.UserConfig.AksEdgeConfig
}
function Read-AideUserConfig {
<#
.SYNOPSIS
Reads from the User Config json file and updates the PSCustomObject cache.
.DESCRIPTION
Reads from the User Config json file and updates the PSCustomObject cache. It also refreshes the AksEdge config data if it was read from AksEdgeConfigFile.
.OUTPUTS
Boolean
True if successfully read.
.EXAMPLE
Read-AideUserConfig
#>
if ($aideSession.UserConfigFile) {
$jsonContent = Get-Content "$($aideSession.UserConfigFile)" | ConvertFrom-Json
if ($jsonContent.AksEdgeProduct) {
$aideSession.UserConfig = $jsonContent
#if there is no AksEdgeConfig object or if it was previously read, re-read from file
if ((-not $jsonContent.AksEdgeConfig) -or (($jsonContent.AksEdgeConfig) -and ($aideSession.ReadFromFile))) {
#there is no embedded aksedge config, so read it from file
$aksfile = $jsonContent.AksEdgeConfigFile
if (($aksfile) -and (Test-Path -Path $aksfile)) {
$aksconfig = Get-Content $aksfile | ConvertFrom-Json
$aideSession.UserConfig | Add-Member -MemberType NoteProperty -Name 'AksEdgeConfig' -Value $aksconfig -Force
$aideSession.ReadFromFile = $true
}
}
$upgraded = UpgradeJsonFormat $jsonContent
if ($upgraded) { Save-AideUserConfig }
return $true
} else {
Write-Host "Error: Incorrect json content" -ForegroundColor Red
}
} else { Write-Host "Error: Aide UserConfigFile not configured" -ForegroundColor Red }
return $false
}
function Set-AideUserConfig {
<#
.SYNOPSIS
Sets the user config PSCustomObject with either jsonFile or jsonString parameter.
.DESCRIPTION
Validates and sets the user config PSCustomObject with either jsonFile or jsonString parameter. Either one must be specified.
.OUTPUTS
Boolean
True if successfully set.
.PARAMETER jsonFile
File path for the json configuration file (aide-userconfig.json), based on the aide-ucschema.json schema.
.PARAMETER jsonString
Json herestring based on the aide-ucschema.json schema.
.EXAMPLE
Set-AideUserConfig -jsonFile .\aide-userconfig.json
#>
Param
(
[String]$jsonFile,
[String]$jsonString
)
if (-not [string]::IsNullOrEmpty($jsonString)) {
$jsonContent = $jsonString | ConvertFrom-Json
if ($jsonContent.AksEdgeProduct) {
$aideSession.UserConfig = $jsonContent
if (-not $jsonContent.AksEdgeConfig) {
#there is no embedded aksedge config, so read it from file
$aksfile = $jsonContent.AksEdgeConfigFile
if (($aksfile) -and (Test-Path -Path $aksfile)) {
$aksconfig = Get-Content $aksfile | ConvertFrom-Json
$aideSession.UserConfig | Add-Member -MemberType NoteProperty -Name 'AksEdgeConfig' -Value $aksconfig -Force
$aideSession.ReadFromFile = $true
}
}
$upgraded = UpgradeJsonFormat $jsonContent
if ($upgraded) { Save-AideUserConfig }
} else {
Write-Host "Error: Incorrect jsonString" -ForegroundColor Red
return $false
}
} else {
if (($jsonFile) -and -not(Test-Path -Path "$jsonFile" -PathType Leaf)) {
Write-Host "Error: Incorrect jsonFile " -ForegroundColor Red
return $false
}
Write-Verbose "Loading $jsonFile.."
$aideSession.UserConfigFile = "$jsonFile"
return Read-AideUserConfig
}
return $true
}
function UpgradeJsonFormat {
Param(
[PSCustomObject] $jsonObj
)
$retval = $false
$azCfg = $jsonObj.Azure
if ($azCfg.Auth.spId) {
$newAuth = @{
ServicePrincipalId = $azCfg.Auth.spId
Password = $azCfg.Auth.password
}
$azCfg | Add-Member -MemberType NoteProperty -Name 'Auth' -Value $newAuth -Force
$retval = $true
}
$arcdata = @{
Location = $azCfg.Location
ResourceGroupName = $azCfg.ResourceGroupName
SubscriptionId = $azCfg.SubscriptionId
TenantId = $azCfg.TenantId
ClientId = $azCfg.Auth.ServicePrincipalId
ClientSecret = $azCfg.Auth.Password
}
if ($azCfg.ClusterName) {
$arcdata += @{ClusterName = $azCfg.ClusterName}
}
#upgrade from public preview format to GA format
$edgeCfg = $jsonObj.AksEdgeConfig
if ([version]$edgeCfg.SchemaVersion -gt [version]"1.4") {
if (($azCfg.Auth.Password) -and ([string]::IsNullOrEmpty($($edgeCfg.Arc.ClientSecret)))) {
#Copy over the Azure parameters to Arc section
$edgeCfg | Add-Member -MemberType NoteProperty -Name 'Arc' -Value $arcdata -Force
$retval = $true
}
return $retval
}
$clustertype = "ScalableCluster"
if ($edgeCfg.DeployOptions.SingleMachineCluster) {
$clustertype = "SingleMachineCluster"
$smcluster = $true
}
$nodetype = $edgeCfg.DeployOptions.NodeType
$newEdgeConfig = New-AksEdgeConfig -DeploymentType $clustertype -NodeType $nodetype
#init section
if ($smcluster) {
$newEdgeConfig.Init.ServiceIPRangeSize = $edgeCfg.Network.ServiceIPRangeSize
} else {
$newEdgeConfig.Init.ServiceIPRangeStart = $edgeCfg.Network.ServiceIPRangeStart
#supporting only Class C address range.. so ip4 prefix 24
$startip = $edgeCfg.Network.ServiceIPRangeStart
$endip = $edgeCfg.Network.ServiceIPRangeEnd
$newEdgeConfig.Init.ServiceIPRangeSize = ($endip.Split(".")[3]) - ($startip.Split(".")[3])
}
#arc section
$newEdgeConfig | Add-Member -MemberType NoteProperty -Name 'Arc' -Value $arcdata -Force
#network section
$edgeCfgNW = $edgeCfg.Network
$nwdata = @{
NetworkPlugin = $edgeCfg.DeployOptions.NetworkPlugin
InternetDisabled = [bool]$edgeCfgNW.InternetDisabled
Proxy = $edgeCfgNW.Proxy
}
if (-not $smcluster) {
$nwdata = $nwdata + @{
ControlPlaneEndpointIp = $edgeCfgNW.ControlPlaneEndpointIp
Ip4GatewayAddress = $edgeCfgNW.Ip4GatewayAddress
Ip4PrefixLength = $edgeCfgNW.Ip4PrefixLength
DnsServers = $edgeCfgNW.DnsServers
SkipAddressFreeCheck = [bool]$edgeCfgNW.SkipAddressFreeCheck
}
}
$newEdgeConfig | Add-Member -MemberType NoteProperty -Name 'Network' -Value $nwdata -Force
#user section
$newEdgeConfig.User.AcceptEula = $edgeCfg.EndUser.AcceptEula
$newEdgeConfig.User.AcceptOptionalTelemetry = $edgeCfg.EndUser.AcceptOptionalTelemetry
#machines section
$machinenode = $newEdgeConfig.Machines[0]
$mtu = 0
if ($machinenode.LinuxNode) {
#TODO: extra Mtu field to be removed.
$machinenode.LinuxNode = $edgeCfg.LinuxVm
$mtu = [int]$edgeCfg.LinuxVm.Mtu
}
if ($machinenode.WindowsNode) {
$machinenode.WindowsNode = $edgeCfg.WindowsVm
$mtu = [int]$edgeCfg.WindowsVm.Mtu
}
if ($machinenode.NetworkConnection) {
$nwnode = $machinenode.NetworkConnection
$nwnode.AdapterName = $edgeCfg.Network.VSwitch.AdapterName
$nwnode.Mtu = $mtu
}
$jsonObj | Add-Member -MemberType NoteProperty -Name 'AksEdgeConfig' -Value $newEdgeConfig -Force
return $true
}
function Save-AideUserConfig {
<#
.DESCRIPTION
Saves the configuration to the JSON file
#>
if ($aideSession.UserConfigFile) {
$ObjToSave = $aideSession.UserConfig
if ($aideSession.ReadFromFile) {
#we dont expect programatic changes to the aide-userconfig. Only in AksEdgeConfig
$ObjToSave.AksEdgeConfig | ConvertTo-Json -Depth 6 | Format-AideJson | Set-Content -Path "$($ObjToSave.AksEdgeConfigFile)" -Force
} else {
$ObjToSave | ConvertTo-Json -Depth 6 | Format-AideJson | Set-Content -Path "$($aideSession.UserConfigFile)" -Force
}
} else {
Write-Verbose "Error: Aide UserConfigFile not configured"
}
}
function Test-IsAzureVM {
return $aideSession.HostOS.IsAzureVM
}
function Get-AzureVMInfo {
if (!$aideSession.HostOS.IsAzureVM) {
Write-Host "Error: Host is not an Azure VM" -ForegroundColor Red
return $null
}
$vmInfo = @{}
#from https://github.com/microsoft/azureimds/blob/master/IMDSSample.ps1
$ImdsServer = "http://169.254.169.254"
$apiVersion = "2021-02-01"
$InstanceUri = $ImdsServer + "/metadata/instance?api-version=$apiVersion"
$Proxy = New-Object System.Net.WebProxy
$WebSession = New-Object Microsoft.PowerShell.Commands.WebRequestSession
$WebSession.Proxy = $Proxy
$response = (Invoke-RestMethod -Headers @{"Metadata" = "true"} -Method GET -Uri $InstanceUri -WebSession $WebSession)
$vmInfo.Add("Name", $response.compute.name)
$vmInfo.Add("vmSize", $response.compute.vmSize)
$vmInfo.Add("offer", $response.compute.offer)
$vmInfo.Add("sku", $response.compute.sku)
return $vmInfo
}
function Disable-WindowsAzureGuestAgent {
if (!$aideSession.HostOS.IsAzureVM) {
Write-Host "Error: Host is not an Azure VM" -ForegroundColor Red
return
}
$service = Get-Service WindowsAzureGuestAgent -ErrorAction SilentlyContinue
if ($service -and ($($service.Status) -ne 'Stopped')) {
Set-Service WindowsAzureGuestAgent -StartupType Disabled -Verbose
Stop-Service WindowsAzureGuestAgent -Force -Verbose
New-NetFirewallRule -Name BlockAzureIMDS -DisplayName "Block access to Azure IMDS" -Enabled True -Profile Any -Direction Outbound -Action Block -RemoteAddress 169.254.169.254
} else {
Write-Host "WindowsAzureGuestAgent is already stopped"
}
}
function Test-AideUserConfigNetwork {
<#
.DESCRIPTION
Checks the AksEdge user configuration needed for AksEdge Network setup
#>
$errCnt = 0
$akseeCfg = Get-AideAksEdgeConfig
if ($akseeCfg.DeploymentType -eq "SingleMachineCluster") {
Write-Host "SingleMachine cluster uses internal switch. Nothing to test."
return $true
} else {
$aideConfig = Get-AideUserConfig
if ([string]::IsNullOrEmpty($aideConfig.VSwitch.Name)) {
Write-Host "Error: VSwitch.Name is required." -ForegroundColor Red
$errCnt += 1
}
if ([string]::IsNullOrEmpty($aideConfig.VSwitch.AdapterName)) {
Write-Host "Error: VSwitch.AdapterName is required for External switch" -ForegroundColor Red
$errCnt += 1
} else {
$nwadapters = (Get-NetAdapter -Physical) | Where-Object { $_.Status -eq "Up" }
if ($nwadapters.Name -notcontains ($aideConfig.VSwitch.AdapterName)) {
Write-Host "Error: $($aideConfig.VSwitch.AdapterName) not found. External switch creation will fail." -ForegroundColor Red
Write-Host "Available NetAdapters : ($nwadapters | Out-String)"
$errCnt += 1
}
}
}
$retval = Test-AksEdgeNetworkParameters -JsonConfigString ($akseeCfg | ConvertTo-Json -Depth 6)
return ($retval -and ($errCnt -eq 0))
}
function Test-AideUserConfigInstall {
$errCnt = 0
$aideConfig = Get-AideUserConfig
Write-Host "`n--- Verifying AksEdge Install Configuration..."
# 1) Check the product requested is valid
if ($aksedgeProducts.ContainsKey($aideConfig.AksEdgeProduct)) {
if ($aideSession.AKSEdge.Product) {
#if already installed, check if they match
if ($aideSession.AKSEdge.Product -ne $aideConfig.AksEdgeProduct) {
Write-Host "Error: Installed product $($aideSession.AKSEdge.Product) does not match requested product $($aideConfig.AksEdgeProduct)." -ForegroundColor Red
$errCnt += 1
} else { Write-Host "* $($aideConfig.AksEdgeProduct) is installed" -ForegroundColor Green }
} else { Write-Host "* $($aideConfig.AksEdgeProduct) to be installed" -ForegroundColor Green }
} else {
Write-Host "Error: Incorrect aksedgeProduct." -ForegroundColor Red
Write-Host "Supported products: [$($aksedgeProducts.Keys -join ',' )]"
$errCnt += 1
}
$windowsRequired = ($null -ne $($aideConfig.AksEdgeConfig.Machines.WindowsNode))
# 2) Check if ProductUrl is valid if specified
if (-not [string]::IsNullOrEmpty($aideConfig.AksEdgeProductUrl)) {
if (Test-Path -Path $aideConfig.AksEdgeProductUrl) {
$isOk = $true
if($windowsRequired) {
$filepath = (Resolve-Path -Path $aideConfig.AksEdgeProductUrl).Path | Split-Path -Parent
if (Test-Path "$filepath\AksEdgeWindows-*.zip") {
Write-Host "Found Windows Install zip.."
} else {
Write-Host "$filepath\\AksEdgeWindows-*.zip not found. Looking for unzipped files." -ForegroundColor Yellow
foreach ($file in $WindowsInstallFiles) {
if (!(Test-Path -Path "$filepath\$file")) {
Write-Host "Error: $filepath\$file not found. Cannot deploy Windows Node." -ForegroundColor Red
$errCnt += 1; $isOk = $false
}
}
}
}
if ($isOk) { Write-Host "Installing from local path - Ok" }
} else {
if (-not ([system.uri]::IsWellFormedUriString($aideConfig.AksEdgeProductUrl, [System.UriKind]::Absolute))) {
Write-Host "Error: aksedgeProductUrl is incorrect. $($aideConfig.AksEdgeProductUrl)." -ForegroundColor Red
$errCnt += 1
}
}
}
# 3) Check if the install options are proper
$InstallOptions = $aideConfig.InstallOptions
if ($InstallOptions) {
$installOptItems = @("InstallPath", "VhdxPath")
foreach ($item in $installOptItems) {
$path = $InstallOptions[$item]
if (-not [string]::IsNullOrEmpty($path) -and
(-not (Test-Path -Path $path -IsValid))) {
Write-Host "Error: Incorrect item. : $path" -ForegroundColor Red
$errCnt += 1
}
}
}
if ($errCnt) {
Write-Host "$errCnt errors found in the Install Configuration. Fix errors before Install" -ForegroundColor Red
} else {
Write-Host "*** No errors found in the Install Configuration." -ForegroundColor Green
}
return ($errCnt -eq 0)
}
function Test-AideUserConfigDeploy {
<#
.DESCRIPTION
Checks the AksEdge user configuration needed for AksEdge VM deployment
Return $true if no blocking errors are found, and $false otherwise
#>
$errCnt = 0
$akseeCfg = Get-AideAksEdgeConfig
$euCfg = $akseeCfg.User
Write-Host "`n--- Verifying AksEdge VM Deployment Configuration..."
# 1) Check Mandatory configuration EULA
Write-Host "--- Verifying EULA..."
if ($euCfg.AcceptEula) {
Write-Host "* EULA accepted." -ForegroundColor Green
} else {
Write-Host "Error: Missing/incorrect mandatory EULA acceptance. Set AcceptEula true for remote deployment" -ForegroundColor Red
$errCnt += 1
}
if ($euCfg.AcceptOptionalTelemetry) {
Write-Host "* Optional telemetry accepted." -ForegroundColor Green
}
if ($errCnt) {
Write-Host "$errCnt errors found in the Deployment Configuration. Fix errors before deployment" -ForegroundColor Red
} else {
Write-Host "*** No errors found in the Deployment Configuration." -ForegroundColor Green
}
return ($errCnt -eq 0)
}
function Test-AideUserConfig {
<#
.SYNOPSIS
Validates the user config PSCustomObject for correctness and completeness.
.DESCRIPTION
Validates the user config PSCustomObject for correctness and completeness. Also validates if the required virtual switch is available.
.OUTPUTS
Boolean
True if successfull.
.EXAMPLE
Test-AideUserConfig
#>
$installResult = Test-AideUserConfigInstall
$deployResult = Test-AideUserConfigDeploy
$arcResult = Test-AideArcUserConfig
return ($installResult -and $deployResult -and $arcResult)
}
function Test-AideMsiInstall {
<#
.SYNOPSIS
Validates if the requested AksEdge Msi flavour is installed.
.DESCRIPTION
Validates if the requested AksEdge Msi flavour is installed. The Switch -Install when specified will install the Msi if not found.
It will also load the AksEdge module into the active PowerShell session.
.OUTPUTS
Boolean
True if successfull.
.PARAMETER Install
Switch parameter , to install the Msi if not found.
.EXAMPLE
Test-AideMsiInstall
#>
Param
(
[Switch] $Install
)
$aksedgeVersion = Get-AideMsiVersion
if ($null -eq $aksedgeVersion) {
if (!$Install) { return $false }
if (-not (Install-AideMsi)) { return $false }
}
$mod = Get-Module -Name AksEdge
#check if module is loaded
if (!$mod) {
Write-Host "Loading AksEdge module.." -ForegroundColor Cyan
Import-Module -Name AksEdge -Force -Global
}
$version = (Get-Module -Name AksEdge).Version.ToString()
Write-Host "AksEdge version `t: $version"
return $true
}
function Test-AideLinuxVmRun {
<#
.SYNOPSIS
Tests if the AksEdge Linux VM is running.
.DESCRIPTION
Tests if the AksEdge Linux VM is running.
.OUTPUTS
Boolean
True if the VM is running.
.EXAMPLE
Test-AideLinuxVmRun
#>
$retval = $false
if ($aideSession.HostOS.IsServerSKU) {
$vm = Get-VM | Where-Object { $_.Name -like '*ledge' }
if ($vm -and ($vm.State -ieq 'Running')) { $retval = $true }
} else {
$retval = (hcsdiag list) | ConvertFrom-String -Delimiter "," -PropertyNames Type, State, Id, Name
$wssd = $retval | Where-Object { $_.Name.Trim() -ieq 'wssdagent' } | Select-Object -Last 1
if ($wssd -and ($wssd.State.Trim() -ieq 'Running')) { $retval = $true }
}
return $retval
}
New-Alias -Name mars -Value Invoke-AideLinuxVmShell
function Invoke-AideLinuxVmShell {
<#
.SYNOPSIS
Invokes the AksEdge Linux VM Shell
.DESCRIPTION
Invokes the AksEdge Linux VM Shell
.OUTPUTS
None
Launches into the Shell
.EXAMPLE
Invoke-AideLinuxVmShell
#>
$provider = (Get-ItemPropertyValue "HKLM:\SOFTWARE\Microsoft\AksEdge" -Name wssdProvider)
if ($provider -ne 'hcs') {
$env:WSSD_CONFIG_PATH="c:\programdata\aksedge\protected\.wssd\cloudconfig"
$LinuxVmTag="9f0ea5f3-5769-47e3-b504-2afacd1fef0f"
$IdLine = & 'C:\Program Files\aksedge\nodectl' compute vm list --query "[?tags.keys(@).contains(@,'$LinuxVmTag')]" | Select-String -Pattern "ledge-id:"
$Id = ($IdLine -split ":")[1].Trim()
Write-Host "Linux VM's VsockId = $Id"
& ssh.exe -o ProxyCommand="C:\Program Files\AksEdge\vsocknc.exe %h %p" -i C:\ProgramData\AksEdge\protected\.sshkey\id_ecdsa "aksedge-user@$Id"
} else {
$retval = (hcsdiag list) | ConvertFrom-String -Delimiter "," -PropertyNames Type, State, Id, Name
$wssd = $retval | Where-Object { $_.Name.Trim() -ieq 'wssdagent' } | Select-Object -Last 1
if ($wssd -and ($wssd.State.Trim() -ieq 'Running')) {
if ($args) {
hcsdiag console $wssd.ID.Trim() $args
} else {
hcsdiag console $wssd.ID.Trim() su aksedge-user
}
} else {
Write-Host "Error: VM is not deployed or VM is not running" -ForegroundColor Red
}
}
}
function Test-AideDeployment {
<#
.SYNOPSIS
Checks if there is a AksEdge deployment on the machine.
.DESCRIPTION
Checks if there is a AksEdge deployment on the machine. It looks for the .vhdx files created for the Linux or Windows VMs.
.OUTPUTS
Boolean
True if vhdx file is found.
.EXAMPLE
Test-AideDeployment
#>
$VhdxPath = "C:\\Program Files\\AksEdge"
$aideConfig = Get-AideUserConfig
if ($aideConfig.InstallOptions.VhdxPath) {
$VhdxPath = $aideConfig.InstallOptions.VhdxPath
}
$retval = $false
if (Get-ChildItem -Path $VhdxPath -Include *ledge.vhdx, *Image.vhdx -Recurse -ErrorAction SilentlyContinue) {
$retval = $true
}
return $retval
}
function Install-AideMsi {
<#
.SYNOPSIS
Checks and installs AksEdge Msi flavour specified in the aide-userconfig.json.
.DESCRIPTION
Checks and installs AksEdge Msi flavour specified in the aide-userconfig.json. When the AksEdgeProduct is specified, it installs the latest available version
using the aka.ms links. When the AksEdgeProductUrl is specified, it installs from that specific Url. The Url can also be a network file share.
.OUTPUTS
Boolean
True if installed successfully.
.EXAMPLE
Install-AideMsi
#>
#TODO : Add Force flag to uninstall and install req product
if ($aideSession.AKSEdge.Version) {
Write-Host "$($aideSession.AKSEdge.Product)-$($aideSession.AKSEdge.Version) is already installed"
return $true
}
$aideConfig = Get-AideUserConfig
if ($null -eq $aideConfig) { return $retval }
if (-not (Test-AideUserConfigInstall)) { return $false } # bail if the validation failed
$reqProduct = $aideConfig.AksEdgeProduct
$url = $aksedgeProducts[$reqProduct]
$winUrl = $WindowsInstallUrl
$msiFile = ".\AksEdge.msi"
$winFile = ".\AksEdgeWindows.zip"
if ($aideConfig.AksEdgeProductUrl) {
$url = $aideConfig.AksEdgeProductUrl
$urlParent = (Resolve-Path -Path $url).Path | Split-Path -Parent
$winUrl = "$urlParent\AksEdgeWindows-*.zip"
}
Write-Host "Installing $reqProduct from $url"
Push-Location $env:Temp
$argList = '/I AksEdge.msi /qn '
$windowsRequired = ($null -ne $($aideConfig.AksEdgeConfig.Machines.WindowsNode))
if (Test-Path -Path $url) {
Copy-Item -Path $url -Destination $msiFile
if($windowsRequired) {
if (Test-Path $winUrl) {
Write-Host "Unzip WindowsInstallFiles.."
Expand-ArchiveLocal $winUrl .
} else {
Write-Host "Copying WindowsInstallFiles.."
foreach ($file in $WindowsInstallFiles) {
Copy-Item -Path "$urlParent\$file" -Destination .
}
}
$argList = '/I AksEdge.msi ADDLOCAL=CoreFeature,WindowsNodeFeature /passive '
}
} else {
$ProgressPreference = 'SilentlyContinue'
try {
Invoke-WebRequest $url -OutFile $msiFile -UseBasicParsing
} catch {
Write-Host "failed to download from $url"
Remove-Item $msiFile -Force -ErrorAction SilentlyContinue
$ProgressPreference = 'Continue'
Pop-Location
return $false
}
if($windowsRequired) {
$argList = '/I AksEdge.msi ADDLOCAL=CoreFeature,WindowsNodeFeature /passive '
try {
Invoke-WebRequest $winUrl -OutFile $winFile -UseBasicParsing
if (Test-Path $winFile) {
Write-Host "Unzip WindowsInstallFiles.."
Expand-ArchiveLocal $winFile .
}
} catch {
Write-Host "failed to download from $winUrl"
Remove-Item $winFile -Force -ErrorAction SilentlyContinue
$ProgressPreference = 'Continue'
Pop-Location
return $false
}
}
}
if ($aideConfig.InstallOptions) {
$InstallPath = $aideConfig.InstallOptions.InstallPath
if ($InstallPath) {
$argList = $argList + "INSTALLDIR=""$($InstallPath)"" "
}
$VhdxPath = $aideConfig.InstallOptions.VhdxPath
if ($VhdxPath) {
$argList = $argList + "VHDXDIR=""$($VhdxPath)"" "
}
}
Write-Verbose $argList
Start-Process msiexec.exe -Wait -ArgumentList $argList
#Refresh the env variables to include path from installed MSI
$Env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine")
$retval = Test-AideMsiInstall
if ($retval) {
Remove-Item $msiFile -ErrorAction SilentlyContinue
if($windowsRequired) {
foreach ($file in $WindowsInstallFiles) {
Remove-Item ".\$file" -ErrorAction SilentlyContinue
}
Remove-Item $winFile -ErrorAction SilentlyContinue
}
Write-Host "$reqProduct successfully installed"
$retval = $true
} else {
Write-Host "Error in install. Check installation" -ForegroundColor Red
$retval = $false
}
Pop-Location
$ProgressPreference = 'Continue'
return $retval
}
function Expand-ArchiveLocal {
Param(
[string] $ZipFile,
[string] $Destination
)
$Shell = New-Object -ComObject "Shell.Application"
$zipContents = $Shell.Namespace((Convert-Path $ZipFile)).items()
$DestinationFolder = $Shell.Namespace((Convert-Path $Destination))
if ($zipContents.Count -eq 1 -and $zipContents.Item(0).isFolder) {
$folderContents = $zipContents.Item(0).GetFolder.items()
$DestinationFolder.CopyHere($folderContents)
} else {
$DestinationFolder.CopyHere($zipContents)
}
}
function Remove-AideMsi {
<#
.SYNOPSIS
Checks and removes the installed AksEdge Msi.
.DESCRIPTION
Checks and removes the installed AksEdge Msi. It also removes the AksEdge module from the active Powershell session, to avoid usage of the cached module after the msi is uninstalled.
.OUTPUTS
Boolean
True if uninstalled successfully.
.EXAMPLE
Remove-AideMsi
#>
$aksedgeInfo = Get-ChildItem -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\' | Get-ItemProperty | Where-Object { $_.DisplayName -match "$aksedgeProductPrefix *" }
if ($null -eq $aksedgeInfo) {
Write-Host "$aksedgeProductPrefix is not installed."
} else {
Write-Host "$($aksedgeInfo.DisplayName) version $($aksedgeInfo.DisplayVersion) is installed. Removing..."
Remove-AideDeployment | Out-Null
Start-Process msiexec.exe -Wait -ArgumentList "/x $($aksedgeInfo.PSChildName) /passive /noreboot"
# Remove the module from Powershell session as well
Remove-Module -Name AksEdge -Force
$aideSession.AKSEdge.Product = $null
$aideSession.AKSEdge.Version = $null
Write-Host "$($aksedgeInfo.DisplayName) successfully removed."
}
}
function Get-AideMsiVersion {
<#
.SYNOPSIS
Checks and returns the AksEdge Msi version.
.DESCRIPTION
Checks and returns the AksEdge Msi version. This is same as the AksEdge module version. (Get-Module AksEdge -ListAvailable).Version
.OUTPUTS
Hashtable with Name and Version keys.
.EXAMPLE
Get-AideMsiVersion
#>
$aksedgeInfo = Get-ChildItem -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\' | Get-ItemProperty | Where-Object { $_.DisplayName -match "$aksedgeProductPrefix *" }
$retval = $null
if ($null -eq $aksedgeInfo) {
Write-Host "$aksedgeProductPrefix is not installed."
} else {
$retval = @{
"Name" = $($aksedgeInfo.DisplayName)
"Version" = $($aksedgeInfo.DisplayVersion)
}
$aideSession.AKSEdge.Version = $aksedgeInfo.DisplayVersion
$aideSession.AKSEdge.Product = $aksedgeInfo.DisplayName
Write-Host "$($aksedgeInfo.DisplayName) $($aksedgeInfo.DisplayVersion) is installed." -ForegroundColor Green
}
return $retval
}
function Invoke-AideDeployment {
<#
.SYNOPSIS
Checks the input json configuration and invokes the New-AksEdgeDeployment.
.DESCRIPTION
Checks the input json configuration and invokes the New-AksEdgeDeployment.
.OUTPUTS
Boolean
True if deployment is successful.
.EXAMPLE
Invoke-AideDeployment
#>
if (Test-AideDeployment) {
Write-Host "Error: AksEdge VM already deployed" -Foreground red
return $false
}
if (-not (Test-AideUserConfigDeploy)) { return $false }
$akseeCfg = Get-AideAksEdgeConfig
$aksedgeDeployParams = $akseeCfg | ConvertTo-Json -Depth 6
Write-Verbose "AksEdge VM deployment parameters for New-AksEdgeDeployment..."
Write-Verbose "$aksedgeDeployParams"
Write-Host "Starting AksEdge VM deployment..."
$retval = New-AksEdgeDeployment -JsonConfigString $aksedgeDeployParams
if ($retval -ieq "Azure Arc parameters not set or invalid") {
$retval = "OK"
}
if ($retval -ieq "OK") {
Write-Host "* AksEdge VM deployment successfull." -ForegroundColor Green
} else {
Write-Host "Error: AksEdge VM deployment failed with the below error message." -ForegroundColor Red
Write-Host "Error message : $retval." -ForegroundColor Red
return $false
}
return $true
}
function Remove-AideDeployment {
<#
.SYNOPSIS
Invokes Remove-AksEdgeDeployment to remove the deployment.
.DESCRIPTION
Invokes Remove-AksEdgeDeployment to remove the deployment.
.OUTPUTS
Boolean
True if deployment is successful.
.EXAMPLE
Remove-AideDeployment
#>
return Remove-AksEdgeDeployment -Force
}
function Test-AideVmSwitch {
<#
.SYNOPSIS
Tests if the specified VM Switch is available and the associated net adapter is connected.
.DESCRIPTION
Tests if the specified VM Switch is available and the associated net adapter is connected. If the -Create flag is specified, it attempts to create a VMMS switch.
.OUTPUTS
Boolean
True if successfull.
.PARAMETER Create
Switch parameter , to create the switch if not found.
.EXAMPLE
Test-AideVmSwitch
#>
Param
(
[Switch] $Create
)
$retval = $true
$aideConfig = Get-AideUserConfig
$akseeCfg = Get-AideAksEdgeConfig
$switchName = $aideConfig.VSwitch.Name
$adapterName = $aideConfig.VSwitch.AdapterName
if ($akseeCfg.DeploymentType -eq "SingleMachineCluster") {
Write-Host "SingleMachine cluster uses internal switch. Nothing to test."
return $true
}
if (-not (Test-AideUserConfigNetwork)) {
Write-Host "Errors in Network configuration." -ForegroundColor Red
return $false
}
# Scalable cluster - check if switch already present
Write-Host "--- Verifying virtual switch..."
if ([string]::IsNullOrEmpty($switchName)) { Write-Host "Switch name required" -ForegroundColor Red; return $false }
$aksedgeSwitch = Get-VMSwitch -Name $switchName -ErrorAction SilentlyContinue
if ($aksedgeSwitch) {
Write-Host "* Name:$($aksedgeSwitch.Name) - Type:$($aksedgeSwitch.SwitchType)" -ForegroundColor Green
$netadapter = (Get-NetAdapter | Where-Object { $_.InstanceID -eq "{$($aksedgeSwitch.NetAdapterInterfaceGuid)}" } )
if ($netadapter.Status -ieq 'Up') {
Write-Host "* Name:$($netadapter.Name) is Up" -ForegroundColor Green
} else {
Write-Host "Error: NetAdapter $($netadapter.Name) is not Up.`nVMSwitch $name has not connectivity." -ForegroundColor Red
$retval = $false
}
if ($netadapter.Name -ine $adapterName) {
Write-Host "Error: NetAdapter $($netadapter.Name) doesnt match the specified value ($adapterName)" -ForegroundColor Red
$retval = $false
}
} else {
# no switch found. Create if requested
if ($Create) {
$retval = New-AideVmSwitch
} else {
Write-Host "Error: VMSwitch $name not found." -ForegroundColor Red
$retval = $false
}
}
return $retval
}
function New-AideVmSwitch {
<#
.SYNOPSIS
Creates the external VM Switch on the specified net adapter.
.DESCRIPTION
Creates the external VM Switch on the specified net adapter
.OUTPUTS
Boolean
True if successfull.
.EXAMPLE
New-AideVmSwitch
#>
$aideConfig = Get-AideUserConfig
$switchName = $aideConfig.VSwitch.Name
$type = "External"
$adapter = $aideConfig.VSwitch.AdapterName
$aksedgeSwitch = Get-VMSwitch -Name $switchName -ErrorAction SilentlyContinue
if ($aksedgeSwitch) {
Write-Host "Error: Name:$($aksedgeSwitch.Name) - Type:$($aksedgeSwitch.SwitchType) already exists" -ForegroundColor Red
return $false
}
# no switch found. Create now
Write-Host "Creating VMSwitch $switchName - $type - $adapter..."
$nwadapters = (Get-NetAdapter -Physical -ErrorAction SilentlyContinue) | Where-Object { $_.Status -eq "Up" }
if ($nwadapters.Name -notcontains $adapter) {
Write-Host "Error: $adapter not found or not connected. External switch not created." -ForegroundColor Red
return $false
}
$aksedgeSwitch = New-VMSwitch -NetAdapterName $adapter -Name $switchName -ErrorAction SilentlyContinue
# give some time for the switch creation to succeed
Start-Sleep 10
$aksedgeSwitchAdapter = Get-NetAdapter | Where-Object { $_.Name -eq "vEthernet ($switchName)" }
if ($null -eq $aksedgeSwitchAdapter) {
Write-Host "Error: [vEthernet ($switchName)] is not found. $switchName switch creation failed. Please try switch creation again."
return $false
}
return $true
}
function Remove-AideVmSwitch {
<#
.SYNOPSIS
Removes the external VM Switch on the specified net adapter.
.DESCRIPTION
Removes the external VM Switch on the specified net adapter
.OUTPUTS
Boolean
True if successfull.
.EXAMPLE
Remove-AideVmSwitch
#>
$aideConfig = Get-AideUserConfig
$switchName = $aideConfig.VSwitch.Name
$aksedgeSwitch = Get-VMSwitch -Name $switchName -ErrorAction SilentlyContinue
if ($aksedgeSwitch) {
Write-Host "Removing $switchName"
Remove-VMSwitch -Name $switchName
}
}
# Main function for full functional path
function Start-AideWorkflow {
<#
.SYNOPSIS
Runs the end to end workflow for AksEdgeDeploy. Based on the input jsonFile/jsonString, it installs required msi, creates switch and deploys the cluster.
.DESCRIPTION
Runs the end to end workflow for AksEdgeDeploy. Based on the input jsonFile/jsonString, it installs required msi, creates switch and deploys the cluster.
This function also enables Hyper-V is it is not enabled and triggers a reboot. The function **doesnot resume** after reboot.
.OUTPUTS
Boolean
True if successfully deployed.
.PARAMETER jsonFile
File path for the json configuration file (aide-userconfig.json), based on the aide-ucschema.json schema.
.PARAMETER jsonString
Json herestring based on the aide-ucschema.json schema.
.EXAMPLE
Start-AideWorkflow -jsonFile .\aide-userconfig.json
#>
Param
(
[String]$jsonFile,
[String]$jsonString
)
$aideVersion = (Get-Module -Name AksEdgeDeploy).Version.ToString()
Write-Host "AksEdgeDeploy version: $aideVersion"
# Validate input parameter. Use default json in the same path if not specified
if (-not [string]::IsNullOrEmpty($jsonString)) {
$retval = Set-AideUserConfig -jsonString $jsonString
if (!$retval) {
Write-Host "Error: $jsonString incorrect" -ForegroundColor Red
return $false
}
} else {
if ([string]::IsNullOrEmpty($jsonFile)) {
$jsonFile = "$PSScriptRoot\aide-userconfig.json"
}
if (!(Test-Path -Path "$jsonFile" -PathType Leaf)) {
$aideConfig = Get-AideUserConfig
if (!$aideConfig) {
Write-Host "Error: $jsonFile not found" -ForegroundColor Red
return $false
}
} else {
$jsonFile = (Resolve-Path -Path $jsonFile).Path
$retval = Set-AideUserConfig -jsonFile $jsonFile # validate later after creating the switch
if (!$retval) {
Write-Host "Error: $jsonFile incorrect after creating switch" -ForegroundColor Red
return $false
}
}
}
Get-AideHostPcInfo
# Check PC prequisites (Hyper-V, AksEdge)
if (!(Test-AideMsiInstall -Install)) { return $false }
# Install required host features - can result in reboot.
Write-Host "Running Install-AksEdgeHostFeatures" -ForegroundColor Cyan
if (!(Install-AksEdgeHostFeatures -Confirm:$false)) { return $false }
# Check if AksEdge is deployed already and bail out
if (Test-AideDeployment) {
Write-Host "AksEdge VM is already deployed." -ForegroundColor Yellow
} else {
if (!(Test-AideVmSwitch -Create)) { return $false } #create switch if specified
# We are here.. all is good so far. Validate and deploy aksedge
return Invoke-AideDeployment
}
return $true
}
# Formats JSON in a nicer format than the built-in ConvertTo-Json does.
# https://github.com/PowerShell/PowerShell/issues/2736
function Format-AideJson([Parameter(Mandatory, ValueFromPipeline)][String] $json) {
<#
.SYNOPSIS
Pretty formats the input json.
.DESCRIPTION
Pretty formats the input json. Based on "https://github.com/PowerShell/PowerShell/issues/2736"
.OUTPUTS
String, formatted json string
.PARAMETER json
Input json string for formatting.
.EXAMPLE
Format-AideJson
#>
$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"
}