Azure Stack HCI: Demolab
Azure Stack HCI is a hyperconverged infrastructure solution that combines software-defined compute, storage, and networking. While it’s a powerful tool for modernizing your data center and integrating with Azure services, setting it up for testing or development can be resource-intensive.
Deploying Azure Stack HCI can often seem daunting, especially when constrained by hardware resources. Traditional methods typically require substantial infrastructure, making it challenging for individuals or small teams to set up testing and lab environments. In this article, I’ll explore a streamlined approach using the AzSHCI scripts, which allow you to deploy Azure Stack HCI on minimal hardware setups, such as a laptop.
Existing Solutions and Their Limitations
Several tools and guides exist to help deploy Azure Stack HCI in lab environments. However, they often come with limitations that make them less suitable for minimal hardware setups.
MSLab by Jaromir Kaspar
MSLab is a comprehensive solution that allows you to deploy various Microsoft products in a lab environment.
- Pros:
- Supports a wide range of scenarios and configurations.
- Highly customizable and feature-rich.
- Served as the foundation for my lab setups for a long time.
- Cons:
- The latest versions, such as Azure Stack HCI 2408, have not been updated and are somewhat discontinued.
- Setup process involves more steps and hardware resources compared to AzSHCI.
Additionally, the DellGEOS Repository offers more current scenarios and use cases. Most scenarios in the DellGEOS repository are designed for infrastructures somewhat more robust than a simple laptop. In the past, DellGEOS has been the base for my configurations on Azure Stack HCI with Dell hardware, providing very current troubleshooting scenarios.
Azure Arc Jumpstart by Lior Kamrat
Azure Arc Jumpstart provides automated deployment scenarios for Azure Arc-enabled resources.
- Pros:
- Automates Azure Arc deployments with ease.
- Contains beautifully documented infrastructure with detailed diagrams.
- Cons:
- Makes it difficult to carry out PoCs that extend beyond HCI, such as AVD on HCI.
- Lack of flexibility in networking due to virtualization and Azure-based implementation, limiting access to VM consoles.
- Costs approximate around $3,000 per month, not accounting for additional expenses. Cost Details
Manual Deployment
Deploying Azure Stack HCI manually using the Deployment Guide is another option. However, this manual approach has several drawbacks:
- Pros:
- Direct control over the deployment process.
- Cons:
- The configuration hasn’t been updated since April this year.
- Missing steps and incomplete documentation make the process cumbersome.
- Performing a manual deployment is much more tedious and time-consuming compared to using AzSHCI scripts.
Why AzSHCI?
The AzSHCI scripts are designed to bridge this gap by providing a lightweight, efficient way to deploy Azure Stack HCI on minimal hardware. Key advantages include:
- Resource Efficiency: Optimized for environments with limited CPU, RAM, and storage.
- Simplicity: Straightforward scripts that are easy to understand and modify.
- Quick Deployment: Allows you to set up a single-node Azure Stack HCI cluster rapidly for testing and development.
Prerequisites
Before you begin, ensure you meet the following requirements:
- Hardware Requirements:
- A computer with a TPM chip.
- A processor capable of running Hyper-V.
- Recommended RAM: 64 GB or more.
- Minimum RAM for Basic Testing: 32 GB.
- Software Requirements:
- Azure Subscription: With the necessary permissions.
Getting Started with AzSHCI
Understanding Nested Virtualization
The AzSHCI scripts leverage Nested Virtualization to create a fully functional Azure Stack HCI environment within your existing hypervisor setup. This means you can run virtual machines (VMs) inside other VMs, allowing you to simulate a complex infrastructure on minimal hardware.
The deployment process involves the following key steps:
-
Installation of Windows Server 2025 Evaluation: The scripts automate the setup of a Windows Server 2025 VM, which will be configured as an Active Directory Domain Controller (DC). This includes installing updates and preparing the environment for cluster registration, which is essential for setting up Azure Stack HCI.
-
Setup of the Azure Stack HCI Node: Another VM is created to serve as the Azure Stack HCI node. Thanks to Nested Virtualization, this node can act as a hypervisor itself, allowing you to run additional workloads or services on top of it.
By utilizing Nested Virtualization, you can simulate a realistic Azure Stack HCI deployment without the need for extensive physical hardware, making it ideal for testing and development purposes.
Repository Overview
The AzSHCI scripts are hosted on GitHub: github.com/schmittnieto/AzSHCI
The repository is structured as follows:
AzSHCI/
│
├── scripts/
│ └── 01Lab/
│ ├── 00_Infra_AzHCI.ps1
│ ├── 01_DC.ps1
│ ├── 02_Cluster.ps1
│ ├── 03_TroubleshootingExtensions.ps1
│ ├── 99_Offboarding.ps1
│
├── README.md
└── LICENSE
Script Breakdown
00_Infra_AzHCI.ps1
Configuration and VM Creation Script
- Purpose: Sets up virtual networking, creates necessary folder structures, and deploys both the HCI node and Domain Controller VMs.
- Features:
- Configures an internal virtual switch with NAT.
- Creates directories for VM and disk storage.
- Automates VM creation and initial configuration.
01_DC.ps1
Domain Controller Configuration Script
- Purpose: Configures the Domain Controller VM, including network settings and Active Directory setup.
- Features:
- Removes ISO media from the VM.
- Renames the VM and sets static IP.
- Sets the time zone and installs necessary Windows features.
- Promotes the server to a Domain Controller.
- Configures DNS forwarders and creates Organizational Units (OUs) in Active Directory.
02_Cluster.ps1
Cluster Node Configuration Script
- Purpose: Configures the Azure Stack HCI node VM.
- Features:
- Removes ISO media from the VM.
- Creates a setup user and renames the VM.
- Configures network adapters with static IPs and RDMA.
- Installs essential Windows features like Hyper-V and Failover Clustering.
- Registers the node with Azure Arc and integrates Azure services.
03_TroubleshootingExtensions.ps1
Troubleshooting Azure Connected Machine Extensions
- Purpose: Manages Azure Connected Machine extensions for the HCI nodes.
- Features:
- Installs required PowerShell modules (
Az.Compute
andAz.StackHCI
) if not already installed. - Connects to Azure using device code authentication and allows you to select a subscription and resource group.
- Retrieves Azure Arc VMs from Azure using
Az.StackHCI
, filtering for machines withCloudMetadataProvider
set to “AzSHCI”. - Validates that required Azure Connected Machine extensions are installed.
- Fixes any failed extensions by removing locks, deleting, and reinstalling them.
- Adds any missing extensions based on a predefined list.
- Installs required PowerShell modules (
99_Offboarding.ps1
Offboarding Script to Clean Up Configurations
- Purpose: Cleans up the deployment by removing VMs, associated VHD files, virtual switches, NAT settings, and designated folder structures.
- Features:
- Stops and removes specified VMs.
- Deletes associated virtual hard disk (VHD) files.
- Removes virtual switches and NAT configurations.
- Cleans up folder structures.
Step-by-Step Deployment Guide
Below is the exact deployment process to set up your Azure Stack HCI environment using the AzSHCI scripts. This guide will help you get everything up and running smoothly.
1. Download the Scripts from the Repository
Clone the AzSHCI repository to your local machine:
git clone https://github.com/schmittnieto/AzSHCI.git
Or download the repository manually: Download Here
2. Download the Necessary ISO Files
- Windows Server 2025 Evaluation ISO: Download Here
- Azure Stack HCI OS ISO: Download directly from the Azure Portal.
Place both ISO files in the C:\ISO
directory and rename the isos if you consider it convenient, for example I renamed the Azure Stack HCI iso to HCI23H2.iso
and the Windows Server 2025 iso to W2025.iso
.
3. Preparing Your Environment
Navigate to the scripts directory:
cd AzSHCI/scripts/01Lab
Ensure your execution policy allows script execution:
Set-ExecutionPolicy RemoteSigned -Scope CurrentUser
4. Initial infrastructure script
Considering that you are in the scripts folder, make sure to change the iso variables ($isoPath_HCI = “C:\ISO\HCI23H2.iso”
and $isoPath_DC = “C:\ISO\WS2025.iso”
) in the 00_Infra_AzHCI.ps1
script to match yours.
The you can run the script:
.\00_Infra_AzHCI.ps1
This script will set up the virtual networking, create necessary folder structures, and deploy the VMs for the Domain Controller (DC) and the Azure Stack HCI node.
5. Manual Installation of the DC Operating System
- Start the DC VM: Open Hyper-V Manager and start the
DC
VM. - Follow the Installation Wizard: Proceed through the Windows Server 2025 installation wizard.
- Perform First Login: Log in as the Administrator for the first time.
No additional configuration is needed at this stage.
6. Domain Controller configuration script
Before running the script, modify the $defaultUser
and $defaultPwd
variables in 01_DC.ps1
to match your administrator credentials.
# In 01_DC.ps1
$defaultUser = "Administrator"
$defaultPwd = "YourAdminPassword"
Then, execute the script:
.\01_DC.ps1
This process takes approximately 30 minutes due to Windows Updates. The script will:
- Remove the ISO from the VM.
- Configure network settings.
- Promote the server to a Domain Controller.
- Install updates and prepare the environment for cluster registration.
7. Manual Installation of the Node Operating System
- Start the NODE VM: Open Hyper-V Manager and start the
NODE
VM. - Follow the Installation Wizard: Proceed through the Azure Stack HCI OS installation wizard.
- Perform First Login: Log in as the Administrator for the first time.
No additional configuration is needed at this stage.
8. Node configuration and ARC registration
Before running the script, modify the $defaultUser
and $defaultPwd
variables in 02_Cluster.ps1
to match your administrator credentials.
# In 02_Cluster.ps1
$defaultUser = "Administrator"
$defaultPwd = "YourAdminPassword"
Then, execute the script:
.\02_Cluster.ps1
This process takes approximately 10 minutes and will require you to:
- Authenticate to Azure.
- Select the Resource Group where the node will be registered.
The script will:
- Remove the ISO from the VM.
- Configure network settings.
- Install required Windows features.
- Register the node with Azure Arc.
9. Verify Node Extension Installation
After running Script 02, the Azure Connected Machine extensions should begin installing automatically. This can take up to 20 minutes. To verify:
- Go to the Azure Portal.
- Navigate to the Azure Arc machines.
- Check that all extensions are installed and in a successful state.
If you encounter any issues or failures with the extensions, run Script 03:
.\03_TroubleshootingExtensions.ps1
This script will troubleshoot and fix common extension issues.
10. Registering the Cluster
This section will be detailed in a future update, complete with step-by-step instructions and screenshots.
Once the cluster node script completes and extensions are correctly installed, follow these steps to register your cluster in Azure:
-
Assign Required Rights:
- Subscription Level:
- If not an Owner or Contributor, assign the following roles to the user performing the registration:
- Azure Stack HCI Administrator
- Reader
- If not an Owner or Contributor, assign the following roles to the user performing the registration:
- Resource Group Level:
- Assign the following roles to the user within the Resource Group where deployment will occur:
- Key Vault Data Access Administrator
- Key Vault Secrets Officer
- Key Vault Contributor
- Storage Account Contributor
- Assign the following roles to the user within the Resource Group where deployment will occur:
- Microsoft Entra Roles and Administrators:
- Assign the following role to the user performing the deployment:
- Cloud Application Administrator
- Assign the following role to the user performing the deployment:
In the near future, I plan to automate this process (granting granular rights to a user for deployment) using a script.
- Subscription Level:
-
Initial Cluster Registration:
-
Use the Azure Portal for initial cluster registration: Azure Stack HCI Deployment via Portal
- Network Configuration:
- Apply the following network settings to the interfaces:
- Personally, I use the following IP configurations:
- Apply the following network settings to the interfaces:
- Custom Location and User Configuration:
- Configure users and custom locations as defined in the scripts (credentials are exposed in the scripts):
- Configure users and custom locations as defined in the scripts (credentials are exposed in the scripts):
- Security Options:
- It’s crucial to disable BitLocker to prevent excessive storage consumption (totaling 2.1 TB), which could render your system inoperable if you lack sufficient capacity:
- It’s crucial to disable BitLocker to prevent excessive storage consumption (totaling 2.1 TB), which could render your system inoperable if you lack sufficient capacity:
- Finalize Configuration:
- Leave the remaining settings at default. The system will be ready for provisioning after the validation, which take approximately 20 minutes.
-
-
Perform Cloud Deployment:
- Initiate the cloud deployment and wait approximately 2 hours for the cluster to be ready for subsequent steps.
Cost Considerations
One of the advantages of using the AzSHCI scripts and Nested Virtualization is the minimal cost involved in setting up your Azure Stack HCI environment.
- Hardware Costs: 0€
- I use my regular work laptop for deployment, so there are no additional hardware expenses.
- License Costs for the Domain Controller: 0€
- We use the Windows Server 2025 Evaluation version, which is free for 90 days—ample time for testing and development.
- License Costs for the Node: 0€
- The Azure Stack HCI OS does not incur costs until you register it with Azure Stack HCI.
- Azure Stack HCI Costs: 0€
- Azure Stack HCI is free for the first 60 days. Since we won’t exceed this period for our testing, no costs will be incurred.
Additionally, every 2 to 3 weeks, I delete all resources in the Resource Group and use Script 99_Offboarding.ps1
to remove all the infrastructure, allowing me to perform a fresh deployment. This practice helps avoid any unintended costs and keeps the environment clean for new tests.
Conclusion
By following these steps, you should have a functional Azure Stack HCI environment running on minimal hardware. The AzSHCI scripts are designed to make the deployment process as smooth as possible, allowing you to focus on exploring Azure Stack HCI features without worrying about extensive setup procedures.
Additional Resources
- AzSHCI GitHub Repository: github.com/schmittnieto/AzSHCI
- MSLab Repository: github.com/microsoft/MSLab
- DellGEOS AzureStackHOLs: github.com/DellGEOS/AzureStackHOLs
- Azure Arc Jumpstart: azurearcjumpstart.io
- Azure Stack HCI Documentation: Microsoft Docs
Future Enhancements
Planned updates include:
- Azure Kubernetes Service (AKS) Integration
- Azure Virtual Desktop (AVD) Deployment
- Azure Arc Managed VMs Setup
In the coming weeks, I will add more tests and case studies, providing detailed articles to cover these topics comprehensively.
Stay tuned for these updates!
Contributing
Contributions are welcome! If you have suggestions, improvements, or bug fixes, feel free to fork the repository and submit a pull request.
License
This project is licensed under the MIT License - see the LICENSE file for details.
Contact
For any questions or issues, please open an issue in the repository.
Thank you for using AzSHCI! I hope these scripts simplify your Azure Stack HCI deployment process and enable efficient testing and development in your environment.
RAW Scripts
To make it easier to read I have moved all the RAW scripts to the end of the article, they will be automatically synchronized via Github Actions to the article. That is to say, in case there are modifications in the Github repository where they are located, these will be picked up in the scripts here.
00_Infra_AzHCI.ps1
# 00_Infra_AzHCI.ps1
# Configuration and VM Creation Script
<#
.SYNOPSIS
Configures virtual networking, creates necessary folder structures, and deploys HCI Node and Domain Controller VMs.
.DESCRIPTION
This script performs the following tasks:
- Checks for required prerequisites.
- Configures an internal virtual switch with NAT.
- Ensures the required folder structures exist.
- Creates two virtual machines: an HCI Node and a Domain Controller.
- Configures networking, storage, and security settings for the VMs.
.NOTES
- Designed by Cristian Schmitt Nieto. For more information and usage, visit: https://schmitt-nieto.com/blog/azure-stack-hci-demolab/
- Run this script with administrative privileges.
- Ensure the ISO paths are correct before execution.
- Execution Policy may need to be set to allow the script to run. To set the execution policy, you can run:
Set-ExecutionPolicy RemoteSigned -Scope CurrentUser
- Approved PowerShell Verb Usage:
- Functions and cmdlets should use approved verbs. This script uses `New` for creating resources.
#>
#region Variables
# Virtual Switch and Network Configuration
$vSwitchName = "azurestackhci"
$vSwitchNIC = "vEthernet ($vSwitchName)"
$vNetIPNetwork = "172.19.19.0/24"
$vIPNetworkPrefixLength = ($vNetIPNetwork -split '/')[1]
$natName = "azurestackhci"
$HCIRootFolder = "C:\HCI"
# ISO Paths
$isoPath_HCI = "C:\ISO\HCI23H2.iso" # Replace with the actual path to your HCI Node ISO
$isoPath_DC = "C:\ISO\WS2025.iso" # Replace with the actual path to your Domain Controller ISO
# HCI Node VM Configuration
$HCIVMName = "NODE"
$HCI_Memory = 32GB
$HCI_Processors = 8
$HCI_Disks = @(
@{ Path = "${HCIVMName}_C.vhdx"; Size = 127GB },
@{ Path = "s2d1.vhdx"; Size = 1024GB },
@{ Path = "s2d2.vhdx"; Size = 1024GB }
)
$HCI_NetworkAdapters = @("MGMT1", "MGMT2")
# Domain Controller VM Configuration
$DCVMName = "DC"
$DC_Memory = 4GB
$DC_Processors = 2
$DC_Disks = @(
@{ Path = "${DCVMName}_C.vhdx"; Size = 60GB }
)
$DC_NetworkAdapters = @("MGMT1")
# Tasks for Progress Bar
$tasks = @(
"Checking Prerequisites",
"Configuring Virtual Switch and NAT",
"Setting Up Folder Structures",
"Creating HCI Node VM",
"Creating Domain Controller VM"
)
$totalTasks = $tasks.Count
$currentTask = 0
#endregion
#region Functions
# Function to Display Messages with Colors
function Write-Message {
param (
[string]$Message,
[ValidateSet("Info", "Success", "Warning", "Error")]
[string]$Type = "Info"
)
switch ($Type) {
"Info" { Write-Host $Message -ForegroundColor Cyan }
"Success" { Write-Host $Message -ForegroundColor Green }
"Warning" { Write-Host $Message -ForegroundColor Yellow }
"Error" { Write-Host $Message -ForegroundColor Red }
}
# Optional: Log messages to a file
# Add-Content -Path "C:\Path\To\Your\LogFile.txt" -Value "$((Get-Date).ToString('yyyy-MM-dd HH:mm:ss')) [$Type] $Message"
}
# Function to Ensure the Folder Structure Exists
function Set-FolderStructure {
param (
[string]$BaseFolder
)
$vmFolder = Join-Path -Path $BaseFolder -ChildPath "VM"
$diskFolder = Join-Path -Path $BaseFolder -ChildPath "Disk"
try {
if (-not (Test-Path -Path $BaseFolder)) {
Write-Message "Base folder does not exist. Creating: $BaseFolder" -Type "Info"
New-Item -Path $BaseFolder -ItemType Directory -Force | Out-Null
Write-Message "Base folder created: $BaseFolder" -Type "Success"
} else {
Write-Message "Base folder already exists: $BaseFolder" -Type "Info"
}
if (-not (Test-Path -Path $vmFolder)) {
Write-Message "VM folder does not exist. Creating: $vmFolder" -Type "Info"
New-Item -Path $vmFolder -ItemType Directory -Force | Out-Null
Write-Message "VM folder created: $vmFolder" -Type "Success"
} else {
Write-Message "VM folder already exists: $vmFolder" -Type "Info"
}
if (-not (Test-Path -Path $diskFolder)) {
Write-Message "Disk folder does not exist. Creating: $diskFolder" -Type "Info"
New-Item -Path $diskFolder -ItemType Directory -Force | Out-Null
Write-Message "Disk folder created: $diskFolder" -Type "Success"
} else {
Write-Message "Disk folder already exists: $diskFolder" -Type "Info"
}
} catch {
Write-Message "Failed to set up folder structure. Error: $_" -Type "Error"
throw
}
}
# Function to Create or Verify the Internal VMSwitch
function Invoke-InternalVMSwitch {
param (
[string]$VMSwitchName
)
try {
$existingSwitch = Get-VMSwitch -Name $VMSwitchName -ErrorAction SilentlyContinue
if ($null -ne $existingSwitch) {
Write-Message "The internal VM Switch '$VMSwitchName' already exists." -Type "Success"
} else {
Write-Message "The internal VM Switch '$VMSwitchName' does not exist. Creating it now..." -Type "Info"
New-VMSwitch -Name $VMSwitchName -SwitchType Internal -ErrorAction Stop | Out-Null
Write-Message "Internal VM Switch '$VMSwitchName' created successfully!" -Type "Success"
}
} catch {
Write-Message "Failed to create internal VM Switch '$VMSwitchName'. Error: $_" -Type "Error"
throw
}
}
# Function to Calculate the Gateway (First Usable Address in the Subnet)
function Get-Gateway {
param (
[string]$IPNetwork
)
try {
$ip, $cidr = $IPNetwork -split '/'
$networkAddress = [System.Net.IPAddress]::Parse($ip)
$addressBytes = $networkAddress.GetAddressBytes()
$addressBytes[3] += 1 # Increment the last octet by 1
$gateway = [System.Net.IPAddress]::new($addressBytes)
return $gateway
} catch {
Write-Message "Invalid IP network format: $IPNetwork. Error: $_" -Type "Error"
throw
}
}
# Function to Create VMs
function New-VMCreation {
param (
[string]$VMName,
[string]$VMFolder,
[string]$DiskFolder,
[string]$ISOPath,
[long]$Memory,
[int]$Processors,
[array]$Disks,
[array]$NetworkAdapters
)
try {
# Create virtual hard disk for the OS
$VHDName = $Disks[0].Path
$VHDPath = Join-Path -Path $DiskFolder -ChildPath $VHDName
if (-not (Test-Path -Path $VHDPath)) {
New-VHD -Path $VHDPath -SizeBytes $Disks[0].Size -ErrorAction Stop | Out-Null
Write-Message "VHD created at '$VHDPath'." -Type "Success"
} else {
Write-Message "VHD already exists at '$VHDPath'. Skipping creation." -Type "Warning"
}
# Create the VM
if (-not (Get-VM -Name $VMName -ErrorAction SilentlyContinue)) {
New-VM -Name $VMName -MemoryStartupBytes $Memory -VHDPath $VHDPath -Generation 2 -Path $VMFolder -ErrorAction Stop | Out-Null
Write-Message "VM '$VMName' created successfully." -Type "Success"
} else {
Write-Message "VM '$VMName' already exists. Skipping creation." -Type "Warning"
return
}
# Configure memory and processors
Set-VMMemory -VMName $VMName -DynamicMemoryEnabled $false -ErrorAction Stop | Out-Null
Set-VMProcessor -VMName $VMName -Count $Processors -ErrorAction Stop | Out-Null
Write-Message "Memory and processor settings configured for VM '$VMName'." -Type "Success"
# Disable checkpoints
Set-VM -VMName $VMName -CheckpointType Disabled -ErrorAction Stop | Out-Null
Write-Message "Checkpoints disabled for VM '$VMName'." -Type "Success"
# Remove default network adapter
Get-VMNetworkAdapter -VMName $VMName | Remove-VMNetworkAdapter -ErrorAction Stop | Out-Null
Write-Message "Default network adapter removed from VM '$VMName'." -Type "Success"
# Add network adapters and connect them to the VMSwitch
foreach ($nic in $NetworkAdapters) {
Add-VMNetworkAdapter -VMName $VMName -Name $nic -ErrorAction Stop | Out-Null
Connect-VMNetworkAdapter -VMName $VMName -Name $nic -SwitchName $vSwitchName -ErrorAction Stop | Out-Null
Write-Message "Network adapter '$nic' added and connected to '$vSwitchName' for VM '$VMName'." -Type "Success"
}
# Enable MAC spoofing
Get-VMNetworkAdapter -VMName $VMName | Set-VMNetworkAdapter -MacAddressSpoofing On -ErrorAction Stop | Out-Null
Write-Message "MAC spoofing enabled for VM '$VMName'." -Type "Success"
# Configure Key Protector and vTPM
$GuardianName = $VMName
$existingGuardian = Get-HgsGuardian -Name $GuardianName -ErrorAction SilentlyContinue
if ($null -ne $existingGuardian) {
Write-Message "HgsGuardian '$GuardianName' already exists. Deleting and recreating..." -Type "Warning"
Remove-HgsGuardian -Name $GuardianName -ErrorAction Stop | Out-Null
Write-Message "HgsGuardian '$GuardianName' deleted." -Type "Success"
} else {
Write-Message "HgsGuardian '$GuardianName' does not exist. Creating it now..." -Type "Info"
}
$newGuardian = New-HgsGuardian -Name $GuardianName -GenerateCertificates -ErrorAction Stop
Write-Message "HgsGuardian '$GuardianName' created successfully!" -Type "Success"
$kp = New-HgsKeyProtector -Owner $newGuardian -AllowUntrustedRoot -ErrorAction Stop
Set-VMKeyProtector -VMName $VMName -KeyProtector $kp.RawData -ErrorAction Stop | Out-Null
Enable-VMTPM -VMName $VMName -ErrorAction Stop | Out-Null
Write-Message "KeyProtector and vTPM applied to VM '$VMName'." -Type "Success"
# Create and attach additional disks
for ($i = 1; $i -lt $Disks.Count; $i++) {
$disk = $Disks[$i]
$diskPath = Join-Path -Path $DiskFolder -ChildPath $disk.Path
if (-not (Test-Path -Path $diskPath)) {
New-VHD -Path $diskPath -SizeBytes $disk.Size -ErrorAction Stop | Out-Null
Write-Message "Additional VHD created at '$diskPath'." -Type "Success"
} else {
Write-Message "Additional VHD already exists at '$diskPath'. Skipping creation." -Type "Warning"
}
Add-VMHardDiskDrive -VMName $VMName -Path $diskPath -ErrorAction Stop | Out-Null
Write-Message "Additional disk '$disk.Path' attached to VM '$VMName'." -Type "Success"
}
# Enable nested virtualization
Set-VMProcessor -VMName $VMName -ExposeVirtualizationExtensions $true -ErrorAction Stop | Out-Null
Write-Message "Nested virtualization enabled for VM '$VMName'." -Type "Success"
# Add boot ISO
Add-VMDvdDrive -VMName $VMName -Path $ISOPath -ErrorAction Stop | Out-Null
Write-Message "ISO '$ISOPath' mounted as DVD drive for VM '$VMName'." -Type "Success"
# Set boot order to prioritize the DVD drive first
$bootOrder = Get-VMFirmware -VMName $VMName -ErrorAction Stop
$dvdBoot = ($bootOrder.BootOrder | Where-Object { $_.Device -like '*Dvd*' })[0]
if ($dvdBoot) {
Set-VMFirmware -VMName $VMName -FirstBootDevice $dvdBoot -ErrorAction Stop | Out-Null
Write-Message "Boot order set to prioritize DVD drive for VM '$VMName'." -Type "Success"
} else {
Write-Message "DVD drive not found in boot order for VM '$VMName'." -Type "Warning"
}
# Configure VM to shut down when the host shuts down
Set-VM -Name $VMName -AutomaticStopAction ShutDown -ErrorAction Stop | Out-Null
Write-Message "Configured VM '$VMName' to shut down when the host shuts down." -Type "Success"
# Configure VM to take no action when the host starts
Set-VM -Name $VMName -AutomaticStartAction Nothing -ErrorAction Stop | Out-Null
Write-Message "Configured VM '$VMName' to take no action on host start." -Type "Success"
} catch {
Write-Message "Failed to create or configure VM '$VMName'. Error: $_" -Type "Error"
throw
}
}
# Function to Test for TPM Chip
function Test-TPM {
try {
$tpm = Get-TPM -ErrorAction SilentlyContinue
if ($null -eq $tpm) {
Write-Message "TPM chip not found on the host. A TPM is required for the Key Protector and vTPM." -Type "Error"
exit 1
} elseif (-not $tpm.TpmEnabled) {
Write-Message "TPM chip is present but not enabled. Please enable TPM in the BIOS/UEFI settings." -Type "Error"
exit 1
} else {
Write-Message "TPM chip is present and enabled." -Type "Success"
}
} catch {
Write-Message "Failed to check TPM status. Error: $_" -Type "Error"
exit 1
}
}
# Function to Test for Hyper-V Role
function Test-HyperV {
try {
# Query Win32_OptionalFeature for Hyper-V
$hyperVFeature = Get-CimInstance -ClassName Win32_OptionalFeature -Filter "Name='Microsoft-Hyper-V-All'" -ErrorAction Stop
if ($hyperVFeature.InstallState -eq 1) {
Write-Message "Hyper-V role is already installed on the host." -Type "Success"
}
else {
Write-Message "Hyper-V role is not installed on the host. Installing Hyper-V role and management tools..." -Type "Info"
# Determine OS type
$os = Get-CimInstance -ClassName Win32_OperatingSystem
$productType = $os.ProductType
# ProductType 1 = Workstation, 2 = Domain Controller, 3 = Server
if ($productType -eq 1) {
# Client OS - use Enable-WindowsOptionalFeature
Write-Message "Detected Client Operating System. Installing Hyper-V using Enable-WindowsOptionalFeature..." -Type "Info"
Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V-All -All -NoRestart -ErrorAction Stop
}
else {
# Server OS - use DISM
Write-Message "Detected Server Operating System. Installing Hyper-V using DISM..." -Type "Info"
dism.exe /Online /Enable-Feature /All /FeatureName:Microsoft-Hyper-V /IncludeManagementTools /NoRestart | Out-Null
}
Write-Message "Hyper-V role and management tools installed successfully. A restart is required." -Type "Success"
# Optional: Implement a progress bar or sleep before restarting
Start-SleepWithProgress -Seconds 60 -Activity "Restarting Host" -Status "Please wait while the host restarts..."
# Restart the computer to apply changes
Restart-Computer -Force
}
}
catch {
Write-Message "Failed to check or install Hyper-V role. Error: $_" -Type "Error"
exit 1
}
}
# Function to Update Progress Bar (Main Progress)
function Update-ProgressBarMain {
param (
[int]$CurrentStep,
[int]$TotalSteps,
[string]$StatusMessage
)
$percent = [math]::Round(($CurrentStep / $TotalSteps) * 100)
Write-Progress -Id 1 -Activity "Overall Progress" -Status $StatusMessage -PercentComplete $percent
}
# Function to Start Sleep with Progress Message and Additional Progress Bar (Subtask)
function Start-SleepWithProgress {
param(
[int]$Seconds,
[string]$Activity = "Waiting",
[string]$Status = "Please wait..."
)
Write-Message "$Activity : $Status" -Type "Info"
for ($i = 1; $i -le $Seconds; $i++) {
$percent = [math]::Round(($i / $Seconds) * 100)
Write-Progress -Id 2 -Activity "Sleep Progress" -Status "$Activity : $i/$Seconds seconds elapsed..." -PercentComplete $percent
Start-Sleep -Seconds 1
}
Write-Progress -Id 2 -Activity "Sleep Progress" -Completed
Write-Message "$Activity : Completed." -Type "Success"
}
#endregion
#region Script Execution
foreach ($task in $tasks) {
$currentTask++
Update-ProgressBarMain -CurrentStep $currentTask -TotalSteps $totalTasks -StatusMessage "$task..."
switch ($task) {
"Checking Prerequisites" {
Write-Message "Checking prerequisites..." -Type "Info"
# Test for TPM
Test-TPM
# Test for Hyper-V role and management tools
Test-HyperV
Write-Message "Prerequisite checks completed successfully." -Type "Success"
}
"Configuring Virtual Switch and NAT" {
Write-Message "Configuring virtual switch and NAT settings..." -Type "Info"
# Create internal VMSwitch
Invoke-InternalVMSwitch -VMSwitchName $vSwitchName
Start-Sleep -Seconds 3
# Calculate Gateway
try {
$vIPNetworkGW = Get-Gateway -IPNetwork $vNetIPNetwork
Write-Message "Calculated gateway: $vIPNetworkGW" -Type "Info"
} catch {
Write-Message "Failed to calculate gateway. Exiting script." -Type "Error"
exit 1
}
# Assign IP and NAT
try {
# Assign the IP address to the host's vSwitch interface
$existingIPAddress = Get-NetIPAddress -IPAddress $vIPNetworkGW -InterfaceAlias $vSwitchNIC -ErrorAction SilentlyContinue
if ($null -eq $existingIPAddress) {
Write-Message "Assigning IP address $vIPNetworkGW to interface $vSwitchNIC" -Type "Info"
New-NetIPAddress -IPAddress $vIPNetworkGW -PrefixLength $vIPNetworkPrefixLength -InterfaceAlias $vSwitchNIC -ErrorAction Stop | Out-Null
Write-Message "IP address $vIPNetworkGW assigned successfully to $vSwitchNIC." -Type "Success"
} else {
Write-Message "IP address $vIPNetworkGW already exists on interface $vSwitchNIC. Skipping assignment." -Type "Warning"
}
# Create NAT configuration if it doesn't exist
$existingNat = Get-NetNat -Name $natName -ErrorAction SilentlyContinue
if ($null -eq $existingNat) {
Write-Message "Creating new NAT with name: $natName" -Type "Info"
New-NetNat -Name $natName -InternalIPInterfaceAddressPrefix $vNetIPNetwork -ErrorAction Stop | Out-Null
Write-Message "NAT '$natName' created successfully." -Type "Success"
} else {
Write-Message "A NAT with the name '$natName' already exists. Skipping creation." -Type "Warning"
}
} catch {
Write-Message "Failed to configure IP address or NAT. Error: $_" -Type "Error"
exit 1
}
Start-Sleep -Seconds 3
Write-Message "Virtual switch and NAT configuration completed successfully!" -Type "Success"
}
"Setting Up Folder Structures" {
Write-Message "Setting up folder structures..." -Type "Info"
try {
Set-FolderStructure -BaseFolder $HCIRootFolder
$HCIDiskFolder = Join-Path -Path $HCIRootFolder -ChildPath "Disk"
$HCIVMFolder = Join-Path -Path $HCIRootFolder -ChildPath "VM"
Write-Message "Folder structures set up successfully." -Type "Success"
} catch {
Write-Message "Failed to set up folder structures. Error: $_" -Type "Error"
exit 1
}
}
"Creating HCI Node VM" {
Write-Message "Creating HCI Node VM..." -Type "Info"
try {
New-VMCreation -VMName $HCIVMName `
-VMFolder $HCIVMFolder `
-DiskFolder $HCIDiskFolder `
-ISOPath $isoPath_HCI `
-Memory $HCI_Memory `
-Processors $HCI_Processors `
-Disks $HCI_Disks `
-NetworkAdapters $HCI_NetworkAdapters
# Disable time synchronization on the HCI Node VM
Get-VMIntegrationService -VMName $HCIVMName | Where-Object { $_.Name -like "*Sync*" } | Disable-VMIntegrationService -ErrorAction Stop | Out-Null
Write-Message "Time synchronization disabled for VM '$HCIVMName'." -Type "Success"
} catch {
Write-Message "Failed to create HCI Node VM '$HCIVMName'. Error: $_" -Type "Error"
exit 1
}
}
"Creating Domain Controller VM" {
Write-Message "Creating Domain Controller VM..." -Type "Info"
try {
New-VMCreation -VMName $DCVMName `
-VMFolder $HCIVMFolder `
-DiskFolder $HCIDiskFolder `
-ISOPath $isoPath_DC `
-Memory $DC_Memory `
-Processors $DC_Processors `
-Disks $DC_Disks `
-NetworkAdapters $DC_NetworkAdapters
# Disable time synchronization on the DC VM
Get-VMIntegrationService -VMName $DCVMName | Where-Object { $_.Name -like "*Sync*" } | Disable-VMIntegrationService -ErrorAction Stop | Out-Null
Write-Message "Time synchronization disabled for VM '$DCVMName'." -Type "Success"
} catch {
Write-Message "Failed to create Domain Controller VM '$DCVMName'. Error: $_" -Type "Error"
exit 1
}
}
}
}
# Complete the Progress Bar
Write-Progress -Id 1 -Activity "Configuring Infrastructure and Creating VMs" -Completed -Status "All tasks completed."
Write-Message "All configurations and VM creations completed successfully." -Type "Success"
#endregion
01_DC.ps1
# 01_DC.ps1
# Domain Controller Configuration Script
<#
.SYNOPSIS
Configures the Domain Controller VM, sets network settings, promotes it to a Domain Controller, installs updates, and sets up Active Directory Organizational Units.
.DESCRIPTION
This script performs the following tasks:
- Sets up credentials and network configurations.
- Removes the ISO from the DC VM.
- Renames the Domain Controller VM.
- Configures network adapters with static IP settings.
- Sets the time zone.
- Promotes the VM to a Domain Controller.
- Installs Windows Updates.
- Configures DNS forwarders.
- Creates Organizational Units (OUs) in Active Directory.
- Installs necessary modules and creates Azure Stack HCI AD objects.
.NOTES
- Designed by Cristian Schmitt Nieto. For more information and usage, visit: https://schmitt-nieto.com/blog/azure-stack-hci-demolab/
- Run this script with administrative privileges.
- Ensure the Execution Policy allows the script to run. To set the execution policy, you can run:
Set-ExecutionPolicy RemoteSigned -Scope CurrentUser
#>
#region Variables
# Define credentials and variables
$defaultUser = "Administrator"
$defaultPwd = "Start#1234"
$DefaultSecuredPassword = ConvertTo-SecureString $defaultPwd -AsPlainText -Force
$DefaultCredentials = New-Object System.Management.Automation.PSCredential ($defaultUser, $DefaultSecuredPassword)
# VM and Domain Variables
$dcVMName = "DC"
$domainName = "azurestack.local"
$netBIOSName = "AZURESTACK"
$NIC1 = "MGMT1"
$nic1IP = "172.19.19.2"
$nic1GW = "172.19.19.1"
$nic1DNS = "172.19.19.2"
# Variables for DNS forwarder and time zone
$dnsForwarder = "8.8.8.8"
$timeZone = "W. Europe Standard Time" # Use "Get-TimeZone -ListAvailable" to get a list of available Time Zones
# User for Azure Stack HCI LCM User (to be used later)
$setupUser = "hciadmin"
$setupPwd = "dgemsc#utquMHDHp3M"
# Sleep durations in seconds
$SleepRename = 20 # Sleep Timer for after PC Renaming
$SleepDomain = 360 # Sleep Timer for after Domain Making
$SleepUpdates = 240 # Sleep Timer for after Update Installation
# $SleepADServices = 30 # Increased Sleep Timer after DC promotion before configuring AD
# Total number of steps for progress calculation
$totalSteps = 12
$currentStep = 0
#endregion
#region Functions
# Function to Display Messages with Colors
function Write-Message {
param (
[string]$Message,
[ValidateSet("Info", "Success", "Warning", "Error")]
[string]$Type = "Info"
)
switch ($Type) {
"Info" { Write-Host $Message -ForegroundColor Cyan }
"Success" { Write-Host $Message -ForegroundColor Green }
"Warning" { Write-Host $Message -ForegroundColor Yellow }
"Error" { Write-Host $Message -ForegroundColor Red }
}
# Optional: Log messages to a file
# Uncomment and configure the following lines to enable logging
# $LogFile = "C:\HCI\DeploymentLogs\01_DC_Log_$(Get-Date -Format 'yyyyMMdd_HHmmss').txt"
# New-Item -Path (Split-Path $LogFile) -ItemType Directory -Force | Out-Null
# Add-Content -Path $LogFile -Value "$((Get-Date).ToString('yyyy-MM-dd HH:mm:ss')) [$Type] $Message"
}
# Function to Update Progress Bar (Main Progress)
function Update-ProgressBar {
param (
[int]$CurrentStep,
[int]$TotalSteps,
[string]$StatusMessage
)
$percent = [math]::Round(($CurrentStep / $TotalSteps) * 100)
Write-Progress -Id 1 -Activity "Overall Progress" -Status $StatusMessage -PercentComplete $percent
}
# Function to Start Sleep with Progress Message and Additional Progress Bar (Subtask)
function Start-SleepWithProgress {
param(
[int]$Seconds,
[string]$Activity = "Waiting",
[string]$Status = "Please wait..."
)
Write-Message "$Activity : $Status" -Type "Info"
for ($i = 1; $i -le $Seconds; $i++) {
# Check if a key has been pressed
if ([Console]::KeyAvailable) {
$key = [Console]::ReadKey($true)
if ($key.Key -eq 'Spacebar') {
Write-Message "Sleep skipped by user." -Type "Warning"
break
}
}
$percent = [math]::Round(($i / $Seconds) * 100)
Write-Progress -Id 2 -Activity "Sleep Progress" -Status "$Activity : $i/$Seconds seconds elapsed... Use Spacebar to Skip" -PercentComplete $percent
Start-Sleep -Seconds 1
}
Write-Progress -Id 2 -Activity "Sleep Progress" -Completed
Write-Message "$Activity : Completed." -Type "Success"
}
# Function to Wait Until Active Directory is Ready
function Wait-UntilADReady {
param(
[string]$VMName,
[int]$Timeout = 600 # Timeout in seconds (10 minutes)
)
$elapsed = 0
$interval = 10
Write-Message "Checking if Active Directory services are operational on VM '$VMName'..." -Type "Info"
while ($elapsed -lt $Timeout) {
try {
Invoke-Command -VMName $VMName -Credential $DomainAdminCredentials -ScriptBlock {
Get-ADDomain -ErrorAction Stop
} -ErrorAction Stop -WarningAction SilentlyContinue | Out-Null
Write-Message "Active Directory services are operational on VM '$VMName'." -Type "Success"
return
} catch {
Write-Message "Active Directory services not ready yet. Waiting..." -Type "Info"
Start-Sleep -Seconds $interval
$elapsed += $interval
}
}
Write-Message "Active Directory services did not become operational within the expected time." -Type "Error"
exit 1
}
#endregion
#region Script Execution
# Step 1: Remove ISO from DC VM
$currentStep++
Update-ProgressBar -CurrentStep $currentStep -TotalSteps $totalSteps -StatusMessage "Removing ISO from VM..."
Write-Message "Removing ISO from DC VM '$dcVMName'..." -Type "Info"
try {
Get-VMDvdDrive -VMName $dcVMName | Where-Object { $_.DvdMediaType -eq "ISO" } | Remove-VMDvdDrive -ErrorAction Stop -WarningAction SilentlyContinue | Out-Null
Write-Message "ISO removed from VM '$dcVMName'." -Type "Success"
} catch {
Write-Message "Failed to remove ISO from VM '$dcVMName'. Error: $_" -Type "Error"
exit 1
}
# Step 2: Retrieve and format MAC address
$currentStep++
Update-ProgressBar -CurrentStep $currentStep -TotalSteps $totalSteps -StatusMessage "Retrieving MAC address..."
Write-Message "Retrieving MAC address for network adapter '$NIC1' on VM '$dcVMName'..." -Type "Info"
try {
$dcMacNIC1 = Get-VMNetworkAdapter -VMName $dcVMName -Name $NIC1 -ErrorAction Stop -WarningAction SilentlyContinue
$dcMacNIC1Address = $dcMacNIC1.MacAddress
$dcFinalMacNIC1 = $dcMacNIC1Address.Insert(2,"-").Insert(5,"-").Insert(8,"-").Insert(11,"-").Insert(14,"-").ToUpper()
Write-Message "Formatted MAC address for '$NIC1': $dcFinalMacNIC1" -Type "Success"
} catch {
Write-Message "Failed to retrieve MAC address for '$NIC1'. Error: $_" -Type "Error"
exit 1
}
# Step 3: Rename the DC VM
$currentStep++
Update-ProgressBar -CurrentStep $currentStep -TotalSteps $totalSteps -StatusMessage "Renaming VM..."
Write-Message "Renaming VM '$dcVMName'..." -Type "Info"
try {
Invoke-Command -VMName $dcVMName -Credential $DefaultCredentials -ScriptBlock {
param($dcVMName)
$ErrorActionPreference = 'Stop'
$WarningPreference = 'SilentlyContinue'
$VerbosePreference = 'SilentlyContinue'
$ProgressPreference = 'SilentlyContinue'
# Rename computer
Rename-Computer -NewName $dcVMName -Force -ErrorAction Stop
# Restart computer
Restart-Computer -Force -ErrorAction Stop
} -ArgumentList $dcVMName -ErrorAction Stop -WarningAction SilentlyContinue -Verbose:$false | Out-Null
Write-Message "VM '$dcVMName' has been renamed and will restart to apply changes." -Type "Success"
# Restart the DC VM to apply changes
Write-Message "VM '$dcVMName' is restarting..." -Type "Info"
Start-SleepWithProgress -Seconds $SleepRename -Activity "Restarting VM" -Status "Waiting for VM to restart" # 20 Seconds
} catch {
Write-Message "Failed to rename or restart VM '$dcVMName'. Error: $_" -Type "Error"
exit 1
}
# Step 4: Configure Network Settings
$currentStep++
Update-ProgressBar -CurrentStep $currentStep -TotalSteps $totalSteps -StatusMessage "Configuring network settings..."
Write-Message "Configuring network settings for VM '$dcVMName'..." -Type "Info"
try {
Invoke-Command -VMName $dcVMName -Credential $DefaultCredentials -ScriptBlock {
param($NIC1, $nic1IP, $nic1GW, $nic1DNS, $dcFinalMacNIC1)
$ErrorActionPreference = 'Stop'
$WarningPreference = 'SilentlyContinue'
$VerbosePreference = 'SilentlyContinue'
$ProgressPreference = 'SilentlyContinue'
# Rename network adapter based on MAC address
Get-NetAdapter -Physical | Where-Object { $_.MacAddress -eq $dcFinalMacNIC1 } | Rename-NetAdapter -NewName $NIC1 -ErrorAction Stop -WarningAction SilentlyContinue | Out-Null
# Disable DHCP and set static IP
Set-NetIPInterface -InterfaceAlias $NIC1 -Dhcp Disabled -ErrorAction Stop -WarningAction SilentlyContinue | Out-Null
New-NetIPAddress -InterfaceAlias $NIC1 -IPAddress $nic1IP -PrefixLength 24 -DefaultGateway $nic1GW -ErrorAction Stop -WarningAction SilentlyContinue | Out-Null
Set-DnsClientServerAddress -InterfaceAlias $NIC1 -ServerAddresses $nic1DNS -ErrorAction Stop -WarningAction SilentlyContinue | Out-Null
} -ArgumentList $NIC1, $nic1IP, $nic1GW, $nic1DNS, $dcFinalMacNIC1 -ErrorAction Stop -WarningAction SilentlyContinue -Verbose:$false | Out-Null
Write-Message "The IP address of NIC '$NIC1' is $nic1IP." -Type "Success"
} catch {
Write-Message "Failed to configure network settings for VM '$dcVMName'. Error: $_" -Type "Error"
exit 1
}
# Step 5: Set the time zone
$currentStep++
Update-ProgressBar -CurrentStep $currentStep -TotalSteps $totalSteps -StatusMessage "Setting time zone..."
Write-Message "Setting time zone for VM '$dcVMName'..." -Type "Info"
try {
Invoke-Command -VMName $dcVMName -Credential $DefaultCredentials -ScriptBlock {
param($timeZone)
$ErrorActionPreference = 'Stop'
$WarningPreference = 'SilentlyContinue'
$VerbosePreference = 'SilentlyContinue'
$ProgressPreference = 'SilentlyContinue'
# Set the time zone
Set-TimeZone -Name $timeZone -ErrorAction Stop -WarningAction SilentlyContinue | Out-Null
} -ArgumentList $timeZone -ErrorAction Stop -WarningAction SilentlyContinue -Verbose:$false | Out-Null
Write-Message "Time zone set to '$timeZone' for VM '$dcVMName'." -Type "Success"
} catch {
Write-Message "Failed to set time zone for VM '$dcVMName'. Error: $_" -Type "Error"
exit 1
}
# Step 6: Promote the DC VM to a Domain Controller
$currentStep++
Update-ProgressBar -CurrentStep $currentStep -TotalSteps $totalSteps -StatusMessage "Promoting to Domain Controller..."
Write-Message "Promoting VM '$dcVMName' to a Domain Controller..." -Type "Info"
try {
Invoke-Command -VMName $dcVMName -Credential $DefaultCredentials -ScriptBlock {
param($domainName, $netBIOSName, $defaultPwd)
$ErrorActionPreference = 'Stop'
$WarningPreference = 'SilentlyContinue'
$VerbosePreference = 'SilentlyContinue'
$ProgressPreference = 'SilentlyContinue'
# Install Active Directory Domain Services
Install-WindowsFeature -Name AD-Domain-Services -IncludeManagementTools -ErrorAction Stop | Out-Null
# Import the ADDSDeployment module after installing the feature
Import-Module ADDSDeployment -ErrorAction Stop | Out-Null
# Secure password for DSRM
$SecureDSRMPassword = ConvertTo-SecureString $defaultPwd -AsPlainText -Force
# Promote to Domain Controller
Install-ADDSForest `
-DomainName $domainName `
-DomainNetbiosName $netBIOSName `
-SafeModeAdministratorPassword $SecureDSRMPassword `
-InstallDns `
-Force:$true `
-Confirm:$false `
-ErrorAction Stop | Out-Null
} -ArgumentList $domainName, $netBIOSName, $defaultPwd -ErrorAction Stop -WarningAction SilentlyContinue -Verbose:$false | Out-Null
Write-Message "Domain Controller promotion initiated for VM '$dcVMName'." -Type "Success"
# Wait for DC promotion to complete
Write-Message "Waiting for Domain Controller promotion to complete..." -Type "Info"
Start-SleepWithProgress -Seconds $SleepDomain -Activity "Waiting for DC promotion" -Status "Waiting for VM to restart and apply domain changes" # 360 Seconds (6 Minutes)
} catch {
Write-Message "Failed to promote VM '$dcVMName' to a Domain Controller. Error: $_" -Type "Error"
exit 1
}
# Step 7: Update credentials to use the domain Administrator account
$currentStep++
Update-ProgressBar -CurrentStep $currentStep -TotalSteps $totalSteps -StatusMessage "Updating credentials..."
Write-Message "Updating credentials to use the domain Administrator account..." -Type "Info"
try {
$domainAdminUser = "$netBIOSName\$defaultUser"
$DomainAdminCredentials = New-Object System.Management.Automation.PSCredential ($domainAdminUser, $DefaultSecuredPassword)
Write-Message "Credentials updated successfully." -Type "Success"
} catch {
Write-Message "Failed to update credentials. Error: $_" -Type "Error"
exit 1
}
# Step 8: Configure DNS Forwarder and Time Server
$currentStep++
Update-ProgressBar -CurrentStep $currentStep -TotalSteps $totalSteps -StatusMessage "Configuring DNS forwarder..."
Write-Message "Configuring DNS forwarder on VM '$dcVMName'..." -Type "Info"
try {
Invoke-Command -VMName $dcVMName -Credential $DomainAdminCredentials -ScriptBlock {
param($dnsForwarder)
$ErrorActionPreference = 'Stop'
$WarningPreference = 'SilentlyContinue'
$VerbosePreference = 'SilentlyContinue'
$ProgressPreference = 'SilentlyContinue'
# Import DNS Server module
Import-Module DNSServer -ErrorAction Stop -WarningAction SilentlyContinue | Out-Null
# Add DNS forwarder
Add-DnsServerForwarder -IPAddress $dnsForwarder -ErrorAction Stop -WarningAction SilentlyContinue | Out-Null
# Configure time synchronization
w32tm /config /manualpeerlist:europe.pool.ntp.org /syncfromflags:manual /reliable:yes /update | Out-Null
Restart-Service w32time -Force | Out-Null
w32tm /resync | Out-Null
} -ArgumentList $dnsForwarder -ErrorAction Stop -WarningAction SilentlyContinue -Verbose:$false | Out-Null
Write-Message "DNS forwarder to $dnsForwarder added successfully on VM '$dcVMName'." -Type "Success"
} catch {
Write-Message "Failed to configure DNS forwarder on VM '$dcVMName'. Error: $_" -Type "Error"
exit 1
}
# Step 9: Install Windows Updates
$currentStep++
Update-ProgressBar -CurrentStep $currentStep -TotalSteps $totalSteps -StatusMessage "Installing Windows Updates..."
Write-Message "Installing Windows Updates on VM '$dcVMName'..." -Type "Info"
try {
Invoke-Command -VMName $dcVMName -Credential $DomainAdminCredentials -ScriptBlock {
$ErrorActionPreference = 'Stop'
$WarningPreference = 'SilentlyContinue'
$VerbosePreference = 'SilentlyContinue'
$ProgressPreference = 'Continue'
# Import the PSWindowsUpdate module
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -ErrorAction Stop -WarningAction SilentlyContinue | Out-Null
Install-Module PSWindowsUpdate -Force -ErrorAction Stop -WarningAction SilentlyContinue | Out-Null
Import-Module PSWindowsUpdate -ErrorAction Stop -WarningAction SilentlyContinue | Out-Null
# Install available updates
Install-WindowsUpdate -MicrosoftUpdate -AcceptAll -AutoReboot -IgnoreReboot -ErrorAction Stop -WarningAction SilentlyContinue | Out-Null
} -ErrorAction Stop -WarningAction SilentlyContinue -Verbose:$false | Out-Null
Write-Message "Windows Updates installation initiated on VM '$dcVMName'." -Type "Success"
# Wait for the DC VM to restart after updates
Write-Message "Waiting for the Domain Controller to restart after updates..." -Type "Info"
Start-SleepWithProgress -Seconds $SleepUpdates -Activity "Waiting for VM to restart" -Status "Waiting for VM to restart and apply updates" # 240 Seconds (4 Minutes)
} catch {
Write-Message "Failed to install Windows Updates on VM '$dcVMName'. Error: $_" -Type "Error"
exit 1
}
# Step 10: Create Organizational Units (OUs)
$currentStep++
Update-ProgressBar -CurrentStep $currentStep -TotalSteps $totalSteps -StatusMessage "Creating Organizational Units..."
Write-Message "Creating Organizational Units (OUs) in Active Directory on VM '$dcVMName'..." -Type "Info"
try {
# Wait until AD is ready
Wait-UntilADReady -VMName $dcVMName -Timeout 600 # 10 minutes
# Proceed to create OUs
Invoke-Command -VMName $dcVMName -Credential $DomainAdminCredentials -ScriptBlock {
param($netBIOSName, $domainName)
$ErrorActionPreference = 'Stop'
$WarningPreference = 'SilentlyContinue'
$VerbosePreference = 'SilentlyContinue'
$ProgressPreference = 'SilentlyContinue'
try {
# Import Active Directory module
Import-Module ActiveDirectory -ErrorAction Stop -WarningAction SilentlyContinue | Out-Null
# Get the current domain
$domainDN = (Get-ADDomain).DistinguishedName
# Define the root OU path
$rootOU = "OU=_LAB,$domainDN"
# Create the root OU "_LAB" (if it doesn't exist)
if (-not (Get-ADOrganizationalUnit -Filter { Name -eq "_LAB" } -SearchBase $domainDN -ErrorAction SilentlyContinue )) {
New-ADOrganizationalUnit -Name "_LAB" -Path $domainDN -ErrorAction Stop | Out-Null
Write-Host "Created root OU '_LAB'."
}
# Create sub-OUs for Users
$userOUs = @("Users", "Administrative", "Technical", "Financial", "Workers")
foreach ($ou in $userOUs) {
if ($ou -eq "Users") {
$path = $rootOU
} else {
$path = "OU=Users,$rootOU"
}
if (-not (Get-ADOrganizationalUnit -Filter { Name -eq $ou } -SearchBase $path -ErrorAction SilentlyContinue )) {
New-ADOrganizationalUnit -Name $ou -Path $path -ErrorAction Stop | Out-Null
Write-Host "Created OU '$ou' under '$path'."
}
}
# Create sub-OUs for Servers
$serverOUs = @("Servers", "Windows", "Linux", "HCI")
foreach ($ou in $serverOUs) {
if ($ou -eq "Servers") {
$path = $rootOU
} else {
$path = "OU=Servers,$rootOU"
}
if (-not (Get-ADOrganizationalUnit -Filter { Name -eq $ou } -SearchBase $path -ErrorAction SilentlyContinue )) {
New-ADOrganizationalUnit -Name $ou -Path $path -ErrorAction Stop | Out-Null
Write-Host "Created OU '$ou' under '$path'."
}
}
# Create sub-OUs for Groups
$groupOUs = @("Groups", "Security", "Distribution")
foreach ($ou in $groupOUs) {
if ($ou -eq "Groups") {
$path = $rootOU
} else {
$path = "OU=Groups,$rootOU"
}
if (-not (Get-ADOrganizationalUnit -Filter { Name -eq $ou } -SearchBase $path -ErrorAction SilentlyContinue )) {
New-ADOrganizationalUnit -Name $ou -Path $path -ErrorAction Stop | Out-Null
Write-Host "Created OU '$ou' under '$path'."
}
}
# Create sub-OUs for Computers
$computerOUs = @("Computers", "Desktops", "Laptops", "AVD")
foreach ($ou in $computerOUs) {
if ($ou -eq "Computers") {
$path = $rootOU
} else {
$path = "OU=Computers,$rootOU"
}
if (-not (Get-ADOrganizationalUnit -Filter { Name -eq $ou } -SearchBase $path -ErrorAction SilentlyContinue )) {
New-ADOrganizationalUnit -Name $ou -Path $path -ErrorAction Stop | Out-Null
Write-Host "Created OU '$ou' under '$path'."
}
}
} catch {
Write-Error "An error occurred while creating OUs: $_"
throw $_
}
} -ArgumentList $netBIOSName, $domainName -ErrorAction Stop -WarningAction SilentlyContinue -Verbose:$false | Out-Null
Write-Message "Organizational Units (OUs) created successfully in Active Directory." -Type "Success"
} catch {
Write-Message "Failed to create Organizational Units (OUs) in Active Directory. Error: $_" -Type "Error"
exit 1
}
# Step 11: Install Azure Stack HCI AD Artifacts
$currentStep++
Update-ProgressBar -CurrentStep $currentStep -TotalSteps $totalSteps -StatusMessage "Installing Azure Stack HCI AD Artifacts..."
Write-Message "Installing Azure Stack HCI AD Artifacts Pre-Creation Tool and creating AD objects..." -Type "Info"
try {
Invoke-Command -VMName $dcVMName -Credential $DomainAdminCredentials -ScriptBlock {
param($setupUser, $setupPwd)
$ErrorActionPreference = 'Stop'
$WarningPreference = 'SilentlyContinue'
$VerbosePreference = 'SilentlyContinue'
$ProgressPreference = 'SilentlyContinue'
# Suppress informational messages
$InformationPreference = 'SilentlyContinue'
# Import Active Directory module
Import-Module ActiveDirectory -ErrorAction Stop -WarningAction SilentlyContinue | Out-Null
# Suppress confirmation prompts
$ConfirmPreferenceBackup = $ConfirmPreference
$ConfirmPreference = 'None'
try {
# Install the NuGet package provider if not already installed
if (-not (Get-PackageProvider -Name NuGet -ErrorAction SilentlyContinue)) {
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -ErrorAction Stop -WarningAction SilentlyContinue | Out-Null
}
# Install the AsHciADArtifactsPreCreationTool module from PSGallery
if (-not (Get-Module -ListAvailable -Name AsHciADArtifactsPreCreationTool)) {
Install-Module AsHciADArtifactsPreCreationTool -Repository PSGallery -Force -ErrorAction Stop -WarningAction SilentlyContinue | Out-Null
}
# Define the OU path for Azure Stack HCI
$AsHciOUPath = "OU=HCI,OU=Servers,OU=_LAB," + (Get-ADDomain).DistinguishedName
# Secure credentials for Azure Stack HCI user
$SecurePassword = ConvertTo-SecureString $setupPwd -AsPlainText -Force
$AzureStackLCMUserCredential = New-Object System.Management.Automation.PSCredential ($setupUser, $SecurePassword)
# Create the AD objects and suppress all outputs
New-HciAdObjectsPreCreation -AzureStackLCMUserCredential $AzureStackLCMUserCredential -AsHciOUName $AsHciOUPath -ErrorAction Stop -WarningAction SilentlyContinue | Out-Null
} catch {
throw $_
} finally {
# Restore the original ConfirmPreference
$ConfirmPreference = $ConfirmPreferenceBackup
}
} -ArgumentList $setupUser, $setupPwd -ErrorAction Stop -WarningAction SilentlyContinue -Verbose:$false | Out-Null
Write-Message "Azure Stack HCI AD objects created successfully." -Type "Success"
} catch {
Write-Message "Failed to create Azure Stack HCI AD objects. Error: $_" -Type "Error"
exit 1
}
# Step 12: Final Completion
$currentStep++
Update-ProgressBar -CurrentStep $currentStep -TotalSteps $totalSteps -StatusMessage "Finalizing..."
Write-Message "Domain Controller configuration completed successfully." -Type "Success"
# Complete the overall progress bar
$currentStep = $totalSteps
Update-ProgressBar -CurrentStep $currentStep -TotalSteps $totalSteps -StatusMessage "All tasks completed."
#endregion
02_Cluster.ps1
# 02_Cluster.ps1
# Cluster Node Creation Script
<#
.SYNOPSIS
Configures the cluster node VM, sets up network settings, installs required features, and registers the node with Azure Arc.
.DESCRIPTION
This script performs the following tasks:
- Sets up credentials and network configurations.
- Removes the ISO from the node VM.
- Renames the node VM.
- Configures network adapters with static IP settings.
- Installs required Windows features.
- Registers the node with Azure Arc.
.NOTES
- Designed by Cristian Schmitt Nieto. For more information and usage, visit: https://schmitt-nieto.com/blog/azure-stack-hci-demolab/
- Run this script with administrative privileges.
- Ensure the Execution Policy allows the script to run. To set the execution policy, you can run:
Set-ExecutionPolicy RemoteSigned -Scope CurrentUser
#>
#region Variables
# Credentials and User Configuration
$defaultUser = "Administrator"
$defaultPwd = "Start#1234"
$DefaultSecuredPassword = ConvertTo-SecureString $defaultPwd -AsPlainText -Force
$DefaultCredentials = New-Object System.Management.Automation.PSCredential ($defaultUser, $DefaultSecuredPassword)
$setupUser = "Setupuser"
$setupPwd = "dgemsc#utquMHDHp3M"
$SecuredSetupPassword = ConvertTo-SecureString $setupPwd -AsPlainText -Force
$SetupCredentials = New-Object System.Management.Automation.PSCredential ($setupUser, $SecuredSetupPassword)
# Node Configuration
$nodeName = "NODE"
$NIC1 = "MGMT1"
$NIC2 = "MGMT2"
$nic1IP = "172.19.19.10"
$nic1GW = "172.19.19.1"
$nic1DNS = "172.19.19.2"
# Azure Configuration
$Location = "westeurope"
$Cloud = "AzureCloud"
# Sleep durations in seconds
$SleepRestart = 60 # Sleep after VM restart
$SleepFeatures = 90 # Sleep after feature installation and restart
#endregion
#region Functions
# Function to Display Messages with Colors
function Write-Message {
param (
[string]$Message,
[ValidateSet("Info", "Success", "Warning", "Error")]
[string]$Type = "Info"
)
switch ($Type) {
"Info" { Write-Host $Message -ForegroundColor Cyan }
"Success" { Write-Host $Message -ForegroundColor Green }
"Warning" { Write-Host $Message -ForegroundColor Yellow }
"Error" { Write-Host $Message -ForegroundColor Red }
}
# Optional: Log messages to a file
# Add-Content -Path "C:\Path\To\Your\LogFile.txt" -Value "$((Get-Date).ToString('yyyy-MM-dd HH:mm:ss')) [$Type] $Message"
}
# Function to Format MAC Addresses
function Format-MacAddress {
param (
[string]$mac
)
return $mac.Insert(2,"-").Insert(5,"-").Insert(8,"-").Insert(11,"-").Insert(14,"-").ToUpper()
}
# Function to Update Progress Bar (Main Progress)
function Update-ProgressBar {
param (
[int]$CurrentStep,
[int]$TotalSteps,
[string]$StatusMessage
)
$percent = [math]::Round(($CurrentStep / $TotalSteps) * 100)
Write-Progress -Id 1 -Activity "Overall Progress" -Status $StatusMessage -PercentComplete $percent
}
# Function to Start Sleep with Progress Message and Additional Progress Bar (Subtask)
function Start-SleepWithProgress {
param(
[int]$Seconds,
[string]$Activity = "Waiting",
[string]$Status = "Please wait..."
)
Write-Message "$Activity : $Status" -Type "Info"
for ($i = 1; $i -le $Seconds; $i++) {
# Check if a key has been pressed
if ([Console]::KeyAvailable) {
$key = [Console]::ReadKey($true)
if ($key.Key -eq 'Spacebar') {
Write-Message "Sleep skipped by user." -Type "Warning"
break
}
}
$percent = [math]::Round(($i / $Seconds) * 100)
Write-Progress -Id 2 -Activity "Sleep Progress" -Status "$Activity : $i/$Seconds seconds elapsed... Use Spacebar to Break" -PercentComplete $percent
Start-Sleep -Seconds 1
}
Write-Progress -Id 2 -Activity "Sleep Progress" -Completed
Write-Message "$Activity : Completed." -Type "Success"
}
# Function to Allow Selection from a List
function Get-Option ($cmd, $filterproperty) {
$items = @("")
$selection = $null
$filteredItems = @()
$i = 0
Invoke-Expression -Command $cmd | Sort-Object $filterproperty | ForEach-Object -Process {
$items += "{0}. {1}" -f $i, $_.$filterproperty
$i++
}
$filteredItems += $items | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
$filteredItems | Format-Wide { $_ } -Column 4 -Force | Out-Host
do {
$r = Read-Host "Select by number"
if ($r -match '^\d+$' -and $r -lt $filteredItems.Count) {
$selection = $filteredItems[$r] -split "\.\s" | Select-Object -Last 1
Write-Host "Selecting $($filteredItems[$r])" -ForegroundColor Green
} else {
Write-Host "You must make a valid selection" -ForegroundColor Red
$selection = $null
}
} until ($null -ne $selection)
return $selection
}
# Function to Attempt Azure Login with Retries
function Connect-AzAccountWithRetry {
param(
[int]$MaxRetries = 3,
[int]$DelaySeconds = 20 # Increased from 10 to 20 seconds
)
$attempt = 0
while ($attempt -lt $MaxRetries) {
try {
Connect-AzAccount -UseDeviceAuthentication -ErrorAction Stop
Write-Message "Successfully connected to Azure." -Type "Success"
return
} catch {
$attempt++
Write-Message "Azure login attempt $attempt failed. Retrying in $DelaySeconds seconds..." -Type "Warning"
Start-Sleep -Seconds $DelaySeconds
}
}
Write-Message "Failed to connect to Azure after $MaxRetries attempts." -Type "Error"
exit 1
}
#endregion
#region Script Execution
# Total number of steps for progress calculation
$totalSteps = 8
$currentStep = 0
# Step 1: Remove ISO from VM
$currentStep++
Update-ProgressBar -CurrentStep $currentStep -TotalSteps $totalSteps -StatusMessage "Removing ISO from VM..."
Write-Message "Removing ISO from VM '$nodeName'..." -Type "Info"
try {
Get-VMDvdDrive -VMName $nodeName | Where-Object { $_.DvdMediaType -eq "ISO" } | Remove-VMDvdDrive -ErrorAction Stop -WarningAction SilentlyContinue | Out-Null
Write-Message "ISO removed from VM '$nodeName'." -Type "Success"
} catch {
Write-Message "Failed to remove ISO from VM '$nodeName'. Error: $_" -Type "Error"
exit 1
}
# Step 2: Create setup user and rename the node
$currentStep++
Update-ProgressBar -CurrentStep $currentStep -TotalSteps $totalSteps -StatusMessage "Creating setup user and renaming VM..."
Write-Message "Creating setup user and renaming VM '$nodeName'..." -Type "Info"
try {
Invoke-Command -VMName $nodeName -Credential $DefaultCredentials -ScriptBlock {
param($setupUser, $setupPwd, $nodeName)
$ErrorActionPreference = 'Stop'
$WarningPreference = 'SilentlyContinue'
$VerbosePreference = 'SilentlyContinue'
$ProgressPreference = 'SilentlyContinue'
$InformationPreference = 'SilentlyContinue'
# Suppress all non-essential outputs
Try {
# Create local user
New-LocalUser -Name $setupUser -Password (ConvertTo-SecureString $setupPwd -AsPlainText -Force) -FullName $setupUser -Description "Setup user" -ErrorAction Stop | Out-Null
Write-Host "User $setupUser created successfully." -ForegroundColor Green | Out-Null
# Add user to Administrators group
Add-LocalGroupMember -Group "Administrators" -Member $setupUser -ErrorAction Stop | Out-Null
Write-Host "User $setupUser added to Administrators group." -ForegroundColor Green | Out-Null
} Catch {
Write-Host "Error occurred: $_" -ForegroundColor Red | Out-Null
throw $_
}
# Rename computer
Rename-Computer -NewName $nodeName -Force -ErrorAction Stop | Out-Null
# Restart computer
Restart-Computer -Force -ErrorAction Stop | Out-Null
} -ArgumentList $setupUser, $setupPwd, $nodeName -ErrorAction Stop -WarningAction SilentlyContinue -Verbose:$false | Out-Null
Write-Message "Setup user created and VM '$nodeName' is restarting..." -Type "Success"
Start-SleepWithProgress -Seconds $SleepRestart -Activity "Restarting VM" -Status "Waiting for VM to restart" # 20 Seconds
} catch {
Write-Message "Failed to create setup user or rename VM '$nodeName'. Error: $_" -Type "Error"
exit 1
}
# Step 3: Retrieve and format MAC addresses of network adapters
$currentStep++
Update-ProgressBar -CurrentStep $currentStep -TotalSteps $totalSteps -StatusMessage "Retrieving and formatting MAC addresses..."
Write-Message "Retrieving and formatting MAC addresses for VM '$nodeName'..." -Type "Info"
try {
$nodeMacNIC1 = Get-VMNetworkAdapter -VMName $nodeName -Name $NIC1 -ErrorAction Stop
$nodeMacNIC1Address = Format-MacAddress $nodeMacNIC1.MacAddress
$nodeMacNIC2 = Get-VMNetworkAdapter -VMName $nodeName -Name $NIC2 -ErrorAction Stop
$nodeMacNIC2Address = Format-MacAddress $nodeMacNIC2.MacAddress
Write-Message "MAC addresses formatted successfully." -Type "Success"
} catch {
Write-Message "Failed to retrieve or format MAC addresses for VM '$nodeName'. Error: $_" -Type "Error"
exit 1
}
# Step 4: Configure Network Settings
$currentStep++
Update-ProgressBar -CurrentStep $currentStep -TotalSteps $totalSteps -StatusMessage "Configuring network settings..."
Write-Message "Configuring network settings for VM '$nodeName'..." -Type "Info"
try {
Invoke-Command -VMName $nodeName -Credential $SetupCredentials -ScriptBlock {
param($NIC1, $NIC2, $nodeMacNIC1Address, $nodeMacNIC2Address, $nic1IP, $nic1GW, $nic1DNS)
$ErrorActionPreference = 'Stop'
$WarningPreference = 'SilentlyContinue'
$VerbosePreference = 'SilentlyContinue'
$ProgressPreference = 'SilentlyContinue'
$InformationPreference = 'SilentlyContinue'
# Suppress all non-essential outputs
# Rename network adapters based on MAC addresses
Get-NetAdapter -Physical | Where-Object { $_.MacAddress -eq $nodeMacNIC1Address } | Rename-NetAdapter -NewName $NIC1 -ErrorAction Stop | Out-Null
Get-NetAdapter -Physical | Where-Object { $_.MacAddress -eq $nodeMacNIC2Address } | Rename-NetAdapter -NewName $NIC2 -ErrorAction Stop | Out-Null
# Disable DHCP and enable RDMA on both NICs
foreach ($nic in @($NIC1, $NIC2)) {
Set-NetIPInterface -InterfaceAlias $nic -Dhcp Disabled -ErrorAction Stop | Out-Null
Enable-NetAdapterRdma -Name $nic -ErrorAction Stop | Out-Null
}
# Configure static IP on NIC1
New-NetIPAddress -InterfaceAlias $NIC1 -IPAddress $nic1IP -PrefixLength 24 -AddressFamily IPv4 -DefaultGateway $nic1GW -ErrorAction Stop | Out-Null
Set-DnsClientServerAddress -InterfaceAlias $NIC1 -ServerAddresses $nic1DNS -ErrorAction Stop | Out-Null
# Configure time synchronization
w32tm /config /manualpeerlist:$nic1DNS /syncfromflags:manual /update | Out-Null
Restart-Service w32time -Force | Out-Null
w32tm /resync | Out-Null
Write-Host "Network settings configured successfully." -ForegroundColor Green | Out-Null
} -ArgumentList $NIC1, $NIC2, $nodeMacNIC1Address, $nodeMacNIC2Address, $nic1IP, $nic1GW, $nic1DNS -ErrorAction Stop -WarningAction SilentlyContinue -Verbose:$false | Out-Null
Write-Message "Network settings configured successfully for VM '$nodeName'." -Type "Success"
} catch {
Write-Message "Failed to configure network settings for VM '$nodeName'. Error: $_" -Type "Error"
exit 1
}
# Step 5: Install required Windows Features
$currentStep++
Update-ProgressBar -CurrentStep $currentStep -TotalSteps $totalSteps -StatusMessage "Installing required Windows features..."
Write-Message "Installing required Windows features on VM '$nodeName'..." -Type "Info"
try {
Invoke-Command -VMName $nodeName -Credential $SetupCredentials -ScriptBlock {
$ErrorActionPreference = 'Stop'
$WarningPreference = 'SilentlyContinue'
$VerbosePreference = 'SilentlyContinue'
$ProgressPreference = 'SilentlyContinue'
$InformationPreference = 'SilentlyContinue'
# Suppress all non-essential outputs
# Enable Hyper-V
Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V -All -NoRestart -ErrorAction Stop | Out-Null
# Install additional features
Install-WindowsFeature -Name Hyper-V, Failover-Clustering, Data-Center-Bridging, BitLocker, FS-FileServer, RSAT-Clustering-PowerShell, FS-Data-Deduplication -IncludeAllSubFeature -IncludeManagementTools -ErrorAction Stop | Out-Null
# Restart to apply changes
Restart-Computer -Force -ErrorAction Stop | Out-Null
} -ErrorAction Stop -WarningAction SilentlyContinue -Verbose:$false | Out-Null
Write-Message "Required Windows features installed and VM '$nodeName' is restarting..." -Type "Success"
Start-SleepWithProgress -Seconds $SleepFeatures -Activity "Restarting VM" -Status "Waiting for VM to restart" # 40 Seconds
} catch {
Write-Message "Failed to install Windows features on VM '$nodeName'. Error: $_" -Type "Error"
exit 1
}
# Step 6: Register PSGallery as a trusted repository on the node
$currentStep++
Update-ProgressBar -CurrentStep $currentStep -TotalSteps $totalSteps -StatusMessage "Registering PSGallery on VM..."
Write-Message "Registering PSGallery as a trusted repository on VM '$nodeName'..." -Type "Info"
try {
Invoke-Command -VMName $nodeName -Credential $SetupCredentials -ScriptBlock {
param($NIC1)
$ErrorActionPreference = 'Stop'
$WarningPreference = 'SilentlyContinue'
$VerbosePreference = 'SilentlyContinue'
$ProgressPreference = 'SilentlyContinue'
$InformationPreference = 'SilentlyContinue'
# Suppress all non-essential outputs
if (-not (Get-PSRepository -Name "PSGallery" -ErrorAction SilentlyContinue)) {
Register-PSRepository -Default -ErrorAction Stop | Out-Null
}
Set-PSRepository -Name "PSGallery" -InstallationPolicy Trusted -ErrorAction Stop | Out-Null
Write-Host "PSGallery registered as a trusted repository." -ForegroundColor Green | Out-Null
} -ArgumentList $NIC1 -ErrorAction Stop -WarningAction SilentlyContinue -Verbose:$false | Out-Null
Write-Message "PSGallery registered successfully on VM '$nodeName'." -Type "Success"
} catch {
Write-Message "Failed to register PSGallery on VM '$nodeName'. Error: $_" -Type "Error"
exit 1
}
# Step 7: Install required PowerShell modules on the node for Azure Arc registration
$currentStep++
Update-ProgressBar -CurrentStep $currentStep -TotalSteps $totalSteps -StatusMessage "Installing required PowerShell modules..."
Write-Message "Installing required PowerShell modules on VM '$nodeName'..." -Type "Info"
try {
Invoke-Command -VMName $nodeName -Credential $SetupCredentials -ScriptBlock {
param($NIC1)
$ErrorActionPreference = 'Stop'
$WarningPreference = 'SilentlyContinue'
$VerbosePreference = 'SilentlyContinue'
$ProgressPreference = 'SilentlyContinue'
$InformationPreference = 'SilentlyContinue'
# Suppress all non-essential outputs
# Install required modules
Install-Module Az.Accounts -RequiredVersion 3.0.0 -Force -ErrorAction Stop | Out-Null
Install-Module Az.Resources -RequiredVersion 6.12.0 -Force -ErrorAction Stop | Out-Null
Install-Module Az.ConnectedMachine -RequiredVersion 0.8.0 -Force -ErrorAction Stop | Out-Null
Install-Module AzsHCI.ArcInstaller -Force -ErrorAction Stop | Out-Null
Write-Host "Required PowerShell modules installed successfully." -ForegroundColor Green | Out-Null
} -ErrorAction Stop -WarningAction SilentlyContinue -Verbose:$false | Out-Null
Write-Message "PowerShell modules installed successfully on VM '$nodeName'." -Type "Success"
} catch {
Write-Message "Failed to install PowerShell modules on VM '$nodeName'. Error: $_" -Type "Error"
exit 1
}
# Step 8: Invoke Azure Stack HCI Arc Initialization on the node
$currentStep++
Update-ProgressBar -CurrentStep $currentStep -TotalSteps $totalSteps -StatusMessage "Registering node with Azure Arc..."
Write-Message "Registering VM '$nodeName' with Azure Arc..." -Type "Info"
try {
# Connect to Azure and set context
Connect-AzAccountWithRetry -MaxRetries 5 -DelaySeconds 20 # Increased DelaySeconds to 20
# Allow user to select Subscription
$subscription = Get-Option "Get-AzSubscription" "Name"
Set-AzContext -SubscriptionName $subscription -ErrorAction Stop
# Allow user to select Resource Group
$resourceGroup = Get-Option "Get-AzResourceGroup" "ResourceGroupName"
$TenantID = (Get-AzContext).Tenant.Id
$SubscriptionID = (Get-AzContext).Subscription.Id
$ARMToken = (Get-AzAccessToken).Token
$AccountId = (Get-AzContext).Account.Id
# Prepare the script to run on the node
$ArcInitScript = @'
param($SubscriptionID, $ResourceGroupName, $TenantID, $Cloud, $Location, $ARMToken, $AccountId)
# Import modules
Import-Module Az.Accounts -RequiredVersion 3.0.0 -ErrorAction Stop
Import-Module Az.Resources -RequiredVersion 6.12.0 -ErrorAction Stop
Import-Module Az.ConnectedMachine -RequiredVersion 0.8.0 -ErrorAction Stop
Import-Module AzsHCI.ArcInstaller -ErrorAction Stop
# Suppress all non-essential outputs
$ErrorActionPreference = 'Stop'
$WarningPreference = 'SilentlyContinue'
$VerbosePreference = 'SilentlyContinue'
$InformationPreference = 'SilentlyContinue'
# Invoke Arc Initialization
Invoke-AzStackHciArcInitialization -SubscriptionID $SubscriptionID `
-ResourceGroup $ResourceGroupName `
-TenantID $TenantID `
-Cloud $Cloud `
-Region $Location `
-ArmAccessToken $ARMToken `
-AccountID $AccountId -ErrorAction Stop | Out-Null
Write-Host "VM '$env:COMPUTERNAME' registered with Azure Arc successfully." -ForegroundColor Green | Out-Null
'@
# Copy the script to the node
$ScriptPath = "C:\Temp\ArcInitScript.ps1"
Invoke-Command -VMName $nodeName -Credential $SetupCredentials -ScriptBlock {
param($ScriptContent, $ScriptPath)
$ErrorActionPreference = 'Stop'
$WarningPreference = 'SilentlyContinue'
$VerbosePreference = 'SilentlyContinue'
$ProgressPreference = 'SilentlyContinue'
$InformationPreference = 'SilentlyContinue'
# Suppress all non-essential outputs
New-Item -Path (Split-Path $ScriptPath) -ItemType Directory -Force | Out-Null
Set-Content -Path $ScriptPath -Value $ScriptContent -Force | Out-Null
} -ArgumentList $ArcInitScript, $ScriptPath -ErrorAction Stop -WarningAction SilentlyContinue -Verbose:$false | Out-Null
# Run the script locally on the node
Invoke-Command -VMName $nodeName -Credential $SetupCredentials -ScriptBlock {
param($ScriptPath, $SubscriptionID, $ResourceGroupName, $TenantID, $Cloud, $Location, $ARMToken, $AccountId)
$ErrorActionPreference = 'Stop'
$WarningPreference = 'SilentlyContinue'
$VerbosePreference = 'SilentlyContinue'
$ProgressPreference = 'SilentlyContinue'
$InformationPreference = 'SilentlyContinue'
# Suppress all non-essential outputs
# Execute the script locally
& $ScriptPath -SubscriptionID $SubscriptionID -ResourceGroupName $ResourceGroupName -TenantID $TenantID -Cloud $Cloud -Location $Location -ARMToken $ARMToken -AccountId $AccountId | Out-Null
} -ArgumentList $ScriptPath, $SubscriptionID, $resourceGroup, $TenantID, $Cloud, $Location, $ARMToken, $AccountId -ErrorAction Stop -WarningAction SilentlyContinue -Verbose:$false | Out-Null
# Write-Message "VM '$nodeName' registered successfully with Azure Arc." -Type "Success"
} catch {
Write-Message "Failed to register VM '$nodeName' with Azure Arc. Error: $_" -Type "Error"
exit 1
}
# Complete the overall progress bar
Update-ProgressBar -CurrentStep $totalSteps -TotalSteps $totalSteps -StatusMessage "All tasks completed."
# Write-Host "Cluster node configuration completed successfully." -ForegroundColor Green
Write-Message "Cluster node configuration completed successfully." -Type "Success"
#endregion
03_TroubleshootingExtensions.ps1
# 03_TroubleshootingExtensions.ps1
# Troubleshooting Azure Connected Machine Extensions for ARC VMs
<#
.SYNOPSIS
Troubleshoots and manages Azure Connected Machine extensions for ARC VMs.
.DESCRIPTION
This script performs the following tasks:
- Installs the Az.Compute and Az.StackHCI modules if not already installed.
- Connects to Azure using device code authentication and allows the user to select a Subscription and Resource Group.
- Retrieves ARC VMs from Azure using Az.StackHCI, filtering for ARC Machines with CloudMetadataProvider "AzSHCI".
- Validates that required Azure Connected Machine extensions are installed.
- Fixes any failed extensions by removing locks, deleting, and reinstalling them.
- Adds any missing extensions based on a predefined list.
.NOTES
- Based on Jaromir´s aproach: https://github.com/DellGEOS/AzureStackHOLs/tree/main/tips%26tricks/05-FixExtensions
- Designed by Cristian Schmitt Nieto. For more information and usage, visit: https://schmitt-nieto.com/blog/azure-stack-hci-demolab/
- Run this script with administrative privileges.
- Ensure the Execution Policy allows the script to run. To set the execution policy, you can run:
Set-ExecutionPolicy RemoteSigned -Scope CurrentUser
#>
#region Variables
# Extension Settings
$Location = "westeurope"
$Settings = @{
"CloudName" = "AzureCloud";
"RegionName" = $Location;
"DeviceType" = "AzureEdge"
}
$ExtensionList = @(
@{ Name = "AzureEdgeTelemetryAndDiagnostics"; Publisher = "Microsoft.AzureStack.Observability"; MachineExtensionType = "TelemetryAndDiagnostics" },
@{ Name = "AzureEdgeDeviceManagement"; Publisher = "Microsoft.Edge"; MachineExtensionType = "DeviceManagementExtension" },
@{ Name = "AzureEdgeLifecycleManager"; Publisher = "Microsoft.AzureStack.Orchestration"; MachineExtensionType = "LcmController" },
@{ Name = "AzureEdgeRemoteSupport"; Publisher = "Microsoft.AzureStack.Observability"; MachineExtensionType = "EdgeRemoteSupport" }
)
# Total number of steps for progress calculation
$totalSteps = 9
$currentStep = 0
#endregion
#region Functions
# Function to Display Messages with Colors
function Write-Message {
param (
[string]$Message,
[ValidateSet("Info", "Success", "Warning", "Error")]
[string]$Type = "Info"
)
switch ($Type) {
"Info" { Write-Host $Message -ForegroundColor Cyan }
"Success" { Write-Host $Message -ForegroundColor Green }
"Warning" { Write-Host $Message -ForegroundColor Yellow }
"Error" { Write-Host $Message -ForegroundColor Red }
}
# Optional: Log messages to a file
# Uncomment and configure the following lines to enable logging
# $LogFile = "C:\Path\To\Your\LogFile.txt"
# Add-Content -Path $LogFile -Value "$((Get-Date).ToString('yyyy-MM-dd HH:mm:ss')) [$Type] $Message"
}
# Function to Update Progress Bar (Main Progress)
function Update-ProgressBar {
param (
[int]$CurrentStep,
[int]$TotalSteps,
[string]$StatusMessage
)
$percent = [math]::Round(($CurrentStep / $TotalSteps) * 100)
Write-Progress -Id 1 -Activity "Overall Progress" -Status $StatusMessage -PercentComplete $percent
}
# Function to Install Az.Compute Module if Not Installed
function Install-AzComputeModule {
if (-not (Get-Module -ListAvailable -Name Az.Compute)) {
Write-Message "Az.Compute module not found. Installing..." -Type "Info"
try {
Install-Module -Name Az.Compute -Repository PSGallery -Force -AllowClobber -ErrorAction Stop
Write-Message "Az.Compute module installed successfully." -Type "Success"
} catch {
Write-Message "Failed to install Az.Compute module. Error: $_" -Type "Error"
exit 1
}
} else {
Write-Message "Az.Compute module is already installed." -Type "Info"
}
}
# Function to Install Az.StackHCI Module if Not Installed
function Install-AzStackHCIModule {
if (-not (Get-Module -ListAvailable -Name Az.StackHCI)) {
Write-Message "Az.StackHCI module not found. Installing..." -Type "Info"
try {
Install-Module -Name Az.StackHCI -Repository PSGallery -Force -AllowClobber -ErrorAction Stop
Write-Message "Az.StackHCI module installed successfully." -Type "Success"
} catch {
Write-Message "Failed to install Az.StackHCI module. Error: $_" -Type "Error"
exit 1
}
} else {
Write-Message "Az.StackHCI module is already installed." -Type "Info"
}
}
# Function to Allow Selection from a List
function Get-Option {
param (
[string]$cmd,
[string]$filterproperty
)
$items = @("")
$selection = $null
$filteredItems = @()
$i = 0
try {
$cmdOutput = Invoke-Expression -Command $cmd | Sort-Object $filterproperty
foreach ($item in $cmdOutput) {
$items += "{0}. {1}" -f $i, $item.$filterproperty
$i++
}
} catch {
Write-Message "Failed to execute command '$cmd'. Error: $_" -Type "Error"
exit 1
}
$filteredItems += $items | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
$filteredItems | Format-Wide { $_ } -Column 4 -Force | Out-Host
do {
$r = Read-Host "Select by number"
if ($r -match '^\d+$' -and $r -lt $filteredItems.Count) {
$selection = $filteredItems[$r] -split "\.\s" | Select-Object -Last 1
Write-Host "Selecting $($filteredItems[$r])" -ForegroundColor Green
} else {
Write-Host "You must make a valid selection" -ForegroundColor Red
$selection = $null
}
} until ($null -ne $selection)
return $selection
}
# Function to Retrieve ARC VMs from Azure using Az.StackHCI
function Get-ARCVMsFromAzure {
param(
[string]$SubscriptionId,
[string]$ResourceGroupName
)
try {
# Retrieve all connected machines in the resource group
$connectedMachines = Get-AzConnectedMachine -ResourceGroupName $ResourceGroupName -ErrorAction Stop
if ($connectedMachines.Count -eq 0) {
Write-Message "No connected machines found in resource group '$ResourceGroupName'." -Type "Warning"
return @()
}
# Filter connected machines where CloudMetadataProvider is "AzSHCI"
$ARCVMs = $connectedMachines | Where-Object { $_.CloudMetadataProvider -eq "AzSHCI" }
if ($ARCVMs.Count -eq 0) {
Write-Message "No ARC VMs (Machines with CloudMetadataProvider 'AzSHCI') found in resource group '$ResourceGroupName'." -Type "Warning"
} else {
Write-Message "Retrieved $($ARCVMs.Count) ARC VM(s) from Azure." -Type "Success"
}
return $ARCVMs
} catch {
Write-Message "Failed to retrieve ARC VMs from Azure. Error: $_" -Type "Error"
exit 1
}
}
# Function to Validate Installed Extensions
function Test-Extensions {
param(
[array]$ARCVMs,
[string]$ResourceGroupName
)
$Extensions = @()
foreach ($ARCVM in $ARCVMs) {
try {
$extensions = Get-AzConnectedMachineExtension -ResourceGroupName $ResourceGroupName -MachineName $ARCVM.Name -ErrorAction Stop
$Extensions += $extensions
} catch {
Write-Message "Failed to retrieve extensions for ARC VM '$($ARCVM.Name)'. Error: $_" -Type "Error"
exit 1
}
}
return $Extensions
}
#endregion
#region Script Execution
# Step 1: Install Az.Compute Module
$currentStep++
Update-ProgressBar -CurrentStep $currentStep -TotalSteps $totalSteps -StatusMessage "Ensuring Az.Compute module is installed..."
Write-Message "Checking for Az.Compute module..." -Type "Info"
try {
Install-AzComputeModule
} catch {
Write-Message "An error occurred while ensuring Az.Compute module is installed. Error: $_" -Type "Error"
exit 1
}
# Step 2: Install Az.StackHCI Module
$currentStep++
Update-ProgressBar -CurrentStep $currentStep -TotalSteps $totalSteps -StatusMessage "Ensuring Az.StackHCI module is installed..."
Write-Message "Checking for Az.StackHCI module..." -Type "Info"
try {
Install-AzStackHCIModule
} catch {
Write-Message "An error occurred while ensuring Az.StackHCI module is installed. Error: $_" -Type "Error"
exit 1
}
# Step 3: Connect to Azure using Device Code Authentication
$currentStep++
Update-ProgressBar -CurrentStep $currentStep -TotalSteps $totalSteps -StatusMessage "Connecting to Azure using device code authentication..."
Write-Message "Connecting to Azure..." -Type "Info"
try {
Connect-AzAccount -UseDeviceAuthentication -ErrorAction Stop
Write-Message "Connected to Azure successfully." -Type "Success"
} catch {
Write-Message "Failed to connect to Azure using device code authentication. Error: $_" -Type "Error"
exit 1
}
# Step 4: Select Subscription
$currentStep++
Update-ProgressBar -CurrentStep $currentStep -TotalSteps $totalSteps -StatusMessage "Selecting Azure Subscription..."
Write-Message "Retrieving available Azure Subscriptions..." -Type "Info"
try {
$subscription = Get-Option "Get-AzSubscription" "Name"
Set-AzContext -SubscriptionName $subscription -ErrorAction Stop
$selectedSubscription = Get-AzContext -ErrorAction Stop
$SubscriptionId = $selectedSubscription.Subscription.Id
Write-Message "Selected Subscription: $($selectedSubscription.Subscription.Name)" -Type "Success"
} catch {
Write-Message "Failed to select or set Azure Subscription. Error: $_" -Type "Error"
exit 1
}
# Step 5: Select Resource Group
$currentStep++
Update-ProgressBar -CurrentStep $currentStep -TotalSteps $totalSteps -StatusMessage "Selecting Resource Group..."
Write-Message "Retrieving available Resource Groups in Subscription '$($selectedSubscription.Subscription.Name)'..." -Type "Info"
try {
$resourceGroup = Get-Option "Get-AzResourceGroup" "ResourceGroupName"
$ResourceGroupName = $resourceGroup
Write-Message "Selected Resource Group: $ResourceGroupName" -Type "Success"
} catch {
Write-Message "Failed to select Resource Group. Error: $_" -Type "Error"
exit 1
}
# Step 6: Retrieve ARC VMs from Azure
$currentStep++
Update-ProgressBar -CurrentStep $currentStep -TotalSteps $totalSteps -StatusMessage "Retrieving ARC VMs from Azure..."
Write-Message "Retrieving ARC VMs from Azure..." -Type "Info"
try {
$ARCVMs = Get-ARCVMsFromAzure -SubscriptionId $SubscriptionId -ResourceGroupName $ResourceGroupName
if ($ARCVMs.Count -eq 0) {
Write-Message "No ARC VMs found. Exiting script." -Type "Error"
exit 1
}
} catch {
Write-Message "An error occurred while retrieving ARC VMs. Error: $_" -Type "Error"
exit 1
}
# Step 7: Select ARC VM
$currentStep++
Update-ProgressBar -CurrentStep $currentStep -TotalSteps $totalSteps -StatusMessage "Selecting ARC VM..."
Write-Message "Selecting an ARC VM..." -Type "Info"
try {
# Prepare the list for selection
$arcVmNames = $ARCVMs | Select-Object -ExpandProperty Name
# Construct the command string for Get-Option
# Create a temporary array to hold objects with Name property
$tempList = @()
foreach ($arcVm in $ARCVMs) {
$tempList += [PSCustomObject]@{ Name = $arcVm.Name }
}
# Export the temporary list to a temporary file
$tempList | Export-Clixml -Path "$env:TEMP\ARCVMList.xml"
# Use Get-Option by importing the temporary list
function Get-Option-FromList {
param (
[string]$filterproperty
)
$items = @("")
$selection = $null
$filteredItems = @()
$i = 0
$cmdOutput = Import-Clixml -Path "$env:TEMP\ARCVMList.xml" | Sort-Object $filterproperty
foreach ($item in $cmdOutput) {
$items += "{0}. {1}" -f $i, $item.$filterproperty
$i++
}
$filteredItems += $items | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
$filteredItems | Format-Wide { $_ } -Column 4 -Force | Out-Host
do {
$r = Read-Host "Select by number"
if ($r -match '^\d+$' -and $r -lt $filteredItems.Count) {
$selection = $filteredItems[$r] -split "\.\s" | Select-Object -Last 1
Write-Host "Selecting $($filteredItems[$r])" -ForegroundColor Green
} else {
Write-Host "You must make a valid selection" -ForegroundColor Red
$selection = $null
}
} until ($null -ne $selection)
return $selection
}
# Allow user to select an ARC VM
$selectedARCVMName = Get-Option-FromList "Name"
# Find the selected ARC VM object
$selectedARCVM = $ARCVMs | Where-Object { $_.Name -eq $selectedARCVMName }
if (-not $selectedARCVM) {
Write-Message "No ARC VM selected. Exiting script." -Type "Error"
exit 1
} else {
Write-Message "Selected ARC VM: $($selectedARCVM.Name)" -Type "Success"
}
# Remove temporary XML file
Remove-Item -Path "$env:TEMP\ARCVMList.xml" -Force -ErrorAction SilentlyContinue
} catch {
Write-Message "An error occurred while selecting ARC VM. Error: $_" -Type "Error"
exit 1
}
# Step 8: Validate Installed Extensions
$currentStep++
Update-ProgressBar -CurrentStep $currentStep -TotalSteps $totalSteps -StatusMessage "Validating installed extensions..."
Write-Message "Validating installed Azure Connected Machine extensions on ARC VM '$($selectedARCVM.Name)'..." -Type "Info"
try {
$Extensions = Test-Extensions -ARCVMs @($selectedARCVM) -ResourceGroupName $ResourceGroupName
Write-Message "Extension validation completed." -Type "Success"
} catch {
Write-Message "An error occurred during extension validation. Error: $_" -Type "Error"
exit 1
}
# Step 9: Fix Failed Extensions and Add Missing Extensions
$currentStep++
Update-ProgressBar -CurrentStep $currentStep -TotalSteps $totalSteps -StatusMessage "Fixing failed extensions and adding missing ones..."
Write-Message "Identifying and fixing failed extensions, then adding any missing extensions on ARC VM '$($selectedARCVM.Name)'..." -Type "Info"
try {
# Fix failed extensions
$FailedExtensions = $Extensions | Where-Object ProvisioningState -eq "Failed"
if ($FailedExtensions.Count -eq 0) {
Write-Message "No failed extensions found." -Type "Success"
} else {
foreach ($FailedExtension in $FailedExtensions) {
$Server = $FailedExtension.MachineName
Write-Message "Processing failed extension '$($FailedExtension.Name)' on server '$Server'..." -Type "Info"
# Remove lock first
$Locks = Get-AzResourceLock -ResourceGroupName $ResourceGroupName | Where-Object ResourceID -like "*HybridCompute/machines/$Server*"
if ($Locks.Count -gt 0) {
foreach ($lock in $Locks) {
Write-Message "Removing lock '$($lock.Name)' from server '$Server'..." -Type "Info"
Remove-AzResourceLock -LockId $lock.LockId -Force -ErrorAction Stop
}
Write-Message "Locks removed from server '$Server'." -Type "Success"
}
# Remove the failed extension
Write-Message "Removing failed extension '$($FailedExtension.Name)' from server '$Server'..." -Type "Info"
Remove-AzConnectedMachineExtension -Name $FailedExtension.Name -ResourceGroupName $FailedExtension.ResourceGroupName -MachineName $Server -ErrorAction Stop
Write-Message "Failed extension '$($FailedExtension.Name)' removed from server '$Server'." -Type "Success"
# Re-add the extension
Write-Message "Reinstalling extension '$($FailedExtension.Name)' on server '$Server'..." -Type "Info"
New-AzConnectedMachineExtension -Name $FailedExtension.Name `
-ResourceGroupName $FailedExtension.ResourceGroupName `
-MachineName $Server `
-Location $FailedExtension.Location `
-Publisher $FailedExtension.Publisher `
-Settings $Settings `
-ExtensionType $FailedExtension.MachineExtensionType `
-ErrorAction Stop
Write-Message "Extension '$($FailedExtension.Name)' reinstalled on server '$Server'." -Type "Success"
}
}
# Add missing extensions
Write-Message "Adding any missing Azure Connected Machine extensions..." -Type "Info"
foreach ($Extension in $ExtensionList) {
# Check if the extension is already installed on the ARC VM
$isInstalled = $Extensions | Where-Object { $_.Name -eq $Extension.Name -and $_.MachineName -eq $selectedARCVM.Name }
if (-not $isInstalled) {
Write-Message "Installing missing extension '$($Extension.Name)' on server '$($selectedARCVM.Name)'..." -Type "Info"
New-AzConnectedMachineExtension -Name $Extension.Name `
-ResourceGroupName $ResourceGroupName `
-MachineName $selectedARCVM.Name `
-Location $Location `
-Publisher $Extension.Publisher `
-Settings $Settings `
-ExtensionType $Extension.MachineExtensionType `
-ErrorAction Stop
Write-Message "Extension '$($Extension.Name)' installed successfully on server '$($selectedARCVM.Name)'." -Type "Success"
} else {
Write-Message "Extension '$($Extension.Name)' already installed on server '$($selectedARCVM.Name)'. Skipping." -Type "Info"
}
}
} catch {
Write-Message "An error occurred while fixing failed extensions or adding missing ones. Error: $_" -Type "Error"
exit 1
}
# Complete the overall progress bar
Update-ProgressBar -CurrentStep $totalSteps -TotalSteps $totalSteps -StatusMessage "All tasks completed."
# Final success message
Write-Message "Azure Connected Machine extensions troubleshooting completed successfully." -Type "Success"
#endregion
99_Offboarding.ps1
# Offboarding Script to Clean Up Configurations
<#
.SYNOPSIS
Cleans up VM configurations, virtual switches, NAT settings, and associated folders.
.DESCRIPTION
This script performs the following tasks:
- Stops and removes specified VMs.
- Deletes associated VHD files.
- Removes HgsGuardian entries.
- Deletes virtual switches and NAT configurations.
- Removes designated folder structures.
.NOTES
- Designed by Cristian Schmitt Nieto. For more information and usage, visit: https://schmitt-nieto.com/blog/azure-stack-hci-demolab/
- Run this script with administrative privileges.
- Ensure the Execution Policy allows the script to run. To set the execution policy, you can run:
Set-ExecutionPolicy RemoteSigned -Scope CurrentUser
#>
#region Variables
# Define VM Names
$HCIVMName = "NODE"
$DCVMName = "DC"
# Define Virtual Switch and NAT Configuration
$vSwitchName = "azurestackhci"
$vSwitchNIC = "vEthernet ($vSwitchName)"
$natName = "azurestackhci"
# Define Root Folder for VMs and Disks
$HCIRootFolder = "C:\HCI"
$HCIDiskFolder = Join-Path -Path $HCIRootFolder -ChildPath "Disk"
# Define Tasks for Progress Bar
$tasks = @(
"Stopping and Removing HCI Node VM",
"Stopping and Removing Domain Controller VM",
"Removing NAT Configuration",
"Removing IP Addresses from Virtual Switch Interface",
"Removing Virtual Switch",
"Removing Folder Structures"
)
$totalTasks = $tasks.Count
$currentTask = 0
#endregion
#region Functions
# Function to Display Messages with Colors
function Write-Message {
param (
[string]$Message,
[ValidateSet("Info", "Success", "Warning", "Error")]
[string]$Type = "Info"
)
switch ($Type) {
"Info" { Write-Host $Message -ForegroundColor Cyan }
"Success" { Write-Host $Message -ForegroundColor Green }
"Warning" { Write-Host $Message -ForegroundColor Yellow }
"Error" { Write-Host $Message -ForegroundColor Red }
}
}
# Function to Remove VMs and Associated Resources
function Remove-VMResources {
param (
[string]$VMName,
[string]$DiskFolder
)
# Suppress non-critical outputs within the function
$ErrorActionPreference = 'Stop'
$WarningPreference = 'SilentlyContinue'
$VerbosePreference = 'SilentlyContinue'
$ProgressPreference = 'SilentlyContinue'
# Stop and remove the VM if it exists
$vm = Get-VM -Name $VMName -ErrorAction SilentlyContinue
if ($null -ne $vm) {
if ($vm.State -in @('Running', 'Paused', 'Suspended')) {
try {
Stop-VM -Name $VMName -Force -ErrorAction Stop | Out-Null
Write-Message "VM '$VMName' stopped." -Type "Success"
} catch {
Write-Message "Failed to stop VM '$VMName'. Error: $_" -Type "Error"
return
}
}
# Remove the VM
try {
Remove-VM -Name $VMName -Force -ErrorAction Stop | Out-Null
Write-Message "VM '$VMName' removed." -Type "Success"
} catch {
Write-Message "Failed to remove VM '$VMName'. Error: $_" -Type "Error"
return
}
# Remove VHD files
$vhdFiles = Get-ChildItem -Path $DiskFolder -Filter "$VMName*.vhdx" -Recurse -ErrorAction SilentlyContinue
foreach ($vhd in $vhdFiles) {
try {
Remove-Item -Path $vhd.FullName -Force -ErrorAction Stop | Out-Null
Write-Message "VHD file '$($vhd.FullName)' deleted." -Type "Success"
} catch {
Write-Message "Failed to delete VHD file '$($vhd.FullName)'. Error: $_" -Type "Error"
}
}
# Remove HgsGuardian if it exists
try {
Remove-HgsGuardian -Name $VMName -ErrorAction SilentlyContinue -WarningAction SilentlyContinue | Out-Null
Write-Message "HgsGuardian '$VMName' removed." -Type "Success"
} catch {
Write-Message "Failed to remove HgsGuardian '$VMName'. Error: $_" -Type "Error"
}
} else {
Write-Message "VM '$VMName' does not exist. Skipping removal." -Type "Warning"
}
# Reset preferences to default
$ErrorActionPreference = 'Continue'
$WarningPreference = 'Continue'
$VerbosePreference = 'Continue'
$ProgressPreference = 'Continue'
}
#endregion
#region Script Execution
foreach ($task in $tasks) {
$currentTask++
Write-Progress -Activity "Cleaning Up Configurations" -Status "$task..." -PercentComplete (($currentTask / $totalTasks) * 100)
switch ($task) {
"Stopping and Removing HCI Node VM" {
Remove-VMResources -VMName $HCIVMName -DiskFolder $HCIDiskFolder
}
"Stopping and Removing Domain Controller VM" {
Remove-VMResources -VMName $DCVMName -DiskFolder $HCIDiskFolder
}
"Removing NAT Configuration" {
Write-Message "Removing NAT configuration '$natName'..." -Type "Info"
try {
Remove-NetNat -Name $natName -Confirm:$false -ErrorAction Stop | Out-Null
Write-Message "NAT '$natName' removed." -Type "Success"
} catch [System.Management.Automation.ItemNotFoundException] {
Write-Message "NAT '$natName' does not exist. Skipping removal." -Type "Warning"
} catch {
Write-Message "Failed to remove NAT '$natName'. Error: $_" -Type "Error"
}
}
"Removing IP Addresses from Virtual Switch Interface" {
Write-Message "Removing IP addresses from interface '$vSwitchNIC'..." -Type "Info"
try {
$ipAddresses = Get-NetIPAddress -InterfaceAlias $vSwitchNIC -ErrorAction Stop
foreach ($ip in $ipAddresses) {
try {
Remove-NetIPAddress -InterfaceAlias $vSwitchNIC -IPAddress $ip.IPAddress -Confirm:$false -ErrorAction Stop | Out-Null
Write-Message "IP address '$($ip.IPAddress)' removed from interface '$vSwitchNIC'." -Type "Success"
} catch {
Write-Message "Failed to remove IP address '$($ip.IPAddress)' from interface '$vSwitchNIC'. Error: $_" -Type "Error"
}
}
} catch [System.Management.Automation.ItemNotFoundException] {
Write-Message "No IP addresses found on interface '$vSwitchNIC'. Skipping removal." -Type "Warning"
} catch {
Write-Message "Failed to retrieve IP addresses from interface '$vSwitchNIC'. Error: $_" -Type "Error"
}
}
"Removing Virtual Switch" {
Write-Message "Removing virtual switch '$vSwitchName'..." -Type "Info"
try {
Remove-VMSwitch -Name $vSwitchName -Force -ErrorAction Stop | Out-Null
Write-Message "Virtual switch '$vSwitchName' removed." -Type "Success"
} catch [System.Management.Automation.ItemNotFoundException] {
Write-Message "Virtual switch '$vSwitchName' does not exist. Skipping removal." -Type "Warning"
} catch {
Write-Message "Failed to remove virtual switch '$vSwitchName'. Error: $_" -Type "Error"
}
}
"Removing Folder Structures" {
Write-Message "Removing folder structures at '$HCIRootFolder'..." -Type "Info"
if (Test-Path -Path $HCIRootFolder) {
try {
Remove-Item -Path $HCIRootFolder -Recurse -Force -ErrorAction Stop | Out-Null
Write-Message "Folder '$HCIRootFolder' and all its contents have been deleted." -Type "Success"
} catch {
Write-Message "Failed to delete folder '$HCIRootFolder'. Error: $_" -Type "Error"
}
} else {
Write-Message "Folder '$HCIRootFolder' does not exist. Skipping removal." -Type "Warning"
}
}
}
}
# Complete the Progress Bar
Write-Progress -Activity "Cleaning Up Configurations" -Completed -Status "All tasks completed."
Write-Message "Cleanup completed successfully." -Type "Success"
#endregion
Comments