AzureLocalStuff.psm1

function Get-AzureLocalExtensionCompatibilityTable {
    <#
    .SYNOPSIS
    Fetches the Azure CLI stack-hci-vm extension compatibility table.
    To know which versions of the Azure CLI stack-hci-vm extension are compatible with specific Azure Local versions.
 
    .DESCRIPTION
    This function retrieves the compatibility table for Azure CLI stack-hci-vm extensions from a markdown file hosted on GitHub at https://raw.githubusercontent.com/Azure-Samples/AzureLocal/refs/heads/main/Arc%20VMs%20Extension%20Compatibility.md.
 
    .OUTPUTS
    Returns a list of custom objects representing the compatibility data.
 
    .EXAMPLE
    Get-AzureHCIExtensionCompatibilityTable
    #>


    [CmdletBinding()]
    param ()

    $url = "https://raw.githubusercontent.com/Azure-Samples/AzureLocal/refs/heads/main/Arc%20VMs%20Extension%20Compatibility.md"

    try {
        # Fetch the raw content of the markdown file
        $markdownContent = Invoke-RestMethod -Uri $url -ErrorAction Stop
    } catch {
        throw "Failed to download the compatibility file from '$url'. Error: $_"
    }

    $lines = $markdownContent -split "`r?`n"
    $tableLines = @()
    $inTable = $false

    # Identify the table (header line) and collect all rows
    foreach ($line in $lines) {
        if ($line -match '^\|\s*Release Build\s*\|\s*Release Series\s*\|\s*vmss-hci\s*\|\s*stack-hci-vm\s*\|\s*API Version\s*\|') {
            $inTable = $true
            $tableLines += $line
            continue
        }
        if ($inTable) {
            if ($line -match '^\|') {
                $tableLines += $line
            } else {
                break
            }
        }
    }

    if ($tableLines.Count -lt 2) {
        throw "Can't find the Markdown table with the expected header."
    }

    # Skip header and separator lines to extract data rows
    $dataRows = $tableLines | Select-Object -Skip 2

    # Parse each row
    $results = foreach ($row in $dataRows) {
        $cols = $row -split '\|' | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' }
        [PSCustomObject]@{
            ReleaseBuild  = $cols[0]
            ReleaseSeries = $cols[1]
            VmssHci       = $cols[2]
            StackHciVm    = $cols[3]
            ApiVersion    = $cols[4]
        }
    }

    # Output the full mapping table
    $results
}

function Get-AzureLocalHealth {
    <#
    .SYNOPSIS
        Retrieves health alerts for an Azure Local (HCI) cluster using Azure Resource Graph.
 
    .DESCRIPTION
        The Get-AzureLocalHealth function queries Azure Resource Graph for active health alerts related to a specified Azure Local (HCI) cluster.
        It filters alerts by subscription, resource group, and cluster name, returning only alerts with a 'Fired' monitor condition.
 
    .PARAMETER SubscriptionId
        The Azure subscription ID containing the Azure Local (HCI) cluster.
 
    .PARAMETER ResourceGroupName
        The resource group name where the Azure Local (HCI) cluster resides.
 
    .PARAMETER ClusterName
        The name of the Azure Local (HCI) cluster.
 
    .EXAMPLE
        Get-AzureLocalHealth -SubscriptionId "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -ResourceGroupName "AHCI-MAIN" -ClusterName "AHCI-MAIN"
 
        Retrieves all active health alerts for the cluster 'AHCI-MAIN' in resource group 'AHCI-MAIN' within the specified subscription.
    #>


    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$subscriptionId,

        [Parameter(Mandatory = $true)]
        [string]$resourceGroupName,

        [Parameter(Mandatory = $true)]
        [string]$clusterName
    )

    $query = @"
alertsmanagementresources | where type == 'microsoft.alertsmanagement/alerts' | extend severity = tostring(properties["essentials"]["severity"]) | where properties["essentials"]["targetResource"] =~ '/subscriptions/$subscriptionId/resourcegroups/$resourceGroupName/providers/microsoft.azurestackhci/clusters/$clusterName' or properties["essentials"]["targetResource"] startswith '/subscriptions/$subscriptionId/resourcegroups/$resourceGroupName/providers/microsoft.azurestackhci/clusters/$clusterName/'
| where properties["essentials"]["monitorCondition"] in~ ('Fired')
"@

    Search-AzGraph -Query $query
}

function Get-AzureLocalImage {
    <#
    .SYNOPSIS
    Retrieves existing Azure Local (HCI) VM images.
 
    .DESCRIPTION
    This function retrieves a list of VM images available in the specified Azure Local (HCI) environment.
 
    .PARAMETER SubscriptionId
    The subscription ID to filter the VM images.
 
    .PARAMETER ResourceGroupName
    The resource group name to filter the VM images.
 
    .EXAMPLE
    Get-AzureLocalImage -SubscriptionId "66cdebf5-fbaf-4040-b777-4507fe1ccb5e" -ResourceGroupName "ahci-main"
 
    .NOTES
    Requires Azure CLI with stack-hci-vm extension installed.
    #>


    [CmdletBinding()]
    param(
        [string]$SubscriptionId,

        [string]$ResourceGroupName
    )

    Write-Verbose "Retrieving existing Azure Local VM images..."

    # Get all VM images from Azure Local
    if ($SubscriptionId) {
        $SubscriptionIdText = " --subscription '$SubscriptionId'"
    } else {
        $SubscriptionIdText = $null
        Write-Verbose "No SubscriptionId provided, using default subscription"
    }

    if ($ResourceGroupName) {
        $ResourceGroupNameText = " --resource-group '$ResourceGroupName'"
    } else {
        $ResourceGroupNameText = $null
        Write-Verbose "No ResourceGroupName provided, using default resource group"
    }

    $azCommand = "az stack-hci-vm image list $SubscriptionIdText $ResourceGroupNameText --output json"
    $result = Invoke-Expression $azCommand

    $result | ConvertFrom-Json
}

function Get-AzureLocalMarketplaceImageVersion {
    <#
    .SYNOPSIS
        Retrieves the latest version of Azure local marketplace images.
 
    .DESCRIPTION
        The Get-AzureLocalMarketplaceImageVersion function queries the Azure local marketplace
        to determine the latest available version of specific images. This is useful for
        automation scripts that need to reference the most recent image versions.
 
    .PARAMETER PublisherName
        The publisher name of the Azure image to query.
 
    .PARAMETER Offer
        The offer name of the Azure image to query.
 
    .PARAMETER Sku
        The SKU of the Azure image to query.
 
    .PARAMETER Location
        The Azure region location to check for image availability.
 
    .PARAMETER Architecture
        The architecture of the image.
 
        Default is 'x64'. Other option is 'Arm64'.
 
    .EXAMPLE
        Get-AzureLocalMarketplaceImageVersion -Publisher 'microsoftwindowsserver' -Offer 'windowsserver' -SKU '2022-datacenter-azure-edition' -Location westeurope
 
    .LINK
        https://docs.microsoft.com/en-us/powershell/module/az.compute/get-azvmimagepublisher
    #>


    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Publisher,

        [Parameter(Mandatory = $true)]
        [string]$Offer,

        [Parameter(Mandatory = $true)]
        [string]$SKU,

        [ValidateNotNullOrEmpty()]
        [string]$Location = $_azureLocation,

        [ValidateSet('Arm64', 'x64')]
        [string] $Architecture = "x64"
    )

    if (!$Location) { throw "Location is not set." }

    Write-Verbose "Retrieving marketplace image versions for $Publisher/$Offer/$SKU in $Location"

    # Get all versions for the specific SKU
    $result = Invoke-Expression "az vm image list --publisher '$Publisher' --offer '$Offer' --sku '$SKU' --location '$Location' --architecture '$Architecture' --all --output json"

    # from some reason even if --sku is used to filter just specific SKUs, even different ones are returned. Same goes for Offer.
    $result = $result | ConvertFrom-Json | ? { $_.Sku -eq $SKU -and $_.Offer -eq $Offer }

    $result | Sort-Object -Property { [version]$_.version } -Descending
}

function Get-AzureLocalVMOverview {
    <#
    .SYNOPSIS
    Retrieves a comprehensive overview of Azure Local (Azure Stack HCI) virtual machines.
 
    .DESCRIPTION
    Get-AzureLocalVMOverview queries Azure Resource Graph to retrieve detailed information about virtual machines running on Azure Local (formerly Azure Stack HCI) infrastructure.
 
    The function gathers:
    - Custom locations and their enabled services
    - Resource bridge appliances
    - Virtual machine instances and their properties
    - Network interface configurations including IP addresses and subnets
    - Data disk information including size and provisioning state
 
    All VMs are enriched with their associated custom location, resource bridge, network interfaces, and disks information.
 
    .EXAMPLE
    Get-AzureLocalVMOverview
 
    Retrieves all Azure Local VMs across all custom locations in the current Azure context.
 
    .OUTPUTS
    System.Object
    Returns custom objects containing VM information with the following properties:
    - id: Resource ID of the VM
    - name: VM name
    - type: Resource type
    - location: Azure region
    - resourceGroup: Resource group name
    - subscriptionId: Subscription ID
    - properties: VM properties (OS profile, hardware profile, storage profile, network profile)
    - extendedLocation: Custom location reference
    - systemData: System metadata
    - basicInfo: Basic Azure resource information
    - nics: Array of network interface configurations
    - disks: Array of data disk configurations
    - customLocation: Associated custom location object
    - resourceBridge: Associated resource bridge appliance object
 
    .NOTES
    This function requires:
    - Azure PowerShell session with appropriate permissions
    - Access to Azure Resource Graph API
    - Read permissions on Azure Local resources
 
    The function uses Azure Resource Graph queries for efficient data retrieval across multiple subscriptions.
 
    #>


    [CmdletBinding()]
    param ()

    $queryUrl = "https://management.azure.com/providers/Microsoft.ResourceGraph/resources?api-version=2021-03-01"

    #region get custom locations, clusters, resource bridge appliances
    [System.Collections.Generic.List[object]] $batchRequest = @()

    #region get custom locations
    $customLocationQuery = @"
resources
| where type =~ "microsoft.extendedLocation/customLocations"
| extend clusterId = tolower(properties.hostResourceId), datacenter = coalesce(tags.Datacenter, ''), customLocationId = tolower(id)
| parse kind = regex clusterId with ".*providers/" provider "/.*"
| join kind=leftouter (
    ExtendedLocationResources
    | where type =~ 'microsoft.extendedlocation/customLocations/enabledResourcetypes'
    | parse kind=regex id with customLocationId "(?i)/enabledresourcetypes/.*"
    | extend extensionId = tolower(properties.clusterExtensionId), extensionType = tolower(properties.extensionType), customLocationId = tolower(customLocationId)
    | parse kind=regex extensionId with "(?i).*/extensions/" extensionName
    | extend extensionDisplayName = case(extensionType =~ 'microsoft.avs','Azure VMware Solution',
                                        extensionType =~ 'microsoft.vmware','VMware',
                                        extensionType =~ 'microsoft.scvmm','System Center Virtual Machine Manager',
                                        extensionType =~ 'microsoft.azstackhci.operator','Azure Local',
                                        extensionType)
    | extend extensionName = strcat(extensionDisplayName, ' (', extensionName, ')')
    | summarize enabledServices = strcat_array(make_set(extensionName), ",") by customLocationId ) on customLocationId
| extend enabledServices = coalesce(enabledServices, 'Unknown')
| project id, name, datacenter, clusterId, enabledServices, resourceGroup, location, subscriptionId, type, kind, tags|where (type !~ ('dell.storage/filesystems'))|where (type !~ ('pinecone.vectordb/organizations'))|where (type !~ ('liftrbasic.samplerp/organizations'))|where (type !~ ('commvault.contentstore/cloudaccounts'))|where (type !~ ('paloaltonetworks.cloudngfw/globalrulestacks'))|where (type !~ ('microsoft.liftrpilot/organizations'))|where (type !~ ('microsoft.agfoodplatform/farmbeats'))|where (type !~ ('microsoft.agricultureplatform/agriservices'))|where (type !~ ('microsoft.arc/allfairfax'))|where (type !~ ('microsoft.arc/all'))|where (type !~ ('microsoft.cdn/profiles/securitypolicies'))|where (type !~ ('microsoft.cdn/profiles/secrets'))|where (type !~ ('microsoft.cdn/profiles/rulesets'))|where (type !~ ('microsoft.cdn/profiles/rulesets/rules'))|where (type !~ ('microsoft.cdn/profiles/afdendpoints/routes'))|where (type !~ ('microsoft.cdn/profiles/origingroups'))|where (type !~ ('microsoft.cdn/profiles/origingroups/origins'))|where (type !~ ('microsoft.cdn/profiles/afdendpoints'))|where (type !~ ('microsoft.cdn/profiles/customdomains'))|where (type !~ ('microsoft.chaos/workspaces'))|where (type !~ ('microsoft.chaos/privateaccesses'))|where (type !~ ('microsoft.sovereign/transparencylogs'))|where (type !~ ('microsoft.classiccompute/domainnames/slots/roles'))|where (type !~ ('microsoft.classiccompute/domainnames'))|where (type !~ ('microsoft.cloudtest/pools'))|where (type !~ ('microsoft.cloudtest/images'))|where (type !~ ('microsoft.cloudtest/hostedpools'))|where (type !~ ('microsoft.cloudtest/buildcaches'))|where (type !~ ('microsoft.cloudtest/accounts'))|where (type !~ ('microsoft.compute/virtualmachineflexinstances'))|where (type !~ ('microsoft.compute/standbypoolinstance'))|where (type !~ ('microsoft.compute/computefleetscalesets'))|where (type !~ ('microsoft.compute/computefleetinstances'))|where (type !~ ('microsoft.containerservice/managedclusters/microsoft.kubernetesconfiguration/fluxconfigurations'))|where (type !~ ('microsoft.kubernetes/connectedclusters/microsoft.kubernetesconfiguration/fluxconfigurations'))|where (type !~ ('microsoft.containerservice/managedclusters/microsoft.kubernetesconfiguration/namespaces'))|where (type !~ ('microsoft.kubernetes/connectedclusters/microsoft.kubernetesconfiguration/namespaces'))|where (type !~ ('microsoft.containerservice/managedclusters/microsoft.kubernetesconfiguration/extensions'))|where (type !~ ('microsoft.portalservices/extensions/deployments'))|where (type !~ ('microsoft.portalservices/extensions'))|where (type !~ ('microsoft.portalservices/extensions/slots'))|where (type !~ ('microsoft.portalservices/extensions/versions'))|where (type !~ ('microsoft.deviceregistry/convergedassets'))|where (type !~ ('microsoft.deviceregistry/devices'))|where (type !~ ('microsoft.deviceupdate/updateaccounts'))|where (type !~ ('microsoft.deviceupdate/updateaccounts/updates'))|where (type !~ ('microsoft.deviceupdate/updateaccounts/deviceclasses'))|where (type !~ ('microsoft.deviceupdate/updateaccounts/deployments'))|where (type !~ ('microsoft.deviceupdate/updateaccounts/agents'))|where (type !~ ('microsoft.deviceupdate/updateaccounts/activedeployments'))|where (type !~ ('microsoft.discovery/supercomputers/nodepools'))|where (type !~ ('microsoft.discovery/datacontainers/dataassets'))|where (type !~ ('microsoft.documentdb/garnetclusters'))|where (type !~ ('microsoft.documentdb/fleetspacepotentialdatabaseaccountswithlocations'))|where (type !~ ('microsoft.documentdb/fleetspacepotentialdatabaseaccounts'))|where (type !~ ('private.easm/workspaces'))|where (type !~ ('microsoft.fairfieldgardens/provisioningresources'))|where (type !~ ('microsoft.fairfieldgardens/provisioningresources/provisioningpolicies'))|where (type !~ ('microsoft.healthmodel/healthmodels'))|where (type !~ ('microsoft.hybridcompute/machinessoftwareassurance'))|where (type !~ ('microsoft.hybridcompute/machinespaygo'))|where (type !~ ('microsoft.hybridcompute/machinesesu'))|where (type !~ ('microsoft.hybridcompute/arcgatewayassociatedresources'))|where (type !~ ('microsoft.hybridconnectivity/publiccloudconnectors/multicloudsyncedresources'))|where (type !~ ('microsoft.hybridcompute/machinessovereign'))|where (type !~ ('microsoft.hybridcompute/arcserverwithwac'))|where (type !~ ('microsoft.network/networkvirtualappliances'))|where (type !~ ('microsoft.network/virtualhubs')) or ((kind =~ ('routeserver')))|where (type !~ ('microsoft.devhub/iacprofiles'))|where (type !~ ('microsoft.gallery/myareas/galleryitems'))|where (type !~ ('private.monitorgrafana/dashboards'))|where (type !~ ('microsoft.insights/diagnosticsettings'))|where (type !~ ('microsoft.network/privatednszones/virtualnetworklinks'))|where not((type =~ ('microsoft.network/serviceendpointpolicies')) and ((kind =~ ('internal'))))|where (type !~ ('microsoft.managednetworkfabric/fabricroutepolicies'))|where (type !~ ('microsoft.managednetworkfabric/fabricnetworktaps'))|where (type !~ ('microsoft.managednetworkfabric/fabricnetworkpacketbrokers'))|where (type !~ ('microsoft.managednetworkfabric/fabricnetworkdevices'))|where (type !~ ('microsoft.managednetworkfabric/fabricresources'))|where (type !~ ('microsoft.networkcloud/clustervolumes'))|where (type !~ ('microsoft.networkcloud/clustertrunkednetworks'))|where (type !~ ('microsoft.networkcloud/clusterstorageappliances'))|where (type !~ ('microsoft.networkcloud/clusterl3networks'))|where (type !~ ('microsoft.networkcloud/clusterl2networks'))|where (type !~ ('microsoft.networkcloud/clusterresources'))|where (type !~ ('microsoft.networkcloud/clusternetworks'))|where (type !~ ('microsoft.networkcloud/clustercloudservicesnetworks'))|where (type !~ ('microsoft.resources/resourcegraphvisualizer'))|where (type !~ ('microsoft.orbital/l2connections'))|where (type !~ ('microsoft.orbital/groundstations'))|where (type !~ ('microsoft.orbital/edgesites'))|where (type !~ ('microsoft.oriondb/clusters'))|where (type !~ ('microsoft.recommendationsservice/accounts/modeling'))|where (type !~ ('microsoft.recommendationsservice/accounts/serviceendpoints'))|where (type !~ ('microsoft.relationships/servicegrouprelationships'))|where (type !~ ('microsoft.resources/virtualsubscriptionsforresourcepicker'))|where (type !~ ('microsoft.resources/deletedresources'))|where (type !~ ('microsoft.deploymentmanager/rollouts'))|where (type !~ ('microsoft.features/featureprovidernamespaces/featureconfigurations'))|where (type !~ ('microsoft.saashub/cloudservices/hidden'))|where (type !~ ('microsoft.providerhub/providerregistrations'))|where (type !~ ('microsoft.providerhub/providerregistrations/customrollouts'))|where (type !~ ('microsoft.providerhub/providerregistrations/defaultrollouts'))|where (type !~ ('microsoft.edge/configurations'))|where not((type =~ ('microsoft.synapse/workspaces/sqlpools')) and ((kind =~ ('v3'))))|where (type !~ ('microsoft.mission/virtualenclaves/workloads'))|where (type !~ ('microsoft.mission/virtualenclaves'))|where (type !~ ('microsoft.mission/communities/transithubs'))|where (type !~ ('microsoft.mission/virtualenclaves/enclaveendpoints'))|where (type !~ ('microsoft.mission/enclaveconnections'))|where (type !~ ('microsoft.mission/communities/communityendpoints'))|where (type !~ ('microsoft.mission/communities'))|where (type !~ ('microsoft.mission/catalogs'))|where (type !~ ('microsoft.mission/approvals'))|where (type !~ ('microsoft.network/virtualnetworkappliances'))|where (type !~ ('microsoft.workloads/insights'))|where (type !~ ('microsoft.zerotrustsegmentation/segmentationmanagers'))|where (type !~ ('private.zerotrustsegmentation/segmentationmanagers'))|where (type !~ ('microsoft.connectedcache/enterprisemcccustomers/enterprisemcccachenodes'))|where (type !~ ('microsoft.premonition/libraries/samples'))|where (type !~ ('microsoft.premonition/libraries/analyses'))|where not((type =~ ('microsoft.sql/servers')) and ((kind =~ ('v12.0,analytics'))))|where not((type =~ ('microsoft.sql/servers/databases')) and ((kind in~ ('system','v2.0,system','v12.0,system','v12.0,system,serverless','v12.0,user,datawarehouse,gen2,analytics'))))|project name,enabledServices,datacenter,resourceGroup,clusterId,id,type,kind,location,subscriptionId,tags|sort by (tolower(tostring(name))) asc
"@

    $batchRequest.add((New-AzureBatchRequest -method POST -url $queryUrl -content @{ query = $customLocationQuery } -name "customLocation"))
    #endregion get custom locations

    #region get clusters
    #TODO nevim jak propojit cluster <> custom location <> resource bridge
    # $clusterQuery = 'resources | where type =~ "microsoft.azurestackhci/clusters"'
    # @"
    # resources
    # | where type =~ "microsoft.azurestackhci/clusters"
    # | project id=tolower(id), name, type, kind, parentResourceId = tolower(id), nodesCount = array_length(properties.reportedProperties.nodes), resourceGroup
    # | sort by tolower(name) asc
    # "@
    # $batchRequest.add((New-AzureBatchRequest -method POST -url $queryUrl -content @{ query = $clusterQuery } -name "cluster"))
    #endregion get clusters

    #region get resource bridge appliances
    $resourceBridgeQuery = @"
resources|where type =~ "microsoft.resourceconnector/appliances"
| extend status = properties.status, version = properties.version, clusterType = properties.distro, hostResource = properties.infrastructureConfig.provider, provisioningState=properties.provisioningState
| project id, name, status, version, clusterType, hostResource, type, tags, location, subscriptionId, resourceGroup, kind, provisioningState|extend tagsString=tostring(tags)|where (type !~ ('dell.storage/filesystems'))|where (type !~ ('pinecone.vectordb/organizations'))|where (type !~ ('liftrbasic.samplerp/organizations'))|where (type !~ ('commvault.contentstore/cloudaccounts'))|where (type !~ ('paloaltonetworks.cloudngfw/globalrulestacks'))|where (type !~ ('microsoft.liftrpilot/organizations'))|where (type !~ ('microsoft.agfoodplatform/farmbeats'))|where (type !~ ('microsoft.agricultureplatform/agriservices'))|where (type !~ ('microsoft.arc/allfairfax'))|where (type !~ ('microsoft.arc/all'))|where (type !~ ('microsoft.cdn/profiles/securitypolicies'))|where (type !~ ('microsoft.cdn/profiles/secrets'))|where (type !~ ('microsoft.cdn/profiles/rulesets'))|where (type !~ ('microsoft.cdn/profiles/rulesets/rules'))|where (type !~ ('microsoft.cdn/profiles/afdendpoints/routes'))|where (type !~ ('microsoft.cdn/profiles/origingroups'))|where (type !~ ('microsoft.cdn/profiles/origingroups/origins'))|where (type !~ ('microsoft.cdn/profiles/afdendpoints'))|where (type !~ ('microsoft.cdn/profiles/customdomains'))|where (type !~ ('microsoft.chaos/workspaces'))|where (type !~ ('microsoft.chaos/privateaccesses'))|where (type !~ ('microsoft.sovereign/transparencylogs'))|where (type !~ ('microsoft.classiccompute/domainnames/slots/roles'))|where (type !~ ('microsoft.classiccompute/domainnames'))|where (type !~ ('microsoft.cloudtest/pools'))|where (type !~ ('microsoft.cloudtest/images'))|where (type !~ ('microsoft.cloudtest/hostedpools'))|where (type !~ ('microsoft.cloudtest/buildcaches'))|where (type !~ ('microsoft.cloudtest/accounts'))|where (type !~ ('microsoft.compute/virtualmachineflexinstances'))|where (type !~ ('microsoft.compute/standbypoolinstance'))|where (type !~ ('microsoft.compute/computefleetscalesets'))|where (type !~ ('microsoft.compute/computefleetinstances'))|where (type !~ ('microsoft.containerservice/managedclusters/microsoft.kubernetesconfiguration/fluxconfigurations'))|where (type !~ ('microsoft.kubernetes/connectedclusters/microsoft.kubernetesconfiguration/fluxconfigurations'))|where (type !~ ('microsoft.containerservice/managedclusters/microsoft.kubernetesconfiguration/namespaces'))|where (type !~ ('microsoft.kubernetes/connectedclusters/microsoft.kubernetesconfiguration/namespaces'))|where (type !~ ('microsoft.containerservice/managedclusters/microsoft.kubernetesconfiguration/extensions'))|where (type !~ ('microsoft.portalservices/extensions/deployments'))|where (type !~ ('microsoft.portalservices/extensions'))|where (type !~ ('microsoft.portalservices/extensions/slots'))|where (type !~ ('microsoft.portalservices/extensions/versions'))|where (type !~ ('microsoft.deviceregistry/convergedassets'))|where (type !~ ('microsoft.deviceregistry/devices'))|where (type !~ ('microsoft.deviceupdate/updateaccounts'))|where (type !~ ('microsoft.deviceupdate/updateaccounts/updates'))|where (type !~ ('microsoft.deviceupdate/updateaccounts/deviceclasses'))|where (type !~ ('microsoft.deviceupdate/updateaccounts/deployments'))|where (type !~ ('microsoft.deviceupdate/updateaccounts/agents'))|where (type !~ ('microsoft.deviceupdate/updateaccounts/activedeployments'))|where (type !~ ('microsoft.discovery/supercomputers/nodepools'))|where (type !~ ('microsoft.discovery/datacontainers/dataassets'))|where (type !~ ('microsoft.documentdb/garnetclusters'))|where (type !~ ('microsoft.documentdb/fleetspacepotentialdatabaseaccountswithlocations'))|where (type !~ ('microsoft.documentdb/fleetspacepotentialdatabaseaccounts'))|where (type !~ ('private.easm/workspaces'))|where (type !~ ('microsoft.fairfieldgardens/provisioningresources'))|where (type !~ ('microsoft.fairfieldgardens/provisioningresources/provisioningpolicies'))|where (type !~ ('microsoft.healthmodel/healthmodels'))|where (type !~ ('microsoft.hybridcompute/machinessoftwareassurance'))|where (type !~ ('microsoft.hybridcompute/machinespaygo'))|where (type !~ ('microsoft.hybridcompute/machinesesu'))|where (type !~ ('microsoft.hybridcompute/arcgatewayassociatedresources'))|where (type !~ ('microsoft.hybridconnectivity/publiccloudconnectors/multicloudsyncedresources'))|where (type !~ ('microsoft.hybridcompute/machinessovereign'))|where (type !~ ('microsoft.hybridcompute/arcserverwithwac'))|where (type !~ ('microsoft.network/networkvirtualappliances'))|where (type !~ ('microsoft.network/virtualhubs')) or ((kind =~ ('routeserver')))|where (type !~ ('microsoft.devhub/iacprofiles'))|where (type !~ ('microsoft.gallery/myareas/galleryitems'))|where (type !~ ('private.monitorgrafana/dashboards'))|where (type !~ ('microsoft.insights/diagnosticsettings'))|where (type !~ ('microsoft.network/privatednszones/virtualnetworklinks'))|where not((type =~ ('microsoft.network/serviceendpointpolicies')) and ((kind =~ ('internal'))))|where (type !~ ('microsoft.managednetworkfabric/fabricroutepolicies'))|where (type !~ ('microsoft.managednetworkfabric/fabricnetworktaps'))|where (type !~ ('microsoft.managednetworkfabric/fabricnetworkpacketbrokers'))|where (type !~ ('microsoft.managednetworkfabric/fabricnetworkdevices'))|where (type !~ ('microsoft.managednetworkfabric/fabricresources'))|where (type !~ ('microsoft.networkcloud/clustervolumes'))|where (type !~ ('microsoft.networkcloud/clustertrunkednetworks'))|where (type !~ ('microsoft.networkcloud/clusterstorageappliances'))|where (type !~ ('microsoft.networkcloud/clusterl3networks'))|where (type !~ ('microsoft.networkcloud/clusterl2networks'))|where (type !~ ('microsoft.networkcloud/clusterresources'))|where (type !~ ('microsoft.networkcloud/clusternetworks'))|where (type !~ ('microsoft.networkcloud/clustercloudservicesnetworks'))|where (type !~ ('microsoft.resources/resourcegraphvisualizer'))|where (type !~ ('microsoft.orbital/l2connections'))|where (type !~ ('microsoft.orbital/groundstations'))|where (type !~ ('microsoft.orbital/edgesites'))|where (type !~ ('microsoft.oriondb/clusters'))|where (type !~ ('microsoft.recommendationsservice/accounts/modeling'))|where (type !~ ('microsoft.recommendationsservice/accounts/serviceendpoints'))|where (type !~ ('microsoft.relationships/servicegrouprelationships'))|where (type !~ ('microsoft.resources/virtualsubscriptionsforresourcepicker'))|where (type !~ ('microsoft.resources/deletedresources'))|where (type !~ ('microsoft.deploymentmanager/rollouts'))|where (type !~ ('microsoft.features/featureprovidernamespaces/featureconfigurations'))|where (type !~ ('microsoft.saashub/cloudservices/hidden'))|where (type !~ ('microsoft.providerhub/providerregistrations'))|where (type !~ ('microsoft.providerhub/providerregistrations/customrollouts'))|where (type !~ ('microsoft.providerhub/providerregistrations/defaultrollouts'))|where (type !~ ('microsoft.edge/configurations'))|where not((type =~ ('microsoft.synapse/workspaces/sqlpools')) and ((kind =~ ('v3'))))|where (type !~ ('microsoft.mission/virtualenclaves/workloads'))|where (type !~ ('microsoft.mission/virtualenclaves'))|where (type !~ ('microsoft.mission/communities/transithubs'))|where (type !~ ('microsoft.mission/virtualenclaves/enclaveendpoints'))|where (type !~ ('microsoft.mission/enclaveconnections'))|where (type !~ ('microsoft.mission/communities/communityendpoints'))|where (type !~ ('microsoft.mission/communities'))|where (type !~ ('microsoft.mission/catalogs'))|where (type !~ ('microsoft.mission/approvals'))|where (type !~ ('microsoft.network/virtualnetworkappliances'))|where (type !~ ('microsoft.workloads/insights'))|where (type !~ ('microsoft.zerotrustsegmentation/segmentationmanagers'))|where (type !~ ('private.zerotrustsegmentation/segmentationmanagers'))|where (type !~ ('microsoft.connectedcache/enterprisemcccustomers/enterprisemcccachenodes'))|where (type !~ ('microsoft.premonition/libraries/samples'))|where (type !~ ('microsoft.premonition/libraries/analyses'))|where not((type =~ ('microsoft.sql/servers')) and ((kind =~ ('v12.0,analytics'))))|where not((type =~ ('microsoft.sql/servers/databases')) and ((kind in~ ('system','v2.0,system','v12.0,system','v12.0,system,serverless','v12.0,user,datawarehouse,gen2,analytics'))))|project name,status,version,tagsString,id,type,kind,location,subscriptionId,resourceGroup,tags|sort by (tolower(tostring(name))) asc
"@

    $batchRequest.add((New-AzureBatchRequest -method POST -url $queryUrl -content @{ query = $resourceBridgeQuery } -name "resourceBridge"))
    #endregion get resource bridge appliances

    $batchResult = Invoke-AzureBatchRequest -batchRequest $batchRequest

    # $clusterList = $batchResult | ? RequestName -EQ "cluster"
    $customLocationList = $batchResult | ? RequestName -EQ "customLocation"
    $resourceBridgeList = $batchResult | ? RequestName -EQ "resourceBridge"
    #endregion get custom locations, clusters, resource bridge appliances

    foreach ($customLocation in $customLocationList) {
        $customLocationId = $customLocation.id

        Write-Verbose "Processing custom location $($customLocation.name) ($($customLocationId))"

        #region get VMs
        Write-Verbose "Getting VMs"
        $vmQuery = @"
resources
| where type =~ "Microsoft.HybridCompute/machines" and kind in~ ("HCI")
| project hostId = tolower(id),name
| join kind=inner (
    ExtensibilityResources
    | where type =~ "microsoft.azurestackhci/virtualmachineinstances"
    | parse kind=regex flags=i id with hostId "/providers/microsoft.azurestackhci/virtualmachineinstances/default"
    | project hostId = tolower(hostId), guestId = tolower(id), properties, extendedLocation, systemData, type, location, resourceGroup, subscriptionId
    | extend customLocation = tostring(extendedLocation["name"])
    | where customLocation =~ "$customLocationId"
) on hostId
| project id = hostId, name, type, location, resourceGroup, subscriptionId, properties, extendedLocation, systemData
"@

        $vmList = New-AzureBatchRequest -method POST -url $queryUrl -content @{ query = $vmQuery } | Invoke-AzureBatchRequest
        #endregion get VMs

        # get basic info for each VM
        Write-Verbose "Getting VMs details"
        $vmListBasicInfo = New-AzureBatchRequest -url "<placeholder>?api-version=2024-05-20-preview" -placeholder $vmList.Id -placeholderAsId | Invoke-AzureBatchRequest

        # get basic HCI specific info for each VM
        [System.Collections.Generic.List[object]] $batchRequest = @()
        New-AzureBatchRequest -url "<placeholder>/providers/Microsoft.AzureStackHCI/virtualMachineInstances/default?api-version=2023-09-01-preview" -placeholder $vmList.Id -placeholderAsId | % { $batchRequest.add($_) }

        $diskQuery = @"
ExtensibilityResources
    | where type =~ "microsoft.azurestackhci/virtualmachineinstances"
    | mv-expand dataDisks=properties.storageProfile.dataDisks
    | extend vmDiskParts = iff(array_length(split(dataDisks.name, "/")) == 1, split(dataDisks.id, "/"), split(dataDisks.name, "/"))
    | extend vmDiskName = tostring(vmDiskParts[array_length(vmDiskParts)-1])
    | extend diskId = tostring(dataDisks.id)
    | join (
        resources
        | where type == "microsoft.azurestackhci/virtualharddisks" and properties.provisioningState =~ "succeeded" and extendedLocation.name =~ "$customLocationId"
        | extend diskParts = split(id, "/")
        | extend diskName = tostring(diskParts[array_length(diskParts)-1])
    ) on `$left.diskId == `$right.id
    | extend updatedProperties = pack("diskSizeBytes",properties1['diskSizeGB'],"provisioningState",properties1['provisioningState'],"diskSizeGB",properties1['diskSizeGB'],"status",properties1['status'],"dynamic",properties1['dynamic'],"containerId",properties1['containerId'])
    | project
            id = id1,
            name = name1,
            type = type1,
            tenantId = tenantId1,
            location = location1,
            resourceGroup = resourceGroup1,
            subscriptionId = subscriptionId1,
            managedBy = managedBy1,
            sku = sku1,
            plan = plan1,
            properties = updatedProperties,
            tags = tags,
            identity = identity1,
            zones = zones1,
            extendedLocation = extendedLocation1
"@

        $batchRequest.add((New-AzureBatchRequest -method POST -url $queryUrl -content @{ query = $diskQuery } -name "diskInfo" ))

        $nicQuery = @"
resources
| where type =~ "Microsoft.AzureStackHCI/networkinterfaces" and
    properties.provisioningState =~ "succeeded" and
    extendedLocation.name =~ "$customLocationId"
| extend ipConfigurationsProperties = properties.ipConfigurations[0].properties
| extend gateway = ipConfigurationsProperties.gateway
| extend ipAddress = ipConfigurationsProperties.privateIPAddress
| extend networkId = tolower(tostring(ipConfigurationsProperties.subnet.id))
| join kind=leftouter (
    resources
    | where type == "microsoft.azurestackhci/logicalnetworks" and properties.provisioningState =~ "succeeded"
    | extend networkId = tolower(tostring(id))
    | extend subnetProperties = properties.subnets[0].properties
    | extend addressPrefix = subnetProperties.addressPrefix
    | extend ipv4Type = coalesce(subnetProperties.ipAllocationMethod, "Dynamic")
    | project networkId, subnetProperties, addressPrefix, ipv4Type
) on networkId
| extend network = pack("id", networkId, "addressPrefix", addressPrefix, "ipv4Type", ipv4Type, "gatewayAddress", gateway)
| project-away ipConfigurationsProperties, networkId, networkId1, addressPrefix, ipv4Type, gateway, subnetProperties
"@

        $batchRequest.add((New-AzureBatchRequest -method POST -url $queryUrl -content @{ query = $nicQuery } -name "nicInfo"))

        Write-Verbose "Getting VMs details - HCI data, disks and nics"
        $batchResult = Invoke-AzureBatchRequest -batchRequest $batchRequest

        $vmListBasicHCIInfo = $batchResult | ? RequestName -In $vmList.Id
        $vmListDiskInfo = $batchResult | ? RequestName -EQ "diskInfo"
        $vmListNicInfo = $batchResult | ? RequestName -EQ "nicInfo"

        foreach ($vm in $vmListBasicHCIInfo) {
            $vmId = $vm.id
            # $vmCustomLocation = $vm.extendedLocation | ? type -EQ 'CustomLocation' | select -ExpandProperty Name
            $vmCustomLocation = $customLocationId

            Write-Verbose "Processing VM $($vm.properties.osProfile.computerName) ($($vm.id))"

            $vmBasicData = $vmListBasicInfo | ? id -EQ $vm.RequestName | select * -ExcludeProperty RequestName

            $nicId = $vm.properties.networkProfile.networkInterfaces.id

            $diskId = $vm.properties.storageProfile.datadisks.id

            $nicData = $vmListNicInfo | ? id -In $nicId | select * -ExcludeProperty RequestName
            $diskData = $vmListDiskInfo | ? id -In $diskId | select * -ExcludeProperty RequestName

            $vm | Add-Member -MemberType NoteProperty -Name "basicInfo" -Value $vmBasicData
            $vm | Add-Member -MemberType NoteProperty -Name "nics" -Value $nicData
            $vm | Add-Member -MemberType NoteProperty -Name "disks" -Value $diskData
            $vm | Add-Member -MemberType NoteProperty -Name "customLocation" -Value $customLocation
            $vm | Add-Member -MemberType NoteProperty -Name "resourceBridge" -Value ($resourceBridgeList | ? id -EQ $customLocation.clusterId)
            $vm | select * -ExcludeProperty RequestName
        }
    }
}

function New-AzureLocalVMImage {
    <#
    .SYNOPSIS
    Creates a new Azure Local (HCI) VM image with a specific version.
 
    .DESCRIPTION
    This function creates VM images on Azure Local (HCI) infrastructure using either
    marketplace images with specific versions or custom VHD/VHDX files.
 
    .PARAMETER ResourceGroupName
    The resource group where the image will be created.
 
    .PARAMETER CustomLocationId
    The custom location ID for the Azure Local environment.
 
    .PARAMETER ImageName
    The name for the new VM image.
 
    Dots are not allowed, because anything behind the dot is considered as file extension.
 
    .PARAMETER Publisher
    The publisher of the marketplace image (e.g., 'MicrosoftWindowsServer').
 
    .PARAMETER Offer
    The offer name (e.g., 'WindowsServer').
 
    .PARAMETER SKU
    The SKU name (e.g., '2022-datacenter-azure-edition').
 
    .PARAMETER Version
    The specific version to use. Use 'latest' for the most recent version.
 
    .PARAMETER VhdPath
    Path to a custom VHD/VHDX file (alternative to marketplace image).
 
    .PARAMETER OSType
    Operating system type. Defaults to 'Windows'.
 
    .PARAMETER StoragePathId
    Storage path ID for the image storage location.
 
    .EXAMPLE
    $cluster = $_azureLocalClusterList[0]
    $publisher = "MicrosoftWindowsServer"
    $offer = "WindowsServer"
    $sku = "2022-datacenter-azure-edition"
 
    az account set --subscription $cluster.SubscriptionName
    $customLocationID = az customlocation show --resource-group $cluster.ResourceGroupName --name $cluster.CustomLocation --query id -o tsv
 
    $imageVersionList = Get-AzureLocalMarketplaceImageVersion -Publisher $publisher -Offer $offer -SKU $sku -Location westeurope
 
    New-AzureLocalVMImage -ResourceGroupName $cluster.ResourceGroupName -CustomLocationId $customLocationID -ImageName "WinServer2022-v1" -Publisher $publisher -Offer $offer -SKU $sku -Version $imageVersionList[0].Version
 
    Create newest available version of Windows Server 2022 image.
 
    .EXAMPLE
    New-AzureLocalVMImage -ResourceGroupName "rg-azlocal" -CustomLocationId "/subscriptions/.../customLocations/cl-hci" -ImageName "CustomWin11" -VhdPath "\\storage\images\Win11Custom.vhdx"
 
    .NOTES
    Requires Azure CLI with stack-hci-vm extension installed.
    #>


    [CmdletBinding(DefaultParameterSetName = 'Marketplace')]
    param(
        [Parameter(Mandatory = $true)]
        [string]$ResourceGroupName,

        [Parameter(Mandatory = $true)]
        [string]$CustomLocationId,

        [Parameter(Mandatory = $true)]
        [ValidateScript( {
                if ($_ -like '*.*') {
                    throw "$_ contains dots, which are not allowed in image names."
                } else {
                    $true
                }
            })]
        [string]$ImageName,

        [Parameter(Mandatory = $true, ParameterSetName = 'Marketplace')]
        [string]$Publisher,

        [Parameter(Mandatory = $true, ParameterSetName = 'Marketplace')]
        [string]$Offer,

        [Parameter(Mandatory = $true, ParameterSetName = 'Marketplace')]
        [string]$SKU,

        [Parameter(Mandatory = $false, ParameterSetName = 'Marketplace')]
        [string]$Version = 'latest',

        [Parameter(Mandatory = $true, ParameterSetName = 'CustomVHD')]
        [string]$VhdPath,

        [Parameter(Mandatory = $false)]
        [ValidateSet('Windows', 'Linux')]
        [string]$OSType = 'Windows',

        [Parameter(Mandatory = $false)]
        [string]$StoragePathId
    )

    Write-Verbose "Creating Azure Local VM image: $ImageName"

    # Check if Azure CLI is available
    $azVersion = az version --output json 2>$null
    if (-not $azVersion) {
        throw "Azure CLI is not available or not properly configured"
    }

    # Build the base command
    $azCommand = @(
        'az', 'stack-hci-vm', 'image', 'create',
        '--resource-group', $ResourceGroupName,
        '--custom-location', $CustomLocationId,
        '--name', $ImageName,
        '--os-type', $OSType
    )

    # Add marketplace-specific parameters
    if ($PSCmdlet.ParameterSetName -eq 'Marketplace') {
        $azCommand += @(
            '--publisher', $Publisher,
            '--offer', $Offer,
            '--sku', $SKU,
            '--version', $Version
        )

        Write-Verbose "Using marketplace image: $Publisher/$Offer/$SKU/$Version"
    }

    # Add custom VHD parameters
    if ($PSCmdlet.ParameterSetName -eq 'CustomVHD') {
        if (-not (Test-Path $VhdPath)) {
            throw "VHD file not found: $VhdPath"
        }
        $azCommand += @('--image-path', $VhdPath)

        Write-Verbose "Using custom VHD: $VhdPath"
    }

    # Add storage path if specified
    if ($StoragePathId) {
        $azCommand += @('--storage-path-id', $StoragePathId)
    }

    # Output the results in JSON format
    $azCommand += @('--output', 'json')

    # Execute the command
    $command = $azCommand -join ' '
    Write-Verbose "Executing: $command"
    $errOutput = $($imageInfo = Invoke-Expression $command | ConvertFrom-Json ) 2>&1 # redirect error stream to success stream, so we can capture it

    if ($LASTEXITCODE -eq 0) {
        Write-Verbose "Successfully created VM image '$($imageInfo.name)'"

        return $imageInfo
    } else {
        throw "Failed to create VM image '$ImageName'. Error was: $errOutput"
    }
}

Export-ModuleMember -function Get-AzureLocalExtensionCompatibilityTable, Get-AzureLocalHealth, Get-AzureLocalImage, Get-AzureLocalMarketplaceImageVersion, Get-AzureLocalVMOverview, New-AzureLocalVMImage