diff --git a/.gitignore b/.gitignore
index 1c59886ca0..e2d4b7e2cd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -53,3 +53,4 @@ terraform.rc
*.out
.VSCodeCounter/*
+azure_jumpstart_ag/manufacturing/bicep/main.*.parameters.json
diff --git a/azure_jumpstart_ag/retail/artifacts/L1Files/AKSEEBootstrap.ps1 b/azure_jumpstart_ag/artifacts/L1Files/AKSEEBootstrap.ps1
similarity index 100%
rename from azure_jumpstart_ag/retail/artifacts/L1Files/AKSEEBootstrap.ps1
rename to azure_jumpstart_ag/artifacts/L1Files/AKSEEBootstrap.ps1
diff --git a/azure_jumpstart_ag/retail/artifacts/L1Files/ScalableCluster.json b/azure_jumpstart_ag/artifacts/L1Files/ScalableCluster.json
similarity index 95%
rename from azure_jumpstart_ag/retail/artifacts/L1Files/ScalableCluster.json
rename to azure_jumpstart_ag/artifacts/L1Files/ScalableCluster.json
index d1058ec19b..d85a6831d3 100644
--- a/azure_jumpstart_ag/retail/artifacts/L1Files/ScalableCluster.json
+++ b/azure_jumpstart_ag/artifacts/L1Files/ScalableCluster.json
@@ -4,7 +4,7 @@
"DeploymentType": "ScalableCluster",
"Init": {
"ServiceIPRangeStart": "ServiceIPRangeStart-null",
- "ServiceIPRangeSize": 1000
+ "ServiceIPRangeSize": 15
},
"Arc": {
"ClusterName": "ClusterName-null",
@@ -37,7 +37,7 @@
"Mtu": 0
},
"LinuxNode": {
- "CpuCount": 4,
+ "CpuCount": 6,
"MemoryInMB": 24576,
"DataSizeInGB": 80,
"LogSizeInGB": 5,
diff --git a/azure_jumpstart_ag/artifacts/L1Files/config.json b/azure_jumpstart_ag/artifacts/L1Files/config.json
new file mode 100644
index 0000000000..3f9a6633d6
--- /dev/null
+++ b/azure_jumpstart_ag/artifacts/L1Files/config.json
@@ -0,0 +1,6 @@
+{
+ "hydra.highAvailability.disk.storageClass": "default",
+ "hydra.acstorController.enabled": false,
+ "hydra.highAvailability.disk.storageClass": "local-path",
+ "hydra.cachedStorageSize": "20Gi"
+}
\ No newline at end of file
diff --git a/azure_jumpstart_ag/artifacts/PowerShell/AgConfig-manufacturing.psd1 b/azure_jumpstart_ag/artifacts/PowerShell/AgConfig-manufacturing.psd1
new file mode 100644
index 0000000000..96737ef211
--- /dev/null
+++ b/azure_jumpstart_ag/artifacts/PowerShell/AgConfig-manufacturing.psd1
@@ -0,0 +1,231 @@
+@{
+ # This is the PowerShell datafile used to provide configuration information for the Agora environment. Product keys and password are not encrypted and will be available on host during installation.
+
+ # Directory paths
+ AgDirectories = @{
+ AgDir = "C:\Ag"
+ AgPowerShellDir = "C:\Ag\PowerShell"
+ AgLogsDir = "C:\Ag\Logs"
+ AgVMDir = "C:\Ag\Virtual Machines"
+ AgIconDir = "C:\Ag\Icons"
+ AgToolsDir = "C:\Tools"
+ AgTempDir = "C:\Temp"
+ AgVHDXDir = "V:\VMs"
+ AgConfigMapDir = "C:\Ag\ConfigMaps"
+ AgL1Files = "C:\Ag\L1Files"
+ AgAppsRepo = "C:\Ag\AppsRepo"
+ AgMonitoringDir = "C:\Ag\Monitoring"
+ AgAdxDashboards = "C:\Ag\AdxDashboards"
+ AgDataEmulator = "C:\Ag\DataEmulator"
+ }
+
+ # Required URLs
+ URLs = @{
+ chocoInstallScript = 'https://chocolatey.org/install.ps1'
+ wslUbuntu = 'https://aka.ms/wslubuntu'
+ wslStoreStorage = 'https://wslstorestorage.blob.core.windows.net/wslblob/wsl_update_x64.msi'
+ docker = 'https://desktop.docker.com/win/main/amd64/Docker%20Desktop%20Installer.exe'
+ githubAPI = 'https://api.github.com'
+ grafana = 'https://api.github.com/repos/grafana/grafana/releases/latest'
+ azurePortal = 'https://portal.azure.com'
+ aksEEk3s = 'https://aka.ms/aks-edge/k3s-msi'
+ nginx = 'https://kubernetes.github.io/ingress-nginx'
+ prometheus = 'https://prometheus-community.github.io/helm-charts'
+ vcLibs = 'https://aka.ms/Microsoft.VCLibs.x64.14.00.Desktop.appx'
+ windowsTerminal = 'https://api.github.com/repos/microsoft/terminal/releases/latest'
+ aksEEReleases = 'https://api.github.com/repos/Azure/AKS-Edge/releases'
+ mqttExplorerReleases = 'https://api.github.com/repos/thomasnordquist/MQTT-Explorer/releases/latest'
+ }
+
+ # Azure required registered resource providers
+ AzureProviders = @(
+ "Microsoft.Kubernetes",
+ "Microsoft.KubernetesConfiguration",
+ "Microsoft.HybridCompute",
+ "Microsoft.GuestConfiguration",
+ "Microsoft.HybridConnectivity",
+ "Microsoft.DeviceRegistry",
+ "Microsoft.EventGrid"
+ )
+
+ # Az CLI required extensions
+ AzCLIExtensions = @(
+ 'k8s-extension',
+ 'k8s-configuration',
+ 'eventgrid',
+ 'customlocation',
+ 'kusto',
+ 'storage-preview'
+ 'azure-iot-ops'
+ )
+
+ # PowerShell modules
+ PowerShellModules = @(
+ 'Az.ConnectedKubernetes',
+ 'Az.KubernetesConfiguration',
+ 'Az.Kusto',
+ 'Az.EventGrid',
+ 'Az.Storage',
+ 'Az.EventHub'
+ )
+
+ # Chocolatey packages list
+ ChocolateyPackagesList = @(
+ 'az.powershell',
+ 'bicep',
+ 'kubernetes-cli',
+ 'vcredist140',
+ 'microsoft-edge',
+ 'azcopy10',
+ 'vscode',
+ 'git',
+ '7zip',
+ 'kubectx',
+ 'putty.install',
+ 'kubernetes-helm',
+ 'dotnet-sdk',
+ 'zoomit',
+ 'openssl.light',
+ 'mqtt-explorer',
+ 'gh',
+ 'python'
+ )
+
+ # Pip packages list
+ PipPackagesList = @(
+ 'paho-mqtt'
+ )
+
+ # VSCode extensions
+ VSCodeExtensions = @(
+ 'ms-vscode-remote.remote-containers',
+ 'ms-vscode-remote.remote-wsl',
+ 'ms-vscode.powershell',
+ 'redhat.vscode-yaml',
+ 'ZainChen.json',
+ 'esbenp.prettier-vscode',
+ 'ms-kubernetes-tools.vscode-kubernetes-tools',
+ 'mindaro.mindaro',
+ 'github.vscode-pull-request-github'
+ )
+
+ # Git branches
+ GitBranches = @(
+ 'production',
+ 'staging',
+ 'canary' ,
+ 'main'
+ )
+
+ # VHDX blob url
+ ProdVHDBlobURL = 'https://jsvhds.blob.core.windows.net/agora/base/prod-w11iot/AGBase.vhdx'
+ PreProdVHDBlobURL = 'https://jsvhds.blob.core.windows.net/agora/base/preprod-w11iot/AGBase.vhdx'
+
+ # L1 virtual machine configuration
+ HostVMDrive = "V" # This value controls the drive letter where the nested virtual
+ L1VMMemory = 32GB # This value controls the amount of RAM for each AKS Edge Essentials host virtual machine
+ L1VMNumVCPU = 8 # This value controls the number of vCPUs to assign to each AKS Edge Essentials host virtual machine.
+ InternalSwitch = "InternalSwitch" # This value controls the Hyper-V internal switch name used by L0 Azure virtual machine.
+ L1Username = "Administrator" # This value controls the Admin credential username for the L1 Hyper-V virtual machines that run on the Agora-Client.
+ L1Password = 'Agora123!!' # This value controls the Admin credential password for the L1 Hyper-V virtual machines that run on the Agora-Client.
+ L1DefaultGateway = "172.20.1.1" # This value controls the default gateway IP address used by each L1 Hyper-V virtual machines that run on the Agora-Client.
+ L1SwitchName = "AKS-Int" # This value controls the Hyper-V internal switch name used by each L1 Hyper-V virtual machines that run on the Agora-Client.
+ L1NatSubnetPrefix = "172.20.1.0/24" # This value controls the network subnet used by each L1 Hyper-V virtual machines that run on the Agora-Client.
+
+ # NAT Configuration
+ natHostSubnet = "192.168.128.0/24"
+ natHostVMSwitchName = "InternalNAT"
+ natConfigure = $true
+ natSubnet = "192.168.46.0/24" # This value is the subnet is the NAT router will use to route to AzSMGMT to access the Internet. It can be any /24 subnet and is only used for routing.
+ natDNS = "%staging-natDNS%" # Do not change - can be configured by passing the optional natDNS parameter to the ARM deployment.
+
+ # AKS Edge Essentials variables
+ SiteConfig = @{
+ Detroit = @{
+ ArcClusterName = "Ag-ArcK8s-Detroit"
+ NetIPAddress = "172.20.1.2"
+ DefaultGateway = "172.20.1.1"
+ PrefixLength = "24"
+ DNSClientServerAddress = "168.63.129.16"
+ ServiceIPRangeStart = "172.20.1.31"
+ ServiceIPRangeSize = "10"
+ ControlPlaneEndpointIp = "172.20.1.21"
+ LinuxNodeIp4Address = "172.20.1.11"
+ Subnet = "172.20.1.0/24"
+ FriendlyName = "Detroit"
+ IsProduction = $true
+ Type = "AKSEE"
+ Branch = "main"
+ HelmSetValue = "alertmanager.enabled=false,grafana.enabled=false,prometheus.service.type=LoadBalancer"
+ HelmService = "service/prometheus-kube-prometheus-prometheus"
+ GrafanaDataSource = "detroit"
+ HelmValuesFile = "prometheus-additional-scrape-config.yaml"
+ clusterLogSize = "1024"
+ AKSEEReleaseUseLatest = $true # If set to true, the latest AKSEE release will be used. If set to false, the n-1 version will be used
+ }
+ Monterrey = @{
+ ArcClusterName = "Ag-ArcK8s-Monterrey"
+ NetIPAddress = "172.20.1.3"
+ DefaultGateway = "172.20.1.1"
+ PrefixLength = "24"
+ DNSClientServerAddress = "168.63.129.16"
+ ServiceIPRangeStart = "172.20.1.71"
+ ServiceIPRangeSize = "10"
+ ControlPlaneEndpointIp = "172.20.1.61"
+ LinuxNodeIp4Address = "172.20.1.51"
+ Subnet = "172.20.1.0/24"
+ FriendlyName = "Monterrey"
+ IsProduction = $true
+ Type = "AKSEE"
+ Branch = "main"
+ HelmSetValue = "alertmanager.enabled=false,grafana.enabled=false,prometheus.service.type=LoadBalancer"
+ HelmService = "service/prometheus-kube-prometheus-prometheus"
+ GrafanaDataSource = "monterrey"
+ HelmValuesFile = "prometheus-additional-scrape-config.yaml"
+ clusterLogSize = "1024"
+ AKSEEReleaseUseLatest = $true # If set to true, the latest AKSEE release will be used. If set to false, the n-1 version will be used
+ }
+ }
+
+ # Universal resource tag and resource types
+ TagName = 'Project'
+ TagValue = 'Jumpstart_Agora'
+ ArcServerResourceType = 'Microsoft.HybridCompute/machines'
+ ArcK8sResourceType = 'Microsoft.Kubernetes/connectedClusters'
+ AksResourceType = 'Microsoft.ContainerService/managedClusters'
+
+
+ # Observability variables
+ Monitoring = @{
+ AdminUser = "admin"
+ User = "Contoso Operator"
+ Email = "operator@contoso.com"
+ Namespace = "observability"
+ ProdURL = "http://localhost:3000"
+ Dashboards = @{
+ "grafana.com" = @() # Dashboards from https://grafana.com/grafana/dashboards
+ "custom" = @('node-exporter-full','cluster-global') # Dashboards from https://github.com/microsoft/azure_arc/tree/main/azure_jumpstart_ag/artifacts/monitoring
+ }
+ }
+
+ Namespaces = @(
+ "observability"
+ "images-cache"
+ )
+
+ AppConfig = @{
+ inferencing_deployment = @{
+ GitOpsConfigName = "contoso-motors"
+ KustomizationName = "contoso-motors"
+ KustomizationPath="./contoso_manufacturing/operations"
+ Namespace = "contoso-motors"
+ Order = 1
+ }
+ }
+
+ # Microsoft Edge startup settings variables
+ EdgeSettingRegistryPath = 'HKLM:\SOFTWARE\Policies\Microsoft\Edge'
+ EdgeSettingValueTrue = '00000001'
+ EdgeSettingValueFalse = '00000000'
+
+}
\ No newline at end of file
diff --git a/azure_jumpstart_ag/retail/artifacts/PowerShell/AgConfig.psd1 b/azure_jumpstart_ag/artifacts/PowerShell/AgConfig-retail.psd1
similarity index 100%
rename from azure_jumpstart_ag/retail/artifacts/PowerShell/AgConfig.psd1
rename to azure_jumpstart_ag/artifacts/PowerShell/AgConfig-retail.psd1
diff --git a/azure_jumpstart_ag/artifacts/PowerShell/AgLogonScript.ps1 b/azure_jumpstart_ag/artifacts/PowerShell/AgLogonScript.ps1
new file mode 100644
index 0000000000..9a9acee928
--- /dev/null
+++ b/azure_jumpstart_ag/artifacts/PowerShell/AgLogonScript.ps1
@@ -0,0 +1,283 @@
+# Script runtime environment: Level-0 Azure virtual machine ("Client VM")
+
+$ProgressPreference = "SilentlyContinue"
+Set-PSDebug -Strict
+
+#####################################################################
+# Initialize the environment
+#####################################################################
+$global:AgConfig = Import-PowerShellDataFile -Path $Env:AgConfigPath
+$global:AgToolsDir = $AgConfig.AgDirectories["AgToolsDir"]
+$global:AgIconsDir = $AgConfig.AgDirectories["AgIconDir"]
+$global:AgAppsRepo = $AgConfig.AgDirectories["AgAppsRepo"]
+$global:configMapDir = $agConfig.AgDirectories["AgConfigMapDir"]
+$global:AgDeploymentFolder = $AgConfig.AgDirectories["AgL1Files"]
+$global:AgPowerShellDir = $AgConfig.AgDirectories["AgPowerShellDir"]
+$global:industry = $Env:industry
+$global:websiteUrls = $AgConfig.URLs
+$global:githubAccount = $Env:githubAccount
+$global:githubBranch = $Env:githubBranch
+$global:resourceGroup = $Env:resourceGroup
+$global:azureLocation = $Env:azureLocation
+$global:spnClientId = $Env:spnClientId
+$global:spnClientSecret = $Env:spnClientSecret
+$global:spnTenantId = $Env:spnTenantId
+$global:subscriptionId = $Env:subscriptionId
+$global:adminUsername = $Env:adminUsername
+$global:templateBaseUrl = $Env:templateBaseUrl
+$global:adxClusterName = $Env:adxClusterName
+$global:namingGuid = $Env:namingGuid
+$global:adminPassword = $Env:adminPassword
+$global:customLocationRPOID = $Env:customLocationRPOID
+$global:appUpstreamRepo = "https://github.com/microsoft/jumpstart-agora-apps"
+$global:appsRepo = "jumpstart-agora-apps"
+if ($industry -eq "retail") {
+ $global:githubUser = $Env:githubUser
+ $global:githubPat = $Env:GITHUB_TOKEN
+ $global:acrName = $Env:acrName.ToLower()
+ $global:cosmosDBName = $Env:cosmosDBName
+ $global:cosmosDBEndpoint = $Env:cosmosDBEndpoint
+ $global:gitHubAPIBaseUri = $websiteUrls["githubAPI"]
+ $global:workflowStatus = ""
+ $global:appClonedRepo = "https://github.com/$githubUser/jumpstart-agora-apps"
+}elseif ($industry -eq "manufacturing") {
+ $global:aioNamespace = "azure-iot-operations"
+ $global:mqListenerService = "aio-mq-dmqtt-frontend"
+ $global:mqttExplorerReleasesUrl = $websiteUrls["mqttExplorerReleases"]
+ $global:stagingStorageAccountName = $Env:stagingStorageAccountName
+ $global:aioStorageAccountName = $Env:aioStorageAccountName
+ $global:spnObjectId = $Env:spnObjectId
+ $global:stcontainerName = $Env:stcontainerName
+}
+
+#####################################################################
+# Importing fuctions
+#####################################################################
+Import-Module "$AgPowerShellDir\common.psm1" -Force -DisableNameChecking
+Import-Module "$AgPowerShellDir\retail.psm1" -Force -DisableNameChecking
+Import-Module "$AgPowerShellDir\manufacturing.psm1" -Force -DisableNameChecking
+
+Start-Transcript -Path ($AgConfig.AgDirectories["AgLogsDir"] + "\AgLogonScript.log")
+Write-Header "Executing Jumpstart Agora automation scripts"
+$startTime = Get-Date
+
+# Disable Windows firewall
+Set-NetFirewallProfile -Profile Domain, Public, Private -Enabled False
+
+# Force TLS 1.2 for connections to prevent TLS/SSL errors
+[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
+
+$global:password = ConvertTo-SecureString $AgConfig.L1Password -AsPlainText -Force
+$global:Credentials = New-Object System.Management.Automation.PSCredential($AgConfig.L1Username, $password)
+
+#####################################################################
+# Setup Azure CLI
+#####################################################################
+Write-Host "[$(Get-Date -Format t)] INFO: Configuring Azure CLI (Step 1/17)" -ForegroundColor DarkGreen
+Write-Host "[$(Get-Date -Format t)] INFO: Logging into Az CLI using the service principal and secret provided at deployment" -ForegroundColor Gray
+az login --service-principal --username $Env:spnClientID --password=$Env:spnClientSecret --tenant $Env:spnTenantId | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\AzCLI.log")
+az account set -s $subscriptionId
+Deploy-AzCLI
+
+#####################################################################
+# Setup Azure PowerShell and register providers
+#####################################################################
+Write-Host "[$(Get-Date -Format t)] INFO: Configuring Azure PowerShell (Step 2/17)" -ForegroundColor DarkGreen
+Deploy-AzPowerShell
+
+#############################################################
+# Install Windows Terminal, WSL2, and Ubuntu
+#############################################################
+Write-Host "[$(Get-Date -Format t)] INFO: Installing dev tools (Step 3/17)" -ForegroundColor DarkGreen
+Deploy-WindowsTools
+
+
+#####################################################################
+# Configure Jumpstart Agora Apps repository
+#####################################################################
+if ($industry -eq "retail") {
+ Write-Host "INFO: Forking and preparing Apps repository locally (Step 4/17)" -ForegroundColor DarkGreen
+ SetupRetailRepo
+}
+
+
+#####################################################################
+# Azure IoT Hub resources preparation
+#####################################################################
+if ($industry -eq "retail") {
+ Write-Host "[$(Get-Date -Format t)] INFO: Creating Azure IoT resources (Step 5/17)" -ForegroundColor DarkGreen
+ Deploy-AzureIoTHub
+}
+
+#####################################################################
+# Configure L1 virtualization infrastructure
+#####################################################################
+Write-Host "[$(Get-Date -Format t)] INFO: Configuring L1 virtualization infrastructure (Step 6/17)" -ForegroundColor DarkGreen
+Deploy-VirtualizationInfrastructure
+
+#####################################################################
+# Setup Azure Container registry on cloud AKS staging environment
+#####################################################################
+if ($industry -eq "retail") {
+ Deploy-AzContainerRegistry
+}
+
+#####################################################################
+# Creating Kubernetes namespaces on clusters
+#####################################################################
+Write-Host "[$(Get-Date -Format t)] INFO: Creating namespaces on clusters (Step 8/17)" -ForegroundColor DarkGreen
+Deploy-ClusterNamespaces
+
+#####################################################################
+# Setup Azure Container registry pull secret on clusters
+#####################################################################
+Write-Host "[$(Get-Date -Format t)] INFO: Configuring secrets on clusters (Step 9/17)" -ForegroundColor DarkGreen
+Deploy-ClusterSecrets
+
+#####################################################################
+# Cache contoso-supermarket images on all clusters
+#####################################################################
+Deploy-K8sImagesCache
+
+#####################################################################
+# Connect the AKS Edge Essentials clusters and hosts to Azure Arc
+#####################################################################
+Write-Host "[$(Get-Date -Format t)] INFO: Connecting AKS Edge clusters to Azure with Azure Arc (Step 10/17)" -ForegroundColor DarkGreen
+Deploy-AzArcK8s
+
+#####################################################################
+# Installing flux extension on clusters
+#####################################################################
+Write-Host "[$(Get-Date -Format t)] INFO: Installing flux extension on clusters (Step 11/17)" -ForegroundColor DarkGreen
+Deploy-ClusterFluxExtension
+
+#####################################################################
+# Deploying nginx on AKS cluster
+#####################################################################
+if ($industry -eq "retail") {
+ Write-Host "[$(Get-Date -Format t)] INFO: Deploying nginx on AKS cluster (Step 12/17)" -ForegroundColor DarkGreen
+ kubectx $AgConfig.SiteConfig.Staging.FriendlyName.ToLower() | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Nginx.log")
+ helm repo add $AgConfig.nginx.RepoName $AgConfig.nginx.RepoURL | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Nginx.log")
+ helm repo update | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Nginx.log")
+
+ helm install $AgConfig.nginx.ReleaseName $AgConfig.nginx.ChartName `
+ --create-namespace `
+ --namespace $AgConfig.nginx.Namespace `
+ --set controller.service.annotations."service\.beta\.kubernetes\.io/azure-load-balancer-health-probe-request-path"=/healthz | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Nginx.log")
+}
+#####################################################################
+# Configuring applications on the clusters using GitOps
+#####################################################################
+if ($industry -eq "retail") {
+ Write-Host "[$(Get-Date -Format t)] INFO: Configuring GitOps (Step 13/17)" -ForegroundColor DarkGreen
+ Deploy-RetailConfigs
+}
+
+if ($industry -eq "manufacturing") {
+ Deploy-AIO
+ Deploy-ManufacturingConfigs
+ $mqttIpArray=Set-MQTTIpAddress
+ #Deploy-MQTTSimulator -mqttIpArray $mqttIpArray # this is now being done via helm
+ Deploy-MQTTExplorer -mqttIpArray $mqttIpArray
+}
+
+##############################################################
+# Deploy Kubernetes Prometheus Stack for Observability
+##############################################################
+Deploy-Prometheus -AgConfig $AgConfig
+
+#####################################################################
+# Deploy Azure Workbook for Infrastructure Observability
+#####################################################################
+Deploy-Workbook "arc-inventory-workbook.bicep"
+#####################################################################
+# Deploy Azure Workbook for OS Performance
+#####################################################################
+Deploy-Workbook "arc-osperformance-workbook.bicep"
+
+#####################################################################
+# Deploy Azure Data Explorer Dashboard Reports
+#####################################################################
+Deploy-ADXDashboardReports
+
+##############################################################
+# Creating bookmarks
+##############################################################
+Write-Host "[$(Get-Date -Format t)] INFO: Creating Microsoft Edge Bookmarks in Favorites Bar (Step 15/17)" -ForegroundColor DarkGreen
+if($industry -eq "retail"){
+ Deploy-RetailBookmarks
+}else{
+ Deploy-ManufacturingBookmarks
+}
+
+##############################################################
+# Cleanup
+##############################################################
+Write-Host "[$(Get-Date -Format t)] INFO: Cleaning up scripts and uploading logs (Step 17/17)" -ForegroundColor DarkGreen
+# Creating Hyper-V Manager desktop shortcut
+Write-Host "[$(Get-Date -Format t)] INFO: Creating Hyper-V desktop shortcut." -ForegroundColor Gray
+Copy-Item -Path "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Administrative Tools\Hyper-V Manager.lnk" -Destination "C:\Users\All Users\Desktop" -Force
+
+if($industry -eq "retail"){
+ Write-Host "[$(Get-Date -Format t)] INFO: Cleaning up images-cache job" -ForegroundColor Gray
+ while ($(Get-Job -Name images-cache-cleanup).State -eq 'Running') {
+ Write-Host "[$(Get-Date -Format t)] INFO: Waiting for images-cache job to complete on all clusters...waiting 60 seconds" -ForegroundColor Gray
+ Receive-Job -Name images-cache-cleanup -WarningAction SilentlyContinue
+ Start-Sleep -Seconds 60
+ }
+ Get-Job -name images-cache-cleanup | Remove-Job
+}
+
+
+# Removing the LogonScript Scheduled Task
+Write-Host "[$(Get-Date -Format t)] INFO: Removing scheduled logon task so it won't run on next login." -ForegroundColor Gray
+Unregister-ScheduledTask -TaskName "AgLogonScript" -Confirm:$false
+
+# Executing the deployment logs bundle PowerShell script in a new window
+Write-Host "[$(Get-Date -Format t)] INFO: Uploading Log Bundle." -ForegroundColor Gray
+$Env:AgLogsDir = $AgConfig.AgDirectories["AgLogsDir"]
+Invoke-Expression 'cmd /c start Powershell -Command {
+$RandomString = -join ((48..57) + (97..122) | Get-Random -Count 6 | % {[char]$_})
+Write-Host "Sleeping for 5 seconds before creating deployment logs bundle..."
+Start-Sleep -Seconds 5
+Write-Host "`n"
+Write-Host "Creating deployment logs bundle"
+7z a $Env:AgLogsDir\LogsBundle-"$RandomString".zip $Env:AgLogsDir\*.log
+}'
+
+Write-Host "[$(Get-Date -Format t)] INFO: Changing Wallpaper" -ForegroundColor Gray
+$imgPath = $AgConfig.AgDirectories["AgDir"] + "\wallpaper.png"
+$code = @'
+using System.Runtime.InteropServices;
+namespace Win32{
+
+ public class Wallpaper{
+ [DllImport("user32.dll", CharSet=CharSet.Auto)]
+ static extern int SystemParametersInfo (int uAction , int uParam , string lpvParam , int fuWinIni) ;
+
+ public static void SetWallpaper(string thePath){
+ SystemParametersInfo(20,0,thePath,3);
+ }
+ }
+}
+'@
+Add-Type $code
+[Win32.Wallpaper]::SetWallpaper($imgPath)
+
+# Kill the open PowerShell monitoring kubectl get pods
+# if ($industry -eq "manufacturing") {
+# foreach ($shell in $kubectlMonShells) {
+# Stop-Process -Id $shell.Id
+# }
+# }
+
+Write-Host "[$(Get-Date -Format t)] INFO: Starting Docker Desktop" -ForegroundColor Green
+Start-Process "C:\Program Files\Docker\Docker\Docker Desktop.exe"
+
+$endTime = Get-Date
+$timeSpan = New-TimeSpan -Start $starttime -End $endtime
+Write-Host
+Write-Host "[$(Get-Date -Format t)] INFO: Deployment is complete. Deployment time was $($timeSpan.Hours) hour and $($timeSpan.Minutes) minutes. Enjoy the Agora experience!" -ForegroundColor Green
+Write-Host
+
+Stop-Transcript
\ No newline at end of file
diff --git a/azure_jumpstart_ag/retail/artifacts/PowerShell/Bootstrap.ps1 b/azure_jumpstart_ag/artifacts/PowerShell/Bootstrap.ps1
similarity index 83%
rename from azure_jumpstart_ag/retail/artifacts/PowerShell/Bootstrap.ps1
rename to azure_jumpstart_ag/artifacts/PowerShell/Bootstrap.ps1
index 65636bb1c9..649c7c21b2 100644
--- a/azure_jumpstart_ag/retail/artifacts/PowerShell/Bootstrap.ps1
+++ b/azure_jumpstart_ag/artifacts/PowerShell/Bootstrap.ps1
@@ -3,6 +3,7 @@ param (
[string]$adminPassword,
[string]$spnClientId,
[string]$spnClientSecret,
+ [string]$spnObjectId,
[string]$spnTenantId,
[string]$spnAuthority,
[string]$subscriptionId,
@@ -15,14 +16,17 @@ param (
[string]$acrName,
[string]$cosmosDBName,
[string]$cosmosDBEndpoint,
- [string]$githubUser,
[string]$templateBaseUrl,
[string]$rdpPort,
[string]$githubAccount,
[string]$githubBranch,
[string]$githubPAT,
[string]$adxClusterName,
- [string]$namingGuid
+ [string]$namingGuid,
+ [string]$industry,
+ [string]$customLocationRPOID,
+ [string]$aioStorageAccountName,
+ [string]$stcontainerName
)
##############################################################
@@ -32,6 +36,7 @@ param (
[System.Environment]::SetEnvironmentVariable('adminPassword', $adminPassword, [System.EnvironmentVariableTarget]::Machine)
[System.Environment]::SetEnvironmentVariable('spnClientID', $spnClientId, [System.EnvironmentVariableTarget]::Machine)
[System.Environment]::SetEnvironmentVariable('spnClientSecret', $spnClientSecret, [System.EnvironmentVariableTarget]::Machine)
+[System.Environment]::SetEnvironmentVariable('spnObjectID', $spnObjectId, [System.EnvironmentVariableTarget]::Machine)
[System.Environment]::SetEnvironmentVariable('spnTenantId', $spnTenantId, [System.EnvironmentVariableTarget]::Machine)
[System.Environment]::SetEnvironmentVariable('spnAuthority', $spnAuthority, [System.EnvironmentVariableTarget]::Machine)
[System.Environment]::SetEnvironmentVariable('SPN_CLIENT_ID', $spnClientId, [System.EnvironmentVariableTarget]::Machine)
@@ -48,7 +53,6 @@ param (
[System.Environment]::SetEnvironmentVariable('acrName', $acrName, [System.EnvironmentVariableTarget]::Machine)
[System.Environment]::SetEnvironmentVariable('cosmosDBName', $cosmosDBName, [System.EnvironmentVariableTarget]::Machine)
[System.Environment]::SetEnvironmentVariable('cosmosDBEndpoint', $cosmosDBEndpoint, [System.EnvironmentVariableTarget]::Machine)
-[System.Environment]::SetEnvironmentVariable('githubUser', $githubUser, [System.EnvironmentVariableTarget]::Machine)
[System.Environment]::SetEnvironmentVariable('templateBaseUrl', $templateBaseUrl, [System.EnvironmentVariableTarget]::Machine)
[System.Environment]::SetEnvironmentVariable('githubAccount', $githubAccount, [System.EnvironmentVariableTarget]::Machine)
[System.Environment]::SetEnvironmentVariable('githubBranch', $githubBranch, [System.EnvironmentVariableTarget]::Machine)
@@ -56,6 +60,10 @@ param (
[System.Environment]::SetEnvironmentVariable('AgDir', "C:\Ag", [System.EnvironmentVariableTarget]::Machine)
[System.Environment]::SetEnvironmentVariable('adxClusterName', $adxClusterName, [System.EnvironmentVariableTarget]::Machine)
[System.Environment]::SetEnvironmentVariable('namingGuid', $namingGuid, [System.EnvironmentVariableTarget]::Machine)
+[System.Environment]::SetEnvironmentVariable('industry', $industry, [System.EnvironmentVariableTarget]::Machine)
+[System.Environment]::SetEnvironmentVariable('customLocationRPOID', $customLocationRPOID, [System.EnvironmentVariableTarget]::Machine)
+[System.Environment]::SetEnvironmentVariable('aioStorageAccountName', $aioStorageAccountName, [System.EnvironmentVariableTarget]::Machine)
+[System.Environment]::SetEnvironmentVariable('stcontainerName', $stcontainerName, [System.EnvironmentVariableTarget]::Machine)
$ErrorActionPreference = 'Continue'
@@ -95,14 +103,20 @@ if (($rdpPort -ne $null) -and ($rdpPort -ne "") -and ($rdpPort -ne "3389")) {
# Download configuration data file and declaring directories
##############################################################
$ConfigurationDataFile = "C:\Temp\AgConfig.psd1"
-Invoke-WebRequest ($templateBaseUrl + "artifacts/PowerShell/AgConfig.psd1") -OutFile $ConfigurationDataFile
-$AgConfig = Import-PowerShellDataFile -Path $ConfigurationDataFile
-$AgDirectory = $AgConfig.AgDirectories["AgDir"]
-$AgToolsDir = $AgConfig.AgDirectories["AgToolsDir"]
-$AgIconsDir = $AgConfig.AgDirectories["AgIconDir"]
-$AgPowerShellDir = $AgConfig.AgDirectories["AgPowerShellDir"]
-$AgMonitoringDir = $AgConfig.AgDirectories["AgMonitoringDir"]
-$websiteUrls = $AgConfig.URLs
+
+switch ($industry) {
+ "retail" { Invoke-WebRequest ($templateBaseUrl + "artifacts/PowerShell/AgConfig-retail.psd1") -OutFile $ConfigurationDataFile }
+ "manufacturing" {Invoke-WebRequest ($templateBaseUrl + "artifacts/PowerShell/AgConfig-manufacturing.psd1") -OutFile $ConfigurationDataFile}
+}
+
+$AgConfig = Import-PowerShellDataFile -Path $ConfigurationDataFile
+$AgDirectory = $AgConfig.AgDirectories["AgDir"]
+$AgToolsDir = $AgConfig.AgDirectories["AgToolsDir"]
+$AgDeploymentFolder = $AgConfig.AgDirectories["AgL1Files"]
+$AgIconsDir = $AgConfig.AgDirectories["AgIconDir"]
+$AgPowerShellDir = $AgConfig.AgDirectories["AgPowerShellDir"]
+$AgMonitoringDir = $AgConfig.AgDirectories["AgMonitoringDir"]
+$websiteUrls = $AgConfig.URLs
function BITSRequest {
Param(
@@ -212,19 +226,35 @@ $latestRelease = (Invoke-RestMethod -Uri $websiteUrls["grafana"]).tag_name.repla
# Download artifacts
##############################################################
[System.Environment]::SetEnvironmentVariable('AgConfigPath', "$AgPowerShellDir\AgConfig.psd1", [System.EnvironmentVariableTarget]::Machine)
+Copy-Item $ConfigurationDataFile "$AgPowerShellDir\AgConfig.psd1" -Force
+
Invoke-WebRequest ($templateBaseUrl + "artifacts/PowerShell/AgLogonScript.ps1") -OutFile "$AgPowerShellDir\AgLogonScript.ps1"
-Invoke-WebRequest ($templateBaseUrl + "artifacts/PowerShell/AgConfig.psd1") -OutFile "$AgPowerShellDir\AgConfig.psd1"
-Invoke-WebRequest ($templateBaseUrl + "artifacts/icons/grafana.ico") -OutFile $AgIconsDir\grafana.ico
-Invoke-WebRequest ($templateBaseUrl + "artifacts/icons/contoso.png") -OutFile $AgIconsDir\contoso.png
-Invoke-WebRequest ($templateBaseUrl + "artifacts/icons/contoso.svg") -OutFile $AgIconsDir\contoso.svg
+Invoke-WebRequest ($templateBaseUrl + "artifacts/PowerShell/Modules/common.psm1") -OutFile "$AgPowerShellDir\common.psm1"
+Invoke-WebRequest ($templateBaseUrl + "artifacts/PowerShell/Modules/retail.psm1") -OutFile "$AgPowerShellDir\retail.psm1"
+Invoke-WebRequest ($templateBaseUrl + "artifacts/PowerShell/Modules/manufacturing.psm1") -OutFile "$AgPowerShellDir\manufacturing.psm1"
Invoke-WebRequest ($templateBaseUrl + "artifacts/settings/DockerDesktopSettings.json") -OutFile "$AgToolsDir\settings.json"
-Invoke-WebRequest ($templateBaseUrl + "artifacts/settings/Bookmarks") -OutFile "$AgToolsDir\Bookmarks"
Invoke-WebRequest "https://raw.githubusercontent.com/Azure/arc_jumpstart_docs/main/img/wallpaper/agora_wallpaper_dark.png" -OutFile $AgDirectory\wallpaper.png
-
-Invoke-WebRequest ($templateBaseUrl + "artifacts/monitoring/grafana-freezer-monitoring.json") -OutFile "$AgMonitoringDir\grafana-freezer-monitoring.json"
Invoke-WebRequest ($templateBaseUrl + "artifacts/monitoring/grafana-node-exporter-full.json") -OutFile "$AgMonitoringDir\grafana-node-exporter-full.json"
Invoke-WebRequest ($templateBaseUrl + "artifacts/monitoring/grafana-cluster-global.json") -OutFile "$AgMonitoringDir\grafana-cluster-global.json"
+Invoke-WebRequest ($templateBaseUrl + "artifacts/monitoring/arc-inventory-workbook.bicep") -OutFile "$AgMonitoringDir\arc-inventory-workbook.bicep"
+Invoke-WebRequest ($templateBaseUrl + "artifacts/monitoring/arc-osperformance-workbook.bicep") -OutFile "$AgMonitoringDir\arc-osperformance-workbook.bicep"
Invoke-WebRequest ($templateBaseUrl + "artifacts/monitoring/prometheus-additional-scrape-config.yaml") -OutFile "$AgMonitoringDir\prometheus-additional-scrape-config.yaml"
+Invoke-WebRequest ($templateBaseUrl + "artifacts/icons/grafana.ico") -OutFile $AgIconsDir\grafana.ico
+Invoke-WebRequest ($templateBaseUrl + "artifacts/icons/contoso.png") -OutFile $AgIconsDir\contoso.png
+Invoke-WebRequest ($templateBaseUrl + "artifacts/icons/contoso.svg") -OutFile $AgIconsDir\contoso.svg
+Invoke-WebRequest ($templateBaseUrl + "artifacts/icons/contoso-motors.png") -OutFile $AgIconsDir\contoso-motors.png
+Invoke-WebRequest ($templateBaseUrl + "artifacts/icons/contoso-motors.svg") -OutFile $AgIconsDir\contoso-motors.svg
+Invoke-WebRequest ($templateBaseUrl + "artifacts/L1Files/config.json") -OutFile $AgDeploymentFolder\config.json
+
+if($industry -eq "retail"){
+ Invoke-WebRequest ($templateBaseUrl + "artifacts/settings/Bookmarks-retail") -OutFile "$AgToolsDir\Bookmarks"
+ Invoke-WebRequest ($templateBaseUrl + "artifacts/monitoring/grafana-freezer-monitoring.json") -OutFile "$AgMonitoringDir\grafana-freezer-monitoring.json"
+}
+elseif ($industry -eq "manufacturing") {
+ Invoke-WebRequest ($templateBaseUrl + "artifacts/settings/Bookmarks-manufacturing") -OutFile "$AgToolsDir\Bookmarks"
+ Invoke-WebRequest ($templateBaseUrl + "artifacts/settings/mq_cloudConnector.yml") -OutFile "$AgToolsDir\mq_cloudConnector.yml"
+ Invoke-WebRequest ($templateBaseUrl + "artifacts/settings/mqtt_explorer_settings.json") -OutFile "$AgToolsDir\mqtt_explorer_settings.json"
+}
BITSRequest -Params @{'Uri' = 'https://aka.ms/wslubuntu'; 'Filename' = "$AgToolsDir\Ubuntu.appx" }
BITSRequest -Params @{'Uri' = $websiteUrls["wslStoreStorage"]; 'Filename' = "$AgToolsDir\wsl_update_x64.msi" }
diff --git a/azure_jumpstart_ag/artifacts/PowerShell/Modules/common.psm1 b/azure_jumpstart_ag/artifacts/PowerShell/Modules/common.psm1
new file mode 100644
index 0000000000..16c8c6332f
--- /dev/null
+++ b/azure_jumpstart_ag/artifacts/PowerShell/Modules/common.psm1
@@ -0,0 +1,1044 @@
+function Deploy-AzCLI {
+ $cliDir = New-Item -Path ($AgConfig.AgDirectories["AgLogsDir"] + "\.cli\") -Name ".Ag" -ItemType Directory
+
+ if (-not $($cliDir.Parent.Attributes.HasFlag([System.IO.FileAttributes]::Hidden))) {
+ $folder = Get-Item $cliDir.Parent.FullName -ErrorAction SilentlyContinue
+ $folder.Attributes += [System.IO.FileAttributes]::Hidden
+ }
+
+ $Env:AZURE_CONFIG_DIR = $cliDir.FullName
+
+ # Making extension install dynamic
+ if ($AgConfig.AzCLIExtensions.Count -ne 0) {
+ Write-Host "[$(Get-Date -Format t)] INFO: Installing Azure CLI extensions: " ($AgConfig.AzCLIExtensions -join ', ') -ForegroundColor Gray
+ az config set extension.use_dynamic_install=yes_without_prompt --only-show-errors
+ # Installing Azure CLI extensions
+ foreach ($extension in $AgConfig.AzCLIExtensions) {
+ az extension add --name $extension --system --only-show-errors
+ }
+ }
+
+ Write-Host "[$(Get-Date -Format t)] INFO: Az CLI configuration complete!" -ForegroundColor Green
+ Write-Host
+}
+
+function Deploy-AzPowerShell {
+ $azurePassword = ConvertTo-SecureString $Env:spnClientSecret -AsPlainText -Force
+ $psCred = New-Object System.Management.Automation.PSCredential($Env:spnClientID , $azurePassword)
+ Connect-AzAccount -Credential $psCred -TenantId $Env:spnTenantId -ServicePrincipal -Subscription $subscriptionId | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\AzPowerShell.log")
+
+ # Install PowerShell modules
+ if ($AgConfig.PowerShellModules.Count -ne 0) {
+ Write-Host "[$(Get-Date -Format t)] INFO: Installing PowerShell modules: " ($AgConfig.PowerShellModules -join ', ') -ForegroundColor Gray
+ foreach ($module in $AgConfig.PowerShellModules) {
+ Install-Module -Name $module -Force | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\AzPowerShell.log")
+ }
+ }
+
+ # Register Azure providers
+ if ($AgConfig.AzureProviders.Count -ne 0) {
+ Write-Host "[$(Get-Date -Format t)] INFO: Registering Azure providers in the current subscription: " ($AgConfig.AzureProviders -join ', ') -ForegroundColor Gray
+ foreach ($provider in $AgConfig.AzureProviders) {
+ Register-AzResourceProvider -ProviderNamespace $provider | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\AzPowerShell.log")
+ }
+ }
+ Write-Host "[$(Get-Date -Format t)] INFO: Azure PowerShell configuration and resource provider registration complete!" -ForegroundColor Green
+ Write-Host
+}
+
+function Deploy-WindowsTools {
+ $DevToolsInstallationJob = Invoke-Command -ScriptBlock {
+ $AgConfig = $using:AgConfig
+ $websiteUrls = $using:websiteUrls
+ $AgToolsDir = $using:AgToolsDir
+ $adminUsername = $using:adminUsername
+
+
+ If ($PSVersionTable.PSVersion.Major -ge 7) { Write-Error "This script needs be run by version of PowerShell prior to 7.0" }
+ $downloadDir = "C:\WinTerminal"
+ $frameworkPkgPath = "$downloadDir\Microsoft.VCLibs.x64.14.00.Desktop.appx"
+ $WindowsTerminalKitPath = "$downloadDir\Microsoft.WindowsTerminal.PreinstallKit.zip"
+ $windowsTerminalPath = "$downloadDir\WindowsTerminal"
+ $filenamePattern = "*PreinstallKit.zip"
+ $terminalDownloadUri = ((Invoke-RestMethod -Method GET -Uri $websiteUrls["windowsTerminal"]).assets | Where-Object name -like $filenamePattern ).browser_download_url | Select-Object -First 1
+
+ # Download C++ Runtime framework packages for Desktop Bridge and Windows Terminal latest release
+ Write-Host "[$(Get-Date -Format t)] INFO: Downloading binaries." -ForegroundColor Gray
+
+ $ProgressPreference = 'SilentlyContinue'
+
+ Invoke-WebRequest -Uri $websiteUrls["vcLibs"] -OutFile ( New-Item -Path $frameworkPkgPath -Force ) | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Tools.log")
+ Invoke-WebRequest -Uri $terminalDownloadUri -OutFile ( New-Item -Path $windowsTerminalKitPath -Force ) | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Tools.log")
+
+ $ProgressPreference = 'Continue'
+
+ # Extract Windows Terminal PreinstallKit
+ Write-Host "[$(Get-Date -Format t)] INFO: Expanding Windows Terminal PreinstallKit." -ForegroundColor Gray
+ Expand-Archive $WindowsTerminalKitPath $windowsTerminalPath | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Tools.log")
+
+ # Install WSL latest kernel update
+ Write-Host "[$(Get-Date -Format t)] INFO: Installing WSL." -ForegroundColor Gray
+ msiexec /i "$AgToolsDir\wsl_update_x64.msi" /qn | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Tools.log")
+
+ # Install C++ Runtime framework packages for Desktop Bridge and Windows Terminal latest release
+ Write-Host "[$(Get-Date -Format t)] INFO: Installing Windows Terminal" -ForegroundColor Gray
+ Add-AppxPackage -Path $frameworkPkgPath | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Tools.log")
+
+ # Install the Windows Terminal prereqs
+ foreach ($file in Get-ChildItem $windowsTerminalPath -Filter *x64*.appx) {
+ Add-AppxPackage -Path $file.FullName | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Tools.log")
+ }
+
+ # Install Windows Terminal
+ foreach ($file in Get-ChildItem $windowsTerminalPath -Filter *.msixbundle) {
+ Add-AppxPackage -Path $file.FullName | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Tools.log")
+ }
+
+ # Configure Windows Terminal
+ Set-Location $Env:LOCALAPPDATA\Packages\Microsoft.WindowsTerminal*\LocalState
+
+ # Launch Windows Terminal for default settings.json to be created
+ $action = New-ScheduledTaskAction -Execute $((Get-Command wt.exe).Source)
+ $trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddSeconds(1)
+ $null = Register-ScheduledTask -Action $action -Trigger $trigger -TaskName WindowsTerminalInit
+
+ # Give process time to initiate and create settings file
+ Start-Sleep 10
+
+ # Stop Windows Terminal process
+ Get-Process WindowsTerminal | Stop-Process
+
+ Unregister-ScheduledTask -TaskName WindowsTerminalInit -Confirm:$false
+
+ $settings = Get-Content .\settings.json | ConvertFrom-Json
+ $settings.profiles.defaults.elevate
+
+ # Configure the default profile setting "Run this profile as Administrator" to "true"
+ $settings.profiles.defaults | Add-Member -Name elevate -MemberType NoteProperty -Value $true -Force
+
+ $settings | ConvertTo-Json -Depth 8 | Set-Content .\settings.json
+
+ # Install Ubuntu
+ Write-Host "[$(Get-Date -Format t)] INFO: Installing Ubuntu" -ForegroundColor Gray
+ Add-AppxPackage -Path "$AgToolsDir\Ubuntu.appx" | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Tools.log")
+
+ # Setting WSL environment variables
+ $userenv = [System.Environment]::GetEnvironmentVariable("Path", "User")
+ [System.Environment]::SetEnvironmentVariable("PATH", $userenv + ";C:\Users\$adminUsername\Ubuntu", "User")
+
+ # Initializing the wsl ubuntu app without requiring user input
+ $ubuntu_path = "c:/users/$adminUsername/AppData/Local/Microsoft/WindowsApps/ubuntu"
+ Invoke-Expression -Command "$ubuntu_path install --root" | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Tools.log")
+
+ # Create Windows Terminal shortcut
+ $WshShell = New-Object -comObject WScript.Shell
+ $WinTerminalPath = (Get-ChildItem "C:\Program Files\WindowsApps" -Recurse | Where-Object { $_.name -eq "wt.exe" }).FullName
+ $Shortcut = $WshShell.CreateShortcut("$Env:USERPROFILE\Desktop\Windows Terminal.lnk")
+ $Shortcut.TargetPath = $WinTerminalPath
+ $shortcut.WindowStyle = 3
+ $shortcut.Save()
+
+ #############################################################
+ # Install VSCode extensions
+ #############################################################
+ Write-Host "[$(Get-Date -Format t)] INFO: Installing VSCode extensions: " + ($AgConfig.VSCodeExtensions -join ', ') -ForegroundColor Gray
+ # Install VSCode extensions
+ foreach ($extension in $AgConfig.VSCodeExtensions) {
+ code --install-extension $extension 2>&1 | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Tools.log")
+ }
+
+ #############################################################
+ # Install Docker Desktop
+ #############################################################
+ Write-Host "[$(Get-Date -Format t)] INFO: Installing Docker Desktop." -ForegroundColor DarkGreen
+ # Download and Install Docker Desktop
+ $arguments = 'install --quiet --accept-license'
+ Start-Process "$AgToolsDir\DockerDesktopInstaller.exe" -Wait -ArgumentList $arguments
+ Get-ChildItem "$Env:USERPROFILE\Desktop\Docker Desktop.lnk" | Remove-Item -Confirm:$false
+ Copy-Item "$AgToolsDir\settings.json" -Destination "$Env:USERPROFILE\AppData\Roaming\Docker\settings.json" -Force
+ Start-Process "C:\Program Files\Docker\Docker\Docker Desktop.exe"
+ Start-Sleep -Seconds 15
+ Get-Process | Where-Object { $_.name -like "Docker Desktop" } | Stop-Process -Force
+ # Cleanup
+ Remove-Item $downloadDir -Recurse -Force
+
+ } -JobName step3 -ThrottleLimit 16 -AsJob -ComputerName .
+
+ Write-Host "[$(Get-Date -Format t)] INFO: Dev Tools installation initiated in background job." -ForegroundColor Green
+
+ $DevToolsInstallationJob
+
+ Write-Host
+}
+
+function Deploy-VirtualizationInfrastructure {
+ $password = ConvertTo-SecureString $AgConfig.L1Password -AsPlainText -Force
+ $Credentials = New-Object System.Management.Automation.PSCredential($AgConfig.L1Username, $password)
+
+ # Turn the .kube folder to a shared folder where all Kubernetes kubeconfig files will be copied to
+ $kubeFolder = "$Env:USERPROFILE\.kube"
+ New-Item -ItemType Directory $kubeFolder -Force | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\L1Infra.log")
+ New-SmbShare -Name "kube" -Path "$Env:USERPROFILE\.kube" -FullAccess "Everyone" | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\L1Infra.log")
+
+ # Enable Enhanced Session Mode on Host
+ Write-Host "[$(Get-Date -Format t)] INFO: Enabling Enhanced Session Mode on Hyper-V host" -ForegroundColor Gray
+ Set-VMHost -EnableEnhancedSessionMode $true | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\L1Infra.log")
+
+ # Create Internal Hyper-V switch for the L1 nested virtual machines
+ New-VMSwitch -Name $AgConfig.L1SwitchName -SwitchType Internal | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\L1Infra.log")
+ $ifIndex = (Get-NetAdapter -Name ("vEthernet (" + $AgConfig.L1SwitchName + ")")).ifIndex
+ New-NetIPAddress -IPAddress $AgConfig.L1DefaultGateway -PrefixLength 24 -InterfaceIndex $ifIndex | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\L1Infra.log")
+ New-NetNat -Name $AgConfig.L1SwitchName -InternalIPInterfaceAddressPrefix $AgConfig.L1NatSubnetPrefix | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\L1Infra.log")
+
+ #####################################################################
+ # Deploying the nested L1 virtual machines
+ #####################################################################
+ Write-Host "[$(Get-Date -Format t)] INFO: Fetching Windows 11 IoT Enterprise VM image from Azure storage. This may take a few minutes." -ForegroundColor Yellow
+ # azcopy cp $AgConfig.PreProdVHDBlobURL $AgConfig.AgDirectories["AgVHDXDir"] --recursive=true --check-length=false --log-level=ERROR | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\L1Infra.log")
+ azcopy cp $AgConfig.ProdVHDBlobURL $AgConfig.AgDirectories["AgVHDXDir"] --recursive=true --check-length=false --log-level=ERROR | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\L1Infra.log")
+
+ # Create three virtual machines from the base VHDX image
+ $vhdxPath = Get-ChildItem $AgConfig.AgDirectories["AgVHDXDir"] -Filter *.vhdx | Select-Object -ExpandProperty FullName
+ foreach ($site in $AgConfig.SiteConfig.GetEnumerator()) {
+ if ($site.Value.Type -eq "AKSEE") {
+ # Create disks for each site host
+ Write-Host "[$(Get-Date -Format t)] INFO: Creating $($site.Name) disk." -ForegroundColor Gray
+ $destVhdxPath = "$($AgConfig.AgDirectories["AgVHDXDir"])\$($site.Name)Disk.vhdx"
+ $destPath = $AgConfig.AgDirectories["AgVHDXDir"]
+ New-VHD -ParentPath $vhdxPath -Path $destVhdxPath -Differencing | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\L1Infra.log")
+
+ # Create a new virtual machine and attach the existing virtual hard disk
+ Write-Host "[$(Get-Date -Format t)] INFO: Creating and configuring $($site.Name) virtual machine." -ForegroundColor Gray
+
+ New-VM -Name $site.Name `
+ -Path $destPath `
+ -MemoryStartupBytes $AgConfig.L1VMMemory `
+ -BootDevice VHD `
+ -VHDPath $destVhdxPath `
+ -Generation 2 `
+ -Switch $AgConfig.L1SwitchName | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\L1Infra.log")
+
+ # Set up the virtual machine before coping all AKS Edge Essentials automation files
+ Set-VMProcessor -VMName $site.Name `
+ -Count $AgConfig.L1VMNumVCPU `
+ -ExposeVirtualizationExtensions $true | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\L1Infra.log")
+
+ Get-VMNetworkAdapter -VMName $site.Name | Set-VMNetworkAdapter -MacAddressSpoofing On | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\L1Infra.log")
+ Enable-VMIntegrationService -VMName $site.Name -Name "Guest Service Interface" | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\L1Infra.log")
+
+ # Start the virtual machine
+ Start-VM -Name $site.Name | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\L1Infra.log")
+ }
+ }
+
+ Start-Sleep -Seconds 20
+ # Create an array with VM names
+ $VMnames = (Get-VM).Name
+
+ $sourcePath = "$PsHome\Profile.ps1"
+ $destinationPath = "C:\Deployment\Profile.ps1"
+ $maxRetries = 3
+
+ foreach ($VM in $VMNames) {
+ $retryCount = 0
+ $copySucceeded = $false
+
+ while (-not $copySucceeded -and $retryCount -lt $maxRetries) {
+ try {
+ Copy-VMFile $VM -SourcePath $sourcePath -DestinationPath $destinationPath -CreateFullPath -FileSource Host -Force -ErrorAction Stop
+ $copySucceeded = $true
+ Write-Host "File copied to $VM successfully."
+ }
+ catch {
+ $retryCount++
+ Write-Host "Attempt $retryCount : File copy to $VM failed. Retrying..."
+ Start-Sleep -Seconds 30 # Wait for 30 seconds before retrying
+ }
+ }
+
+ if (-not $copySucceeded) {
+ Write-Host "File copy to $VM failed after $maxRetries attempts."
+ }
+ }
+
+ ########################################################################
+ # Prepare L1 nested virtual machines for AKS Edge Essentials bootstrap
+ ########################################################################
+ foreach ($site in $AgConfig.SiteConfig.GetEnumerator()) {
+ if ($site.Value.Type -eq "AKSEE") {
+ Write-Host "[$(Get-Date -Format t)] INFO: Renaming computer name of $($site.Name)" -ForegroundColor Gray
+ $ErrorActionPreference = "SilentlyContinue"
+ Invoke-Command -VMName $site.Name -Credential $Credentials -ScriptBlock {
+ $site = $using:site
+ (gwmi win32_computersystem).Rename($site.Name)
+ } | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\L1Infra.log")
+ $ErrorActionPreference = "Continue"
+ Stop-VM -Name $site.Name -Force -Confirm:$false
+ Start-VM -Name $site.Name
+ }
+ }
+
+ foreach ($VM in $VMNames) {
+ $VMStatus = Get-VMIntegrationService -VMName $VM -Name Heartbeat
+ while ($VMStatus.PrimaryStatusDescription -ne "OK") {
+ $VMStatus = Get-VMIntegrationService -VMName $VM -Name Heartbeat
+ write-host "[$(Get-Date -Format t)] INFO: Waiting for $VM to finish booting." -ForegroundColor Gray
+ Start-Sleep -Seconds 5
+ }
+ }
+
+ Write-Host "[$(Get-Date -Format t)] INFO: Fetching the latest two AKS Edge Essentials releases." -ForegroundColor Gray
+ $latestReleaseTag = (Invoke-WebRequest $websiteUrls["aksEEReleases"] | ConvertFrom-Json)[0].tag_name
+ $beforeLatestReleaseTag = (Invoke-WebRequest $websiteUrls["aksEEReleases"] | ConvertFrom-Json)[1].tag_name
+ $AKSEEReleasesTags = ($latestReleaseTag, $beforeLatestReleaseTag)
+ $AKSEESchemaVersions = @()
+
+ for ($i = 0; $i -lt $AKSEEReleasesTags.Count; $i++) {
+ $releaseTag = (Invoke-WebRequest $websiteUrls["aksEEReleases"] | ConvertFrom-Json)[$i].tag_name
+ $AKSEEReleaseDownloadUrl = "https://github.com/Azure/AKS-Edge/archive/refs/tags/$releaseTag.zip"
+ $output = Join-Path $AgToolsDir "$releaseTag.zip"
+ Invoke-WebRequest $AKSEEReleaseDownloadUrl -OutFile $output
+ Expand-Archive $output -DestinationPath $AgToolsDir -Force
+ $AKSEEReleaseConfigFilePath = "$AgToolsDir\AKS-Edge-$releaseTag\tools\aksedge-config.json"
+ $jsonContent = Get-Content -Raw -Path $AKSEEReleaseConfigFilePath | ConvertFrom-Json
+ $schemaVersion = $jsonContent.SchemaVersion
+ $AKSEESchemaVersions += $schemaVersion
+ # Clean up the downloaded release files
+ Remove-Item -Path $output -Force
+ Remove-Item -Path "$AgToolsDir\AKS-Edge-$releaseTag" -Force -Recurse
+ }
+
+ Invoke-Command -VMName $VMnames -Credential $Credentials -ScriptBlock {
+ $hostname = hostname
+ $ProgressPreference = "SilentlyContinue"
+ ###########################################
+ # Preparing environment folders structure
+ ###########################################
+ Write-Host "[$(Get-Date -Format t)] INFO: Preparing folder structure on $hostname." -ForegroundColor Gray
+ $deploymentFolder = "C:\Deployment" # Deployment folder is already pre-created in the VHD image
+ $logsFolder = "$deploymentFolder\Logs"
+ $kubeFolder = "$Env:USERPROFILE\.kube"
+
+ # Set up an array of folders
+ $folders = @($logsFolder, $kubeFolder)
+
+ # Loop through each folder and create it
+ foreach ($Folder in $folders) {
+ New-Item -ItemType Directory $Folder -Force
+ }
+ } | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\L1Infra.log")
+
+ Invoke-Command -VMName $VMnames -Credential $Credentials -ScriptBlock {
+ # Start logging
+ $hostname = hostname
+ $ProgressPreference = "SilentlyContinue"
+ $deploymentFolder = "C:\Deployment" # Deployment folder is already pre-created in the VHD image
+ $logsFolder = "$deploymentFolder\Logs"
+ Start-Transcript -Path $logsFolder\AKSEEBootstrap.log
+ $AgConfig = $using:AgConfig
+ $AgToolsDir = $using:AgToolsDir
+ $websiteUrls = $using:websiteUrls
+
+ ##########################################
+ # Deploying AKS Edge Essentials clusters
+ #########################################
+
+ # Assigning network adapter IP address
+ $NetIPAddress = $AgConfig.SiteConfig[$Env:COMPUTERNAME].NetIPAddress
+ $DefaultGateway = $AgConfig.SiteConfig[$Env:COMPUTERNAME].DefaultGateway
+ $PrefixLength = $AgConfig.SiteConfig[$Env:COMPUTERNAME].PrefixLength
+ $DNSClientServerAddress = $AgConfig.SiteConfig[$Env:COMPUTERNAME].DNSClientServerAddress
+ Write-Host "[$(Get-Date -Format t)] INFO: Configuring networking interface on $hostname with IP address $NetIPAddress." -ForegroundColor Gray
+ $AdapterName = (Get-NetAdapter -Name Ethernet*).Name
+ $ifIndex = (Get-NetAdapter -Name $AdapterName).ifIndex
+ New-NetIPAddress -IPAddress $NetIPAddress -DefaultGateway $DefaultGateway -PrefixLength $PrefixLength -InterfaceIndex $ifIndex | Out-Null
+ Set-DNSClientServerAddress -InterfaceIndex $ifIndex -ServerAddresses $DNSClientServerAddress | Out-Null
+
+ ###########################################
+ # Validating internet connectivity
+ ###########################################
+ $timeElapsed = 0
+ do {
+ Write-Host "[$(Get-Date -Format t)] INFO: Waiting for internet connection to be healthy on $hostname." -ForegroundColor Gray
+ Start-Sleep -Seconds 5
+ $timeElapsed = $timeElapsed + 10
+ } until ((Test-Connection bing.com -Count 1 -ErrorAction SilentlyContinue) -or ($timeElapsed -eq 60))
+
+ # Fetching latest AKS Edge Essentials msi file
+ Write-Host "[$(Get-Date -Format t)] INFO: Fetching latest AKS Edge Essentials install file on $hostname." -ForegroundColor Gray
+ Invoke-WebRequest $websiteUrls["aksEEk3s"] -OutFile $deploymentFolder\AKSEEK3s.msi
+
+ # Fetching required GitHub artifacts from Jumpstart repository
+ Write-Host "[$(Get-Date -Format t)] INFO: Fetching GitHub artifacts" -ForegroundColor Gray
+ $repoName = "azure_arc" # While testing, change to your GitHub fork's repository name
+ $githubApiUrl = "https://api.github.com/repos/$using:githubAccount/$repoName/contents/azure_jumpstart_ag/artifacts/L1Files?ref=$using:githubBranch"
+ $response = Invoke-RestMethod -Uri $githubApiUrl
+ $fileUrls = $response | Where-Object { $_.type -eq "file" } | Select-Object -ExpandProperty download_url
+ $fileUrls | ForEach-Object {
+ $fileName = $_.Substring($_.LastIndexOf("/") + 1)
+ $outputFile = Join-Path $deploymentFolder $fileName
+ Invoke-RestMethod -Uri $_ -OutFile $outputFile
+ }
+
+ ###############################################################################
+ # Setting up replacement parameters for AKS Edge Essentials config json file
+ ###############################################################################
+ Write-Host "[$(Get-Date -Format t)] INFO: Building AKS Edge Essentials config json file on $hostname." -ForegroundColor Gray
+ $AKSEEConfigFilePath = "$deploymentFolder\ScalableCluster.json"
+ $AdapterName = (Get-NetAdapter -Name Ethernet*).Name
+ $namingGuid = $using:namingGuid
+ $arcClusterName = $AgConfig.SiteConfig[$Env:COMPUTERNAME].ArcClusterName + "-$namingGuid"
+
+ # Fetch schemaVersion release from the AgConfig file
+ $AKSEESchemaVersionUseLatest = $AgConfig.SiteConfig[$Env:COMPUTERNAME].AKSEEReleaseUseLatest
+ if ($AKSEESchemaVersionUseLatest) {
+ $SchemaVersion = $using:AKSEESchemaVersions[0]
+ }
+ else {
+ $SchemaVersion = $using:AKSEESchemaVersions[1]
+ }
+
+ $replacementParams = @{
+ "SchemaVersion-null" = $SchemaVersion
+ "ServiceIPRangeStart-null" = $AgConfig.SiteConfig[$Env:COMPUTERNAME].ServiceIPRangeStart
+ "1000" = $AgConfig.SiteConfig[$Env:COMPUTERNAME].ServiceIPRangeSize
+ "ControlPlaneEndpointIp-null" = $AgConfig.SiteConfig[$Env:COMPUTERNAME].ControlPlaneEndpointIp
+ "Ip4GatewayAddress-null" = $AgConfig.SiteConfig[$Env:COMPUTERNAME].DefaultGateway
+ "2000" = $AgConfig.SiteConfig[$Env:COMPUTERNAME].PrefixLength
+ "DnsServer-null" = $AgConfig.SiteConfig[$Env:COMPUTERNAME].DNSClientServerAddress
+ "Ethernet-Null" = $AdapterName
+ "Ip4Address-null" = $AgConfig.SiteConfig[$Env:COMPUTERNAME].LinuxNodeIp4Address
+ "ClusterName-null" = $arcClusterName
+ "Location-null" = $using:azureLocation
+ "ResourceGroupName-null" = $using:resourceGroup
+ "SubscriptionId-null" = $using:subscriptionId
+ "TenantId-null" = $using:spnTenantId
+ "ClientId-null" = $using:spnClientId
+ "ClientSecret-null" = $using:spnClientSecret
+ }
+
+ ###################################################
+ # Preparing AKS Edge Essentials config json file
+ ###################################################
+ $content = Get-Content $AKSEEConfigFilePath
+ foreach ($key in $replacementParams.Keys) {
+ $content = $content -replace $key, $replacementParams[$key]
+ }
+ Set-Content "$deploymentFolder\Config.json" -Value $content
+ }
+ Write-Host "[$(Get-Date -Format t)] INFO: Initial L1 virtualization infrastructure configuration complete." -ForegroundColor Green
+ Write-Host
+
+ Write-Host "[$(Get-Date -Format t)] INFO: Installing AKS Edge Essentials (Step 7/17)" -ForegroundColor DarkGreen
+ foreach ($VMName in $VMNames) {
+ $Session = New-PSSession -VMName $VMName -Credential $Credentials
+ Write-Host "[$(Get-Date -Format t)] INFO: Rebooting $VMName." -ForegroundColor Gray
+ Invoke-Command -Session $Session -ScriptBlock {
+ $Action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-ExecutionPolicy Bypass -File C:\Deployment\AKSEEBootstrap.ps1"
+ $Trigger = New-ScheduledTaskTrigger -AtStartup
+ Register-ScheduledTask -TaskName "Startup Scan" -Action $Action -Trigger $Trigger -User $Env:USERNAME -Password 'Agora123!!' -RunLevel Highest | Out-Null
+ Restart-Computer -Force -Confirm:$false
+ } | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\L1AKSInfra.log")
+ Remove-PSSession $Session | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\L1AKSInfra.log")
+ }
+
+ Write-Host "[$(Get-Date -Format t)] INFO: Sleeping for three (3) minutes to allow for AKS EE installs to complete." -ForegroundColor Gray
+ Start-Sleep -Seconds 180 # Give some time for the AKS EE installs to complete. This will take a few minutes.
+
+ #####################################################################
+ # Monitor until the kubeconfig files are detected and copied over
+ #####################################################################
+ $elapsedTime = Measure-Command {
+ foreach ($VMName in $VMNames) {
+ $path = "C:\Users\Administrator\.kube\config-" + $VMName.ToLower()
+ $user = $AgConfig.L1Username
+ [securestring]$secStringPassword = ConvertTo-SecureString $AgConfig.L1Password -AsPlainText -Force
+ $credential = New-Object System.Management.Automation.PSCredential($user, $secStringPassword)
+ Start-Sleep 5
+ while (!(Invoke-Command -VMName $VMName -Credential $credential -ScriptBlock { Test-Path $using:path })) {
+ Start-Sleep 30
+ Write-Host "[$(Get-Date -Format t)] INFO: Waiting for AKS Edge Essentials kubeconfig to be available on $VMName." -ForegroundColor Gray
+ }
+
+ Write-Host "[$(Get-Date -Format t)] INFO: $VMName's kubeconfig is ready - copying over config-$VMName" -ForegroundColor DarkGreen
+ $destinationPath = $Env:USERPROFILE + "\.kube\config-" + $VMName
+ $s = New-PSSession -VMName $VMName -Credential $credential
+ Copy-Item -FromSession $s -Path $path -Destination $destinationPath
+ $file = Get-Item $destinationPath
+ if ($file.Length -eq 0) {
+ Write-Host "[$(Get-Date -Format t)] ERROR: Kubeconfig on $VMName is corrupt. This error is unrecoverable. Exiting." -ForegroundColor White -BackgroundColor Red
+ exit 1
+ }
+ }
+ }
+
+ # Display the elapsed time in seconds it took for kubeconfig files to show up in folder
+ Write-Host "[$(Get-Date -Format t)] INFO: Waiting on kubeconfig files took $($elapsedTime.ToString("g"))." -ForegroundColor Gray
+
+ #####################################################################
+ # Merging kubeconfig files on the L0 virtual machine
+ #####################################################################
+ Write-Host "[$(Get-Date -Format t)] INFO: All three kubeconfig files are present. Merging kubeconfig files for use with kubectx." -ForegroundColor Gray
+ $kubeconfigpath = ""
+ foreach ($VMName in $VMNames) {
+ $kubeconfigpath = $kubeconfigpath + "$Env:USERPROFILE\.kube\config-" + $VMName.ToLower() + ";"
+ }
+ $Env:KUBECONFIG = $kubeconfigpath
+ kubectl config view --merge --flatten > "$Env:USERPROFILE\.kube\config-raw" | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\L1AKSInfra.log")
+ kubectl config get-clusters --kubeconfig="$Env:USERPROFILE\.kube\config-raw" | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\L1AKSInfra.log")
+ Rename-Item -Path "$Env:USERPROFILE\.kube\config-raw" -NewName "$Env:USERPROFILE\.kube\config"
+ $Env:KUBECONFIG = "$Env:USERPROFILE\.kube\config"
+
+ # Print a message indicating that the merge is complete
+ Write-Host "[$(Get-Date -Format t)] INFO: All three kubeconfig files merged successfully." -ForegroundColor Gray
+
+ # Validate context switching using kubectx & kubectl
+ foreach ($cluster in $VMNames) {
+ Write-Host "[$(Get-Date -Format t)] INFO: Testing connectivity to kube api on $cluster cluster." -ForegroundColor Gray
+ kubectx $cluster.ToLower()
+ kubectl get nodes -o wide
+ }
+ Write-Host "[$(Get-Date -Format t)] INFO: AKS Edge Essentials installs are complete!" -ForegroundColor Green
+ Write-Host
+}
+
+function Deploy-AzContainerRegistry {
+ az aks get-credentials --resource-group $Env:resourceGroup --name $Env:aksStagingClusterName --admin | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\ClusterSecrets.log")
+ kubectx staging="$Env:aksStagingClusterName-admin" | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\ClusterSecrets.log")
+
+ # Attach ACR to staging cluster
+ Write-Host "[$(Get-Date -Format t)] INFO: Attaching Azure Container Registry to AKS staging cluster." -ForegroundColor Gray
+ az aks update -n $Env:aksStagingClusterName -g $Env:resourceGroup --attach-acr $acrName | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\ClusterSecrets.log")
+}
+
+function Deploy-ClusterNamespaces {
+ foreach ($cluster in $AgConfig.SiteConfig.GetEnumerator()) {
+ $clusterName = $cluster.Name.ToLower()
+ kubectx $clusterName | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\ClusterSecrets.log")
+ foreach ($namespace in $AgConfig.Namespaces) {
+ Write-Host "[$(Get-Date -Format t)] INFO: Creating namespace $namespace on $clusterName" -ForegroundColor Gray
+ kubectl create namespace $namespace | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\ClusterSecrets.log")
+ }
+ }
+}
+
+function Deploy-ClusterSecrets {
+ foreach ($cluster in $AgConfig.SiteConfig.GetEnumerator()) {
+ $clusterName = $cluster.Name.ToLower()
+ foreach ($namespace in $AgConfig.Namespaces) {
+ if ($namespace -eq "contoso-supermarket" -or $namespace -eq "images-cache") {
+ Write-Host "[$(Get-Date -Format t)] INFO: Configuring Azure Container registry on $clusterName"
+ kubectx $clusterName | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\ClusterSecrets.log")
+ kubectl create secret docker-registry acr-secret `
+ --namespace $namespace `
+ --docker-server="$acrName.azurecr.io" `
+ --docker-username="$Env:spnClientId" `
+ --docker-password="$Env:spnClientSecret" | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\ClusterSecrets.log")
+ }
+ }
+ }
+
+ #####################################################################
+ # Create secrets for GitHub actions
+ #####################################################################
+ if ($Env:industry -eq "retail") {
+ Write-Host "[$(Get-Date -Format t)] INFO: Creating Kubernetes secrets" -ForegroundColor Gray
+ $cosmosDBKey = $(az cosmosdb keys list --name $cosmosDBName --resource-group $resourceGroup --query primaryMasterKey --output tsv)
+ foreach ($cluster in $AgConfig.SiteConfig.GetEnumerator()) {
+ $clusterName = $cluster.Name.ToLower()
+ Write-Host "[$(Get-Date -Format t)] INFO: Creating Kubernetes secrets on $clusterName" -ForegroundColor Gray
+ foreach ($namespace in $AgConfig.Namespaces) {
+ if ($namespace -eq "contoso-supermarket" -or $namespace -eq "images-cache") {
+ kubectx $cluster.Name.ToLower() | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\ClusterSecrets.log")
+ kubectl create secret generic postgrespw --from-literal=POSTGRES_PASSWORD='Agora123!!' --namespace $namespace | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\ClusterSecrets.log")
+ kubectl create secret generic cosmoskey --from-literal=COSMOS_KEY=$cosmosDBKey --namespace $namespace | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\ClusterSecrets.log")
+ kubectl create secret generic github-token --from-literal=token=$githubPat --namespace $namespace | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\ClusterSecrets.log")
+ }
+ }
+ }
+ Write-Host "[$(Get-Date -Format t)] INFO: Cluster secrets configuration complete." -ForegroundColor Green
+ Write-Host
+ }
+}
+
+function Deploy-AzArcK8s {
+ # Running pre-checks to ensure that the aksedge ConfigMap is present on all clusters
+ $maxRetries = 5
+ $retryInterval = 30 # seconds
+ $retryCount = 0
+ foreach ($cluster in $AgConfig.SiteConfig.GetEnumerator()) {
+ $clusterName = $cluster.Name.ToLower()
+ if ($clusterName -ne "staging") {
+ while ($retryCount -lt $maxRetries) {
+ kubectx $clusterName
+ $configMap = kubectl get configmap -n aksedge aksedge
+ if ($null -eq $configMap) {
+ $retryCount++
+ Write-Host "Retry ${retryCount}/${maxRetries}: aksedge ConfigMap not found on $clusterName. Retrying in $retryInterval seconds..." | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\ArcConnectivity.log")
+ Start-Sleep -Seconds $retryInterval
+ }
+ else {
+ # ConfigMap found, continue with the rest of the script
+ Write-Host "aksedge ConfigMap found on $clusterName. Continuing with the script..." | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\ArcConnectivity.log")
+ break # Exit the loop
+ }
+ }
+
+ if ($retryCount -eq $maxRetries) {
+ Write-Host "[$(Get-Date -Format t)] ERROR: aksedge ConfigMap not found on $clusterName. Exiting..." -ForegroundColor White -BackgroundColor Red | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\ArcConnectivity.log")
+ exit 1 # Exit the script
+ }
+ }
+ }
+ $VMnames = (Get-VM).Name
+ foreach ($VM in $VMNames) {
+ $secret = $Env:spnClientSecret
+ $clientId = $Env:spnClientId
+ $tenantId = $Env:spnTenantId
+ $location = $Env:azureLocation
+ $resourceGroup = $Env:resourceGroup
+
+ Invoke-Command -VMName $VM -Credential $Credentials -ScriptBlock {
+ # Install prerequisites
+ . C:\Deployment\Profile.ps1
+ $hostname = hostname
+ $ProgressPreference = "SilentlyContinue"
+ Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force
+ Install-Module Az.Resources -Repository PSGallery -Force -AllowClobber -ErrorAction Stop
+ Install-Module Az.Accounts -Repository PSGallery -Force -AllowClobber -ErrorAction Stop
+ Install-Module Az.ConnectedKubernetes -Repository PSGallery -Force -AllowClobber -ErrorAction Stop
+ Install-Module Az.ConnectedMachine -Force -AllowClobber -ErrorAction Stop
+
+ # Connect servers to Arc
+ $azurePassword = ConvertTo-SecureString $using:secret -AsPlainText -Force
+ $psCred = New-Object System.Management.Automation.PSCredential($using:clientId, $azurePassword)
+ Connect-AzAccount -Credential $psCred -TenantId $using:tenantId -ServicePrincipal -Subscription $using:subscriptionId
+ Write-Host "[$(Get-Date -Format t)] INFO: Arc-enabling $hostname server." -ForegroundColor Gray
+ Redo-Command -ScriptBlock { Connect-AzConnectedMachine -ResourceGroupName $using:resourceGroup -Name "Ag-$hostname-Host" -Location $using:location }
+
+ # Connect clusters to Arc
+ $deploymentPath = "C:\Deployment\config.json"
+ Write-Host "[$(Get-Date -Format t)] INFO: Arc-enabling $hostname AKS Edge Essentials cluster." -ForegroundColor Gray
+
+ kubectl get svc
+
+ $retryCount = 5 # Number of times to retry the operation
+ $retryDelay = 30 # Delay in seconds between retries
+
+ for ($retry = 1; $retry -le $retryCount; $retry++) {
+ $return = Connect-AksEdgeArc -JsonConfigFilePath $deploymentPath
+ if ($return -ne "OK") {
+ Write-Output "Failed to onboard AKS Edge Essentials cluster to Azure Arc. Retrying (Attempt $retry of $retryCount)..."
+ if ($retry -lt $retryCount) {
+ Start-Sleep -Seconds $retryDelay # Wait before retrying
+ }
+ else {
+ Write-Output "Exceeded maximum retry attempts. Exiting."
+ break # Exit the loop after the maximum number of retries
+ }
+ }
+ else {
+ Write-Output "Successfully onboarded AKS Edge Essentials cluster to Azure Arc."
+ break # Exit the loop if the connection is successful
+ }
+ }
+
+
+ } 2>&1 | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\ArcConnectivity.log")
+ }
+
+ #####################################################################
+ # Tag Azure Arc resources
+ #####################################################################
+ $arcResourceTypes = $AgConfig.ArcServerResourceType, $AgConfig.ArcK8sResourceType
+ $Tag = @{$AgConfig.TagName = $AgConfig.TagValue }
+
+ # Iterate over the Arc resources and tag it
+ foreach ($arcResourceType in $arcResourceTypes) {
+ $arcResources = Get-AzResource -ResourceType $arcResourceType -ResourceGroupName $Env:resourceGroup
+ foreach ($arcResource in $arcResources) {
+ Update-AzTag -ResourceId $arcResource.Id -Tag $Tag -Operation Merge | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\ArcConnectivity.log")
+ }
+ }
+
+ Write-Host "[$(Get-Date -Format t)] INFO: AKS Edge Essentials clusters and hosts have been registered with Azure Arc!" -ForegroundColor Green
+ Write-Host
+
+}
+
+function Deploy-ClusterFluxExtension {
+ $resourceTypes = @($AgConfig.ArcK8sResourceType, $AgConfig.AksResourceType)
+ $resources = Get-AzResource -ResourceGroupName $Env:resourceGroup | Where-Object { $_.ResourceType -in $resourceTypes }
+
+ $jobs = @()
+ foreach ($resource in $resources) {
+ $resourceName = $resource.Name
+ $resourceType = $resource.Type
+
+ Write-Host "[$(Get-Date -Format t)] INFO: Installing flux extension on $resourceName" -ForegroundColor Gray
+ $job = Start-Job -Name $resourceName -ScriptBlock {
+ param($resourceName, $resourceType)
+
+ $retryCount = 10
+ $retryDelaySeconds = 60
+
+ switch ($resourceType) {
+ 'Microsoft.Kubernetes/connectedClusters' { $ClusterType = 'ConnectedClusters' }
+ 'Microsoft.ContainerService/managedClusters' { $ClusterType = 'ManagedClusters' }
+ }
+ if ($clusterType -eq 'ConnectedClusters') {
+ # Check if cluster is connected to Azure Arc control plane
+ $ConnectivityStatus = (Get-AzConnectedKubernetes -ResourceGroupName $Env:resourceGroup -ClusterName $resourceName).ConnectivityStatus
+ if (-not ($ConnectivityStatus -eq 'Connected')) {
+ for ($attempt = 1; $attempt -le $retryCount; $attempt++) {
+ $ConnectivityStatus = (Get-AzConnectedKubernetes -ResourceGroupName $Env:resourceGroup -ClusterName $resourceName).ConnectivityStatus
+
+ # Check the condition
+ if ($ConnectivityStatus -eq 'Connected') {
+ # Condition is true, break out of the loop
+ break
+ }
+
+ # Wait for a specific duration before re-evaluating the condition
+ Start-Sleep -Seconds $retryDelaySeconds
+
+
+ if ($attempt -lt $retryCount) {
+ Write-Host "Retrying in $retryDelaySeconds seconds..."
+ Start-Sleep -Seconds $retryDelaySeconds
+ }
+ else {
+ $ProvisioningState = "Timed out after $($retryDelaySeconds * $retryCount) seconds while waiting for cluster to become connected to Azure Arc control plane. Current status: $ConnectivityStatus"
+ break # Max retry attempts reached, exit the loop
+ }
+
+ }
+ }
+ }
+
+ az login --service-principal --username $Env:spnClientID --password=$Env:spnClientSecret --tenant $Env:spnTenantId
+ $extension = az k8s-extension list --cluster-name $resourceName --resource-group $Env:resourceGroup --cluster-type $ClusterType --output json | ConvertFrom-Json
+ $extension = $extension | Where-Object extensionType -eq 'microsoft.flux'
+
+ if ($extension.ProvisioningState -ne 'Succeeded' -and ($ConnectivityStatus -eq 'Connected' -or $clusterType -eq "ManagedClusters")) {
+ for ($attempt = 1; $attempt -le $retryCount; $attempt++) {
+ try {
+ if ($extension) {
+ az k8s-extension delete --name "flux" --cluster-name $resourceName --resource-group $Env:resourceGroup --cluster-type $ClusterType --force --yes
+ }
+ az k8s-extension create --name "flux" --extension-type "microsoft.flux" --cluster-name $resourceName --resource-group $Env:resourceGroup --cluster-type $ClusterType --output json | ConvertFrom-Json -OutVariable extension
+ break # Command succeeded, exit the loop
+ }
+ catch {
+ Write-Warning "An error occurred: $($_.Exception.Message)"
+
+ if ($attempt -lt $retryCount) {
+ Write-Host "Retrying in $retryDelaySeconds seconds..."
+ Start-Sleep -Seconds $retryDelaySeconds
+ }
+ else {
+ Write-Error "Failed to execute the command after $retryCount attempts."
+ $ProvisioningState = $($_.Exception.Message)
+ break # Max retry attempts reached, exit the loop
+ }
+ }
+ }
+ }
+ $ProvisioningState = $extension.ProvisioningState
+ [PSCustomObject]@{
+ ResourceName = $resourceName
+ ResourceType = $resourceType
+ ProvisioningState = $ProvisioningState
+ }
+ } -ArgumentList $resourceName, $resourceType
+ $jobs += $job
+ }
+
+ # Wait for all jobs to complete
+ $FluxExtensionJobs = $jobs | Wait-Job | Receive-Job -Keep
+ $jobs | Format-Table Name, PSBeginTime, PSEndTime -AutoSize
+
+ # Clean up jobs
+ $jobs | Remove-Job
+ # Abort if Flux-extension fails on any cluster
+ # if ($FluxExtensionJobs | Where-Object ProvisioningState -ne 'Succeeded') {
+ # throw "One or more Flux-extension deployments failed - aborting"
+ # }
+}
+
+function Deploy-Workbook ($workbookFileName) {
+ $AgMonitoringDir = $AgConfig.AgDirectories["AgMonitoringDir"]
+ Write-Host "[$(Get-Date -Format t)] INFO: Deploying Azure Workbook $workbookFileName."
+ Write-Host "`n"
+ $workbookTemplateFilePath = "$AgMonitoringDir\$workbookFileName"
+ # Read the content of the workbook template-file
+ $content = Get-Content -Path $workbookTemplateFilePath -Raw
+ # Replace placeholders with actual values
+ $updatedContent = $content -replace 'rg-placeholder', $resourceGroup
+ $updatedContent = $updatedContent -replace'/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/xxxx/providers/Microsoft.OperationalInsights/workspaces/xxxx', "/subscriptions/$($subscriptionId)/resourceGroups/$($Env:resourceGroup)/providers/Microsoft.OperationalInsights/workspaces/$($Env:workspaceName)"
+ $updatedContent = $updatedContent -replace'/subscriptions/00000000-0000-0000-0000-000000000000', "/subscriptions/$($subscriptionId)"
+
+ # Write the updated content back to the file
+ Set-Content -Path $workbookTemplateFilePath -Value $updatedContent
+ # Deploy the workbook
+ try {
+ New-AzResourceGroupDeployment -ResourceGroupName $Env:resourceGroup -TemplateFile $workbookTemplateFilePath -ErrorAction Stop
+ Write-Host "[$(Get-Date -Format t)] INFO: Deployment of template-file $workbookTemplateFilePath succeeded."
+ } catch {
+ Write-Error "[$(Get-Date -Format t)] ERROR: Deployment of template-file $workbookTemplateFilePath failed. Error details: $PSItem.Exception.Message"
+ }
+}
+
+function Deploy-Prometheus {
+ param (
+ $AgConfig
+ )
+ $AgMonitoringDir = $AgConfig.AgDirectories["AgMonitoringDir"]
+ $observabilityNamespace = $AgConfig.Monitoring["Namespace"]
+ $observabilityDashboards = $AgConfig.Monitoring["Dashboards"]
+ $adminPassword = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($env:adminPassword))
+
+ # Set Prod Grafana API endpoint
+ $grafanaDS = $AgConfig.Monitoring["ProdURL"] + "/api/datasources"
+
+ # Installing Grafana
+ Write-Host "[$(Get-Date -Format t)] INFO: Installing and Configuring Observability components (Step 14/17)" -ForegroundColor DarkGreen
+ Write-Host "[$(Get-Date -Format t)] INFO: Installing Grafana." -ForegroundColor Gray
+ $latestRelease = (Invoke-WebRequest -Uri $websiteUrls["grafana"] | ConvertFrom-Json).tag_name.replace('v', '')
+ Start-Process msiexec.exe -Wait -ArgumentList "/I $AgToolsDir\grafana-$latestRelease.windows-amd64.msi /quiet" | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Observability.log")
+
+ # Update Prometheus Helm charts
+ helm repo add prometheus-community $websiteUrls["prometheus"] | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Observability.log")
+ helm repo update | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Observability.log")
+
+ if ($Env:industry -eq "retail") {
+ # Update Grafana Icons
+ Copy-Item -Path $AgIconsDir\contoso.png -Destination "C:\Program Files\GrafanaLabs\grafana\public\img"
+ Copy-Item -Path $AgIconsDir\contoso.svg -Destination "C:\Program Files\GrafanaLabs\grafana\public\img\grafana_icon.svg"
+
+ Get-ChildItem -Path 'C:\Program Files\GrafanaLabs\grafana\public\build\*.js' -Recurse -File | ForEach-Object {
+ (Get-Content $_.FullName) -replace 'className:u,src:"public/img/grafana_icon.svg"', 'className:u,src:"public/img/contoso.png"' | Set-Content $_.FullName
+ }
+
+ # Reset Grafana UI
+ Get-ChildItem -Path 'C:\Program Files\GrafanaLabs\grafana\public\build\*.js' -Recurse -File | ForEach-Object {
+ (Get-Content $_.FullName) -replace 'Welcome to Grafana', 'Welcome to Grafana for Contoso Supermarket Production' | Set-Content $_.FullName
+ }
+ }
+ elseif ($Env:industry -eq "manufacturing") {
+ # Update Grafana Icons
+ Copy-Item -Path $AgIconsDir\contoso-motors.png -Destination "C:\Program Files\GrafanaLabs\grafana\public\img"
+ Copy-Item -Path $AgIconsDir\contoso-motors.svg -Destination "C:\Program Files\GrafanaLabs\grafana\public\img\grafana_icon.svg"
+
+ Get-ChildItem -Path 'C:\Program Files\GrafanaLabs\grafana\public\build\*.js' -Recurse -File | ForEach-Object {
+ (Get-Content $_.FullName) -replace 'className:u,src:"public/img/grafana_icon.svg"', 'className:u,src:"public/img/contoso-motors.png"' | Set-Content $_.FullName
+ }
+
+ # Reset Grafana UI
+ Get-ChildItem -Path 'C:\Program Files\GrafanaLabs\grafana\public\build\*.js' -Recurse -File | ForEach-Object {
+ (Get-Content $_.FullName) -replace 'Welcome to Grafana', 'Welcome to Grafana for Contoso Motors' | Set-Content $_.FullName
+ }
+ }
+
+ # Reset Grafana Password
+ $Env:Path += ';C:\Program Files\GrafanaLabs\grafana\bin'
+ $retryCount = 5
+ $retryDelay = 30
+ do {
+ try {
+ grafana-cli --homepath "C:\Program Files\GrafanaLabs\grafana" admin reset-admin-password $adminPassword | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Observability.log")
+ $retryCount = 0
+ }
+ catch {
+ $retryCount--
+ if ($retryCount -gt 0) {
+ Write-Host "[$(Get-Date -Format t)] INFO: Retrying in $retryDelay seconds..." -ForegroundColor Gray
+ Start-Sleep -Seconds $retryDelay
+ }
+ }
+ } while ($retryCount -gt 0)
+
+ # Get Grafana Admin credentials
+ $adminCredentials = $AgConfig.Monitoring["AdminUser"] + ':' + $adminPassword
+ $adminEncodedcredentials = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($adminCredentials))
+
+ $adminHeaders = @{
+ "Authorization" = ("Basic " + $adminEncodedcredentials)
+ "Content-Type" = "application/json"
+ }
+
+ # Get Contoso User credentials
+ $userCredentials = $adminUsername + ':' + $adminPassword
+ $userEncodedcredentials = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($userCredentials))
+
+ $userHeaders = @{
+ "Authorization" = ("Basic " + $userEncodedcredentials)
+ "Content-Type" = "application/json"
+ }
+
+ # Download dashboards
+ foreach ($dashboard in $observabilityDashboards.'grafana.com') {
+ $grafanaDBPath = "$AgMonitoringDir\grafana-$dashboard.json"
+ $dashboardmetadata = Invoke-RestMethod -Uri https://grafana.com/api/dashboards/$dashboard/revisions
+ $dashboardversion = $dashboardmetadata.items | Sort-Object revision | Select-Object -Last 1 | Select-Object -ExpandProperty revision
+ Invoke-WebRequest https://grafana.com/api/dashboards/$dashboard/revisions/$dashboardversion/download -OutFile $grafanaDBPath
+ }
+
+ $observabilityDashboardstoImport = @()
+ $observabilityDashboardstoImport += $observabilityDashboards.'grafana.com'
+ $observabilityDashboardstoImport += $observabilityDashboards.'custom'
+
+ Write-Host "[$(Get-Date -Format t)] INFO: Creating Prod Grafana User" -ForegroundColor Gray
+ # Add Contoso Operator User
+ $grafanaUserBody = @{
+ name = $AgConfig.Monitoring["User"] # Display Name
+ email = $AgConfig.Monitoring["Email"]
+ login = $adminUsername
+ password = $adminPassword
+ } | ConvertTo-Json
+
+ # Make HTTP request to the API to create user
+ $retryCount = 10
+ $retryDelay = 30
+ do {
+ try {
+ Invoke-RestMethod -Method Post -Uri "$($AgConfig.Monitoring["ProdURL"])/api/admin/users" -Headers $adminHeaders -Body $grafanaUserBody | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Observability.log")
+ $retryCount = 0
+ }
+ catch {
+ $retryCount--
+ if ($retryCount -gt 0) {
+ Write-Host "[$(Get-Date -Format t)] INFO: Retrying in $retryDelay seconds..." -ForegroundColor Gray
+ Start-Sleep -Seconds $retryDelay
+ }
+ }
+ } while ($retryCount -gt 0)
+
+ # Deploying Kube Prometheus Stack for stores
+ $AgConfig.SiteConfig.GetEnumerator() | ForEach-Object {
+ Write-Host "[$(Get-Date -Format t)] INFO: Deploying Kube Prometheus Stack for $($_.Value.FriendlyName) environment" -ForegroundColor Gray
+ kubectx $_.Value.FriendlyName.ToLower() | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Observability.log")
+
+ # Wait for Kubernetes API server to become available
+ $apiServer = kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}'
+ $apiServerAddress = $apiServer -replace '.*https://| .*$'
+ $apiServerFqdn = ($apiServerAddress -split ":")[0]
+ $apiServerPort = ($apiServerAddress -split ":")[1]
+
+ do {
+ $result = Test-NetConnection -ComputerName $apiServerFqdn -Port $apiServerPort -WarningAction SilentlyContinue
+ if ($result.TcpTestSucceeded) {
+ Write-Host "[$(Get-Date -Format t)] INFO: Kubernetes API server $apiServer is available" -ForegroundColor Gray
+ break
+ }
+ else {
+ Write-Host "[$(Get-Date -Format t)] INFO: Kubernetes API server $apiServer is not yet available. Retrying in 10 seconds..." -ForegroundColor Gray
+ Start-Sleep -Seconds 10
+ }
+ } while ($true)
+
+ # Install Prometheus Operator
+ $helmSetValue = $_.Value.HelmSetValue -replace 'adminPasswordPlaceholder', $adminPassword
+ helm install prometheus prometheus-community/kube-prometheus-stack --set $helmSetValue --namespace $observabilityNamespace --create-namespace --values "$AgMonitoringDir\$($_.Value.HelmValuesFile)" | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Observability.log")
+
+ Do {
+ Write-Host "[$(Get-Date -Format t)] INFO: Waiting for $($_.Value.FriendlyName) monitoring service to provision.." -ForegroundColor Gray
+ Start-Sleep -Seconds 45
+ $monitorIP = $(if (kubectl get $_.Value.HelmService --namespace $observabilityNamespace --output=jsonpath='{.status.loadBalancer}' | Select-String "ingress" -Quiet) { "Ready!" }Else { "Nope" })
+ } while ($monitorIP -eq "Nope" )
+ # Get Load Balancer IP
+ $monitorLBIP = kubectl --namespace $observabilityNamespace get $_.Value.HelmService --output=jsonpath='{.status.loadBalancer.ingress[0].ip}'
+
+ if ($_.Value.IsProduction) {
+ Write-Host "[$(Get-Date -Format t)] INFO: Add $($_.Value.FriendlyName) Data Source to Grafana"
+ # Request body with information about the data source to add
+ $grafanaDSBody = @{
+ name = $_.Value.FriendlyName.ToLower()
+ type = 'prometheus'
+ url = ("http://" + $monitorLBIP + ":9090")
+ access = 'proxy'
+ basicAuth = $false
+ isDefault = $true
+ } | ConvertTo-Json
+
+ # Make HTTP request to the API
+ Invoke-RestMethod -Method Post -Uri $grafanaDS -Headers $adminHeaders -Body $grafanaDSBody | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Observability.log")
+ }
+
+ # Add Contoso Operator User
+ if (!$_.Value.IsProduction) {
+ Write-Host "[$(Get-Date -Format t)] INFO: Creating $($_.Value.FriendlyName) Grafana User" -ForegroundColor Gray
+ $grafanaUserBody = @{
+ name = $AgConfig.Monitoring["User"] # Display Name
+ email = $AgConfig.Monitoring["Email"]
+ login = $adminUsername
+ password = $adminPassword
+ } | ConvertTo-Json
+
+ # Make HTTP request to the API to create user
+ $retryCount = 10
+ $retryDelay = 30
+
+ do {
+ try {
+ Invoke-RestMethod -Method Post -Uri "http://$monitorLBIP/api/admin/users" -Headers $adminHeaders -Body $grafanaUserBody | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Observability.log")
+ $retryCount = 0
+ }
+ catch {
+ $retryCount--
+ if ($retryCount -gt 0) {
+ Write-Host "[$(Get-Date -Format t)] INFO: Retrying in $retryDelay seconds..." -ForegroundColor Gray
+ Start-Sleep -Seconds $retryDelay
+ }
+ }
+ } while ($retryCount -gt 0)
+ }
+
+ Write-Host "[$(Get-Date -Format t)] INFO: Importing dashboards for $($_.Value.FriendlyName) environment" -ForegroundColor Gray
+ # Add dashboards
+ foreach ($dashboard in $observabilityDashboardstoImport) {
+ $grafanaDBPath = "$AgMonitoringDir\grafana-$dashboard.json"
+ # Replace the datasource
+ $replacementParams = @{
+ "\$\{DS_PROMETHEUS}" = $_.Value.GrafanaDataSource
+ }
+ $content = Get-Content $grafanaDBPath
+ foreach ($key in $replacementParams.Keys) {
+ $content = $content -replace $key, $replacementParams[$key]
+ }
+ # Set dashboard JSON
+ $dashboardObject = $content | ConvertFrom-Json
+ # Best practice is to generate a random UID, such as a GUID
+ $dashboardObject.uid = [guid]::NewGuid().ToString()
+
+ # Need to set this to null to let Grafana generate a new ID
+ $dashboardObject.id = $null
+ # Set dashboard title
+ $dashboardObject.title = $_.Value.FriendlyName + ' - ' + $dashboardObject.title
+ # Request body with dashboard to add
+ $grafanaDBBody = @{
+ dashboard = $dashboardObject
+ overwrite = $true
+ } | ConvertTo-Json -Depth 8
+
+ if ($_.Value.IsProduction) {
+ # Set Grafana Dashboard endpoint
+ $grafanaDBURI = $AgConfig.Monitoring["ProdURL"] + "/api/dashboards/db"
+ $grafanaDBStarURI = $AgConfig.Monitoring["ProdURL"] + "/api/user/stars/dashboard"
+ }
+ else {
+ # Set Grafana Dashboard endpoint
+ $grafanaDBURI = "http://$monitorLBIP/api/dashboards/db"
+ $grafanaDBStarURI = "http://$monitorLBIP/api/user/stars/dashboard"
+ }
+
+ # Make HTTP request to the API
+ $dashboardID = (Invoke-RestMethod -Method Post -Uri $grafanaDBURI -Headers $adminHeaders -Body $grafanaDBBody).id
+
+ Invoke-RestMethod -Method Post -Uri "$grafanaDBStarURI/$dashboardID" -Headers $userHeaders | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Observability.log")
+
+ }
+
+ }
+ Write-Host
+}
diff --git a/azure_jumpstart_ag/artifacts/PowerShell/Modules/manufacturing.psm1 b/azure_jumpstart_ag/artifacts/PowerShell/Modules/manufacturing.psm1
new file mode 100644
index 0000000000..01f79802a9
--- /dev/null
+++ b/azure_jumpstart_ag/artifacts/PowerShell/Modules/manufacturing.psm1
@@ -0,0 +1,568 @@
+function Deploy-ManufacturingConfigs {
+ Write-Host "[$(Get-Date -Format t)] INFO: Configuring OVMS prerequisites on Kubernetes nodes." -ForegroundColor Gray
+ $VMs = (Get-VM).Name
+ foreach ($VM in $VMs) {
+ Invoke-Command -VMName $VM -Credential $Credentials -ScriptBlock {
+ Invoke-AksEdgeNodeCommand -NodeType Linux -command "curl -sL https://github.com/operator-framework/operator-lifecycle-manager/releases/download/v0.27.0/install.sh | bash -s v0.27.0"
+ }
+ kubectx $VM.ToLower()
+ kubectl create -f https://operatorhub.io/install/ovms-operator.yaml
+ }
+
+ # Loop through the clusters and deploy the configs in AppConfig hashtable in AgConfig-manufacturing.psd1
+ foreach ($cluster in $AgConfig.SiteConfig.GetEnumerator()) {
+ Start-Job -Name gitops -ScriptBlock {
+ $AgConfig = $using:AgConfig
+ $cluster = $using:cluster
+ $namingGuid = $using:namingGuid
+ $resourceGroup = $using:resourceGroup
+ $appClonedRepo = $using:appUpstreamRepo
+ $appsRepo = $using:appsRepo
+
+ $AgConfig.AppConfig.GetEnumerator() | sort-object -Property @{Expression = { $_.value.Order }; Ascending = $true } | ForEach-Object {
+ $app = $_
+ $clusterName = $cluster.value.ArcClusterName + "-$namingGuid"
+ $branch = $cluster.value.Branch.ToLower()
+ $configName = $app.value.GitOpsConfigName.ToLower()
+ $namespace = $app.value.Namespace
+ $appName = $app.Value.KustomizationName
+ $appPath = $app.Value.KustomizationPath
+ $retryCount = 0
+ $maxRetries = 2
+
+ Write-Host "[$(Get-Date -Format t)] INFO: Creating GitOps config for $configName on $($cluster.Value.ArcClusterName+"-$namingGuid")" -ForegroundColor Gray
+ $type = "connectedClusters"
+
+ # Wait for Kubernetes API server to become available
+ $apiServer = kubectl config view --context $cluster.Name.ToLower() --minify -o jsonpath='{.clusters[0].cluster.server}'
+ $apiServerAddress = $apiServer -replace '.*https://| .*$'
+ $apiServerFqdn = ($apiServerAddress -split ":")[0]
+ $apiServerPort = ($apiServerAddress -split ":")[1]
+
+ do {
+ $result = Test-NetConnection -ComputerName $apiServerFqdn -Port $apiServerPort -WarningAction SilentlyContinue
+ if ($result.TcpTestSucceeded) {
+ break
+ }
+ else {
+ Start-Sleep -Seconds 5
+ }
+ } while ($true)
+
+
+ az k8s-configuration flux create `
+ --cluster-name $clusterName `
+ --resource-group $resourceGroup `
+ --name $configName `
+ --cluster-type $type `
+ --scope cluster `
+ --url $appClonedRepo `
+ --branch $branch `
+ --sync-interval 3s `
+ --kustomization name=$appName path=$appPath prune=true retry_interval=1m `
+ --timeout 10m `
+ --namespace $namespace `
+ --only-show-errors `
+ 2>&1 | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\GitOps-$clusterName.log")
+
+ do {
+ $configStatus = $(az k8s-configuration flux show --name $configName --cluster-name $clusterName --cluster-type $type --resource-group $resourceGroup -o json 2>$null) | convertFrom-JSON
+ if ($configStatus.ComplianceState -eq "Compliant") {
+ Write-Host "[$(Get-Date -Format t)] INFO: GitOps configuration $configName is ready on $clusterName" -ForegroundColor DarkGreen | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\GitOps-$clusterName.log")
+ }
+ else {
+ if ($configStatus.ComplianceState -ne "Non-compliant") {
+ Start-Sleep -Seconds 20
+ }
+ elseif ($configStatus.ComplianceState -eq "Non-compliant" -and $retryCount -lt $maxRetries) {
+ Start-Sleep -Seconds 20
+ $configStatus = $(az k8s-configuration flux show --name $configName --cluster-name $clusterName --cluster-type $type --resource-group $resourceGroup -o json 2>$null) | convertFrom-JSON
+ if ($configStatus.ComplianceState -eq "Non-compliant" -and $retryCount -lt $maxRetries) {
+ $retryCount++
+ Write-Host "[$(Get-Date -Format t)] INFO: Attempting to re-install $configName on $clusterName" -ForegroundColor Gray | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\GitOps-$clusterName.log")
+ Write-Host "[$(Get-Date -Format t)] INFO: Deleting $configName on $clusterName" -ForegroundColor Gray | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\GitOps-$clusterName.log")
+ az k8s-configuration flux delete `
+ --resource-group $resourceGroup `
+ --cluster-name $clusterName `
+ --cluster-type $type `
+ --name $configName `
+ --force `
+ --yes `
+ --only-show-errors `
+ 2>&1 | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\GitOps-$clusterName.log")
+
+ Start-Sleep -Seconds 10
+ Write-Host "[$(Get-Date -Format t)] INFO: Re-creating $configName on $clusterName" -ForegroundColor Gray | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\GitOps-$clusterName.log")
+
+ az k8s-configuration flux create `
+ --cluster-name $clusterName `
+ --resource-group $resourceGroup `
+ --name $configName `
+ --cluster-type $type `
+ --scope cluster `
+ --url $appClonedRepo `
+ --branch $branch `
+ --sync-interval 3s `
+ --kustomization name=$appName path=$appPath prune=true `
+ --timeout 30m `
+ --namespace $namespace `
+ --only-show-errors `
+ 2>&1 | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\GitOps-$clusterName.log")
+ }
+ }
+ elseif ($configStatus.ComplianceState -eq "Non-compliant" -and $retryCount -eq $maxRetries) {
+ Write-Host "[$(Get-Date -Format t)] ERROR: GitOps configuration $configName has failed on $clusterName. Exiting..." -ForegroundColor White -BackgroundColor Red | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\GitOps-$clusterName.log")
+ break
+ }
+ }
+ } until ($configStatus.ComplianceState -eq "Compliant")
+ }
+ }
+ }
+
+ while ($(Get-Job -Name gitops).State -eq 'Running') {
+ #Write-Host "[$(Get-Date -Format t)] INFO: Waiting for GitOps configuration to complete on all clusters...waiting 60 seconds" -ForegroundColor Gray
+ Receive-Job -Name gitops -WarningAction SilentlyContinue
+ Start-Sleep -Seconds 60
+ }
+
+ Get-Job -name gitops | Remove-Job
+ Write-Host "[$(Get-Date -Format t)] INFO: GitOps configuration complete." -ForegroundColor Green
+ Write-Host
+}
+
+function Deploy-AIO {
+ # Deploys Azure IoT Operations on all k8s clusters in the config file
+
+ ##############################################################
+ # Preparing clusters for aio
+ ##############################################################
+ $VMnames = $AgConfig.SiteConfig.GetEnumerator().Name.ToLower()
+
+ Invoke-Command -VMName $VMnames -Credential $Credentials -ScriptBlock {
+ $ProgressPreference = "SilentlyContinue"
+ ###########################################
+ # Preparing environment folders structure
+ ###########################################
+ Write-Host "[$(Get-Date -Format t)] INFO: Preparing AKSEE clusters for AIO" -ForegroundColor DarkGray
+ Write-Host "`n"
+ try {
+ $localPathProvisionerYaml = "https://raw.githubusercontent.com/Azure/AKS-Edge/main/samples/storage/local-path-provisioner/local-path-storage.yaml"
+ & kubectl apply -f $localPathProvisionerYaml
+ $pvcYaml = @"
+ apiVersion: v1
+ kind: PersistentVolumeClaim
+ metadata:
+ name: local-path-pvc
+ namespace: default
+ spec:
+ accessModes:
+ - ReadWriteOnce
+ storageClassName: local-path
+ resources:
+ requests:
+ storage: 15Gi
+"@
+
+ $pvcYaml | kubectl apply -f -
+
+ Write-Host "Successfully deployment the local path provisioner"
+ }
+ catch {
+ Write-Host "Error: local path provisioner deployment failed" -ForegroundColor Red
+ }
+
+ Write-Host "Configuring firewall specific to AIO"
+ Write-Host "Add firewall rule for AIO MQTT Broker"
+ New-NetFirewallRule -DisplayName "AIO MQTT Broker" -Direction Inbound -Action Allow | Out-Null
+ try {
+ $deploymentInfo = Get-AksEdgeDeploymentInfo
+ # Get the service ip address start to determine the connect address
+ $connectAddress = $deploymentInfo.LinuxNodeConfig.ServiceIpRange.split("-")[0]
+ $portProxyRulExists = netsh interface portproxy show v4tov4 | findstr /C:"1883" | findstr /C:"$connectAddress"
+ if ( $null -eq $portProxyRulExists ) {
+ Write-Host "Configure port proxy for AIO"
+ netsh interface portproxy add v4tov4 listenport=1883 listenaddress=0.0.0.0 connectport=1883 connectaddress=$connectAddress | Out-Null
+ netsh interface portproxy add v4tov4 listenport=1883 listenaddress=0.0.0.0 connectport=18883 connectaddress=$connectAddress | Out-Null
+ netsh interface portproxy add v4tov4 listenport=1883 listenaddress=0.0.0.0 connectport=8883 connectaddress=$connectAddress | Out-Null
+ }
+ else {
+ Write-Host "Port proxy rule for AIO exists, skip configuring port proxy..."
+ }
+ }
+ catch {
+ Write-Host "Error: port proxy update for aio failed" -ForegroundColor Red
+ }
+ Write-Host "Update the iptables rules"
+ try {
+ $iptableRulesExist = Invoke-AksEdgeNodeCommand -NodeType "Linux" -command "sudo iptables-save | grep -- '-m tcp --dport 9110 -j ACCEPT'" -ignoreError
+ if ( $null -eq $iptableRulesExist ) {
+ Invoke-AksEdgeNodeCommand -NodeType "Linux" -command "sudo iptables -A INPUT -p tcp -m state --state NEW -m tcp --dport 9110 -j ACCEPT"
+ Write-Host "Updated runtime iptable rules for node exporter"
+ Invoke-AksEdgeNodeCommand -NodeType "Linux" -command "sudo sed -i '/-A OUTPUT -j ACCEPT/i-A INPUT -p tcp -m tcp --dport 9110 -j ACCEPT' /etc/systemd/scripts/ip4save"
+ Write-Host "Persisted iptable rules for node exporter"
+ # increase the maximum number of files
+ Invoke-AksEdgeNodeCommand -NodeType "Linux" -Command "echo 'fs.inotify.max_user_instances = 1024' | sudo tee -a /etc/sysctl.conf && sudo sysctl -p"
+ }
+ else {
+ Write-Host "iptable rule exists, skip configuring iptable rules..."
+ }
+ }
+ catch {
+ Write-Host "Error: iptable rule update failed" -ForegroundColor Red
+ }
+ } | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\L1Infra.log")
+
+ #############################################################
+ # Deploying AIO on the clusters
+ #############################################################
+
+ Write-Host "[$(Get-Date -Format t)] INFO: Deploying AIO to the clusters" -ForegroundColor DarkGray
+ Write-Host "`n"
+ $kvIndex = 0
+
+ foreach ($cluster in $AgConfig.SiteConfig.GetEnumerator()) {
+ $clusterName = $cluster.Name.ToLower()
+ Write-Host "[$(Get-Date -Format t)] INFO: Deploying AIO to the $clusterName cluster" -ForegroundColor Gray
+ Write-Host "`n"
+ kubectx $clusterName
+ $arcClusterName = $AgConfig.SiteConfig[$clusterName].ArcClusterName + "-$namingGuid"
+ $keyVaultId = (az keyvault list -g $resourceGroup --resource-type vault --query "[$kvIndex].id" -o tsv)
+ $retryCount = 0
+ $maxRetries = 5
+ $aioStatus = "notDeployed"
+
+ # Enable custom locations on the Arc-enabled cluster
+ Write-Host "[$(Get-Date -Format t)] INFO: Enabling custom locations on the Arc-enabled cluster" -ForegroundColor DarkGray
+ Write-Host "`n"
+ az config set extension.use_dynamic_install=yes_without_prompt
+ az connectedk8s enable-features --name $arcClusterName `
+ --resource-group $resourceGroup `
+ --features cluster-connect custom-locations `
+ --custom-locations-oid $customLocationRPOID `
+ --only-show-errors
+
+ do {
+ az iot ops init --cluster $arcClusterName -g $resourceGroup --kv-id $keyVaultId --sp-app-id $spnClientId --sp-secret $spnClientSecret --sp-object-id $spnObjectId --mq-service-type loadBalancer --mq-insecure true --simulate-plc false --no-block --only-show-errors
+ if ($? -eq $false) {
+ $aioStatus = "notDeployed"
+ Write-Host "`n"
+ Write-Host "[$(Get-Date -Format t)] Error: An error occured while deploying AIO on the cluster...Retrying" -ForegroundColor DarkRed
+ Write-Host "`n"
+ az iot ops init --cluster $arcClusterName -g $resourceGroup --kv-id $keyVaultId --sp-app-id $spnClientId --sp-secret $spnClientSecret --sp-object-id $spnObjectId --mq-service-type loadBalancer --mq-insecure true --simulate-plc false --no-block --only-show-errors
+ $retryCount++
+ }
+ else {
+ $aioStatus = "deployed"
+ }
+ } until ($aioStatus -eq "deployed" -or $retryCount -eq $maxRetries)
+ $kvIndex++
+ }
+ foreach ($cluster in $AgConfig.SiteConfig.GetEnumerator()) {
+ $clusterName = $cluster.Name.ToLower()
+ $retryCount = 0
+ $maxRetries = 25
+ kubectx $clusterName
+ do {
+ $output = az iot ops check --as-object --only-show-errors
+ $output = $output | ConvertFrom-Json
+ $mqServiceStatus = ($output.postDeployment | Where-Object { $_.name -eq "evalBrokerListeners" }).status
+ if ($mqServiceStatus -ne "Success") {
+ Write-Host "Waiting for AIO to be deployed successfully on $clusterName...waiting for 60 seconds" -ForegroundColor DarkGray
+ Start-Sleep -Seconds 60
+ $retryCount++
+ }
+ } until ($mqServiceStatus -eq "Success" -or $retryCount -eq $maxRetries)
+
+ if ($retryCount -eq $maxRetries) {
+ Write-Host "[$(Get-Date -Format t)] ERROR: AIO deployment failed. Exiting..." -ForegroundColor White -BackgroundColor Red
+ exit 1 # Exit the script
+ }
+ Write-Host "AIO deployed successfully on the $clusterName cluster" -ForegroundColor Green
+ Write-Host "`n"
+ Write-Host "[$(Get-Date -Format t)] INFO: Started Event Grid role assignment process" -ForegroundColor DarkGray
+ $extensionPrincipalId =(az k8s-extension list --cluster-name $arcClusterName --resource-group $resourceGroup --cluster-type "connectedClusters" --query "[?extensionType=='microsoft.iotoperations.mq']" --output json | ConvertFrom-Json)[0].identity.principalId
+ $eventGridTopicId = (az eventgrid topic list --resource-group $resourceGroup --query "[0].id" -o tsv --only-show-errors)
+ $eventGridNamespaceName = (az eventgrid namespace list --resource-group $resourceGroup --query "[0].name" -o tsv --only-show-errors)
+ $eventGridNamespaceId = (az eventgrid namespace list --resource-group $resourceGroup --query "[0].id" -o tsv --only-show-errors)
+ $eventGridNamespacePrincipalId = (az eventgrid namespace list --resource-group $resourceGroup -o json --only-show-errors | ConvertFrom-Json)[0].identity.principalId
+
+ az role assignment create --assignee-object-id $extensionPrincipalId --role "EventGrid Data Sender" --scope $eventGridTopicId --assignee-principal-type ServicePrincipal --only-show-errors
+ az role assignment create --assignee-object-id $eventGridNamespacePrincipalId --role "EventGrid Data Sender" --scope $eventGridTopicId --assignee-principal-type ServicePrincipal --only-show-errors
+ #az role assignment create --assignee-object-id $spnObjectId --role "EventGrid Data Sender" --scope $eventGridTopicId --assignee-principal-type ServicePrincipal --only-show-errors
+ az role assignment create --assignee-object-id $extensionPrincipalId --role "EventGrid TopicSpaces Subscriber" --scope $eventGridNamespaceId --assignee-principal-type ServicePrincipal --only-show-errors
+ az role assignment create --assignee-object-id $extensionPrincipalId --role 'EventGrid TopicSpaces Publisher' --scope $eventGridNamespaceId --assignee-principal-type ServicePrincipal --only-show-errors
+ az role assignment create --assignee-object-id $extensionPrincipalId --role "EventGrid TopicSpaces Subscriber" --scope $eventGridTopicId --assignee-principal-type ServicePrincipal --only-show-errors
+ az role assignment create --assignee-object-id $extensionPrincipalId --role 'EventGrid TopicSpaces Publisher' --scope $eventGridTopicId --assignee-principal-type ServicePrincipal --only-show-errors
+
+ Start-Sleep -Seconds 60
+
+ Write-Host "[$(Get-Date -Format t)] INFO: Configuring routing to use system-managed identity" -ForegroundColor DarkGray
+ $eventGridConfig = "{routing-identity-info:{type:'SystemAssigned'}}"
+ az eventgrid namespace update -g $resourceGroup -n $eventGridNamespaceName --topic-spaces-configuration $eventGridConfig --only-show-errors
+
+ Start-Sleep -Seconds 60
+
+ ## Adding MQTT bridge to Event Grid MQTT
+ $mqconfigfile = "$AgToolsDir\mq_cloudConnector.yml"
+ Copy-Item $mqconfigfile "$AgToolsDir\mq_cloudConnector_$clusterName.yml" -Force
+ $bridgeConfig = "$AgToolsDir\mq_cloudConnector_$clusterName.yml"
+ (Get-Content $bridgeConfig) -replace 'clusterName', $clusterName | Set-Content $bridgeConfig
+ Write-Host "[$(Get-Date -Format t)] INFO: Configuring the MQ Event Grid bridge" -ForegroundColor DarkGray
+ $eventGridHostName = (az eventgrid namespace list --resource-group $resourceGroup --query "[0].topicSpacesConfiguration.hostname" -o tsv --only-show-errors)
+ (Get-Content -Path $bridgeConfig) -replace 'eventGridPlaceholder', $eventGridHostName | Set-Content -Path $bridgeConfig
+ kubectl apply -f $bridgeConfig -n $aioNamespace
+
+ ## Patching MQTT listener
+ }
+}
+
+function Set-MQTTIpAddress {
+ $mqttIpArray = @()
+ $clusters = $AgConfig.SiteConfig.GetEnumerator()
+ foreach ($cluster in $clusters) {
+ $clusterName = $cluster.Name.ToLower()
+ kubectx $clusterName | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\ClusterSecrets.log")
+ Write-Host "[$(Get-Date -Format t)] INFO: Getting MQ IP address" -ForegroundColor DarkGray
+
+ do {
+ $mqttIp = kubectl get service $mqListenerService -n $aioNamespace -o jsonpath="{.status.loadBalancer.ingress[0].ip}"
+ $services = kubectl get pods -n $aioNamespace -o json | ConvertFrom-Json
+ $matchingServices = $services.items | Where-Object {
+ $_.metadata.name -match "aio-mq-dmqtt" -and
+ $_.status.phase -notmatch "running"
+ }
+ Write-Host "[$(Get-Date -Format t)] INFO: Waiting for MQTT services to initialize and the service Ip address to be assigned...Waiting for 20 seconds" -ForegroundColor DarkGray
+ Start-Sleep -Seconds 20
+ } while (
+ $null -eq $mqttIp -and $matchingServices.Count -ne 0
+ )
+ if (-not [string]::IsNullOrEmpty($mqttIp)) {
+ $newObject = [PSCustomObject]@{
+ cluster = $clusterName
+ ip = $mqttIp
+ }
+ $mqttIpArray += $newObject
+ }
+
+ Invoke-Command -VMName $clusterName -Credential $Credentials -ScriptBlock {
+ netsh interface portproxy add v4tov4 listenport=1883 listenaddress=0.0.0.0 connectport=1883 connectaddress=$using:mqttIp
+ }
+ }
+
+ $mqttIpArray = $mqttIpArray | Where-Object { $_ -ne "" }
+
+ return $mqttIpArray
+}
+
+function Deploy-MQTTSimulator {
+ param (
+ [array]$mqttIpArray
+ )
+
+ $mqsimulatorfile = "$AgToolsDir\mqtt_simulator.yml"
+
+ $clusters = $AgConfig.SiteConfig.GetEnumerator()
+
+ foreach ($cluster in $clusters) {
+ $clusterName = $cluster.Name.ToLower()
+ Copy-Item $mqsimulatorfile "$AgToolsDir\mqtt_simulator_$clusterName.yml" -Force
+ $simualtorConfig = "$AgToolsDir\mqtt_simulator_$clusterName.yml"
+ $mqttIp = $mqttIpArray | Where-Object { $_.cluster -eq $clusterName } | Select-Object -ExpandProperty ip
+ Write-Host "[$(Get-Date -Format t)] INFO: Deploying MQTT Simulator to the $clusterName cluster" -ForegroundColor Gray
+ Write-Host "`n"
+ kubectx $clusterName
+ (Get-Content $simualtorConfig ) -replace 'MQTTIpPlaceholder', $mqttIp | Set-Content $simualtorConfig
+ netsh interface portproxy add v4tov4 listenport=1883 listenaddress=0.0.0.0 connectport=1883 connectaddress=$mqttIp
+ kubectl apply -f $simualtorConfig -n $aioNamespace
+ }
+}
+
+##############################################################
+# Install MQTT Explorer
+##############################################################
+function Deploy-MQTTExplorer {
+ param (
+ [array]$mqttIpArray
+ )
+ Write-Host "`n"
+ Write-Host "[$(Get-Date -Format t)] INFO: Installing MQTT Explorer." -ForegroundColor DarkGreen
+ Write-Host "`n"
+ $aioToolsDir = $AgConfig.AgDirectories["AgToolsDir"]
+ $mqttExplorerSettings = "$env:USERPROFILE\AppData\Roaming\MQTT-Explorer\settings.json"
+ $latestReleaseTag = (Invoke-WebRequest $mqttExplorerReleasesUrl | ConvertFrom-Json)[0].tag_name
+ $versionToDownload = $latestReleaseTag.Split("v")[1]
+ $mqttExplorerReleaseDownloadUrl = ((Invoke-WebRequest $mqttExplorerReleasesUrl | ConvertFrom-Json)[0].assets | Where-object { $_.name -like "MQTT-Explorer-Setup-${versionToDownload}.exe" }).browser_download_url
+ $output = Join-Path $aioToolsDir "mqtt-explorer-$latestReleaseTag.exe"
+ $clusters = $AgConfig.SiteConfig.GetEnumerator()
+
+ $ProgressPreference = "SilentlyContinue"
+ Invoke-WebRequest $mqttExplorerReleaseDownloadUrl -OutFile $output
+ Start-Process -FilePath $output -ArgumentList "/S" -Wait
+
+ Write-Host "[$(Get-Date -Format t)] INFO: Configuring MQTT explorer" -ForegroundColor DarkGray
+ Start-Process "$env:USERPROFILE\AppData\Local\Programs\MQTT-Explorer\MQTT Explorer.exe"
+ Start-Sleep -Seconds 5
+ Stop-Process -Name "MQTT Explorer"
+ Copy-Item "$aioToolsDir\mqtt_explorer_settings.json" -Destination $mqttExplorerSettings -Force
+ foreach ($cluster in $clusters) {
+ $clusterName = $cluster.Name.ToLower()
+ $mqttIp = $mqttIpArray | Where-Object { $_.cluster -eq $clusterName } | Select-Object -ExpandProperty ip
+ (Get-Content $mqttExplorerSettings ) -replace "${clusterName}IpPlaceholder", $mqttIp | Set-Content $mqttExplorerSettings
+ }
+ $ProgressPreference = "Continue"
+}
+
+# Function to deploy Azure Data Explorer dashboard reports
+function Deploy-ADXDashboardReports {
+ ### BELOW IS AN ALTERNATIVE APPROACH TO IMPORT DASHBOARD USING README INSTRUCTIONS
+ $adxDashBoardsDir = $AgConfig.AgDirectories["AgAdxDashboards"]
+
+ # Create directory if do not exist
+ if (-not (Test-Path -LiteralPath $adxDashBoardsDir)) {
+ New-Item -Path $adxDashBoardsDir -ItemType Directory -ErrorAction Stop | Out-Null #-Force
+ }
+
+ #$dataEmulatorDir = $AgConfig.AgDirectories["AgDataEmulator"]
+ $kustoCluster = Get-AzKustoCluster -ResourceGroupName $resourceGroup -Name $adxClusterName
+ if ($null -ne $kustoCluster) {
+ $adxEndPoint = $kustoCluster.Uri
+ if ($null -ne $adxEndPoint -and $adxEndPoint -ne "") {
+ $ordersDashboardBody = (Invoke-WebRequest -Method Get -Uri "$templateBaseUrl/artifacts/adx_dashboards/adx-dashboard-contoso-motors-auto-parts.json").Content -replace '{{ADX_CLUSTER_URI}}', $adxEndPoint -replace '{{ADX_CLUSTER_NAME}}', $adxClusterName -replace '{{GITHUB_BRANCH}}', $env:githubBranch -replace '{{GITHUB_ACCOUNT}}', $env:githubAccount
+ Set-Content -Path "$adxDashBoardsDir\adx-dashboard-contoso-motors-auto-parts.json" -Value $ordersDashboardBody -Force -ErrorAction Ignore
+ }
+ else {
+ Write-Host "[$(Get-Date -Format t)] ERROR: Unable to find Azure Data Explorer endpoint from the cluster resource in the resource group."
+ }
+ }
+
+ # Create EventHub environment variables
+ $eventHubNamespace = (az eventhubs namespace list --resource-group $env:resourceGroup --query [0].name --output tsv)
+ if ($null -ne $eventHubNamespace) {
+ # Find EventHub and create connection string
+ $eventHub = (az eventhubs eventhub list --namespace-name $eventHubNamespace --resource-group $env:resourceGroup --query [0].name --output tsv)
+
+ # Create authorization rule
+ $authRuleName = "data-emulator"
+ az eventhubs eventhub authorization-rule create --authorization-rule-name $authRuleName --eventhub-name $eventHub --namespace-name $eventHubNamespace --resource-group $env:resourceGroup --rights Send Listen
+
+ # Get connection string
+ $connectionString = (az eventhubs eventhub authorization-rule keys list --resource-group $env:resourceGroup --namespace-name $eventHubNamespace --eventhub-name $eventHub --name $authRuleName --query primaryConnectionString --output tsv)
+
+ # Set environment variables
+ [System.Environment]::SetEnvironmentVariable('EVENTHUB_CONNECTION_STRING', $connectionString, [System.EnvironmentVariableTarget]::Machine)
+ [System.Environment]::SetEnvironmentVariable('EVENTHUB_NAME', $eventHub, [System.EnvironmentVariableTarget]::Machine)
+ }
+
+ # Create desktop icons
+ $AgDataEmulatorDir = $AgConfig.AgDirectories["AgDataEmulator"]
+ $dataEmulatorFile = "$AgDataEmulatorDir\data-emulator.py"
+ Invoke-WebRequest -Method Get -Uri "$templateBaseUrl/artifacts/data_emulator/data-emulator.py" -OutFile $dataEmulatorFile
+ if (!(Test-Path -Path $dataEmulatorFile)) {
+ Write-Host "Unabled to download data-emulator.py file. Please download manually from GitHub into the DataEmulator folder."
+ }
+
+ $emulationScriptContent = "@echo off `r`ncmd /k `"cd /d $AgDataEmulatorDir & python data-emulator.py`""
+ $emulatorLocation = "$AgDataEmulatorDir\dataemulator.cmd"
+ Set-Content -Path $emulatorLocation -Value $emulationScriptContent
+
+ # Download icon file
+ $AgIconsDir = $AgConfig.AgDirectories["AgIconDir"]
+
+ $iconPath = "$AgIconsDir\emulator.ico"
+ Invoke-WebRequest -Method Get -Uri "$templateBaseUrl/artifacts/icons/emulator.ico" -OutFile $iconPath
+ if (!(Test-Path -Path $iconPath)) {
+ Write-Host "Unabled to download emulator.ico file. Please download manually from GitHub into the icons folder."
+ }
+
+ # Create desktop shortcut
+ $shortcutLocation = "$Env:Public\Desktop\Data Emulator.lnk"
+ $wScriptShell = New-Object -ComObject WScript.Shell
+ $shortcut = $wScriptShell.CreateShortcut($shortcutLocation)
+ $shortcut.TargetPath = $emulatorLocation
+ $shortcut.IconLocation = "$iconPath, 0"
+ $shortcut.WindowStyle = 8
+ $shortcut.Save()
+
+ # Install azure.eventhub python module to run data emulator
+ pip install azure.eventhub
+}
+
+function Deploy-ManufacturingBookmarks {
+ $bookmarksFileName = "$AgToolsDir\Bookmarks"
+ $edgeBookmarksPath = "$Env:LOCALAPPDATA\Microsoft\Edge\User Data\Default"
+
+ foreach ($cluster in $AgConfig.SiteConfig.GetEnumerator()) {
+ kubectx $cluster.Name.ToLower() | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Bookmarks.log")
+ $services = kubectl get services --all-namespaces -o json | ConvertFrom-Json
+
+ # Matching url: flask app
+ $matchingServices = $services.items | Where-Object {
+ $_.metadata.name -eq 'flask-app-service' -and
+ $_.spec.ports.port -contains 8888
+ }
+ $flaskIps = $matchingServices.status.loadBalancer.ingress.ip
+
+ foreach ($flaskIp in $flaskIps) {
+ $output = "http://${flaskIp}:8888"
+ $output | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Bookmarks.log")
+
+ # Replace matching value in the Bookmarks file
+ $content = Get-Content -Path $bookmarksFileName
+ $newContent = $content -replace ("Flask-" + $cluster.Name + "-URL"), $output
+ $newContent | Set-Content -Path $bookmarksFileName
+
+ Start-Sleep -Seconds 2
+ }
+
+ # Matching url: Influxdb
+ $matchingServices = $services.items | Where-Object {
+ $_.metadata.name -eq 'Influxdb' -and
+ $_.spec.ports.port -contains 8086
+ }
+ $influxdbIps = $matchingServices.status.loadBalancer.ingress.ip
+
+ foreach ($influxdbIp in $influxdbIps) {
+ $output = "http://${influxdbIp}:8086"
+ $output | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Bookmarks.log")
+
+ # Replace matching value in the Bookmarks file
+ $content = Get-Content -Path $bookmarksFileName
+ $newContent = $content -replace ("Influxdb-" + $cluster.Name + "-URL"), $output
+ $newContent | Set-Content -Path $bookmarksFileName
+
+ Start-Sleep -Seconds 2
+ }
+
+ # Matching url: prometheus
+ $matchingServices = $services.items | Where-Object {
+ $_.spec.ports.port -contains 9090 -and
+ $_.spec.type -eq "LoadBalancer"
+ }
+ $prometheusIps = $matchingServices.status.loadBalancer.ingress.ip
+
+ foreach ($prometheusIp in $prometheusIps) {
+ $output = "http://${prometheusIp}:9090"
+ $output | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Bookmarks.log")
+
+ # Replace matching value in the Bookmarks file
+ $content = Get-Content -Path $bookmarksFileName
+ $newContent = $content -replace ("Prometheus-" + $cluster.Name + "-URL"), $output
+ $newContent | Set-Content -Path $bookmarksFileName
+
+ Start-Sleep -Seconds 2
+ }
+ }
+
+ Start-Sleep -Seconds 2
+
+ Copy-Item -Path $bookmarksFileName -Destination $edgeBookmarksPath -Force
+
+ ##############################################################
+ # Pinning important directories to Quick access
+ ##############################################################
+ Write-Host "[$(Get-Date -Format t)] INFO: Pinning important directories to Quick access (Step 16/17)" -ForegroundColor DarkGreen
+ $quickAccess = new-object -com shell.application
+ $quickAccess.Namespace($AgConfig.AgDirectories.AgDir).Self.InvokeVerb("pintohome")
+ $quickAccess.Namespace($AgConfig.AgDirectories.AgLogsDir).Self.InvokeVerb("pintohome")
+}
diff --git a/azure_jumpstart_ag/artifacts/PowerShell/Modules/retail.psm1 b/azure_jumpstart_ag/artifacts/PowerShell/Modules/retail.psm1
new file mode 100644
index 0000000000..77d684c59b
--- /dev/null
+++ b/azure_jumpstart_ag/artifacts/PowerShell/Modules/retail.psm1
@@ -0,0 +1,719 @@
+function SetupRetailRepo {
+ Set-Location $AgAppsRepo
+ Write-Host "INFO: Checking if the $appsRepo repository is forked" -ForegroundColor Gray
+ $retryCount = 0
+ $maxRetries = 5
+ do {
+ $forkExists = $false
+ try {
+ $response = Invoke-RestMethod -Uri "$gitHubAPIBaseUri/repos/$githubUser/$appsRepo"
+ if ($response) {
+ write-host "INFO: Fork exists....Proceeding" -ForegroundColor Gray
+ $forkExists = $true
+ }
+ }
+ catch {
+ if ($retryCount -lt $maxRetries) {
+ Write-Host "ERROR: $githubUser/$appsRepo Fork doesn't exist, please fork https://github.com/microsoft/jumpstart-agora-apps to proceed (attempt $retryCount/$maxRetries) . . . waiting 60 seconds" -ForegroundColor Red
+ $retryCount++
+ $forkExists = $false
+ start-sleep -Seconds 60
+ }
+ else {
+ Write-Host "[$(Get-Date -Format t)] ERROR: Retry limit reached, $githubUser/$appsRepo Fork doesn't exist. Exiting." -ForegroundColor Red
+ exit
+ }
+ }
+ } until ($forkExists -eq $true)
+
+ Write-Host "INFO: Checking if the GitHub access token is valid." -ForegroundColor Gray
+ do {
+ $response = gh auth status 2>&1
+ if ($response -match "authentication failed") {
+ write-host "ERROR: The GitHub Personal access token is not valid" -ForegroundColor Red
+ Write-Host "INFO: Please try to re-generate the personal access token and provide it here (https://aka.ms/AgoraPreReqs): "
+ do {
+ $githubPAT = Read-Host "GitHub personal access token"
+ } while ($githubPAT -eq "")
+ }
+ } until (
+ $response -notmatch "authentication failed"
+ )
+
+ Write-Host "INFO: The GitHub Personal access token is valid. Proceeding." -ForegroundColor DarkGreen
+ $Env:GITHUB_TOKEN = $githubPAT.Trim()
+ [System.Environment]::SetEnvironmentVariable('GITHUB_TOKEN', $githubPAT.Trim(), [System.EnvironmentVariableTarget]::Machine)
+
+ Write-Host "INFO: Checking if the personal access token is assigned on the $githubUser/$appsRepo Fork" -ForegroundColor Gray
+ $headers = @{
+ Authorization = "token $githubPat"
+ "Content-Type" = "application/json"
+ }
+ $retryCount = 0
+ $maxRetries = 5
+ $uri = "$gitHubAPIBaseUri/repos/$githubUser/$appsRepo/actions/secrets"
+ do {
+ try {
+ $response = Invoke-RestMethod -Uri $uri -Method Get -Headers $headers
+ Write-Host "INFO: Personal access token is assigned on $githubUser/$appsRepo fork" -ForegroundColor DarkGreen
+ $PatAssigned = $true
+ }
+ catch {
+ if ($retryCount -lt $maxRetries) {
+ Write-Host "ERROR: Personal access token is not assigned on $githubUser/$appsRepo fork. Please assign the personal access token to your fork (https://aka.ms/AgoraPreReqs) (attempt $retryCount/$maxRetries).....waiting 60 seconds" -ForegroundColor Red
+ $PatAssigned = $false
+ $retryCount++
+ start-sleep -Seconds 60
+ }
+ else {
+ Write-Host "[$(Get-Date -Format t)] ERROR: Retry limit reached, the personal access token is not assigned to $githubUser/$appsRepo. Exiting." -ForegroundColor Red
+ exit
+ }
+ }
+ } until ($PatAssigned -eq $true)
+
+
+ Write-Host "INFO: Cloning the GitHub repository locally" -ForegroundColor Gray
+ git clone "https://$githubPat@github.com/$githubUser/$appsRepo.git" "$AgAppsRepo\$appsRepo"
+ Set-Location "$AgAppsRepo\$appsRepo"
+
+ Write-Host "INFO: Verifying 'Administration' permissions" -ForegroundColor Gray
+ $retryCount = 0
+ $maxRetries = 5
+
+ $body = @{
+ required_status_checks = $null
+ enforce_admins = $false
+ required_pull_request_reviews = @{
+ required_approving_review_count = 0
+ }
+ dismiss_stale_reviews = $true
+ restrictions = $null
+ } | ConvertTo-Json
+
+ do {
+ try {
+ $response = Invoke-WebRequest -Uri "$gitHubAPIBaseUri/repos/$githubUser/$appsRepo/branches/main/protection" -Method Put -Headers $headers -Body $body -ContentType "application/json"
+ }
+ catch {
+ if ($retryCount -lt $maxRetries) {
+ Write-Host "ERROR: The GitHub Personal access token doesn't seem to have 'Administration' write permissions, please assign the right permissions (https://aka.ms/AgoraPreReqs) (attempt $retryCount/$maxRetries)...waiting 60 seconds" -ForegroundColor Red
+ $retryCount++
+ start-sleep -Seconds 60
+ }
+ else {
+ Write-Host "[$(Get-Date -Format t)] ERROR: Retry limit reached, the personal access token doesn't have 'Administration' write permissions assigned. Exiting." -ForegroundColor Red
+ exit
+ }
+ }
+ } until ($response)
+ Write-Host "INFO: 'Administration' write permissions verified" -ForegroundColor DarkGreen
+
+
+ Write-Host "INFO: Checking if there are existing branch protection policies" -ForegroundColor Gray
+ $protectedBranches = Invoke-RestMethod -Uri "$gitHubAPIBaseUri/repos/$githubUser/$appsRepo/branches?protected=true" -Method GET -Headers $headers
+ foreach ($branch in $protectedBranches) {
+ $branchName = $branch.name
+ $deleteProtectionUrl = "$gitHubAPIBaseUri/repos/$githubUser/$appsRepo/branches/$branchName/protection"
+ Invoke-RestMethod -Uri $deleteProtectionUrl -Headers $headers -Method Delete
+ Write-Host "INFO: Deleted protection policy for branch: $branchName" -ForegroundColor Gray
+ }
+
+ Write-Host "INFO: Pulling latests changes to GitHub repository" -ForegroundColor Gray
+ git config --global user.email "dev@agora.com"
+ git config --global user.name "Agora Dev"
+ git remote add upstream "$appUpstreamRepo.git"
+ git fetch upstream
+ git checkout main
+ git reset --hard upstream/main
+ git push origin main -f
+ git pull
+ git remote remove upstream
+ git remote add upstream "$appClonedRepo.git"
+
+ Write-Host "INFO: Creating GitHub workflows" -ForegroundColor Gray
+ New-Item -ItemType Directory ".github/workflows" -Force
+ $githubApiUrl = "$gitHubAPIBaseUri/repos/$githubAccount/azure_arc/contents/azure_jumpstart_ag/artifacts/workflows?ref=$githubBranch"
+ $response = Invoke-RestMethod -Uri $githubApiUrl
+ $fileUrls = $response | Where-Object { $_.type -eq "file" } | Select-Object -ExpandProperty download_url
+ $fileUrls | ForEach-Object {
+ $fileName = $_.Substring($_.LastIndexOf("/") + 1)
+ $outputFile = Join-Path "$AgAppsRepo\$appsRepo\.github\workflows" $fileName
+ Invoke-RestMethod -Uri $_ -OutFile $outputFile
+ }
+ git add .
+ git commit -m "Pushing GitHub Actions to apps fork"
+ git push
+ Start-Sleep -Seconds 20
+
+ Write-Host "INFO: Verifying 'Secrets' permissions" -ForegroundColor Gray
+ $retryCount = 0
+ $maxRetries = 5
+ do {
+ $response = gh secret set "test" -b "test" 2>&1
+ if ($response -match "error") {
+ if ($retryCount -eq $maxRetries) {
+ Write-Host "[$(Get-Date -Format t)] ERROR: Retry limit reached, the personal access token doesn't have 'Secrets' write permissions assigned. Exiting." -ForegroundColor Red
+ exit
+ }
+ else {
+ $retryCount++
+ write-host "ERROR: The GitHub Personal access token doesn't seem to have 'Secrets' write permissions, please assign the right permissions (https://aka.ms/AgoraPreReqs) (attempt $retryCount/$maxRetries)...waiting 60 seconds" -ForegroundColor Red
+ Start-Sleep -Seconds 60
+ }
+ }
+ } while ($response -match "error" -or $retryCount -ge $maxRetries)
+ gh secret delete test
+ Write-Host "INFO: 'Secrets' write permissions verified" -ForegroundColor DarkGreen
+
+ Write-Host "INFO: Verifying 'Actions' permissions" -ForegroundColor Gray
+ $retryCount = 0
+ $maxRetries = 5
+ do {
+ $response = gh workflow enable update-files.yml 2>&1
+ if ($response -match "failed") {
+ if ($retryCount -eq $maxRetries) {
+ Write-Host "[$(Get-Date -Format t)] ERROR: Retry limit reached, the personal access token doesn't have 'Actions' write permissions assigned. Exiting." -ForegroundColor Red
+ exit
+ }
+ else {
+ $retryCount++
+ write-host "ERROR: The GitHub Personal access token doesn't seem to have 'Actions' write permissions, please assign the right permissions (https://aka.ms/AgoraPreReqs) (attempt $retryCount/$maxRetries)...waiting 60 seconds" -ForegroundColor Red
+ Start-Sleep -Seconds 60
+ }
+ }
+ } while ($response -match "failed" -or $retryCount -ge $maxRetries)
+ Write-Host "INFO: 'Actions' write permissions verified" -ForegroundColor DarkGreen
+
+ write-host "INFO: Creating GitHub secrets" -ForegroundColor Gray
+ Write-Host "INFO: Getting Cosmos DB access key" -ForegroundColor Gray
+ Write-Host "INFO: Adding GitHub secrets to apps fork" -ForegroundColor Gray
+ gh api -X PUT "/repos/$githubUser/$appsRepo/actions/permissions/workflow" -F can_approve_pull_request_reviews=true
+ gh repo set-default "$githubUser/$appsRepo"
+ gh secret set "SPN_CLIENT_ID" -b $spnClientID
+ gh secret set "SPN_CLIENT_SECRET" -b $spnClientSecret
+ gh secret set "ACR_NAME" -b $acrName
+ gh secret set "PAT_GITHUB" -b $githubPat
+ gh secret set "COSMOS_DB_ENDPOINT" -b $cosmosDBEndpoint
+ gh secret set "SPN_TENANT_ID" -b $spnTenantId
+
+ Write-Host "INFO: Updating ACR name and Cosmos DB endpoint in all branches" -ForegroundColor Gray
+ gh workflow run update-files.yml
+ while ($workflowStatus.status -ne "completed") {
+ Write-Host "INFO: Waiting for update-files workflow to complete" -ForegroundColor Gray
+ Start-Sleep -Seconds 10
+ $workflowStatus = (gh run list --workflow=update-files.yml --json status) | ConvertFrom-Json
+ }
+ Write-Host "INFO: Starting Contoso supermarket pos application v1.0 image build" -ForegroundColor Gray
+ gh workflow run pos-app-initial-images-build.yml
+
+ Write-Host "INFO: Creating GitHub branches to $appsRepo fork" -ForegroundColor Gray
+ $branches = $AgConfig.GitBranches
+ foreach ($branch in $branches) {
+ try {
+ $response = Invoke-RestMethod -Uri "$gitHubAPIBaseUri/repos/$githubUser/$appsRepo/branches/$branch"
+ if ($response) {
+ if ($branch -ne "main") {
+ Write-Host "INFO: branch $branch already exists! Deleting and recreating the branch" -ForegroundColor Gray
+ git push origin --delete $branch
+ git branch -d $branch
+ git fetch origin
+ git checkout main
+ git pull origin main
+ git checkout -b $branch main
+ git pull origin main
+ git push --set-upstream origin $branch
+ }
+ }
+ }
+ catch {
+ Write-Host "INFO: Creating $branch branch" -ForegroundColor Gray
+ git fetch origin
+ git checkout main
+ git pull origin main
+ git checkout -b $branch main
+ git pull origin main
+ git push --set-upstream origin $branch
+ }
+ }
+ Write-Host "INFO: Cleaning up any other branches" -ForegroundColor Gray
+ $existingBranches = gh api "repos/$githubUser/$appsRepo/branches" | ConvertFrom-Json
+ $branches = $AgConfig.GitBranches
+ foreach ($branch in $existingBranches) {
+ if ($branches -notcontains $branch.name) {
+ $branchToDelete = $branch.name
+ git push origin --delete $branchToDelete
+ }
+ }
+
+ Write-Host "INFO: Switching to main branch" -ForegroundColor Gray
+ git checkout main
+
+ Write-Host "INFO: Adding branch protection policies for all branches" -ForegroundColor Gray
+ foreach ($branch in $branches) {
+ Write-Host "INFO: Adding branch protection policies for $branch branch" -ForegroundColor Gray
+ $headers = @{
+ "Authorization" = "Bearer $githubPat"
+ "Accept" = "application/vnd.github+json"
+ }
+ $body = @{
+ required_status_checks = $null
+ enforce_admins = $false
+ required_pull_request_reviews = @{
+ required_approving_review_count = 0
+ }
+ dismiss_stale_reviews = $true
+ restrictions = $null
+ } | ConvertTo-Json
+
+ Invoke-WebRequest -Uri "$gitHubAPIBaseUri/repos/$githubUser/$appsRepo/branches/$branch/protection" -Method Put -Headers $headers -Body $body -ContentType "application/json"
+ }
+ Write-Host "INFO: GitHub repo configuration complete!" -ForegroundColor Green
+ Write-Host
+}
+
+function Deploy-AzureIOTHub {
+ if ($githubUser -ne "microsoft") {
+ $iotHubHostName = $Env:iotHubHostName
+ $iotHubName = $iotHubHostName.replace(".azure-devices.net", "")
+ $sites = $AgConfig.SiteConfig.Values
+ Write-Host "[$(Get-Date -Format t)] INFO: Create an Azure IoT device for each site" -ForegroundColor Gray
+ foreach ($site in $sites) {
+ foreach ($device in $site.IoTDevices) {
+ $deviceId = "$device-$($site.FriendlyName)"
+ Add-AzIotHubDevice -ResourceGroupName $resourceGroup -IotHubName $iotHubName -DeviceId $deviceId -EdgeEnabled | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\IoT.log")
+ }
+ }
+ Write-Host "[$(Get-Date -Format t)] INFO: Azure IoT Hub configuration complete!" -ForegroundColor Green
+ Write-Host
+ }
+ else {
+ Write-Host "[$(Get-Date -Format t)] ERROR: You have to fork the jumpstart-agora-apps repository!" -ForegroundColor Red
+ }
+
+ ### BELOW IS AN ALTERNATIVE APPROACH TO IMPORT DASHBOARD USING README INSTRUCTIONS
+ $adxDashBoardsDir = $AgConfig.AgDirectories["AgAdxDashboards"]
+ $dataEmulatorDir = $AgConfig.AgDirectories["AgDataEmulator"]
+ $kustoCluster = Get-AzKustoCluster -ResourceGroupName $resourceGroup -Name $adxClusterName
+ if ($null -ne $kustoCluster) {
+ $adxEndPoint = $kustoCluster.Uri
+ if ($null -ne $adxEndPoint -and $adxEndPoint -ne "") {
+ $ordersDashboardBody = (Invoke-WebRequest -Method Get -Uri "$templateBaseUrl/artifacts/adx_dashboards/adx-dashboard-orders-payload.json").Content -replace '{{ADX_CLUSTER_URI}}', $adxEndPoint -replace '{{ADX_CLUSTER_NAME}}', $adxClusterName
+ Set-Content -Path "$adxDashBoardsDir\adx-dashboard-orders-payload.json" -Value $ordersDashboardBody -Force -ErrorAction Ignore
+ $iotSensorsDashboardBody = (Invoke-WebRequest -Method Get -Uri "$templateBaseUrl/artifacts/adx_dashboards/adx-dashboard-iotsensor-payload.json") -replace '{{ADX_CLUSTER_URI}}', $adxEndPoint -replace '{{ADX_CLUSTER_NAME}}', $adxClusterName
+ Set-Content -Path "$adxDashBoardsDir\adx-dashboard-iotsensor-payload.json" -Value $iotSensorsDashboardBody -Force -ErrorAction Ignore
+ }
+ else {
+ Write-Host "[$(Get-Date -Format t)] ERROR: Unable to find Azure Data Explorer endpoint from the cluster resource in the resource group."
+ }
+ }
+
+ # Download DataEmulator.zip into Agora folder and unzip
+ $emulatorPath = "$dataEmulatorDir\DataEmulator.zip"
+ Invoke-WebRequest -Method Get -Uri "$templateBaseUrl/artifacts/data_emulator/DataEmulator.zip" -OutFile $emulatorPath
+
+ # Unzip DataEmulator.zip to copy DataEmulator exe and config file to generate sample data for dashboards
+ if (Test-Path -Path $emulatorPath) {
+ Expand-Archive -Path "$emulatorPath" -DestinationPath "$dataEmulatorDir" -ErrorAction SilentlyContinue -Force
+ }
+
+ # Download products.json and stores.json file to use in Data Emulator
+ $productsJsonPath = "$dataEmulatorDir\products.json"
+ Invoke-WebRequest -Method Get -Uri "$templateBaseUrl/artifacts/data_emulator/products.json" -OutFile $productsJsonPath
+ if (!(Test-Path -Path $productsJsonPath)) {
+ Write-Host "Unabled to download products.json file. Please download manually from GitHub into the data_emulator folder."
+ }
+
+ $storesJsonPath = "$dataEmulatorDir\stores.json"
+ Invoke-WebRequest -Method Get -Uri "$templateBaseUrl/artifacts/data_emulator/stores.json" -OutFile $storesJsonPath
+ if (!(Test-Path -Path $storesJsonPath)) {
+ Write-Host "Unabled to download stores.json file. Please download manually from GitHub into the data_emulator folder."
+ }
+
+ # Download icon file
+ $iconPath = "$AgIconsDir\emulator.ico"
+ Invoke-WebRequest -Method Get -Uri "$templateBaseUrl/artifacts/icons/emulator.ico" -OutFile $iconPath
+ if (!(Test-Path -Path $iconPath)) {
+ Write-Host "Unabled to download emulator.ico file. Please download manually from GitHub into the icons folder."
+ }
+
+ # Create desktop shortcut
+ $shortcutLocation = "$Env:Public\Desktop\Data Emulator.lnk"
+ $wScriptShell = New-Object -ComObject WScript.Shell
+ $shortcut = $wScriptShell.CreateShortcut($shortcutLocation)
+ $shortcut.TargetPath = "$dataEmulatorDir\DataEmulator.exe"
+ $shortcut.IconLocation = "$iconPath, 0"
+ $shortcut.WindowStyle = 7
+ $shortcut.Save()
+}
+
+function Deploy-K8sImagesCache {
+ if ($Env:industry -eq "retail") {
+ Write-Host "[$(Get-Date -Format t)] INFO: Caching contoso-supermarket images on all clusters" -ForegroundColor Gray
+ while ($workflowStatus.status -ne "completed") {
+ Write-Host "INFO: Waiting for pos-app-initial-images-build workflow to complete" -ForegroundColor Gray
+ Start-Sleep -Seconds 10
+ $workflowStatus = (gh run list --workflow=pos-app-initial-images-build.yml --json status) | ConvertFrom-Json
+ }
+ foreach ($cluster in $AgConfig.SiteConfig.GetEnumerator()) {
+ $branch = $cluster.Name.ToLower()
+ $context = $cluster.Name.ToLower()
+ $applicationName = "contoso-supermarket"
+ $imageTag = "v1.0"
+ $imagePullSecret = "acr-secret"
+ $namespace = "images-cache"
+ if ($branch -eq "chicago") {
+ $branch = "canary"
+ }
+ if ($branch -eq "seattle") {
+ $branch = "production"
+ }
+ Save-K8sImage -applicationName $applicationName -imageName "contosoai" -imageTag $imageTag -namespace $namespace -imagePullSecret $imagePullSecret -branch $branch -acrName $acrName -context $context
+ Save-K8sImage -applicationName $applicationName -imageName "pos" -imageTag $imageTag -namespace $namespace -imagePullSecret $imagePullSecret -branch $branch -acrName $acrName -context $context
+ Save-K8sImage -applicationName $applicationName -imageName "pos-cloudsync" -imageTag $imageTag -namespace $namespace -imagePullSecret $imagePullSecret -branch $branch -acrName $acrName -context $context
+ Save-K8sImage -applicationName $applicationName -imageName "queue-monitoring-backend" -imageTag $imageTag -namespace $namespace -imagePullSecret $imagePullSecret -branch $branch -acrName $acrName -context $context
+ Save-K8sImage -applicationName $applicationName -imageName "queue-monitoring-frontend" -imageTag $imageTag -namespace $namespace -imagePullSecret $imagePullSecret -branch $branch -acrName $acrName -context $context
+ }
+ }
+}
+function Get-GitHubFiles ($githubApiUrl, $folderPath, [Switch]$excludeFolders) {
+ # Force TLS 1.2 for connections to prevent TLS/SSL errors
+ [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
+
+ $response = Invoke-RestMethod -Uri $githubApiUrl
+ $fileUrls = $response | Where-Object { $_.type -eq "file" } | Select-Object -ExpandProperty download_url
+ $fileUrls | ForEach-Object {
+ $fileName = $_.Substring($_.LastIndexOf("/") + 1)
+ $outputFile = Join-Path $folderPath $fileName
+ Invoke-RestMethod -Uri $_ -OutFile $outputFile
+ }
+
+ If (-not $excludeFolders) {
+ $response | Where-Object { $_.type -eq "dir" } | ForEach-Object {
+ $folderName = $_.name
+ $path = Join-Path $folderPath $folderName
+ New-Item $path -ItemType Directory -Force -ErrorAction Continue
+ Get-GitHubFiles -githubApiUrl $_.url -folderPath $path
+ }
+ }
+}
+function Deploy-RetailConfigs {
+ Write-Host "[$(Get-Date -Format t)] INFO: Cleaning up images-cache namespace on all clusters" -ForegroundColor Gray
+ # Cleaning up images-cache namespace on all clusters
+ foreach ($cluster in $AgConfig.SiteConfig.GetEnumerator()) {
+ Start-Job -Name images-cache-cleanup -ScriptBlock {
+ $cluster = $using:cluster
+ $clusterName = $cluster.Name.ToLower()
+ Write-Host "[$(Get-Date -Format t)] INFO: Deleting images-cache namespace on cluster $clusterName" -ForegroundColor Gray
+ kubectl delete namespace "images-cache" --context $clusterName
+ }
+ }
+
+ # TODO - this looks app-specific so should perhaps be moved to the app loop
+ while ($workflowStatus.status -ne "completed") {
+ Write-Host "INFO: Waiting for pos-app-initial-images-build workflow to complete" -ForegroundColor Gray
+ Start-Sleep -Seconds 10
+ $workflowStatus = (gh run list --workflow=pos-app-initial-images-build.yml --json status) | ConvertFrom-Json
+ }
+
+ foreach ($cluster in $AgConfig.SiteConfig.GetEnumerator()) {
+ Start-Job -Name gitops -ScriptBlock {
+
+ Function Get-GitHubFiles ($githubApiUrl, $folderPath, [Switch]$excludeFolders) {
+ # Force TLS 1.2 for connections to prevent TLS/SSL errors
+ [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
+
+ $response = Invoke-RestMethod -Uri $githubApiUrl
+ $fileUrls = $response | Where-Object { $_.type -eq "file" } | Select-Object -ExpandProperty download_url
+ $fileUrls | ForEach-Object {
+ $fileName = $_.Substring($_.LastIndexOf("/") + 1)
+ $outputFile = Join-Path $folderPath $fileName
+ Invoke-RestMethod -Uri $_ -OutFile $outputFile
+ }
+
+ If (-not $excludeFolders) {
+ $response | Where-Object { $_.type -eq "dir" } | ForEach-Object {
+ $folderName = $_.name
+ $path = Join-Path $folderPath $folderName
+ New-Item $path -ItemType Directory -Force -ErrorAction Continue
+ Get-GitHubFiles -githubApiUrl $_.url -folderPath $path
+ }
+ }
+ }
+
+ $AgConfig = $using:AgConfig
+ $cluster = $using:cluster
+ $site = $cluster.Value
+ $siteName = $site.FriendlyName.ToLower()
+ $namingGuid = $using:namingGuid
+ $resourceGroup = $using:resourceGroup
+ $appClonedRepo = $using:appClonedRepo
+ $appsRepo = $using:appsRepo
+
+ $AgConfig.AppConfig.GetEnumerator() | sort-object -Property @{Expression = { $_.value.Order }; Ascending = $true } | ForEach-Object {
+ $app = $_
+ $store = $cluster.value.Branch.ToLower()
+ $clusterName = $cluster.value.ArcClusterName + "-$namingGuid"
+ $branch = $cluster.value.Branch.ToLower()
+ $configName = $app.value.GitOpsConfigName.ToLower()
+ $clusterType = $cluster.value.Type
+ $namespace = $app.value.Namespace
+ $appName = $app.Value.KustomizationName
+ $appPath = $app.Value.KustomizationPath
+ $retryCount = 0
+ $maxRetries = 2
+
+ Write-Host "[$(Get-Date -Format t)] INFO: Creating GitOps config for $configName on $($cluster.Value.ArcClusterName+"-$namingGuid")" -ForegroundColor Gray
+ if ($clusterType -eq "AKS") {
+ $type = "managedClusters"
+ $clusterName = $cluster.value.ArcClusterName
+ }
+ else {
+ $type = "connectedClusters"
+ }
+ if ($branch -eq "main") {
+ $store = "dev"
+ }
+
+ # Wait for Kubernetes API server to become available
+ $apiServer = kubectl config view --context $cluster.Name.ToLower() --minify -o jsonpath='{.clusters[0].cluster.server}'
+ $apiServerAddress = $apiServer -replace '.*https://| .*$'
+ $apiServerFqdn = ($apiServerAddress -split ":")[0]
+ $apiServerPort = ($apiServerAddress -split ":")[1]
+
+ do {
+ $result = Test-NetConnection -ComputerName $apiServerFqdn -Port $apiServerPort -WarningAction SilentlyContinue
+ if ($result.TcpTestSucceeded) {
+ break
+ }
+ else {
+ Start-Sleep -Seconds 5
+ }
+ } while ($true)
+ If ($app.Value.ConfigMaps) {
+ # download the config files
+ foreach ($configMap in $app.value.ConfigMaps.GetEnumerator()) {
+ $repoPath = $configMap.value.RepoPath
+ $configPath = "$configMapDir\$appPath\config\$($configMap.Name)\$branch"
+ $iotHubName = $Env:iotHubHostName.replace(".azure-devices.net", "")
+ $gitHubUser = $Env:gitHubUser
+ $githubBranch = $Env:githubBranch
+
+ New-Item -Path $configPath -ItemType Directory -Force | Out-Null
+
+ $githubApiUrl = "https://api.github.com/repos/$gitHubUser/$appsRepo/$($repoPath)?ref=$branch"
+ Get-GitHubFiles -githubApiUrl $githubApiUrl -folderPath $configPath
+
+ # replace the IoT Hub name and the SAS Tokens with the deployment specific values
+ # this is a one-off for the broker, but needs to be generalized if/when another app needs it
+ If ($configMap.Name -eq "mqtt-broker-config") {
+ $configFile = "$configPath\mosquitto.conf"
+ $update = (Get-Content $configFile -Raw)
+ $update = $update -replace "Ag-IotHub-\w*", $iotHubName
+
+ foreach ($device in $site.IoTDevices) {
+ $deviceId = "$device-$($site.FriendlyName)"
+ $deviceSASToken = $(az iot hub generate-sas-token --device-id $deviceId --hub-name $iotHubName --resource-group $resourceGroup --duration (60 * 60 * 24 * 30) --query sas -o tsv --only-show-errors)
+ $update = $update -replace "Chicago", $site.FriendlyName
+ $update = $update -replace "SharedAccessSignature.*$($device).*", $deviceSASToken
+ }
+
+ $update | Set-Content $configFile
+ }
+
+ # create the namespace if needed
+ If (-not (kubectl get namespace $namespace --context $siteName)) {
+ kubectl create namespace $namespace --context $siteName
+ }
+ # create the configmap
+ kubectl create configmap $configMap.name --from-file=$configPath --namespace $namespace --context $siteName
+ }
+ }
+
+ az k8s-configuration flux create `
+ --cluster-name $clusterName `
+ --resource-group $resourceGroup `
+ --name $configName `
+ --cluster-type $type `
+ --url $appClonedRepo `
+ --branch $branch `
+ --sync-interval 5s `
+ --kustomization name=$appName path=$appPath/$store prune=true retry_interval=1m `
+ --timeout 10m `
+ --namespace $namespace `
+ --only-show-errors `
+ 2>&1 | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\GitOps-$clusterName.log")
+
+ do {
+ $configStatus = $(az k8s-configuration flux show --name $configName --cluster-name $clusterName --cluster-type $type --resource-group $resourceGroup -o json 2>$null) | convertFrom-JSON
+ if ($configStatus.ComplianceState -eq "Compliant") {
+ Write-Host "[$(Get-Date -Format t)] INFO: GitOps configuration $configName is ready on $clusterName" -ForegroundColor DarkGreen | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\GitOps-$clusterName.log")
+ }
+ else {
+ if ($configStatus.ComplianceState -ne "Non-compliant") {
+ Start-Sleep -Seconds 20
+ }
+ elseif ($configStatus.ComplianceState -eq "Non-compliant" -and $retryCount -lt $maxRetries) {
+ Start-Sleep -Seconds 20
+ $configStatus = $(az k8s-configuration flux show --name $configName --cluster-name $clusterName --cluster-type $type --resource-group $resourceGroup -o json 2>$null) | convertFrom-JSON
+ if ($configStatus.ComplianceState -eq "Non-compliant" -and $retryCount -lt $maxRetries) {
+ $retryCount++
+ Write-Host "[$(Get-Date -Format t)] INFO: Attempting to re-install $configName on $clusterName" -ForegroundColor Gray | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\GitOps-$clusterName.log")
+ Write-Host "[$(Get-Date -Format t)] INFO: Deleting $configName on $clusterName" -ForegroundColor Gray | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\GitOps-$clusterName.log")
+ az k8s-configuration flux delete `
+ --resource-group $resourceGroup `
+ --cluster-name $clusterName `
+ --cluster-type $type `
+ --name $configName `
+ --force `
+ --yes `
+ --only-show-errors `
+ 2>&1 | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\GitOps-$clusterName.log")
+
+ Start-Sleep -Seconds 10
+ Write-Host "[$(Get-Date -Format t)] INFO: Re-creating $configName on $clusterName" -ForegroundColor Gray | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\GitOps-$clusterName.log")
+
+ az k8s-configuration flux create `
+ --cluster-name $clusterName `
+ --resource-group $resourceGroup `
+ --name $configName `
+ --cluster-type $type `
+ --url $appClonedRepo `
+ --branch $branch `
+ --sync-interval 5s `
+ --kustomization name=$appName path=$appPath/$store prune=true `
+ --timeout 30m `
+ --namespace $namespace `
+ --only-show-errors `
+ 2>&1 | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\GitOps-$clusterName.log")
+ }
+ }
+ elseif ($configStatus.ComplianceState -eq "Non-compliant" -and $retryCount -eq $maxRetries) {
+ Write-Host "[$(Get-Date -Format t)] ERROR: GitOps configuration $configName has failed on $clusterName. Exiting..." -ForegroundColor White -BackgroundColor Red | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\GitOps-$clusterName.log")
+ break
+ }
+ }
+ } until ($configStatus.ComplianceState -eq "Compliant")
+ }
+ }
+ }
+
+ while ($(Get-Job -Name gitops).State -eq 'Running') {
+ #Write-Host "[$(Get-Date -Format t)] INFO: Waiting for GitOps configuration to complete on all clusters...waiting 60 seconds" -ForegroundColor Gray
+ Receive-Job -Name gitops -WarningAction SilentlyContinue
+ Start-Sleep -Seconds 60
+ }
+
+ Get-Job -name gitops | Remove-Job
+ Write-Host "[$(Get-Date -Format t)] INFO: GitOps configuration complete." -ForegroundColor Green
+ Write-Host
+}
+
+function Deploy-RetailBookmarks {
+ $bookmarksFileName = "$AgToolsDir\Bookmarks"
+ $edgeBookmarksPath = "$Env:LOCALAPPDATA\Microsoft\Edge\User Data\Default"
+
+ foreach ($cluster in $AgConfig.SiteConfig.GetEnumerator()) {
+ kubectx $cluster.Name.ToLower() | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Bookmarks.log")
+ $services = kubectl get services --all-namespaces -o json | ConvertFrom-Json
+
+ # Matching url: pos - customer
+ $matchingServices = $services.items | Where-Object {
+ $_.spec.ports.port -contains 5000 -and
+ $_.spec.type -eq "LoadBalancer"
+ }
+ $posIps = $matchingServices.status.loadBalancer.ingress.ip
+
+ foreach ($posIp in $posIps) {
+ $output = "http://$posIp" + ':5000'
+ $output | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Bookmarks.log")
+
+ # Replace matching value in the Bookmarks file
+ $content = Get-Content -Path $bookmarksFileName
+ $newContent = $content -replace ("POS-" + $cluster.Name + "-URL-Customer"), $output
+ $newContent | Set-Content -Path $bookmarksFileName
+
+ Start-Sleep -Seconds 2
+ }
+
+ # Matching url: pos - manager
+ $matchingServices = $services.items | Where-Object {
+ $_.spec.ports.port -contains 81 -and
+ $_.spec.type -eq "LoadBalancer"
+ }
+ $posIps = $matchingServices.status.loadBalancer.ingress.ip
+
+ foreach ($posIp in $posIps) {
+ $output = "http://$posIp" + ':81'
+ $output | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Bookmarks.log")
+
+ # Replace matching value in the Bookmarks file
+ $content = Get-Content -Path $bookmarksFileName
+ $newContent = $content -replace ("POS-" + $cluster.Name + "-URL-Manager"), $output
+ $newContent | Set-Content -Path $bookmarksFileName
+
+ Start-Sleep -Seconds 2
+ }
+
+ # Matching url: prometheus-grafana
+ if ($cluster.Name -eq "Staging" -or $cluster.Name -eq "Dev") {
+ $matchingServices = $services.items | Where-Object {
+ $_.metadata.name -eq 'prometheus-grafana'
+ }
+ $grafanaIps = $matchingServices.status.loadBalancer.ingress.ip
+
+ foreach ($grafanaIp in $grafanaIps) {
+ $output = "http://$grafanaIp"
+ $output | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Bookmarks.log")
+
+ # Replace matching value in the Bookmarks file
+ $content = Get-Content -Path $bookmarksFileName
+ $newContent = $content -replace ("Grafana-" + $cluster.Name + "-URL"), $output
+ $newContent | Set-Content -Path $bookmarksFileName
+
+ Start-Sleep -Seconds 2
+ }
+ }
+
+ # Matching url: prometheus
+ $matchingServices = $services.items | Where-Object {
+ $_.spec.ports.port -contains 9090 -and
+ $_.spec.type -eq "LoadBalancer"
+ }
+ $prometheusIps = $matchingServices.status.loadBalancer.ingress.ip
+
+ foreach ($prometheusIp in $prometheusIps) {
+ $output = "http://$prometheusIp" + ':9090'
+ $output | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Bookmarks.log")
+
+ # Replace matching value in the Bookmarks file
+ $content = Get-Content -Path $bookmarksFileName
+ $newContent = $content -replace ("Prometheus-" + $cluster.Name + "-URL"), $output
+ $newContent | Set-Content -Path $bookmarksFileName
+
+ Start-Sleep -Seconds 2
+ }
+ }
+
+ # Matching url: Agora apps forked repo
+ $output = $appClonedRepo
+ $output | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Bookmarks.log")
+
+ # Replace matching value in the Bookmarks file
+ $content = Get-Content -Path $bookmarksFileName
+ $newContent = $content -replace "Agora-Apps-Repo-Clone-URL", $output
+ $newContent = $newContent -replace "Agora-Apps-Repo-Your-Fork", "Agora Apps Repo - $githubUser"
+ $newContent | Set-Content -Path $bookmarksFileName
+
+ Start-Sleep -Seconds 2
+
+ Copy-Item -Path $bookmarksFileName -Destination $edgeBookmarksPath -Force
+
+ ##############################################################
+ # Pinning important directories to Quick access
+ ##############################################################
+ Write-Host "[$(Get-Date -Format t)] INFO: Pinning important directories to Quick access (Step 16/17)" -ForegroundColor DarkGreen
+ $quickAccess = new-object -com shell.application
+ $quickAccess.Namespace($AgConfig.AgDirectories.AgDir).Self.InvokeVerb("pintohome")
+ $quickAccess.Namespace($AgConfig.AgDirectories.AgLogsDir).Self.InvokeVerb("pintohome")
+}
diff --git a/azure_jumpstart_ag/retail/artifacts/PowerShell/PSProfile.ps1 b/azure_jumpstart_ag/artifacts/PowerShell/PSProfile.ps1
similarity index 100%
rename from azure_jumpstart_ag/retail/artifacts/PowerShell/PSProfile.ps1
rename to azure_jumpstart_ag/artifacts/PowerShell/PSProfile.ps1
diff --git a/azure_jumpstart_ag/artifacts/adx_dashboards/adx-dashboard-contoso-motors-auto-parts.json b/azure_jumpstart_ag/artifacts/adx_dashboards/adx-dashboard-contoso-motors-auto-parts.json
new file mode 100644
index 0000000000..a8d6e9de68
--- /dev/null
+++ b/azure_jumpstart_ag/artifacts/adx_dashboards/adx-dashboard-contoso-motors-auto-parts.json
@@ -0,0 +1,789 @@
+{
+ "$schema": "https://dataexplorer.azure.com/static/d/schema/51/dashboard.json",
+ "id": "71f47749-912f-46d4-92ca-b61286839e73",
+ "eTag": "8b1a68cd-b2c1-46b1-8e36-20bb29f97673",
+ "schema_version": "51",
+ "title": "Contoso Motors and Auto Parts",
+ "tiles": [
+ {
+ "id": "86ba667a-63c7-4b8b-ba61-76c21d506910",
+ "title": "Type of Cars Manufactured",
+ "visualType": "multistat",
+ "pageId": "25083f67-f980-4066-a550-5f8bb1a183d6",
+ "layout": {
+ "x": 0,
+ "y": 12,
+ "width": 9,
+ "height": 7
+ },
+ "queryRef": {
+ "kind": "query",
+ "queryId": "266487a6-71ec-4dd7-9990-5f1b6a2aca2a"
+ },
+ "visualOptions": {
+ "multiStat__textSize": "auto",
+ "multiStat__valueColumn": "Model",
+ "colorRulesDisabled": false,
+ "colorRules": [
+ {
+ "id": "c693c85b-9b9a-4ea1-b296-b24cb8971319",
+ "ruleType": "colorByCondition",
+ "applyToColumn": null,
+ "hideText": false,
+ "applyTo": "cells",
+ "conditions": [
+ {
+ "operator": "==",
+ "column": "EngineType",
+ "values": [
+ "electric"
+ ]
+ }
+ ],
+ "chainingOperator": "and",
+ "visualType": "stat",
+ "colorStyle": "bold",
+ "color": "blue",
+ "tag": "Electric",
+ "icon": "car",
+ "ruleName": ""
+ },
+ {
+ "id": "cb98f18a-cf84-431a-b2c0-245671583f57",
+ "ruleType": "colorByCondition",
+ "applyToColumn": null,
+ "hideText": false,
+ "applyTo": "cells",
+ "conditions": [
+ {
+ "operator": "==",
+ "column": "EngineType",
+ "values": [
+ "hybrid"
+ ]
+ }
+ ],
+ "chainingOperator": "and",
+ "visualType": "stat",
+ "colorStyle": "bold",
+ "color": "green",
+ "tag": "Hybrid",
+ "icon": "car",
+ "ruleName": ""
+ },
+ {
+ "id": "561d4a9f-d446-4dcd-a534-f8ba657e21f9",
+ "ruleType": "colorByCondition",
+ "applyToColumn": null,
+ "hideText": false,
+ "applyTo": "cells",
+ "conditions": [
+ {
+ "values": [
+ "gasoline"
+ ],
+ "operator": "==",
+ "column": "EngineType"
+ }
+ ],
+ "chainingOperator": "and",
+ "visualType": "stat",
+ "colorStyle": "bold",
+ "color": "yellow",
+ "tag": "Gasoline",
+ "icon": "car",
+ "ruleName": ""
+ }
+ ],
+ "colorStyle": "light",
+ "multiStat__displayOrientation": "horizontal",
+ "multiStat__labelColumn": "EngineType",
+ "multiStat__slot": {
+ "width": 3,
+ "height": 1
+ }
+ }
+ },
+ {
+ "id": "2fce3f37-c0f9-4edd-9be5-83188a2a6a46",
+ "title": "Production Quality Control",
+ "visualType": "multistat",
+ "pageId": "25083f67-f980-4066-a550-5f8bb1a183d6",
+ "layout": {
+ "x": 3,
+ "y": 8,
+ "width": 6,
+ "height": 4
+ },
+ "queryRef": {
+ "kind": "query",
+ "queryId": "caa8a37f-0829-497e-ac88-6b96c03137e1"
+ },
+ "visualOptions": {
+ "multiStat__textSize": "auto",
+ "multiStat__valueColumn": "Value",
+ "colorRulesDisabled": false,
+ "colorRules": [
+ {
+ "id": "fadb24cf-114b-49d4-bbc4-e09ec77d8dce",
+ "ruleType": "colorByCondition",
+ "applyToColumn": null,
+ "hideText": false,
+ "applyTo": "cells",
+ "conditions": [
+ {
+ "operator": "==",
+ "column": "Column",
+ "values": [
+ "Passed"
+ ]
+ }
+ ],
+ "chainingOperator": "and",
+ "visualType": "stat",
+ "colorStyle": "bold",
+ "color": "green",
+ "tag": "",
+ "icon": null,
+ "ruleName": ""
+ },
+ {
+ "id": "fc43b0fb-e9bd-4372-b018-4119bc5db734",
+ "ruleType": "colorByCondition",
+ "applyToColumn": null,
+ "hideText": false,
+ "applyTo": "cells",
+ "conditions": [
+ {
+ "operator": "==",
+ "column": "Column",
+ "values": [
+ "Failed"
+ ]
+ }
+ ],
+ "chainingOperator": "and",
+ "visualType": "stat",
+ "colorStyle": "bold",
+ "color": "red",
+ "tag": "",
+ "icon": null,
+ "ruleName": ""
+ }
+ ],
+ "colorStyle": "light",
+ "multiStat__displayOrientation": "horizontal",
+ "multiStat__labelColumn": null,
+ "multiStat__slot": {
+ "width": 2,
+ "height": 1
+ }
+ }
+ },
+ {
+ "id": "a805f6c6-c356-4915-9ab3-2a13d17302a2",
+ "title": "Actual Production Statistics",
+ "visualType": "multistat",
+ "pageId": "25083f67-f980-4066-a550-5f8bb1a183d6",
+ "layout": {
+ "x": 3,
+ "y": 4,
+ "width": 6,
+ "height": 4
+ },
+ "queryRef": {
+ "kind": "query",
+ "queryId": "3f86cb95-ad80-450b-9b07-f0c4f324a70e"
+ },
+ "visualOptions": {
+ "multiStat__textSize": "auto",
+ "multiStat__valueColumn": "Value",
+ "colorRulesDisabled": false,
+ "colorRules": [
+ {
+ "id": "c9b5efb8-db51-4db1-b681-fe6f408829c0",
+ "ruleType": "colorByCondition",
+ "applyToColumn": null,
+ "hideText": false,
+ "applyTo": "cells",
+ "conditions": [
+ {
+ "operator": "==",
+ "column": "Column",
+ "values": [
+ "UnitsManufactured"
+ ]
+ }
+ ],
+ "chainingOperator": "and",
+ "visualType": "stat",
+ "colorStyle": "bold",
+ "color": "green",
+ "tag": "",
+ "icon": "completed",
+ "ruleName": ""
+ },
+ {
+ "id": "f7e65c83-ba3d-4177-abbe-008df52934c4",
+ "ruleType": "colorByCondition",
+ "applyToColumn": null,
+ "hideText": false,
+ "applyTo": "cells",
+ "conditions": [
+ {
+ "operator": "==",
+ "column": "Column",
+ "values": [
+ "UnitsRejected"
+ ]
+ }
+ ],
+ "chainingOperator": "and",
+ "visualType": "stat",
+ "colorStyle": "bold",
+ "color": "red",
+ "tag": "",
+ "icon": "critical",
+ "ruleName": ""
+ }
+ ],
+ "colorStyle": "light",
+ "multiStat__displayOrientation": "vertical",
+ "multiStat__labelColumn": null,
+ "multiStat__slot": {
+ "width": 2,
+ "height": 1
+ }
+ }
+ },
+ {
+ "id": "fc15aa93-a039-465b-8fc9-1bcf97c76cf7",
+ "title": "Equipment Overall Efficiency",
+ "visualType": "multistat",
+ "pageId": "25083f67-f980-4066-a550-5f8bb1a183d6",
+ "layout": {
+ "x": 9,
+ "y": 4,
+ "width": 9,
+ "height": 8
+ },
+ "queryRef": {
+ "kind": "query",
+ "queryId": "ceb76c0b-64cf-487f-9b1a-41c7c1adf68a"
+ },
+ "visualOptions": {
+ "multiStat__textSize": "auto",
+ "multiStat__valueColumn": "Efficiency",
+ "colorRulesDisabled": false,
+ "colorRules": [
+ {
+ "id": "63328d11-72ad-4d5f-80fd-75b27c1d69a9",
+ "ruleType": "colorByCondition",
+ "applyToColumn": null,
+ "hideText": false,
+ "applyTo": "cells",
+ "conditions": [
+ {
+ "operator": ">",
+ "column": "Efficiency",
+ "values": [
+ "95%"
+ ]
+ }
+ ],
+ "chainingOperator": "and",
+ "visualType": "stat",
+ "colorStyle": "bold",
+ "color": "green",
+ "tag": "Excellent",
+ "icon": "happy",
+ "ruleName": ""
+ },
+ {
+ "id": "4712b226-a175-4f58-b3ca-27f0233f6790",
+ "ruleType": "colorByCondition",
+ "applyToColumn": null,
+ "hideText": false,
+ "applyTo": "cells",
+ "conditions": [
+ {
+ "operator": ">=",
+ "column": "Efficiency",
+ "values": [
+ "90"
+ ]
+ },
+ {
+ "values": [
+ "95"
+ ],
+ "operator": "<",
+ "column": "Efficiency"
+ }
+ ],
+ "chainingOperator": "and",
+ "visualType": "stat",
+ "colorStyle": "bold",
+ "color": "blue",
+ "tag": "Good",
+ "icon": "emojiNeutral",
+ "ruleName": ""
+ },
+ {
+ "id": "a55db212-3c59-4b6c-af8f-da020772e4fe",
+ "ruleType": "colorByCondition",
+ "applyToColumn": null,
+ "hideText": false,
+ "applyTo": "cells",
+ "conditions": [
+ {
+ "operator": "<",
+ "column": "Efficiency",
+ "values": [
+ "90"
+ ]
+ }
+ ],
+ "chainingOperator": "and",
+ "visualType": "stat",
+ "colorStyle": "bold",
+ "color": "yellow",
+ "tag": "Fair",
+ "icon": "sad",
+ "ruleName": ""
+ }
+ ],
+ "colorStyle": "light",
+ "multiStat__displayOrientation": "horizontal",
+ "multiStat__labelColumn": "EquipmentID",
+ "multiStat__slot": {
+ "width": 3,
+ "height": 2
+ }
+ }
+ },
+ {
+ "id": "8edfae11-6e9f-4d42-a6df-5ac010294610",
+ "title": "Contoso Motors and Auto Parts",
+ "visualType": "markdownCard",
+ "pageId": "25083f67-f980-4066-a550-5f8bb1a183d6",
+ "layout": {
+ "x": 0,
+ "y": 0,
+ "width": 18,
+ "height": 4
+ },
+ "markdownText": "![Contoso Motors adn Auto Parts](https://github.com/{{GITHUB_ACCOUNT}}/azure_arc/blob/{{GITHUB_BRANCH}}/azure_jumpstart_ag/artifacts/adx_dashboards/contoso-motors-autoparts.png?raw=true)",
+ "visualOptions": {}
+ },
+ {
+ "id": "67de7813-5182-483d-8e7f-b360b1ce1d5f",
+ "title": "Overall Efficiency",
+ "visualType": "card",
+ "pageId": "25083f67-f980-4066-a550-5f8bb1a183d6",
+ "layout": {
+ "x": 0,
+ "y": 4,
+ "width": 3,
+ "height": 4
+ },
+ "queryRef": {
+ "kind": "query",
+ "queryId": "6e57f2be-95d5-4805-98e4-3a57278c4f90"
+ },
+ "visualOptions": {
+ "multiStat__textSize": "auto",
+ "multiStat__valueColumn": null,
+ "colorRulesDisabled": false,
+ "colorRules": [
+ {
+ "id": "2d8c2bed-f661-4ad2-97fa-cee1b1a7cbae",
+ "ruleType": "colorByCondition",
+ "applyToColumn": null,
+ "hideText": false,
+ "applyTo": "cells",
+ "conditions": [
+ {
+ "operator": ">",
+ "column": null,
+ "values": [
+ "90"
+ ]
+ }
+ ],
+ "chainingOperator": "and",
+ "visualType": "stat",
+ "colorStyle": "bold",
+ "color": "green",
+ "tag": "",
+ "icon": "happy",
+ "ruleName": ""
+ },
+ {
+ "id": "b615fe3a-54a1-4b10-bff2-b51ac7c5bd5d",
+ "ruleType": "colorByCondition",
+ "applyToColumn": null,
+ "hideText": false,
+ "applyTo": "cells",
+ "conditions": [
+ {
+ "operator": ">",
+ "column": null,
+ "values": [
+ "80"
+ ]
+ },
+ {
+ "values": [
+ "90"
+ ],
+ "operator": "==",
+ "column": null
+ }
+ ],
+ "chainingOperator": "and",
+ "visualType": "stat",
+ "colorStyle": "bold",
+ "color": "yellow",
+ "tag": "",
+ "icon": "emojiNeutral",
+ "ruleName": ""
+ },
+ {
+ "id": "4eba33ea-9772-4f05-817c-aedc751609d8",
+ "ruleType": "colorByCondition",
+ "applyToColumn": null,
+ "hideText": false,
+ "applyTo": "cells",
+ "conditions": [
+ {
+ "operator": "<",
+ "column": null,
+ "values": [
+ "80"
+ ]
+ }
+ ],
+ "chainingOperator": "and",
+ "visualType": "stat",
+ "colorStyle": "bold",
+ "color": "red",
+ "tag": "Poor",
+ "icon": "sad",
+ "ruleName": ""
+ }
+ ],
+ "colorStyle": "light"
+ }
+ },
+ {
+ "id": "0236925d-7aec-4fb6-93cd-95e9934a26f3",
+ "title": "Contoso Motors and Auto Parts",
+ "visualType": "markdownCard",
+ "pageId": "b26b3811-394b-4451-9407-40d162f34d1c",
+ "layout": {
+ "x": 0,
+ "y": 0,
+ "width": 18,
+ "height": 4
+ },
+ "markdownText": "![Contoso Motors adn Auto Parts](https://github.com/{{GITHUB_ACCOUNT}}/azure_arc/blob/{{GITHUB_BRANCH}}/azure_jumpstart_ag/artifacts/adx_dashboards/contoso-motors-autoparts.png?raw=true)",
+ "visualOptions": {}
+ },
+ {
+ "id": "8ca3cd34-13aa-481d-82ee-ccdf69603f76",
+ "title": "Cars Manufactured by Model",
+ "visualType": "pie",
+ "pageId": "b26b3811-394b-4451-9407-40d162f34d1c",
+ "layout": {
+ "x": 9,
+ "y": 4,
+ "width": 9,
+ "height": 7
+ },
+ "queryRef": {
+ "kind": "query",
+ "queryId": "9277db4c-0c4d-4739-b571-df776263f87d"
+ },
+ "visualOptions": {
+ "hideLegend": false,
+ "xColumn": null,
+ "yColumns": null,
+ "seriesColumns": null,
+ "crossFilterDisabled": false,
+ "drillthroughDisabled": false,
+ "labelDisabled": false,
+ "pie__label": [
+ "name",
+ "percentage"
+ ],
+ "tooltipDisabled": false,
+ "pie__tooltip": [
+ "name",
+ "percentage",
+ "value"
+ ],
+ "pie__orderBy": "size",
+ "pie__kind": "pie",
+ "pie__topNSlices": null,
+ "crossFilter": [],
+ "drillthrough": []
+ }
+ },
+ {
+ "id": "ffdc38cb-2792-467f-8445-1c4030398257",
+ "title": "Cars Manufactured by Plant Location",
+ "visualType": "bar",
+ "pageId": "b26b3811-394b-4451-9407-40d162f34d1c",
+ "layout": {
+ "x": 0,
+ "y": 4,
+ "width": 9,
+ "height": 7
+ },
+ "queryRef": {
+ "kind": "query",
+ "queryId": "bd5c10b0-a055-4888-bab0-9ba38fce173b"
+ },
+ "visualOptions": {
+ "multipleYAxes": {
+ "base": {
+ "id": "-1",
+ "label": "Units Manufactured vs Rejected",
+ "columns": [],
+ "yAxisMaximumValue": null,
+ "yAxisMinimumValue": null,
+ "yAxisScale": "linear",
+ "horizontalLines": []
+ },
+ "additional": [],
+ "showMultiplePanels": false
+ },
+ "hideLegend": false,
+ "xColumnTitle": "Plant Location",
+ "xColumn": null,
+ "yColumns": null,
+ "seriesColumns": null,
+ "xAxisScale": "linear",
+ "verticalLine": "",
+ "crossFilterDisabled": false,
+ "drillthroughDisabled": false,
+ "crossFilter": [],
+ "drillthrough": []
+ }
+ },
+ {
+ "id": "c2ec69a9-9f1e-485f-a49c-44a28c27ba65",
+ "title": "Availability",
+ "visualType": "card",
+ "pageId": "25083f67-f980-4066-a550-5f8bb1a183d6",
+ "layout": {
+ "x": 0,
+ "y": 8,
+ "width": 3,
+ "height": 4
+ },
+ "queryRef": {
+ "kind": "query",
+ "queryId": "bfab8c5d-284e-4309-a647-7e37c4b8c2ba"
+ },
+ "visualOptions": {
+ "multiStat__textSize": "auto",
+ "multiStat__valueColumn": null,
+ "colorRulesDisabled": false,
+ "colorRules": [
+ {
+ "id": "a7d41089-df67-4dae-a974-a5fd4116003f",
+ "ruleType": "colorByCondition",
+ "applyToColumn": null,
+ "hideText": false,
+ "applyTo": "cells",
+ "conditions": [
+ {
+ "operator": ">",
+ "column": null,
+ "values": [
+ "90"
+ ]
+ }
+ ],
+ "chainingOperator": "and",
+ "visualType": "stat",
+ "colorStyle": "bold",
+ "color": "green",
+ "tag": "Good",
+ "icon": "completed",
+ "ruleName": ""
+ },
+ {
+ "id": "3d2d5448-8bf9-4b7f-add5-d050d8a2b75d",
+ "ruleType": "colorByCondition",
+ "applyToColumn": null,
+ "hideText": false,
+ "applyTo": "cells",
+ "conditions": [
+ {
+ "operator": ">",
+ "column": null,
+ "values": [
+ "80"
+ ]
+ },
+ {
+ "operator": "<=",
+ "column": null,
+ "values": [
+ "90"
+ ]
+ }
+ ],
+ "chainingOperator": "and",
+ "visualType": "stat",
+ "colorStyle": "bold",
+ "color": "yellow",
+ "tag": "Fair",
+ "icon": "critical",
+ "ruleName": ""
+ },
+ {
+ "id": "912eb6bb-40f4-4a1e-82b4-c287343c878a",
+ "ruleType": "colorByCondition",
+ "applyToColumn": null,
+ "hideText": false,
+ "applyTo": "cells",
+ "conditions": [
+ {
+ "operator": "<",
+ "column": null,
+ "values": [
+ "80"
+ ]
+ }
+ ],
+ "chainingOperator": "and",
+ "visualType": "stat",
+ "colorStyle": "bold",
+ "color": "red",
+ "tag": "",
+ "icon": "warning",
+ "ruleName": ""
+ }
+ ],
+ "colorStyle": "light"
+ }
+ }
+ ],
+ "baseQueries": [],
+ "parameters": [
+ {
+ "kind": "duration",
+ "id": "72e78587-ba97-4390-9374-b93688428c00",
+ "displayName": "Time range",
+ "description": "",
+ "beginVariableName": "_startTime",
+ "endVariableName": "_endTime",
+ "defaultValue": {
+ "kind": "dynamic",
+ "count": 1,
+ "unit": "hours"
+ },
+ "showOnPages": {
+ "kind": "all"
+ }
+ }
+ ],
+ "dataSources": [
+ {
+ "id": "0a4a6594-6c22-4a61-9c94-63ddb16967af",
+ "name": "{{ADX_CLUSTER_NAME}}",
+ "scopeId": "kusto",
+ "clusterUri": "{{ADX_CLUSTER_URI}}",
+ "database": "manufacturing",
+ "kind": "manual-kusto"
+ }
+ ],
+ "pages": [
+ {
+ "name": "Production Efficiency",
+ "id": "25083f67-f980-4066-a550-5f8bb1a183d6"
+ },
+ {
+ "id": "b26b3811-394b-4451-9407-40d162f34d1c",
+ "name": "Production Metrics"
+ }
+ ],
+ "queries": [
+ {
+ "dataSource": {
+ "kind": "inline",
+ "dataSourceId": "0a4a6594-6c22-4a61-9c94-63ddb16967af"
+ },
+ "text": "assemblyline\n| extend cp= parse_json(cars_produced)\n| mv-apply cp on (extend cp.model, cp.engine_type)\n| distinct Model = tostring(cp.model), EngineType = tostring(cp.engine_type)\n",
+ "id": "266487a6-71ec-4dd7-9990-5f1b6a2aca2a",
+ "usedVariables": []
+ },
+ {
+ "dataSource": {
+ "kind": "inline",
+ "dataSourceId": "0a4a6594-6c22-4a61-9c94-63ddb16967af"
+ },
+ "text": "assemblyline\n| extend cp= parse_json(cars_produced)\n| mv-apply cp on (extend cp.model, cp.engine_type, cp.assembly_status, cp.quality_check)\n| where cp.assembly_status == \"completed\"\n| summarize Passed = countif(cp.quality_check == \"pass\"), Failed = countif(cp.quality_check == \"fail\") | evaluate narrow()\n| project Column, toint(Value)",
+ "id": "caa8a37f-0829-497e-ac88-6b96c03137e1",
+ "usedVariables": []
+ },
+ {
+ "dataSource": {
+ "kind": "inline",
+ "dataSourceId": "0a4a6594-6c22-4a61-9c94-63ddb16967af"
+ },
+ "text": "// Final cars production\nassemblyline\n| extend ap= parse_json(actual_production)\n| mv-apply ap on (extend ap.units_manufactured, ap.units_rejected)\n| summarize UnitsManufactured = sum(toint(ap.units_manufactured)), UnitsRejected = sum(toint(ap.units_rejected)) | evaluate narrow()\n| project Column, toint(Value)",
+ "id": "3f86cb95-ad80-450b-9b07-f0c4f324a70e",
+ "usedVariables": []
+ },
+ {
+ "dataSource": {
+ "kind": "inline",
+ "dataSourceId": "0a4a6594-6c22-4a61-9c94-63ddb16967af"
+ },
+ "text": "assemblyline\n| extend ee = parse_json(equipment_telemetry)\n| mv-apply ee on (extend ee.date_time, ee.equipment_id, ee.efficiency)\n| summarize Efficiency = round(avg( toreal(ee.efficiency)), 2) by EquipmentID = tostring(ee.equipment_id) \n",
+ "id": "ceb76c0b-64cf-487f-9b1a-41c7c1adf68a",
+ "usedVariables": []
+ },
+ {
+ "dataSource": {
+ "kind": "inline",
+ "dataSourceId": "0a4a6594-6c22-4a61-9c94-63ddb16967af"
+ },
+ "text": "assemblyline\n| extend ee = parse_json(equipment_telemetry)\n| mv-apply ee on (extend ee.date_time, ee.equipment_id, ee.efficiency)\n| summarize Efficiency = round(avg( toreal(ee.efficiency)), 2)",
+ "id": "6e57f2be-95d5-4805-98e4-3a57278c4f90",
+ "usedVariables": []
+ },
+ {
+ "dataSource": {
+ "kind": "inline",
+ "dataSourceId": "0a4a6594-6c22-4a61-9c94-63ddb16967af"
+ },
+ "text": "// Total cars manufactured by type\nassemblyline\n| extend cp= parse_json(cars_produced)\n| mv-apply cp on (extend cp.model, cp.engine_type, cp.assembly_status, cp.quality_check)\n| where cp.assembly_status == \"completed\"\n| summarize count() by tostring(cp.model)",
+ "id": "9277db4c-0c4d-4739-b571-df776263f87d",
+ "usedVariables": []
+ },
+ {
+ "dataSource": {
+ "kind": "inline",
+ "dataSourceId": "0a4a6594-6c22-4a61-9c94-63ddb16967af"
+ },
+ "text": "// Car production by plant\nassemblyline\n| extend ap= parse_json(actual_production), pd = parse_json(plant_details)\n| mv-apply ap on (extend ap.units_manufactured, ap.units_rejected)\n| mv-apply ap on (extend pd.plant_id, pd.location)\n| where ap.units_manufactured > 0 or ap.units_rejected > 0\n| project plant_id = pd.plant_id, location = pd.location, units_manufactured = ap.units_manufactured, units_rejected = ap.units_rejected\n| summarize UnitsManufactured = sum(toint(units_manufactured)), UnitsRejected = sum(toint(units_rejected)) by Location = tostring(location) \n",
+ "id": "bd5c10b0-a055-4888-bab0-9ba38fce173b",
+ "usedVariables": []
+ },
+ {
+ "dataSource": {
+ "kind": "inline",
+ "dataSourceId": "0a4a6594-6c22-4a61-9c94-63ddb16967af"
+ },
+ "text": "assemblyline\n| extend pm = parse_json(performance_metrics)\n| mv-apply pm on (extend pm.availability_oee)\n| summarize availability = round(avg(toreal(pm.availability_oee)) * 100.0, 2)",
+ "id": "bfab8c5d-284e-4309-a647-7e37c4b8c2ba",
+ "usedVariables": []
+ }
+ ]
+}
\ No newline at end of file
diff --git a/azure_jumpstart_ag/retail/artifacts/adx_dashboards/adx-dashboard-iotsensor-payload.json b/azure_jumpstart_ag/artifacts/adx_dashboards/adx-dashboard-iotsensor-payload.json
similarity index 100%
rename from azure_jumpstart_ag/retail/artifacts/adx_dashboards/adx-dashboard-iotsensor-payload.json
rename to azure_jumpstart_ag/artifacts/adx_dashboards/adx-dashboard-iotsensor-payload.json
diff --git a/azure_jumpstart_ag/retail/artifacts/adx_dashboards/adx-dashboard-orders-payload.json b/azure_jumpstart_ag/artifacts/adx_dashboards/adx-dashboard-orders-payload.json
similarity index 100%
rename from azure_jumpstart_ag/retail/artifacts/adx_dashboards/adx-dashboard-orders-payload.json
rename to azure_jumpstart_ag/artifacts/adx_dashboards/adx-dashboard-orders-payload.json
diff --git a/azure_jumpstart_ag/artifacts/adx_dashboards/contoso-motors-autoparts.png b/azure_jumpstart_ag/artifacts/adx_dashboards/contoso-motors-autoparts.png
new file mode 100644
index 0000000000..eebca5a038
Binary files /dev/null and b/azure_jumpstart_ag/artifacts/adx_dashboards/contoso-motors-autoparts.png differ
diff --git a/azure_jumpstart_ag/retail/artifacts/data_emulator/DataEmulator.zip b/azure_jumpstart_ag/artifacts/data_emulator/DataEmulator.zip
similarity index 100%
rename from azure_jumpstart_ag/retail/artifacts/data_emulator/DataEmulator.zip
rename to azure_jumpstart_ag/artifacts/data_emulator/DataEmulator.zip
diff --git a/azure_jumpstart_ag/artifacts/data_emulator/data-emulator.py b/azure_jumpstart_ag/artifacts/data_emulator/data-emulator.py
new file mode 100644
index 0000000000..f312a22f3d
--- /dev/null
+++ b/azure_jumpstart_ag/artifacts/data_emulator/data-emulator.py
@@ -0,0 +1,369 @@
+import json
+import random
+from datetime import timedelta, time, datetime
+import time
+import os
+from azure.eventhub import EventHubProducerClient, EventData
+import sys
+import uuid
+
+# Create following environment variables to publish events to Event Hub
+# EVENTHUB_CONNECTION_STRING = ''
+# EVENTHUB_NAME = 'manufacturing'
+# HISTORICAL_DATA_DAYS = 7
+
+EVENTHUB_CONNECTION_STRING = os.environ.get('EVENTHUB_CONNECTION_STRING')
+EVENTHUB_NAME = os.environ.get('EVENTHUB_NAME')
+HISTORICAL_DATA_DAYS = (0-int(os.environ.get('HISTORICAL_DATA_DAYS', 7))) # This is to generate date prior to current date for dashboards.
+
+# Create event producer as a global object to avoid connection creation overhead for each event.
+if EVENTHUB_CONNECTION_STRING == "" or EVENTHUB_NAME == "":
+ print('Event Hub connection string and/or Event Hub name not configured.')
+ sys.exit()
+
+# Initialize EventHub connection to send events
+event_producer = EventHubProducerClient.from_connection_string(conn_str=EVENTHUB_CONNECTION_STRING, eventhub_name=EVENTHUB_NAME)
+
+maintenance_last_generated = datetime.now()
+production_last_generated = datetime.now()
+
+# Define common schema templates to generate telemetry data
+plant_details = [
+ { "plant_id": "PT-01", "location": "Detroit, US" },
+ { "plant_id": "PT-02", "location": "Monterrey, MX" },
+ { "plant_id": "PT-03", "location": "Shanghai, CN" },
+ { "plant_id": "PT-04", "location": "Hamburg, DE" }
+]
+
+production_schedule = {"production_date": "2024-03-20", "scheduled_start": "20:00", "scheduled_end": "08:00", "planned_production_time_hours": 12, "actual_production_time_hours": 10}
+
+employees = [
+ {"employee_id": "E-001", "name": "John Doe", "role": "supervisor"},
+ {"employee_id": "E-002", "name": "Jane Smith", "role": "engineer"},
+ {"employee_id": "E-003", "name": "Mike Johnson", "role": "technician"}
+]
+
+cars_produced = [
+ {"assembly_line": "AL-01", "car_id": "E-001", "model": "Sedan", "color": "red", "engine_type": "electric", "assembly_status": "none", "quality_check": "none" },
+ {"assembly_line": "AL-02", "car_id": "H-002", "model": "SUV", "color": "blue", "engine_type": "hybrid", "assembly_status": "none", "quality_check": "none" },
+ {"assembly_line": "AL-03", "car_id": "G-003", "model": "Coupe", "color": "black", "engine_type": "gasoline", "assembly_status": "none", "quality_check": "none" }
+]
+
+equipment_info = [
+ {
+ "equipment_id": "EQ-001",
+ "type": "assembly_robot",
+ "maintenance_schedule": "30d",
+ "technical_specs": {}
+ },
+ {
+ "equipment_id": "EQ-002",
+ "type": "conveyor_belt",
+ "maintenance_schedule": "10d",
+ "technical_specs": {}
+ },
+ {
+ "equipment_id": "EQ-003",
+ "type": "paint_station",
+ "maintenance_schedule": "90d",
+ "technical_specs": {}
+ },
+ {
+ "equipment_id": "EQ-004",
+ "type": "welding-assembly_robot",
+ "model": "RoboArm X2000",
+ "maintenance_schedule": "15d",
+ "technical_specs": {
+ "arm_reach": "1.5 meters",
+ "load_capacity": "10 kg",
+ "precision": "0.02 mm",
+ "rotation": "360 degrees"
+ }
+ },
+ {
+ "equipment_id": "WLD-001",
+ "type": "welding_robot",
+ "model": "WeldMaster 3000",
+ "maintenance_schedule": "5d",
+ "technical_specs": {
+ "welding_speed": "1.5 meters per minute",
+ "welding_technologies": ["MIG", "TIG"],
+ "maximum_thickness": "10 mm",
+ "precision": "+/- 0.5 mm"
+ }
+ }
+]
+
+# Equipment
+equipment_maintenance_history = [
+ {
+ "equipment_id": "WLD-001",
+ "date": "2024-03-20",
+ "start_time": "10:21",
+ "end_time": "12:30",
+ "type": "routine_check",
+ "notes": "All systems operational, no issues found."
+ },
+ {
+ "equipment_id": "WLD-001",
+ "date": "2024-01-15",
+ "start_time": "8:00",
+ "end_time": "8:30",
+ "type": "repair",
+ "notes": "Replaced servo motor in joint 3."
+ }
+]
+
+maintenance_types = [
+ { "type": "routine_check", "message": "All systems operational, no issues found" },
+ { "type": "emergency_check", "message": "Equipment is in critical condition, need immediate attention" }
+]
+
+production_shifts = [
+ { "shift":"morning", "utc_start_hour":0, "utc_end_hour":7 },
+ { "shift":"afternoon", "utc_start_hour":8, "utc_end_hour":15 },
+ { "shift":"night", "utc_start_hour":16, "utc_end_hour":23 }
+]
+
+# Get shift information
+def get_shift(current_date_time):
+ for shift in production_shifts:
+ if current_date_time.hour >= shift['utc_start_hour'] and current_date_time.hour <= shift['utc_end_hour']:
+ return shift['shift']
+
+# produce this randomly with random delay between 1 to 3 hours
+def simulate_equipment_maintenance(current_time):
+ return {
+ "equipment_id": "WLD-001", # Pick random equipment
+ "maintenance_date": str(current_time.date),
+ "start_time": "10:21", # Produce random
+ "end_time": "12:30", # Add random duration
+ "type": "routine_check",
+ "notes": "All systems operational, no issues found."
+ }
+
+# Produce equipment telemetry every 30 seconds or a minute
+def simulate_equipment_telemetry(current_time):
+ # Convert time string
+ current_time = str(current_time)
+
+ equipment_telemetry = [
+ # Assembly Robot
+ {
+ "date_time": current_time,
+ "equipment_id": "EQ-001",
+ "type": "assembly_robot",
+ "status": random.choice(["operational", "maintenance_required"]), # Instead of random, use periodic maintenance
+ "operational_time_hours": random.uniform(10, 12),
+ "cycles_completed": random.randint(3400, 3500),
+ "efficiency": random.uniform(93, 97),
+ "maintenance_alert": random.choice(["none", "scheduled_check", "urgent_maintenance_required"]), # Instead of random, use periodic maintenance alerts
+ "last_maintenance": "2024-02-20",
+ "next_scheduled_maintenance": "2024-04-01"
+ },
+ # Conveyor Belt
+ {
+ "date_time": current_time,
+ "equipment_id": "EQ-002",
+ "type": "conveyor_belt",
+ "status": random.choice(["operational", "maintenance_required"]),
+ "operational_time_hours": random.uniform(10, 12),
+ "distance_covered_meters": random.randint(10000, 12000),
+ "efficiency": random.uniform(98, 99),
+ "maintenance_alert": random.choice(["none", "scheduled_check"]),
+ "last_maintenance": "2024-03-15",
+ "next_scheduled_maintenance": "2024-03-30"
+ },
+ # Paint Station
+ {
+ "date_time": current_time,
+ "equipment_id": "EQ-003",
+ "type": "paint_station",
+ "status": random.choice(["operational", "maintenance_required"]),
+ "operational_time_hours": random.uniform(7, 9),
+ "units_processed": random.randint(400, 500),
+ "efficiency": random.uniform(90, 93),
+ "maintenance_alert": random.choice(["none", "urgent_maintenance_required"]),
+ "last_maintenance": "2024-02-25",
+ "next_scheduled_maintenance": "Overdue"
+ },
+ # Welding-Assembly Robot
+ {
+ "date_time": current_time,
+ "equipment_id": "EQ-004",
+ "status": random.choice(["operational", "maintenance_required"]),
+ "operational_time_hours": random.uniform(10, 12),
+ "cycles_completed": random.randint(3400, 3500),
+ "efficiency": random.uniform(94, 96),
+ "maintenance_alert": random.choice(["none", "scheduled_check"]),
+ "last_maintenance": "2024-03-20",
+ "next_scheduled_maintenance": "2024-04-01",
+ "operation_stats": {
+ "average_cycle_time": "10 seconds",
+ "failures_last_month": random.randint(1, 3),
+ "success_rate": random.uniform(98.5, 99.9)
+ }
+ },
+ # Welding Robot
+ {
+ "date_time": current_time,
+ "equipment_id": "WLD-001",
+ "status": random.choice(["operational", "maintenance_required"]),
+ "operational_time_hours": random.uniform(9, 11),
+ "welds_completed": random.randint(5100, 5300),
+ "efficiency": random.uniform(97, 99),
+ "maintenance_alert": random.choice(["none", "scheduled_check"]),
+ "last_maintenance": "2024-03-22",
+ "next_scheduled_maintenance": "2024-04-05",
+ "operation_stats": {
+ "average_weld_time": "30 seconds",
+ "failures_last_month": random.randint(0, 3),
+ "success_rate": random.uniform(98, 99.9)
+ }
+ }
+ ]
+
+ return equipment_telemetry
+
+# Update assembly status with progress, start with in_progress and end with completed. Update status every 1 minutes
+def simulate_production_telemetry():
+ # Once the status completed change production id
+ for car in cars_produced:
+ car['assembly_status'] = random.choice(["completed", "in_progress"])
+ if car['assembly_status'] == "completed":
+ car['quality_check'] = random.choice(["pass", "fail"])
+
+ return cars_produced
+
+# Produce this data for every 8 hours
+def simulate_production_by_shift():
+ return {}
+
+# Generate performance metrics
+def generate_performance_metrics():
+ return {
+ "availability_oee": random.uniform(0.95, 0.99),
+ "reject_rate": random.uniform(0.04, 0.06),
+ "comments": "Minor downtime due to equipment maintenance. Overall production efficiency remains high."
+ }
+
+# Generate actual production data
+def generate_actual_production_data(date_time):
+ return {
+ "start_time": "20:00",
+ "end_time": "07:30",
+ "actual_production_time_hours": 11.5,
+ "production_downtime_hours": 0.5,
+ "units_manufactured": random.randint(690, 710),
+ "units_rejected": random.randint(30, 40),
+ "details": [
+ {"utc_hour": "20", "units_produced": random.randint(55, 60), "units_rejected": random.randint(2, 4)},
+ # Additional hourly details can be added here
+ ]
+ }
+
+# Produce assembly production data every minute for realtime data and every hour for historical data
+def simulate_assembly_line_data(date_time):
+
+ # Get current shift
+ current_shift = get_shift(date_time)
+
+ # Produce cars production progress data every time this method is called
+ cars_produced = simulate_production_telemetry()
+
+ # Produce cars production stats every one hour
+ global maintenance_last_generated
+ if int((date_time - maintenance_last_generated).total_seconds() / 60) > 5:
+ actual_production = simulate_production_by_shift()
+ else:
+ actual_production = {}
+
+ # Produce equipment telemetry data every time this method is called
+ equipment_telemetry = simulate_equipment_telemetry(date_time)
+
+ # Produce equipment maintenance data every 5 minutes
+ global production_last_generated
+ if int((date_time - production_last_generated).total_seconds() / (5 *60)) > 5:
+ equipment_maintenance = simulate_equipment_maintenance(date_time)
+ maintenance_last_generated = date_time
+ else:
+ equipment_maintenance = {}
+
+ # Make this by shift production information
+ actual_production = generate_actual_production_data(date_time)
+
+ # Include in the hourly production performance metrics
+ performance_metrics = generate_performance_metrics()
+
+ # Split this into hourly and by shift
+ current_time = date_time.isoformat() + "Z"
+ simulation_data = {
+ "date_time": current_time,
+ "plant_details": plant_details[random.randint(0, len(plant_details)-1)],
+ "shift": current_shift,
+ "employees_on_shift": employees,
+ "cars_produced": cars_produced,
+ "equipment_maintenance": equipment_maintenance,
+ "production_schedule": production_schedule,
+ "actual_production": actual_production,
+ "equipment_telemetry": equipment_telemetry,
+ "performance_metrics": performance_metrics
+ }
+
+ return simulation_data
+
+def send_product_to_event_hub(simulation_data):
+
+ # format data to pubish into staging table
+ payload_data = {
+ "id": str(uuid.uuid4()),
+ "source": "data_emulator",
+ "type": "data_emulator",
+ "data_base64": simulation_data, # Data in JSON format
+ "time": str(datetime.now().isoformat()) + "Z",
+ "specversion": 1,
+ "subject": "topic/dataemulator"
+ }
+ event_data = EventData(json.dumps(payload_data))
+ event_producer.send_batch([event_data])
+
+if __name__ == "__main__":
+
+ # Generate batch and continue with live data
+ user_option = input("Would you like generate past data (1) or current data (2) choose your option: ")
+ if user_option == "":
+ user_option = 1
+ else:
+ user_option = int(user_option)
+
+ if user_option == 1:
+ number_of_days = input("Enter number of days to generate past data (default is 7 days): ")
+ if number_of_days == "":
+ number_of_days = -7
+ else:
+ number_of_days = (0 - int(number_of_days))
+
+ production_datetime = datetime.now() + timedelta(days=number_of_days)
+
+ try:
+ if user_option == 1:
+ while production_datetime <= datetime.now():
+ print('Generating for: ', production_datetime)
+ simulated_data = simulate_assembly_line_data(production_datetime)
+ send_product_to_event_hub(simulated_data)
+
+ # Increment time
+ production_datetime += timedelta(minutes=1)
+ else:
+ # Generate live data
+ print('Now generating live date...')
+ while True:
+ current_time = datetime.now()
+ # Produce equipment telemetry data every 30 seconds
+ print('Generating for: ', current_time)
+ simulated_data = simulate_assembly_line_data(current_time)
+ send_product_to_event_hub(simulated_data)
+
+ time.sleep(30) # send data every 30 seconds
+ finally:
+ event_producer.close()
diff --git a/azure_jumpstart_ag/retail/artifacts/data_emulator/products.json b/azure_jumpstart_ag/artifacts/data_emulator/products.json
similarity index 100%
rename from azure_jumpstart_ag/retail/artifacts/data_emulator/products.json
rename to azure_jumpstart_ag/artifacts/data_emulator/products.json
diff --git a/azure_jumpstart_ag/retail/artifacts/data_emulator/stores.json b/azure_jumpstart_ag/artifacts/data_emulator/stores.json
similarity index 100%
rename from azure_jumpstart_ag/retail/artifacts/data_emulator/stores.json
rename to azure_jumpstart_ag/artifacts/data_emulator/stores.json
diff --git a/azure_jumpstart_ag/artifacts/icons/contoso-motors.png b/azure_jumpstart_ag/artifacts/icons/contoso-motors.png
new file mode 100644
index 0000000000..8efc39e558
Binary files /dev/null and b/azure_jumpstart_ag/artifacts/icons/contoso-motors.png differ
diff --git a/azure_jumpstart_ag/artifacts/icons/contoso-motors.svg b/azure_jumpstart_ag/artifacts/icons/contoso-motors.svg
new file mode 100644
index 0000000000..010d622981
--- /dev/null
+++ b/azure_jumpstart_ag/artifacts/icons/contoso-motors.svg
@@ -0,0 +1,31 @@
+
diff --git a/azure_jumpstart_ag/retail/artifacts/icons/contoso.png b/azure_jumpstart_ag/artifacts/icons/contoso.png
similarity index 100%
rename from azure_jumpstart_ag/retail/artifacts/icons/contoso.png
rename to azure_jumpstart_ag/artifacts/icons/contoso.png
diff --git a/azure_jumpstart_ag/retail/artifacts/icons/contoso.svg b/azure_jumpstart_ag/artifacts/icons/contoso.svg
similarity index 100%
rename from azure_jumpstart_ag/retail/artifacts/icons/contoso.svg
rename to azure_jumpstart_ag/artifacts/icons/contoso.svg
diff --git a/azure_jumpstart_ag/retail/artifacts/icons/emulator.ico b/azure_jumpstart_ag/artifacts/icons/emulator.ico
similarity index 100%
rename from azure_jumpstart_ag/retail/artifacts/icons/emulator.ico
rename to azure_jumpstart_ag/artifacts/icons/emulator.ico
diff --git a/azure_jumpstart_ag/retail/artifacts/icons/grafana.ico b/azure_jumpstart_ag/artifacts/icons/grafana.ico
similarity index 100%
rename from azure_jumpstart_ag/retail/artifacts/icons/grafana.ico
rename to azure_jumpstart_ag/artifacts/icons/grafana.ico
diff --git a/azure_jumpstart_ag/artifacts/monitoring/arc-inventory-workbook.bicep b/azure_jumpstart_ag/artifacts/monitoring/arc-inventory-workbook.bicep
new file mode 100644
index 0000000000..48c761318c
--- /dev/null
+++ b/azure_jumpstart_ag/artifacts/monitoring/arc-inventory-workbook.bicep
@@ -0,0 +1,30 @@
+@description('The friendly name for the workbook that is used in the Gallery or Saved List. This name must be unique within a resource group.')
+param workbookDisplayName string = 'Azure Arc-enabled resources inventory'
+
+@description('The gallery that the workbook will been shown under. Supported values include workbook, tsg, etc. Usually, this is \'workbook\'')
+param workbookType string = 'workbook'
+
+@description('The id of resource instance to which the workbook will be associated')
+param workbookSourceId string = 'azure monitor'
+
+@description('The unique guid for this workbook instance')
+param workbookId string = 'c5c6a9e5-74fc-465a-9f11-1dd10aad501b'
+
+@description('The location to deploy the workbook to')
+param location string = resourceGroup().location
+
+resource workbookId_resource 'microsoft.insights/workbooks@2022-04-01' = {
+ name: workbookId
+ location: location
+ kind: 'shared'
+ properties: {
+ displayName: workbookDisplayName
+ serializedData: '{"version":"Notebook/1.0","items":[{"type":9,"content":{"version":"KqlParameterItem/1.0","parameters":[{"id":"d8a4990e-5fd5-4c61-92a2-d07d04736a00","version":"KqlParameterItem/1.0","name":"Subscription","type":6,"isRequired":true,"typeSettings":{"additionalResourceOptions":[],"includeAll":false},"timeContext":{"durationMs":86400000},"value":"/subscriptions/00000000-0000-0000-0000-000000000000"},{"id":"b616a3a3-4271-4208-b1a9-a92a78efed08","version":"KqlParameterItem/1.0","name":"ResourceGroup","label":"Resource group","type":2,"isRequired":true,"query":"resourcecontainers \\r\\n| where type =~ \'microsoft.resources/subscriptions/resourcegroups\' \\r\\n| project name","crossComponentResources":["{Subscription}"],"typeSettings":{"additionalResourceOptions":[],"showDefault":false},"queryType":1,"resourceType":"microsoft.resourcegraph/resources","value":"rg-placeholder"},{"id":"05578175-fbe8-4dd2-9c6a-dec2f503d6cf","version":"KqlParameterItem/1.0","name":"Location","type":8,"isRequired":true,"multiSelect":true,"quote":"\'","delimiter":",","value":["value::all"],"typeSettings":{"additionalResourceOptions":["value::all"],"includeAll":true},"defaultValue":"value::all"},{"id":"3f095357-8b61-4bd5-bb16-bb03da10f44c","version":"KqlParameterItem/1.0","name":"ResourceType","type":7,"isRequired":true,"multiSelect":true,"quote":"\'","delimiter":",","value":["microsoft.hybridcompute/machines","microsoft.compute/virtualmachines"],"typeSettings":{"additionalResourceOptions":[],"showDefault":false},"jsonData":" [\\r\\n { \\"value\\": \\"microsoft.compute/virtualmachines\\", \\"label\\": \\"Azure Virtual Machine\\", \\"selected\\":true}, \\r\\n \\r\\n { \\"value\\": \\"microsoft.hybridcompute/machines\\", \\"label\\": \\"Arc enabled server\\", \\"selected\\":true }]"}],"style":"pills","queryType":0,"resourceType":"microsoft.operationalinsights/workspaces"},"name":"parameters - 4"},{"type":1,"content":{"json":"# Azure Arc-enabled resources inventory\\n"},"name":"text - 1"},{"type":12,"content":{"version":"NotebookGroup/1.0","groupType":"editable","title":"Machines overall status & configurations","expandable":true,"expanded":true,"items":[{"type":3,"content":{"version":"KqlItem/1.0","query":"resources \\r\\n| where subscriptionId == \'{Subscription:subscriptionid}\' and resourceGroup == tolower(\'{ResourceGroup}\')\\r\\n| extend osType = coalesce(tostring(properties.osName), tostring(properties.osType), tostring(properties.storageProfile.osDisk.osType))\\r\\n| summarize\\r\\nazureLinux = countif(type =~ \\"microsoft.compute/virtualmachines\\" and osType =~ \\"Linux\\"),\\r\\narcLinux = countif(type =~ \\"microsoft.hybridcompute/machines\\" and osType =~ \\"Linux\\"),\\r\\nazureWindows = countif(type =~ \\"microsoft.compute/virtualmachines\\" and osType =~ \\"Windows\\"),\\r\\narcWindows = countif(type =~ \\"microsoft.hybridcompute/machines\\" and osType =~ \\"Windows\\")\\r\\n| project machinePack = pack(\\"Azure virtual machines-Linux\\", azureLinux, \\"Arc enabled servers-Linux\\", arcLinux, \\"Azure virtual machines-Windows\\", azureWindows, \\"Arc enabled servers-Windows\\", arcWindows)\\r\\n| mv-expand machinePack\\r\\n| extend machine = tostring(bag_keys(machinePack)[0])\\r\\n| extend count_ = tolong(machinePack[machine])\\r\\n| project machine, count_ ","size":3,"title":"Total machines","queryType":1,"resourceType":"microsoft.resourcegraph/resources","crossComponentResources":["{Subscription}"],"visualization":"piechart","tileSettings":{"showBorder":false},"graphSettings":{"type":0},"mapSettings":{"locInfo":"LatLong","sizeSettings":"azureLinux","sizeAggregation":"Sum","legendMetric":"azureLinux","legendAggregation":"Sum","itemColorSettings":{"type":"heatmap","colorAggregation":"Sum","nodeColorField":"azureLinux","heatmapPalette":"greenRed"}}},"customWidth":"50","name":"query - 0","styleSettings":{"maxWidth":"50%"}},{"type":3,"content":{"version":"KqlItem/1.0","query":"resources\\r\\n| where subscriptionId == \'{Subscription:subscriptionid}\' and resourceGroup == tolower(\'{ResourceGroup}\')\\r\\n| where type in~ ({ResourceType})\\r\\n| where location in~ ({Location})\\r\\n| extend statuso = iff(isnull(properties.extended.instanceView.powerState.displayStatus), (properties.status), (properties.extended.instanceView.powerState.displayStatus))\\r\\n| where isnotnull(statuso)\\r\\n| summarize count() by tostring(statuso)","size":0,"title":"Status of machines","queryType":1,"resourceType":"microsoft.resourcegraph/resources","crossComponentResources":["{Subscription}"],"visualization":"tiles","tileSettings":{"titleContent":{"columnMatch":"statuso","formatter":1},"leftContent":{"columnMatch":"count_","formatter":12,"formatOptions":{"palette":"auto"},"numberFormat":{"unit":17,"options":{"maximumSignificantDigits":3,"maximumFractionDigits":2}}},"showBorder":false,"sortCriteriaField":"statuso","sortOrderField":2}},"customWidth":"50","name":"query - 7","styleSettings":{"maxWidth":"50%"}}]},"name":"confGroup","styleSettings":{"showBorder":true}},{"type":3,"content":{"version":"KqlItem/1.0","query":"resources\\r\\n| where subscriptionId == \'{Subscription:subscriptionid}\' and resourceGroup == tolower(\'{ResourceGroup}\')\\r\\n| where type =~ \'microsoft.hybridcompute/machines\'\\r\\n| extend id = tolower(id)\\r\\n| join(policyresources\\r\\n| extend ComplianceState = tostring(properties[\'complianceState\'])\\r\\n| extend id = tolower(properties[\'resourceId\'])) on id\\r\\n| summarize compliantCount = countif(ComplianceState == \\"Compliant\\"), nonCompliantCount = countif(ComplianceState == \\"NonCompliant\\") by name, type, id\\r\\n| project id, name, type, compliantCount, nonCompliantCount\\r\\n| sort by name asc\\r\\n| join (\\r\\nresources\\r\\n| where resourceGroup =~ \'{ResourceGroup}\'\\r\\n| where type =~ \'microsoft.hybridcompute/machines\'\\r\\n| extend id = tolower(id)\\r\\n| extend state = properties.status\\r\\n| extend status = case(\\r\\n state =~ \'Connected\', \'Connected\',\\r\\n state =~ \'Disconnected\', \'Offline\',\\r\\n state =~ \'Error\', \'Error\',\\r\\n state =~ \'Expired\', \'Expired\',\\r\\n \'\')\\r\\n| extend agentVersion = properties.agentVersion\\r\\n| extend Application = tags.Application\\r\\n| extend operatingSystem = properties.osSku\\r\\n| extend resourceGroup = strcat(\\"/subscriptions/\\", subscriptionId, \\"/resourceGroups/\\", resourceGroup)\\r\\n| extend majorVersion = tostring(split(agentVersion, \'.\')[0])\\r\\n| extend minorVersion = tostring(split(agentVersion, \'.\')[1])\\r\\n| extend agentVersion = strcat(majorVersion, \'.\', minorVersion)\\r\\n| project id, status, agentVersion, operatingSystem, location, Application) on id\\r\\n| project id, status, agentVersion, operatingSystem, location, Application, CompliantPolicies=compliantCount, NonCompliantPolicies=nonCompliantCount\\r\\n","size":1,"title":"Azure Arc-enabled servers inventory","queryType":1,"resourceType":"microsoft.resourcegraph/resources","crossComponentResources":["value::all"],"gridSettings":{"formatters":[{"columnMatch":"status","formatter":18,"formatOptions":{"thresholdsOptions":"icons","thresholdsGrid":[{"operator":"==","thresholdValue":"Connected","representation":"success","text":"{0}{1}"},{"operator":"Default","thresholdValue":null,"representation":"warning","text":"{0}{1}"}]}},{"columnMatch":"CompliantPolicies","formatter":18,"formatOptions":{"thresholdsOptions":"icons","thresholdsGrid":[{"operator":"Default","thresholdValue":null,"representation":"success","text":"{0}{1}"}]}},{"columnMatch":"NonCompliantPolicies","formatter":18,"formatOptions":{"thresholdsOptions":"icons","thresholdsGrid":[{"operator":"Default","thresholdValue":null,"representation":"3","text":"{0}{1}"}]}},{"columnMatch":"resourceGroup","formatter":14,"formatOptions":{"linkTarget":null,"showIcon":true}},{"columnMatch":"subscriptionId","formatter":15,"formatOptions":{"linkTarget":null,"showIcon":true}}]}},"name":"query - 0"},{"type":3,"content":{"version":"KqlItem/1.0","query":"resources\\r\\n| where subscriptionId == \'{Subscription:subscriptionid}\' and resourceGroup == tolower(\'{ResourceGroup}\')\\r\\n| where type =~ \'microsoft.kubernetes/connectedclusters\'\\r\\n| extend id = tolower(id)\\r\\n| join(policyresources\\r\\n| extend ComplianceState = tostring(properties[\'complianceState\'])\\r\\n| extend id = tolower(properties[\'resourceId\'])) on id\\r\\n| summarize compliantCount = countif(ComplianceState == \\"Compliant\\"), nonCompliantCount = countif(ComplianceState == \\"NonCompliant\\") by name, type, id\\r\\n| project id, name, type, compliantCount, nonCompliantCount\\r\\n| sort by name asc\\r\\n| join (\\r\\nresources\\r\\n| where resourceGroup =~ \'{ResourceGroup}\'\\r\\n| where type =~ \'microsoft.kubernetes/connectedclusters\'\\r\\n| extend id = tolower(id)\\r\\n| extend state = properties.connectivityStatus\\r\\n| extend status = case(\\r\\n state =~ \'Connected\', \'Connected\',\\r\\n state =~ \'Disconnected\', \'Offline\',\\r\\n state =~ \'Error\', \'Error\',\\r\\n state =~ \'Expired\', \'Expired\',\\r\\n \'\')\\r\\n| extend resourceGroup = strcat(\\"/subscriptions/\\", subscriptionId, \\"/resourceGroups/\\", resourceGroup)\\r\\n| extend kubernetesVersion = properties.kubernetesVersion\\r\\n| project id, status, kubernetesVersion) on id\\r\\n| project id, status,kubernetesVersion,CompliantPolicies=compliantCount, NonCompliantPolicies=nonCompliantCount\\r\\n\\r\\n\\r\\n","size":1,"title":"Azure Arc-enabled Kubernetes clusters inventory","queryType":1,"resourceType":"microsoft.resourcegraph/resources","crossComponentResources":["value::all"],"gridSettings":{"formatters":[{"columnMatch":"status","formatter":18,"formatOptions":{"thresholdsOptions":"icons","thresholdsGrid":[{"operator":"==","thresholdValue":"Connected","representation":"success","text":"{0}{1}"},{"operator":"Default","thresholdValue":null,"representation":"warning","text":"{0}{1}"}]}},{"columnMatch":"CompliantPolicies","formatter":18,"formatOptions":{"thresholdsOptions":"icons","thresholdsGrid":[{"operator":"Default","thresholdValue":null,"representation":"success","text":"{0}{1}"}]}},{"columnMatch":"NonCompliantPolicies","formatter":18,"formatOptions":{"thresholdsOptions":"icons","thresholdsGrid":[{"operator":"Default","thresholdValue":null,"representation":"3","text":"{0}{1}"}]}},{"columnMatch":"resourceGroup","formatter":14,"formatOptions":{"linkTarget":null,"showIcon":true}},{"columnMatch":"subscriptionId","formatter":15,"formatOptions":{"linkTarget":null,"showIcon":true}}]}},"name":"query - 0 - Copy"},{"type":12,"content":{"version":"NotebookGroup/1.0","groupType":"editable","title":"Updates Data Overview","expandable":true,"expanded":true,"items":[{"type":3,"content":{"version":"KqlItem/1.0","query":"\\r\\n(resources //join of virtual machines, you can play with params as you see fit.\\r\\n| where type in~ ({ResourceType}\\r\\n)\\r\\n| where subscriptionId == \'{Subscription:subscriptionid}\' and resourceGroup == tolower(\'{ResourceGroup}\')\\r\\n| where location in ({Location})\\r\\n| extend os = iff(type =~ \\"microsoft.compute/virtualmachines\\", tolower(tostring(properties.storageProfile.osDisk.osType)), tolower(coalesce(tostring(properties.osName), tostring(properties.osType))))\\r\\n| extend id=tolower(id)\\r\\n| extend status=iff(type =~ \\"microsoft.compute/virtualmachines\\", properties.extended.instanceView.powerState.displayStatus, properties.status)\\r\\n| project id, name, os, status, resourceProperties=properties)\\r\\n| join kind=leftouter //finally, making a left outer join to fetch updates details from patchassessment\\r\\n((patchassessmentresources\\r\\n| where type in~ (\\"microsoft.compute/virtualmachines/patchassessmentresults\\", \\"microsoft.hybridcompute/machines/patchassessmentresults\\")\\r\\n| where location in ({Location})\\r\\n| where properties.status == \\"Succeeded\\"\\r\\n| parse id with resourceId \\"/patchAssessmentResults\\" *\\r\\n| extend resourceId=tolower(resourceId)\\r\\n| project resourceId, assessProperties=properties))\\r\\non $left.id == $right.resourceId //join on resources id & patchassessment id that is parsed.\\r\\n| summarize\\r\\ntotal = countif(1 == 1),\\r\\nnodata = countif(isnull(assessProperties) == true),\\r\\npendingReboot = countif(isnotnull(assessProperties) and assessProperties.rebootPending == \\"true\\"),\\r\\n//pendingUpdates - when any classification has > 0 updates\\r\\npendingUpdatesWindows = countif(isnotnull(assessProperties) and assessProperties.osType =~ \\"Windows\\" and (assessProperties.availablePatchCountByClassification.critical>0 or assessProperties.availablePatchCountByClassification.security>0 or assessProperties.availablePatchCountByClassification.updateRollup>0 or assessProperties.availablePatchCountByClassification.featurePack>0 or assessProperties.availablePatchCountByClassification.servicePack>0 or assessProperties.availablePatchCountByClassification.definition>0 or assessProperties.availablePatchCountByClassification.tools>0 or assessProperties.availablePatchCountByClassification.updates>0)),\\r\\npendingUpdatesLinux = countif(isnotnull(assessProperties) and assessProperties.osType =~ \\"Linux\\" and (assessProperties.availablePatchCountByClassification.security>0 or assessProperties.availablePatchCountByClassification.other>0)),\\r\\n//noPendingUpdates - when all classifications has 0 updates\\r\\nnoPendingUpdatesWindows = countif(isnotnull(assessProperties) and assessProperties.osType =~ \\"Windows\\" and (assessProperties.availablePatchCountByClassification.critical==0 and assessProperties.availablePatchCountByClassification.security==0 and assessProperties.availablePatchCountByClassification.updateRollup==0 and assessProperties.availablePatchCountByClassification.featurePack==0 and assessProperties.availablePatchCountByClassification.servicePack==0 and assessProperties.availablePatchCountByClassification.definition==0 and assessProperties.availablePatchCountByClassification.tools==0 and assessProperties.availablePatchCountByClassification.updates==0)),\\r\\nnoPendingUpdatesLinux = countif(isnotnull(assessProperties) and assessProperties.osType =~ \\"Linux\\" and (assessProperties.availablePatchCountByClassification.security==0 and assessProperties.availablePatchCountByClassification.other==0))\\r\\n| project machinePack = pack(\\"No updates available - Linux\\", noPendingUpdatesLinux, \\"No updates available - Windows\\", noPendingUpdatesWindows, \\"Updates available - Linux\\", pendingUpdatesLinux, \\"Updates available - Windows\\", pendingUpdatesWindows, \\"Reboot required\\", pendingReboot, \\"No updates data\\", nodata, \\"Total machines\\", total)\\r\\n| mv-expand machinePack\\r\\n| extend machine = tostring(bag_keys(machinePack)[0])\\r\\n| extend count_ = tolong(machinePack[machine])\\r\\n| project machine, count_ \\r\\n","size":4,"title":"Updates status of machines","queryType":1,"resourceType":"microsoft.resourcegraph/resources","crossComponentResources":["{Subscription}"],"visualization":"tiles","tileSettings":{"titleContent":{"columnMatch":"machine","formatter":1},"leftContent":{"columnMatch":"count_","formatter":12,"formatOptions":{"palette":"auto"},"numberFormat":{"unit":17,"options":{"maximumSignificantDigits":3,"maximumFractionDigits":2}}},"showBorder":false,"sortCriteriaField":"count_","sortOrderField":2,"size":"auto"},"graphSettings":{"type":0,"topContent":{"columnMatch":"machine","formatter":1},"centerContent":{"columnMatch":"count_","formatter":1,"numberFormat":{"unit":17,"options":{"maximumSignificantDigits":3,"maximumFractionDigits":2}}}},"mapSettings":{"locInfo":"LatLong","sizeSettings":"count_","sizeAggregation":"Sum","legendMetric":"count_","legendAggregation":"Sum","itemColorSettings":{"type":"heatmap","colorAggregation":"Sum","nodeColorField":"count_","heatmapPalette":"greenRed"}}},"name":"query - 5"},{"type":3,"content":{"version":"KqlItem/1.0","query":"resources\\r\\n| where type in~ ({ResourceType})\\r\\n| where subscriptionId == \'{Subscription:subscriptionid}\' and resourceGroup == tolower(\'{ResourceGroup}\')\\r\\n| extend joinId = tolower(id)\\r\\n| project joinId\\r\\n| join kind=leftouter\\r\\n(\\r\\npatchassessmentresources\\r\\n| where type in~ (\\"microsoft.compute/virtualmachines/patchassessmentresults\\", \\"microsoft.hybridcompute/machines/patchassessmentresults\\")\\r\\n| extend assessment = properties.availablePatchCountByClassification\\r\\n| where isnotnull(assessment)\\r\\n| parse id with resourceId \\"/patchAssessmentResults\\" *\\r\\n| extend joinId=tolower(resourceId)\\r\\n) on $left.joinId == $right.joinId\\r\\n| summarize\\r\\ntotal = 0,\\r\\nsecurityWindowsUpdates = sumif(toint(assessment.security), (isnotnull(properties) and properties.osType =~ \\"Windows\\" and (assessment.security>0))),\\r\\ncriticalWindowsUpdates = sumif(toint(assessment.critical), (isnotnull(properties) and properties.osType =~ \\"Windows\\" and (assessment.critical>0))),\\r\\nsecurityLinuxUpdates = sumif(toint(assessment.security), (isnotnull(properties) and properties.osType =~ \\"Linux\\" and (assessment.security>0))),\\r\\notherLinuxUpdates = sumif(toint(assessment.other), (isnotnull(properties) and properties.osType =~ \\"Linux\\" and (assessment.other>0))),\\r\\notherWindowsUpdates = sumif(toint(assessment.updateRollup) + toint(assessment.featurePack) + toint(assessment.servicePack) + toint(assessment.definition) +\\r\\ntoint(assessment.tools) + toint(assessment.updates), isnotnull(properties) and properties.osType =~ \\"Windows\\" and\\r\\n(assessment.updateRollup>0 or assessment.featurePack>0 or assessment.servicePack>0 or assessment.definition>0 or assessment.tools>0 or assessment.updates>0))","size":0,"title":"Pending Windows and Linux updates by classification ","queryType":1,"resourceType":"microsoft.resourcegraph/resources","crossComponentResources":["{Subscription}"],"visualization":"unstackedbar","tileSettings":{"showBorder":false},"chartSettings":{"seriesLabelSettings":[{"seriesName":"criticalWindowsUpdates","label":"Critical updates - Windows","color":"orange"},{"seriesName":"securityLinuxUpdates","label":"Security updates - Linux","color":"redBright"},{"seriesName":"otherLinuxUpdates","label":"Other updates - Linux","color":"turquoise"},{"seriesName":"otherWindowsUpdates","label":"Other updates - Windows","color":"gray"},{"seriesName":"securityWindowsUpdates","label":"Security updates - Windows","color":"redBright"}]}},"customWidth":"50","name":"query - 4","styleSettings":{"maxWidth":"50%"}},{"type":3,"content":{"version":"KqlItem/1.0","query":"resources\\r\\n| where type in~ ({ResourceType})\\r\\n| where subscriptionId == \'{Subscription:subscriptionid}\' and resourceGroup == tolower(\'{ResourceGroup}\')\\r\\n| extend joinId = tolower(id)\\r\\n| project joinId\\r\\n| join kind=leftouter\\r\\n(\\r\\npatchassessmentresources\\r\\n| where type in~ (\\"microsoft.compute/virtualmachines/patchassessmentresults\\", \\"microsoft.hybridcompute/machines/patchassessmentresults\\")\\r\\n| extend assessment = properties.availablePatchCountByClassification\\r\\n| where isnotnull(assessment)\\r\\n| parse id with resourceId \\"/patchAssessmentResults\\" *\\r\\n| extend joinId=tolower(resourceId)\\r\\n) on $left.joinId == $right.joinId\\r\\n| summarize\\r\\ntotal = 0,\\r\\nsecurityWindowsMachines = countif(isnotnull(properties) and properties.osType =~ \\"Windows\\" and (assessment.security>0)),\\r\\ncriticalWindowsMachines = countif(isnotnull(properties) and properties.osType =~ \\"Windows\\" and (assessment.critical>0)),\\r\\notherWindowsMachines = countif(isnotnull(properties) and properties.osType =~ \\"Windows\\" and (assessment.updateRollup>0 or assessment.featurePack>0 or assessment.servicePack>0 or assessment.definition>0 or assessment.tools>0 or assessment.updates>0)),\\r\\nsecurityLinuxMachines = countif(isnotnull(properties) and properties.osType =~ \\"Linux\\" and (assessment.security>0)),\\r\\notherLinuxMachines = countif(isnotnull(properties) and properties.osType =~ \\"Linux\\" and (assessment.other>0))\\r\\n","size":0,"title":"Machines with Pending Updates by classification","queryType":1,"resourceType":"microsoft.resourcegraph/resources","crossComponentResources":["{Subscription}"],"visualization":"unstackedbar","chartSettings":{"seriesLabelSettings":[{"seriesName":"criticalWindowsMachines","label":"Critical - Windows","color":"orange"},{"seriesName":"otherWindowsMachines","label":"Other - Windows","color":"turquoise"},{"seriesName":"securityLinuxMachines","label":"Security - Linux","color":"redBright"},{"seriesName":"otherLinuxMachines","label":"Other - Linux","color":"turquoise"},{"seriesName":"securityWindowsMachines","label":"Security - Windows","color":"redBright"}]}},"customWidth":"50","name":"query - 3","styleSettings":{"maxWidth":"50%"}},{"type":3,"content":{"version":"KqlItem/1.0","query":"resources\\r\\n| where type in~ ({ResourceType})\\r\\n| where subscriptionId == \'{Subscription:subscriptionid}\' and resourceGroup == tolower(\'{ResourceGroup}\')\\r\\n| extend joinId = tolower(id)\\r\\n| project joinId\\r\\n| join kind=inner \\r\\n(\\r\\npatchassessmentresources\\r\\n| where type in~ (\\"microsoft.compute/virtualmachines/patchassessmentresults/softwarepatches\\", \\"microsoft.hybridcompute/machines/patchassessmentresults/softwarepatches\\")\\r\\n| extend id = tolower(id)\\r\\n| parse id with resourceId \\"/patchassessmentresults\\" *\\r\\n| extend joinId=tolower(resourceId)\\r\\n| where isnotnull(properties.kbId)\\r\\n| extend MissingUpdate = tostring(properties.patchName)\\r\\n| extend Classification = tostring(properties.classifications[0])\\r\\n| project joinId, MissingUpdate, Classification\\r\\n) \\r\\non $left.joinId == $right.joinId\\r\\n| summarize Machines = count() by MissingUpdate, Classification\\r\\n| order by Machines desc\\r\\n| take 10\\r\\n","size":0,"title":"Top 10 Pending Windows Updates (by machine count)","queryType":1,"resourceType":"microsoft.resourcegraph/resources","crossComponentResources":["{Subscription}"],"visualization":"table","gridSettings":{"sortBy":[{"itemKey":"Classification","sortOrder":2}],"labelSettings":[{"columnId":"MissingUpdate","label":"Missing update"}]},"sortBy":[{"itemKey":"Classification","sortOrder":2}],"tileSettings":{"showBorder":false,"titleContent":{"columnMatch":"properties_patchName","formatter":1},"leftContent":{"columnMatch":"count_","formatter":12,"formatOptions":{"palette":"auto"},"numberFormat":{"unit":17,"options":{"maximumSignificantDigits":3,"maximumFractionDigits":2}}}},"graphSettings":{"type":0,"topContent":{"columnMatch":"properties_patchName","formatter":1},"centerContent":{"columnMatch":"count_","formatter":1,"numberFormat":{"unit":17,"options":{"maximumSignificantDigits":3,"maximumFractionDigits":2}}},"nodeIdField":"properties_patchName","sourceIdField":"properties_patchName","targetIdField":"count_","graphOrientation":3,"showOrientationToggles":false,"nodeSize":null,"staticNodeSize":100,"colorSettings":null,"hivesMargin":5},"mapSettings":{"locInfo":"LatLong","sizeSettings":"count_","sizeAggregation":"Sum","legendMetric":"count_","legendAggregation":"Sum","itemColorSettings":{"type":"heatmap","colorAggregation":"Sum","nodeColorField":"count_","heatmapPalette":"greenRed"}}},"customWidth":"50","name":"query - 9","styleSettings":{"maxWidth":"50%"}},{"type":3,"content":{"version":"KqlItem/1.0","query":"resources\\r\\n| where type in~ ({ResourceType})\\r\\n| where subscriptionId == \'{Subscription:subscriptionid}\' and resourceGroup == tolower(\'{ResourceGroup}\')\\r\\n| extend joinId = tolower(id)\\r\\n| project joinId\\r\\n| join kind=inner \\r\\n(\\r\\npatchassessmentresources\\r\\n| where type in~ (\\"microsoft.compute/virtualmachines/patchassessmentresults/softwarepatches\\", \\"microsoft.hybridcompute/machines/patchassessmentresults/softwarepatches\\")\\r\\n| extend id = tolower(id)\\r\\n| parse id with resourceId \\"/patchassessmentresults\\" *\\r\\n| extend joinId=tolower(resourceId)\\r\\n| where isnull(properties.kbId)\\r\\n| extend MissingUpdate = tostring(properties.patchName)\\r\\n| extend Classification = tostring(properties.classifications[0])\\r\\n| project joinId, MissingUpdate, Classification\\r\\n) \\r\\non $left.joinId == $right.joinId\\r\\n| summarize Machines = count() by MissingUpdate, Classification\\r\\n| order by Machines desc\\r\\n| take 10","size":0,"title":"Top 10 Pending Linux Updates (by machine count)","queryType":1,"resourceType":"microsoft.resourcegraph/resources","crossComponentResources":["{Subscription}"],"visualization":"table","gridSettings":{"sortBy":[{"itemKey":"Machines","sortOrder":2}],"labelSettings":[{"columnId":"MissingUpdate","label":"Missing update"}]},"sortBy":[{"itemKey":"Machines","sortOrder":2}]},"customWidth":"50","name":"query - 10","styleSettings":{"maxWidth":"50%"}}]},"name":"updatesGroup","styleSettings":{"showBorder":true}},{"type":3,"content":{"version":"KqlItem/1.0","query":"securityresources\\r\\n| where type == \\"microsoft.security/locations/alerts\\"\\r\\n| where subscriptionId == \'{Subscription:subscriptionid}\' and resourceGroup == tolower(\'{ResourceGroup}\')\\r\\n| project-rename P= properties\\r\\n| extend Details = parse_json(P)\\r\\n| extend IsIncident = Details.[\\"IsIncident\\"]\\r\\n| extend AlertDisplayName = Details.[\\"AlertDisplayName\\"]\\r\\n| extend SystemAlertId = Details.[\\"SystemAlertId\\"]\\r\\n| extend Severity = tostring(Details.[\\"Severity\\"])\\r\\n| where Severity == \\"High\\"\\r\\n| extend AlertUri = Details.[\\"AlertUri\\"]\\r\\n| extend Status = tostring(Details.[\\"Status\\"])\\r\\n| extend Tactics = tostring(Details.[\\"Intent\\"])\\r\\n| extend ResourceIdentifiers = Details.[\\"ResourceIdentifiers\\"]\\r\\n| mv-expand ResourceIdentifiers\\r\\n| extend ResourceId = parse_json(ResourceIdentifiers).[\\"AzureResourceId\\"]\\r\\n| where Status == \\"Active\\"\\r\\n| extend SeverityRank = case(\\r\\n Severity == \'High\', 3,\\r\\n Severity == \'Medium\', 2,\\r\\n Severity == \'Low\', 1,\\r\\n 0\\r\\n )\\r\\n| parse AlertUri with * \'/subscriptionId/\' SubscriptionId \'/\' *\\r\\n| parse AlertUri with * \'/resourceGroup/\' ResourceGroup \'/\' *\\r\\n| parse AlertUri with * \'/location/\' Location \\r\\n| project\\r\\n Severity,\\r\\n SystemAlertId,\\r\\n AlertDisplayName,\\r\\n IsIncident = iif(IsIncident == \\"true\\", \\"Incident\\", \\"Alert\\"),\\r\\n AlertUri,\\r\\n Tactics,\\r\\n SeverityRank,\\r\\n SubscriptionId,\\r\\n ResourceGroup,\\r\\n Location,\\r\\n ResourceId\\r\\n| sort by SeverityRank","size":0,"title":"Defender for Cloud {$rowCount} Active Alerts ","noDataMessage":"No active alerts","noDataMessageStyle":3,"exportedParameters":[{"fieldName":"ResourceId","parameterName":"Resource","parameterType":1},{"fieldName":"AlertUri","parameterName":"AlertUri","parameterType":1},{"fieldName":"SystemAlertId","parameterName":"SystemAlertId","parameterType":1},{"fieldName":"SubscriptionId","parameterName":"SubscriptionId","parameterType":1},{"fieldName":"ResourceGroup","parameterName":"ResourceGroup","parameterType":1},{"fieldName":"Location","parameterName":"Location","parameterType":1}],"queryType":1,"resourceType":"microsoft.resourcegraph/resources","crossComponentResources":["{Subscription}"],"gridSettings":{"formatters":[{"columnMatch":"Severity","formatter":18,"formatOptions":{"thresholdsOptions":"colors","thresholdsGrid":[{"operator":"contains","thresholdValue":"High","representation":"redBright","text":"{0}{1}"},{"operator":"contains","thresholdValue":"Medium","representation":"orange","text":"{0}{1}"},{"operator":"contains","thresholdValue":"Low","representation":"yellow","text":"{0}{1}"},{"operator":"contains","thresholdValue":"Informational ","representation":"gray","text":"{0}{1}"},{"operator":"Default","thresholdValue":null,"representation":null,"text":"{0}{1}"}]}},{"columnMatch":"SystemAlertId","formatter":5},{"columnMatch":"AlertDisplayName","formatter":1,"formatOptions":{"linkTarget":"OpenBlade","bladeOpenContext":{"bladeName":"AlertBlade","extensionName":"Microsoft_Azure_Security","bladeParameters":[{"name":"alertId","source":"column","value":"SystemAlertId"},{"name":"subscriptionId","source":"column","value":"SubscriptionId"},{"name":"resourceGroup","source":"column","value":"ResourceGroup"},{"name":"referencedFrom","source":"static","value":"activeAlertsWorkbook"},{"name":"location","source":"column","value":"Location"}]}}},{"columnMatch":"IsIncident","formatter":1},{"columnMatch":"AlertUri","formatter":5},{"columnMatch":"Tactics","formatter":1},{"columnMatch":"SubscriptionId","formatter":15,"formatOptions":{"linkTarget":"Resource","showIcon":true}},{"columnMatch":"Location","formatter":17},{"columnMatch":"ResourceId","formatter":13,"formatOptions":{"linkTarget":"Resource","showIcon":true}},{"columnMatch":"TenantId","formatter":5},{"columnMatch":"AlertName","formatter":5},{"columnMatch":"Description","formatter":5},{"columnMatch":"ProviderName","formatter":5},{"columnMatch":"VendorName","formatter":5},{"columnMatch":"VendorOriginalId","formatter":5},{"columnMatch":"SourceComputerId","formatter":5},{"columnMatch":"AlertType","formatter":5},{"columnMatch":"ConfidenceLevel","formatter":5},{"columnMatch":"ConfidenceScore","formatter":5},{"columnMatch":"StartTime","formatter":5},{"columnMatch":"EndTime","formatter":5},{"columnMatch":"ProcessingEndTime","formatter":5},{"columnMatch":"RemediationSteps","formatter":5},{"columnMatch":"ExtendedProperties","formatter":5},{"columnMatch":"Entities","formatter":5},{"columnMatch":"SourceSystem","formatter":5},{"columnMatch":"WorkspaceSubscriptionId","formatter":5},{"columnMatch":"WorkspaceResourceGroup","formatter":5},{"columnMatch":"ExtendedLinks","formatter":5},{"columnMatch":"ProductName","formatter":5},{"columnMatch":"ProductComponentName","formatter":5},{"columnMatch":"AlertLink","formatter":7,"formatOptions":{"linkTarget":"Url"}},{"columnMatch":"SystemIncidentId","formatter":5},{"columnMatch":"SystemAlertId1","formatter":5}],"labelSettings":[{"columnId":"SystemAlertId","label":"Alert ID"},{"columnId":"AlertDisplayName","label":"Alert name"},{"columnId":"IsIncident","label":"Incident/alert"},{"columnId":"SeverityRank","label":"Severity"},{"columnId":"SubscriptionId","label":"Subscription"},{"columnId":"ResourceGroup","label":"Resource group"},{"columnId":"ResourceId","label":"Resource"}]},"sortBy":[]},"showPin":true,"name":"SecurityIncidents - FilterbyResourceId","styleSettings":{"showBorder":true}}],"isLocked":false,"fallbackResourceIds":["azure monitor"]}'
+ version: '1.0'
+ sourceId: workbookSourceId
+ category: workbookType
+ }
+ dependsOn: []
+}
+
+output workbookId string = workbookId_resource.id
diff --git a/azure_jumpstart_ag/artifacts/monitoring/arc-osperformance-workbook.bicep b/azure_jumpstart_ag/artifacts/monitoring/arc-osperformance-workbook.bicep
new file mode 100644
index 0000000000..cb91ea5128
--- /dev/null
+++ b/azure_jumpstart_ag/artifacts/monitoring/arc-osperformance-workbook.bicep
@@ -0,0 +1,1225 @@
+@description('The friendly name for the workbook that is used in the Gallery or Saved List. This name must be unique within a resource group.')
+param workbookDisplayName string = 'Azure Arc-enabled servers OS Performance'
+@description('The gallery that the workbook will been shown under. Supported values include workbook, tsg, etc. Usually, this is \'workbook\'')
+param workbookType string = 'workbook'
+@description('The id of resource instance to which the workbook will be associated')
+param workbookSourceId string = 'azure monitor'
+@description('The unique guid for this workbook instance')
+param workbookId string = guid('OSPerformance')
+@description('The location to deploy the workbook to')
+param location string = resourceGroup().location
+@description('Workbook content')
+var workbookContent = {
+ version: 'Notebook/1.0'
+ items: [
+ {
+ type: 1
+ content: {
+ json: '# Operating System - Performance and capacity'
+ }
+ name: 'text - 0'
+ }
+ {
+ type: 9
+ content: {
+ version: 'KqlParameterItem/1.0'
+ crossComponentResources: [
+ '{Workspace}'
+ ]
+ parameters: [
+ {
+ id: 'b82b64ff-f991-4f44-ac88-aee7c086cc48'
+ version: 'KqlParameterItem/1.0'
+ name: 'TimeRange'
+ type: 4
+ isRequired: true
+ value: {
+ durationMs: 86400000
+ }
+ typeSettings: {
+ selectableValues: [
+ {
+ durationMs: 3600000
+ }
+ {
+ durationMs: 43200000
+ }
+ {
+ durationMs: 86400000
+ }
+ {
+ durationMs: 259200000
+ }
+ {
+ durationMs: 604800000
+ }
+ {
+ durationMs: 1209600000
+ }
+ {
+ durationMs: 2592000000
+ }
+ ]
+ allowCustom: true
+ }
+ }
+ {
+ id: '23e3bd37-240d-492a-99c1-5b4f3d79d75e'
+ version: 'KqlParameterItem/1.0'
+ name: 'Subscription'
+ type: 6
+ isRequired: true
+ multiSelect: true
+ quote: '\''
+ delimiter: ','
+ value: [
+ '/subscriptions/00000000-0000-0000-0000-000000000000'
+ ]
+ query: 'where type =~ \'microsoft.compute/virtualmachines\' or type =~ \'microsoft.hybridcompute/machines\' \r\n| summarize Count = count() by subscriptionId\r\n\t| order by Count desc\r\n\t| extend Rank = row_number()\r\n\t| project value = subscriptionId, label = subscriptionId, selected = Rank == 1'
+ crossComponentResources: [
+ 'value::all'
+ ]
+ typeSettings: {
+ limitSelectTo: 100
+ additionalResourceOptions: [
+ 'value::all'
+ ]
+ showDefault: false
+ }
+ queryType: 1
+ resourceType: 'microsoft.resourcegraph/resources'
+ }
+ {
+ id: 'e1ecac91-1691-4f48-b4c0-803e39e00f43'
+ version: 'KqlParameterItem/1.0'
+ name: 'Workspace'
+ type: 5
+ isRequired: true
+ multiSelect: true
+ quote: '\''
+ delimiter: ','
+ query: 'where type =~ \'microsoft.operationalinsights/workspaces\'\r\n| summarize by id, name\r\n'
+ crossComponentResources: [
+ '{Subscription}'
+ ]
+ typeSettings: {
+ additionalResourceOptions: []
+ showDefault: false
+ }
+ timeContext: {
+ durationMs: 0
+ }
+ timeContextFromParameter: 'TimeRange'
+ queryType: 1
+ resourceType: 'microsoft.resourcegraph/resources'
+ value: [
+ '/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/xxxx/providers/Microsoft.OperationalInsights/workspaces/xxxx'
+ ]
+ }
+ {
+ id: '98c624e3-84f5-43e2-8be3-56b84c75bbb2'
+ version: 'KqlParameterItem/1.0'
+ name: 'ResourceGroup'
+ type: 2
+ isRequired: true
+ multiSelect: true
+ quote: '\''
+ delimiter: ','
+ query: 'Heartbeat\r\n| distinct RGName = tolower(ResourceGroup)'
+ crossComponentResources: [
+ '{Workspace}'
+ ]
+ typeSettings: {
+ limitSelectTo: 500
+ additionalResourceOptions: [
+ 'value::all'
+ ]
+ showDefault: false
+ }
+ timeContext: {
+ durationMs: 0
+ }
+ timeContextFromParameter: 'TimeRange'
+ queryType: 0
+ resourceType: 'microsoft.operationalinsights/workspaces'
+ value: [
+ 'value::all'
+ ]
+ }
+ ]
+ style: 'pills'
+ queryType: 0
+ resourceType: 'microsoft.operationalinsights/workspaces'
+ }
+ name: 'parameters - 1'
+ }
+ {
+ type: 3
+ content: {
+ version: 'KqlItem/1.0'
+ query: 'let trend = (Heartbeat\r\n | extend RGName = tolower(split(_ResourceId, "/")[4])\r\n | where RGName in ({ResourceGroup})\r\n | make-series InternalTrend=iff(count() > 0, 1, 0) default = 0 on TimeGenerated from ago(3d) to now() step 15m by _ResourceId\r\n | extend Trend=array_slice(InternalTrend, array_length(InternalTrend) - 30, array_length(InternalTrend) - 1)); \r\nlet PerfCPU = (InsightsMetrics\r\n | where Origin == "vm.azm.ms"\r\n | extend RGName = tolower(split(_ResourceId, "/")[4])\r\n | where RGName in ({ResourceGroup})\r\n | where Namespace == "Processor" and Name == "UtilizationPercentage"\r\n | summarize AvgCPU=round(avg(Val), 2), MaxCPU=round(max(Val), 2) by _ResourceId\r\n | extend StatusCPU = case (\r\n AvgCPU > 80,\r\n 2,\r\n AvgCPU > 50,\r\n 1,\r\n AvgCPU <= 50,\r\n 0,\r\n -1\r\n )\r\n );\r\nlet PerfMemory = (InsightsMetrics\r\n | where Origin == "vm.azm.ms"\r\n | extend RGName = tolower(split(_ResourceId, "/")[4])\r\n | where RGName in ({ResourceGroup})\r\n | where Namespace == "Memory" and Name == "AvailableMB"\r\n | summarize AvgMEM=round(avg(Val), 2), MaxMEM=round(max(Val), 2) by _ResourceId\r\n | extend StatusMEM = case (\r\n AvgMEM > 4,\r\n 0,\r\n AvgMEM >= 1,\r\n 1,\r\n AvgMEM < 1,\r\n 2,\r\n -1\r\n )\r\n );\r\nlet PerfDisk = (InsightsMetrics\r\n | where Origin == "vm.azm.ms"\r\n | extend RGName = tolower(split(_ResourceId, "/")[4])\r\n | where RGName in ({ResourceGroup})\r\n | where Namespace == "LogicalDisk" and Name == "FreeSpaceMB"\r\n | extend Disk=tostring(todynamic(Tags)["vm.azm.ms/mountId"])\r\n | where (Disk =~ "C:" or Disk == "/")\r\n | summarize\r\n AvgDisk=round(avg(Val), 2),\r\n (TimeGenerated, LastDisk)=arg_max(TimeGenerated, round(Val, 2))\r\n by _ResourceId\r\n | extend StatusDisk = case (\r\n AvgDisk < 5000,\r\n 2,\r\n AvgDisk < 30000,\r\n 1,\r\n AvgDisk >= 30000,\r\n 0,\r\n -1\r\n )\r\n | project _ResourceId, AvgDisk, LastDisk, StatusDisk\r\n );\r\nPerfCPU\r\n| join (PerfMemory) on _ResourceId\r\n| join (PerfDisk) on _ResourceId\r\n| join (trend) on _ResourceId\r\n| project\r\n _ResourceId,\r\n StatusCPU,\r\n AvgCPU,\r\n MaxCPU,\r\n StatusMEM,\r\n AvgMEM,\r\n MaxMEM,\r\n StatusDisk,\r\n AvgDisk,\r\n LastDisk,\r\n ["Heartbeat Trend"] = Trend\r\n| sort by StatusCPU,StatusDisk desc'
+ size: 0
+ showAnalytics: true
+ title: 'Top servers (data aggregated based on TimeRange)'
+ timeContextFromParameter: 'TimeRange'
+ exportFieldName: '_ResourceId'
+ exportParameterName: '_ResourceId'
+ exportDefaultValue: 'All'
+ queryType: 0
+ resourceType: 'microsoft.operationalinsights/workspaces'
+ crossComponentResources: [
+ '{Workspace}'
+ ]
+ gridSettings: {
+ formatters: [
+ {
+ columnMatch: 'StatusCPU'
+ formatter: 18
+ formatOptions: {
+ thresholdsOptions: 'icons'
+ thresholdsGrid: [
+ {
+ operator: '=='
+ thresholdValue: '0'
+ representation: 'success'
+ text: '{1}'
+ }
+ {
+ operator: '=='
+ thresholdValue: '1'
+ representation: '2'
+ text: '{1}'
+ }
+ {
+ operator: '=='
+ thresholdValue: '2'
+ representation: '4'
+ text: '{1}'
+ }
+ {
+ operator: 'Default'
+ thresholdValue: null
+ representation: 'Unknown'
+ text: '{1}'
+ }
+ ]
+ }
+ }
+ {
+ columnMatch: 'AvgCPU'
+ formatter: 0
+ numberFormat: {
+ unit: 1
+ options: {
+ style: 'decimal'
+ }
+ }
+ }
+ {
+ columnMatch: 'MaxCPU'
+ formatter: 0
+ numberFormat: {
+ unit: 1
+ options: {
+ style: 'decimal'
+ }
+ }
+ }
+ {
+ columnMatch: 'StatusMEM'
+ formatter: 18
+ formatOptions: {
+ thresholdsOptions: 'icons'
+ thresholdsGrid: [
+ {
+ operator: '=='
+ thresholdValue: '0'
+ representation: 'success'
+ text: '{1}'
+ }
+ {
+ operator: '=='
+ thresholdValue: '1'
+ representation: '2'
+ text: '{1}'
+ }
+ {
+ operator: '=='
+ thresholdValue: '2'
+ representation: 'critical'
+ text: '{1}'
+ }
+ {
+ operator: 'Default'
+ thresholdValue: null
+ representation: 'unknown'
+ text: '{1}'
+ }
+ ]
+ }
+ }
+ {
+ columnMatch: 'AvgMEM'
+ formatter: 0
+ numberFormat: {
+ unit: 38
+ options: {
+ style: 'decimal'
+ maximumFractionDigits: 2
+ }
+ }
+ }
+ {
+ columnMatch: 'MaxMEM'
+ formatter: 0
+ numberFormat: {
+ unit: 38
+ options: {
+ style: 'decimal'
+ maximumFractionDigits: 2
+ }
+ }
+ }
+ {
+ columnMatch: 'StatusDisk'
+ formatter: 18
+ formatOptions: {
+ thresholdsOptions: 'icons'
+ thresholdsGrid: [
+ {
+ operator: '=='
+ thresholdValue: '0'
+ representation: 'success'
+ text: '{1}'
+ }
+ {
+ operator: '=='
+ thresholdValue: '1'
+ representation: '2'
+ text: '{1}'
+ }
+ {
+ operator: '=='
+ thresholdValue: '2'
+ representation: '4'
+ text: '{1}'
+ }
+ {
+ operator: 'Default'
+ thresholdValue: null
+ representation: 'success'
+ text: '{1}'
+ }
+ ]
+ }
+ }
+ {
+ columnMatch: 'AvgDisk'
+ formatter: 0
+ numberFormat: {
+ unit: 38
+ options: {
+ style: 'decimal'
+ maximumFractionDigits: 2
+ }
+ }
+ }
+ {
+ columnMatch: 'LastDisk'
+ formatter: 0
+ numberFormat: {
+ unit: 4
+ options: {
+ style: 'decimal'
+ maximumFractionDigits: 2
+ }
+ }
+ }
+ {
+ columnMatch: 'Trend'
+ formatter: 10
+ formatOptions: {
+ palette: 'blue'
+ }
+ }
+ {
+ columnMatch: 'Max'
+ formatter: 0
+ numberFormat: {
+ unit: 0
+ options: {
+ style: 'decimal'
+ }
+ }
+ }
+ {
+ columnMatch: 'Average'
+ formatter: 8
+ formatOptions: {
+ palette: 'yellowOrangeRed'
+ }
+ numberFormat: {
+ unit: 0
+ options: {
+ style: 'decimal'
+ useGrouping: false
+ }
+ }
+ }
+ {
+ columnMatch: 'Min'
+ formatter: 8
+ formatOptions: {
+ palette: 'yellowOrangeRed'
+ aggregation: 'Min'
+ }
+ numberFormat: {
+ unit: 0
+ options: {
+ style: 'decimal'
+ }
+ }
+ }
+ ]
+ filter: true
+ labelSettings: [
+ {
+ columnId: '_ResourceId'
+ label: 'Computer'
+ }
+ ]
+ }
+ sortBy: []
+ }
+ showPin: true
+ name: 'query - 2'
+ styleSettings: {
+ showBorder: true
+ }
+ }
+ {
+ type: 1
+ content: {
+ json: '# Top Performance'
+ }
+ name: 'text - 8'
+ }
+ {
+ type: 1
+ content: {
+ json: '## Processor(_Total)\\% Processor Time'
+ }
+ name: 'text - 10'
+ }
+ {
+ type: 3
+ content: {
+ version: 'KqlItem/1.0'
+ query: 'let TopComputers = InsightsMetrics \r\n| where Origin == "vm.azm.ms"\r\n| extend RGName = tolower(split(_ResourceId, "/")[4])\r\n| where RGName in ({ResourceGroup})\r\n| where _ResourceId contains "{_ResourceId}" or "{_ResourceId}"=="All"\r\n| where Namespace == "Processor" and Name == "UtilizationPercentage"\r\n| summarize AvgCPU = avg(Val) by Computer \r\n| top 10 by AvgCPU desc\r\n| project Computer; \r\nInsightsMetrics \r\n| where Origin == "vm.azm.ms"\r\n| where Computer in (TopComputers) \r\n| where Namespace == "Processor" and Name == "UtilizationPercentage"\r\n| summarize Used_CPU = round(avg(Val),1) by Computer, bin(TimeGenerated, ({TimeRange:end} - {TimeRange:start})/100)\r\n| render timechart'
+ size: 0
+ aggregation: 3
+ showAnalytics: true
+ title: '% Processor Time - Top 10 Computers'
+ timeContextFromParameter: 'TimeRange'
+ queryType: 0
+ resourceType: 'microsoft.operationalinsights/workspaces'
+ crossComponentResources: [
+ '{Workspace}'
+ ]
+ chartSettings: {
+ showLegend: true
+ }
+ }
+ customWidth: '50'
+ name: 'query - 4'
+ styleSettings: {
+ showBorder: true
+ }
+ }
+ {
+ type: 3
+ content: {
+ version: 'KqlItem/1.0'
+ query: 'let trend = \r\nInsightsMetrics\r\n| where Origin == "vm.azm.ms" \r\n| extend RGName = tolower(split(_ResourceId, "/")[4])\r\n| where _ResourceId contains "{_ResourceId}" or "{_ResourceId}"=="All"\r\n| where RGName in ({ResourceGroup})\r\n| where Namespace == "Processor" and Name == "UtilizationPercentage"\r\n| make-series Average = round(avg(Val), 3) default = 0 on TimeGenerated from {TimeRange:start} to {TimeRange:end} step totimespan(\'00:30:00\') by _ResourceId \r\n| project _ResourceId, [\'Trend\'] = Average; \r\n\r\nInsightsMetrics\r\n| where Origin == "vm.azm.ms"\r\n| extend RGName = tolower(split(_ResourceId, "/")[4])\r\n| where RGName in ({ResourceGroup})\r\n| where _ResourceId contains "{_ResourceId}" or "{_ResourceId}"=="All"\r\n| where Namespace == "Processor" and Name == "UtilizationPercentage"\r\n| summarize Average=round(avg(Val),3) by _ResourceId\r\n| join (trend) on _ResourceId\r\n| extend Status = case (\r\n Average > 80, "Critical",\r\n Average > 50, "Warning",\r\n Average <= 50, "Healthy", "Unknown"\r\n)\r\n\r\n| project Status, _ResourceId, Average, Trend\r\n| sort by Status desc'
+ size: 0
+ showAnalytics: true
+ title: 'Thresholds (Warning=50; Critical=80) - All Computers'
+ timeContextFromParameter: 'TimeRange'
+ queryType: 0
+ resourceType: 'microsoft.operationalinsights/workspaces'
+ crossComponentResources: [
+ '{Workspace}'
+ ]
+ gridSettings: {
+ formatters: [
+ {
+ columnMatch: 'Status'
+ formatter: 18
+ formatOptions: {
+ thresholdsOptions: 'icons'
+ thresholdsGrid: [
+ {
+ operator: '=='
+ thresholdValue: 'Healthy'
+ representation: 'success'
+ text: '{1}'
+ }
+ {
+ operator: '=='
+ thresholdValue: 'Warning'
+ representation: '2'
+ text: '{1}'
+ }
+ {
+ operator: '=='
+ thresholdValue: 'Critical'
+ representation: 'critical'
+ text: '{1}'
+ }
+ {
+ operator: 'Default'
+ thresholdValue: null
+ representation: 'unknown'
+ text: '{1}'
+ }
+ ]
+ }
+ }
+ {
+ columnMatch: 'Average'
+ formatter: 8
+ formatOptions: {
+ min: 0
+ max: 100
+ palette: 'greenRed'
+ }
+ }
+ {
+ columnMatch: 'Trend'
+ formatter: 21
+ formatOptions: {
+ palette: 'green'
+ }
+ }
+ ]
+ filter: true
+ sortBy: [
+ {
+ itemKey: '$gen_link__ResourceId_1'
+ sortOrder: 2
+ }
+ ]
+ labelSettings: [
+ {
+ columnId: 'Status'
+ label: 'Status'
+ }
+ {
+ columnId: '_ResourceId'
+ label: 'Computer'
+ }
+ ]
+ }
+ sortBy: [
+ {
+ itemKey: '$gen_link__ResourceId_1'
+ sortOrder: 2
+ }
+ ]
+ }
+ customWidth: '50'
+ name: 'query - 9'
+ styleSettings: {
+ showBorder: true
+ }
+ }
+ {
+ type: 1
+ content: {
+ json: '## Memory: _Available MBytes_ and _% Committed Bytes in Use_'
+ }
+ name: 'text - 11'
+ }
+ {
+ type: 3
+ content: {
+ version: 'KqlItem/1.0'
+ query: 'let TopComputers = InsightsMetrics \r\n| where Origin == "vm.azm.ms"\r\n| extend RGName = tolower(split(_ResourceId, "/")[4])\r\n| where RGName in ({ResourceGroup})\r\n| where _ResourceId contains "{_ResourceId}" or "{_ResourceId}"=="All"\r\n| where Namespace == "Memory" and Name == "AvailableMB"\r\n| summarize AvailableGBytes = round(avg(Val)/1024,2) by Computer\r\n| top 10 by AvailableGBytes asc\r\n| project Computer; \r\nInsightsMetrics \r\n| where Origin == "vm.azm.ms"\r\n| where Computer in (TopComputers) \r\n| where Namespace == "Memory" and Name == "AvailableMB"\r\n| summarize AvailableGBytes = round(avg(Val)/1024,2) by Computer, bin(TimeGenerated, ({TimeRange:end} - {TimeRange:start})/100)\r\n| render timechart'
+ size: 0
+ aggregation: 3
+ showAnalytics: true
+ title: 'Available MBytes - Top 10 Computers'
+ timeContextFromParameter: 'TimeRange'
+ queryType: 0
+ resourceType: 'microsoft.operationalinsights/workspaces'
+ crossComponentResources: [
+ '{Workspace}'
+ ]
+ visualization: 'timechart'
+ gridSettings: {
+ formatters: [
+ {
+ columnMatch: 'AvailableMBytes'
+ formatter: 0
+ formatOptions: {
+ showIcon: true
+ }
+ numberFormat: {
+ unit: 4
+ options: {
+ style: 'decimal'
+ useGrouping: false
+ }
+ }
+ }
+ ]
+ }
+ chartSettings: {
+ createOtherGroup: 0
+ showLegend: true
+ ySettings: {
+ unit: 5
+ min: null
+ max: null
+ }
+ }
+ }
+ customWidth: '50'
+ name: 'query - 5'
+ styleSettings: {
+ showBorder: true
+ }
+ }
+ {
+ type: 3
+ content: {
+ version: 'KqlItem/1.0'
+ query: 'let trend = \r\nInsightsMetrics\r\n| where Origin == "vm.azm.ms" \r\n| extend RGName = tolower(split(_ResourceId, "/")[4])\r\n| where RGName in ({ResourceGroup})\r\n| where _ResourceId contains "{_ResourceId}" or "{_ResourceId}"=="All"\r\n| where Namespace == "Memory" and Name == "AvailableMB"\r\n| make-series Average = round(avg(Val), 3) default = 0 on TimeGenerated from {TimeRange:start} to {TimeRange:end} step totimespan(\'00:30:00\') by _ResourceId \r\n| project _ResourceId, [\'Trend\'] = Average; \r\n\r\nInsightsMetrics\r\n| where Origin == "vm.azm.ms"\r\n| extend RGName = tolower(split(_ResourceId, "/")[4])\r\n| where RGName in ({ResourceGroup})\r\n| where _ResourceId contains "{_ResourceId}" or "{_ResourceId}"=="All"\r\n| where Namespace == "Memory" and Name == "AvailableMB"\r\n| summarize ["Available GBytes"]=round(avg(Val)/1024,2) by _ResourceId\r\n| join (trend) on _ResourceId\r\n| extend Status = case (\r\n ["Available GBytes"] > 4, "Healthy",\r\n ["Available GBytes"] >= 1, "Warning",\r\n ["Available GBytes"] < 1, "Critical", "Unknown"\r\n)\r\n| project Status, _ResourceId, ["Available GBytes"], Trend\r\n| sort by ["Available GBytes"] asc'
+ size: 0
+ showAnalytics: true
+ title: 'Thresholds (Warning < 4 GB; Critical < 1 GB) - All Computers'
+ timeContextFromParameter: 'TimeRange'
+ queryType: 0
+ resourceType: 'microsoft.operationalinsights/workspaces'
+ crossComponentResources: [
+ '{Workspace}'
+ ]
+ gridSettings: {
+ formatters: [
+ {
+ columnMatch: 'Status'
+ formatter: 18
+ formatOptions: {
+ thresholdsOptions: 'icons'
+ thresholdsGrid: [
+ {
+ operator: '=='
+ thresholdValue: 'Healthy'
+ representation: 'success'
+ text: '{1}'
+ }
+ {
+ operator: '=='
+ thresholdValue: 'Warning'
+ representation: '2'
+ text: '{1}'
+ }
+ {
+ operator: '=='
+ thresholdValue: 'Critical'
+ representation: 'critical'
+ text: '{1}'
+ }
+ {
+ operator: 'Default'
+ thresholdValue: null
+ representation: 'Unknown'
+ text: '{1}'
+ }
+ ]
+ }
+ }
+ {
+ columnMatch: 'Available GBytes'
+ formatter: 8
+ formatOptions: {
+ min: 0
+ max: 20
+ palette: 'redGreen'
+ }
+ }
+ {
+ columnMatch: 'Trend'
+ formatter: 21
+ formatOptions: {
+ palette: 'green'
+ }
+ }
+ ]
+ filter: true
+ sortBy: [
+ {
+ itemKey: '$gen_link__ResourceId_1'
+ sortOrder: 2
+ }
+ ]
+ labelSettings: [
+ {
+ columnId: 'Status'
+ label: 'Status'
+ }
+ {
+ columnId: '_ResourceId'
+ label: 'Computer'
+ }
+ ]
+ }
+ sortBy: [
+ {
+ itemKey: '$gen_link__ResourceId_1'
+ sortOrder: 2
+ }
+ ]
+ }
+ customWidth: '50'
+ name: 'query - 9 - Copy'
+ styleSettings: {
+ showBorder: true
+ }
+ }
+ {
+ type: 3
+ content: {
+ version: 'KqlItem/1.0'
+ query: 'let TopComputers = InsightsMetrics \r\n| where Origin == "vm.azm.ms"\r\n| extend RGName = tolower(split(_ResourceId, "/")[4])\r\n| where RGName in ({ResourceGroup})\r\n| where _ResourceId contains "{_ResourceId}" or "{_ResourceId}"=="All"\r\n| where Namespace == "Memory" and Name == "AvailableMB"\r\n| extend TotalMemory = toreal(todynamic(Tags)["vm.azm.ms/memorySizeMB"]) \r\n| extend CommittedMemoryPercentage = 100-((toreal(Val) / TotalMemory) * 100.0)\r\n| summarize PctCommittedBytes = round(avg(CommittedMemoryPercentage),2) by Computer\r\n| top 10 by PctCommittedBytes desc\r\n| project Computer; \r\nInsightsMetrics \r\n| where Origin == "vm.azm.ms"\r\n| where Computer in (TopComputers) \r\n| where Namespace == "Memory" and Name == "AvailableMB"\r\n| extend TotalMemory = toreal(todynamic(Tags)["vm.azm.ms/memorySizeMB"]) \r\n| extend CommittedMemoryPercentage = 100-((toreal(Val) / TotalMemory) * 100.0)\r\n| summarize PctCommittedBytes = round(avg(CommittedMemoryPercentage),2) by Computer, bin(TimeGenerated, ({TimeRange:end} - {TimeRange:start})/100)\r\n| render timechart'
+ size: 0
+ aggregation: 3
+ showAnalytics: true
+ title: '% Committed Bytes In Use - Top 10 Computers'
+ timeContextFromParameter: 'TimeRange'
+ queryType: 0
+ resourceType: 'microsoft.operationalinsights/workspaces'
+ crossComponentResources: [
+ '{Workspace}'
+ ]
+ chartSettings: {
+ group: 'Computer'
+ createOtherGroup: 0
+ showLegend: true
+ ySettings: {
+ unit: 1
+ min: 0
+ max: 100
+ }
+ }
+ }
+ customWidth: '50'
+ name: 'query - 9'
+ styleSettings: {
+ showBorder: true
+ }
+ }
+ {
+ type: 3
+ content: {
+ version: 'KqlItem/1.0'
+ query: 'let trend = \r\nInsightsMetrics \r\n| where Origin == "vm.azm.ms" \r\n| extend RGName = tolower(split(_ResourceId, "/")[4])\r\n| where RGName in ({ResourceGroup})\r\n| where _ResourceId contains "{_ResourceId}" or "{_ResourceId}"=="All"\r\n| where Namespace == "Memory" and Name == "AvailableMB"\r\n| extend TotalMemory = toreal(todynamic(Tags)["vm.azm.ms/memorySizeMB"]) \r\n| extend CommittedMemoryPercentage = 100-((toreal(Val) / TotalMemory) * 100.0)\r\n| make-series Average = round(avg(CommittedMemoryPercentage), 3) default = 0 on TimeGenerated from {TimeRange:start} to {TimeRange:end} step totimespan(\'00:30:00\') by _ResourceId \r\n| project _ResourceId, [\'Trend\'] = Average; \r\n\r\nInsightsMetrics\r\n| where Origin == "vm.azm.ms"\r\n| extend RGName = tolower(split(_ResourceId, "/")[4])\r\n| where RGName in ({ResourceGroup})\r\n| where _ResourceId contains "{_ResourceId}" or "{_ResourceId}"=="All"\r\n| where Namespace == "Memory" and Name == "AvailableMB"\r\n| extend TotalMemory = toreal(todynamic(Tags)["vm.azm.ms/memorySizeMB"]) \r\n| extend CommittedMemoryPercentage = 100-((toreal(Val) / TotalMemory) * 100.0)\r\n| summarize Average=round(avg(CommittedMemoryPercentage),3) by _ResourceId\r\n| join (trend) on _ResourceId\r\n| extend Status = case (\r\n Average > 90, "Critical",\r\n Average > 60, "Warning",\r\n Average <= 60, "Healthy", "Unknown"\r\n)\r\n\r\n| project Status, _ResourceId, Average, Trend\r\n| sort by Average '
+ size: 0
+ showAnalytics: true
+ title: 'Thresholds (Warning>60; Critical>90) - All Computers'
+ timeContextFromParameter: 'TimeRange'
+ queryType: 0
+ resourceType: 'microsoft.operationalinsights/workspaces'
+ crossComponentResources: [
+ '{Workspace}'
+ ]
+ gridSettings: {
+ formatters: [
+ {
+ columnMatch: 'Status'
+ formatter: 18
+ formatOptions: {
+ thresholdsOptions: 'icons'
+ thresholdsGrid: [
+ {
+ operator: '=='
+ thresholdValue: 'Healthy'
+ representation: 'success'
+ text: '{1}'
+ }
+ {
+ operator: '=='
+ thresholdValue: 'Warning'
+ representation: '2'
+ text: '{1}'
+ }
+ {
+ operator: '=='
+ thresholdValue: 'Critical'
+ representation: 'critical'
+ text: '{1}'
+ }
+ {
+ operator: 'Default'
+ thresholdValue: null
+ representation: 'unknown'
+ text: '{1}'
+ }
+ ]
+ }
+ }
+ {
+ columnMatch: 'Average'
+ formatter: 8
+ formatOptions: {
+ min: 0
+ max: 100
+ palette: 'greenRed'
+ }
+ }
+ {
+ columnMatch: 'Trend'
+ formatter: 21
+ formatOptions: {
+ palette: 'green'
+ }
+ }
+ ]
+ filter: true
+ sortBy: [
+ {
+ itemKey: '$gen_link__ResourceId_1'
+ sortOrder: 2
+ }
+ ]
+ labelSettings: [
+ {
+ columnId: 'Status'
+ label: 'Status'
+ }
+ {
+ columnId: '_ResourceId'
+ label: 'Computer'
+ }
+ ]
+ }
+ sortBy: [
+ {
+ itemKey: '$gen_link__ResourceId_1'
+ sortOrder: 2
+ }
+ ]
+ }
+ customWidth: '50'
+ name: 'query - 9 - Copy'
+ styleSettings: {
+ showBorder: true
+ }
+ }
+ {
+ type: 1
+ content: {
+ json: '## Logical Disk: _Free Megabytes_ and _Avg read/write per sec_ '
+ }
+ name: 'text - 12'
+ }
+ {
+ type: 3
+ content: {
+ version: 'KqlItem/1.0'
+ query: 'let TopDiscos = InsightsMetrics \r\n| where Origin == "vm.azm.ms"\r\n| extend RGName = tolower(split(_ResourceId, "/")[4])\r\n| where RGName in ({ResourceGroup})\r\n| where _ResourceId contains "{_ResourceId}" or "{_ResourceId}"=="All"\r\n| where Namespace == "LogicalDisk" and Name == "FreeSpaceMB"\r\n| extend Disk = tostring(todynamic(Tags)["vm.azm.ms/mountId"])\r\n| where (strlen(Disk) ==2 and Disk contains ":") or Disk=="/"\r\n| extend Disco = strcat(Disk, " - ",Computer )\r\n| summarize FreeSpace = round(avg(Val),2) by Disco\r\n| top 10 by FreeSpace asc\r\n| project Disco; \r\nInsightsMetrics \r\n| where Origin == "vm.azm.ms"\r\n| where Namespace == "LogicalDisk" and Name == "FreeSpaceMB"\r\n| extend Disk = tostring(todynamic(Tags)["vm.azm.ms/mountId"])\r\n| where (strlen(Disk) ==2 and Disk contains ":") or Disk=="/"\r\n| extend Disco = strcat(Disk, " - ",Computer )\r\n| where Disco in (TopDiscos) \r\n| summarize FreeSpace = round(avg(Val),2) by Disco, bin(TimeGenerated, ({TimeRange:end} - {TimeRange:start})/100)\r\n\r\n\r\n'
+ size: 0
+ aggregation: 3
+ showAnalytics: true
+ title: 'Free Megabytes - Top 10 Computers-Volumes'
+ timeContextFromParameter: 'TimeRange'
+ queryType: 0
+ resourceType: 'microsoft.operationalinsights/workspaces'
+ crossComponentResources: [
+ '{Workspace}'
+ ]
+ visualization: 'timechart'
+ chartSettings: {
+ showLegend: true
+ ySettings: {
+ numberFormatSettings: {
+ unit: 4
+ options: {
+ style: 'decimal'
+ useGrouping: true
+ }
+ }
+ }
+ }
+ }
+ customWidth: '50'
+ name: 'query - 6'
+ styleSettings: {
+ showBorder: true
+ }
+ }
+ {
+ type: 3
+ content: {
+ version: 'KqlItem/1.0'
+ query: 'let trend = \r\nInsightsMetrics \r\n| where Origin == "vm.azm.ms"\r\n| extend RGName = tolower(split(_ResourceId, "/")[4])\r\n| where RGName in ({ResourceGroup}) \r\n| where _ResourceId contains "{_ResourceId}" or "{_ResourceId}"=="All"\r\n| where Namespace == "LogicalDisk" and Name == "FreeSpaceMB"\r\n| extend Disk=tostring(todynamic(Tags)["vm.azm.ms/mountId"])\r\n| where (strlen(Disk) ==2 and Disk contains ":") or Disk=="/"\r\n| extend Disco = strcat(Disk, " - ", _ResourceId)\r\n| make-series Average = round(avg(Val), 3) default = 0 on TimeGenerated from {TimeRange:start} to {TimeRange:end} step totimespan(\'00:30:00\') by Disco \r\n| project Disco, [\'Trend\'] = Average; \r\n\r\nInsightsMetrics\r\n| where Origin == "vm.azm.ms"\r\n| extend RGName = tolower(split(_ResourceId, "/")[4])\r\n| where RGName in ({ResourceGroup})\r\n| where _ResourceId contains "{_ResourceId}" or "{_ResourceId}"=="All"\r\n| where Namespace == "LogicalDisk" and Name == "FreeSpaceMB"\r\n| extend Disk=tostring(todynamic(Tags)["vm.azm.ms/mountId"])\r\n| where (strlen(Disk) ==2 and Disk contains ":") or Disk=="/"\r\n| extend Disco = strcat(Disk, " - ",_ResourceId )\r\n| summarize Average=round(avg(Val),2) by Disco,_ResourceId,Disk\r\n| join (trend) on Disco\r\n| extend Status = case (\r\n Average < 5000, "Critical",\r\n Average < 30000, "Warning",\r\n Average >= 30000, "Healthy", "Unknown"\r\n)\r\n\r\n| project Status, _ResourceId,Disk, Average, Trend\r\n| sort by Average asc '
+ size: 0
+ showAnalytics: true
+ title: 'Thresholds (Warning < 30GB; Critical < 5GB) - All Computers-Volumes'
+ timeContextFromParameter: 'TimeRange'
+ queryType: 0
+ resourceType: 'microsoft.operationalinsights/workspaces'
+ crossComponentResources: [
+ '{Workspace}'
+ ]
+ gridSettings: {
+ formatters: [
+ {
+ columnMatch: 'Status'
+ formatter: 18
+ formatOptions: {
+ thresholdsOptions: 'icons'
+ thresholdsGrid: [
+ {
+ operator: '=='
+ thresholdValue: 'Healthy'
+ representation: 'success'
+ text: '{1}'
+ }
+ {
+ operator: '=='
+ thresholdValue: 'Warning'
+ representation: '2'
+ text: '{1}'
+ }
+ {
+ operator: '=='
+ thresholdValue: 'Critical'
+ representation: 'critical'
+ text: '{1}'
+ }
+ {
+ operator: 'Default'
+ thresholdValue: null
+ representation: 'unknown'
+ text: '{1}'
+ }
+ ]
+ }
+ }
+ {
+ columnMatch: 'Average'
+ formatter: 8
+ formatOptions: {
+ min: 0
+ max: 40000
+ palette: 'redGreen'
+ }
+ numberFormat: {
+ unit: 4
+ options: {
+ style: 'decimal'
+ useGrouping: false
+ minimumFractionDigits: 2
+ maximumFractionDigits: 2
+ }
+ }
+ }
+ {
+ columnMatch: 'Trend'
+ formatter: 21
+ formatOptions: {
+ palette: 'green'
+ }
+ }
+ ]
+ filter: true
+ sortBy: [
+ {
+ itemKey: '$gen_link__ResourceId_1'
+ sortOrder: 2
+ }
+ ]
+ labelSettings: [
+ {
+ columnId: 'Status'
+ label: 'Status'
+ }
+ {
+ columnId: '_ResourceId'
+ label: 'Computer'
+ }
+ ]
+ }
+ sortBy: [
+ {
+ itemKey: '$gen_link__ResourceId_1'
+ sortOrder: 2
+ }
+ ]
+ }
+ customWidth: '50'
+ name: 'query - 9 - Copy - Copy'
+ styleSettings: {
+ showBorder: true
+ }
+ }
+ {
+ type: 3
+ content: {
+ version: 'KqlItem/1.0'
+ query: 'let TopDiscos = InsightsMetrics \r\n| where Origin == "vm.azm.ms"\r\n| extend RGName = tolower(split(_ResourceId, "/")[4])\r\n| where RGName in ({ResourceGroup})\r\n| where _ResourceId contains "{_ResourceId}" or "{_ResourceId}"=="All"\r\n| where Namespace == "LogicalDisk" and Name == "ReadsPerSecond"\r\n| extend Disk=tostring(todynamic(Tags)["vm.azm.ms/mountId"])\r\n| where (strlen(Disk) ==2 and Disk contains ":") or ( Disk == "/")\r\n| extend Disco = strcat(Disk, " - ",Computer )\r\n| summarize MilliSeconds = avg(Val) by Disco\r\n| top 10 by MilliSeconds desc\r\n| project Disco; \r\nInsightsMetrics \r\n| where Origin == "vm.azm.ms"\r\n| where Namespace == "LogicalDisk" and Name == "ReadsPerSecond"\r\n| extend Disk=tostring(todynamic(Tags)["vm.azm.ms/mountId"])\r\n| where (strlen(Disk) ==2 and Disk contains ":") or ( Disk == "/")\r\n| extend Disco = strcat(Disk, " - ",Computer )\r\n| where Disco in (TopDiscos) \r\n| summarize MilliSeconds = avg(Val) by Disco, bin(TimeGenerated, ({TimeRange:end} - {TimeRange:start})/100)\r\n| render timechart\r\n\r\n\r\n'
+ size: 0
+ aggregation: 3
+ showAnalytics: true
+ title: 'Disk Reads/sec - Top 10 Computers-Volume'
+ timeContextFromParameter: 'TimeRange'
+ queryType: 0
+ resourceType: 'microsoft.operationalinsights/workspaces'
+ crossComponentResources: [
+ '{Workspace}'
+ ]
+ visualization: 'timechart'
+ chartSettings: {
+ group: 'Disco'
+ createOtherGroup: 22
+ showLegend: true
+ }
+ }
+ customWidth: '50'
+ name: 'query - 6 - Copy'
+ styleSettings: {
+ showBorder: true
+ }
+ }
+ {
+ type: 3
+ content: {
+ version: 'KqlItem/1.0'
+ query: 'let trend = \r\nInsightsMetrics\r\n| where Origin == "vm.azm.ms" \r\n| extend RGName = tolower(split(_ResourceId, "/")[4])\r\n| where RGName in ({ResourceGroup}) \r\n| where _ResourceId contains "{_ResourceId}" or "{_ResourceId}"=="All" \r\n| where Namespace == "LogicalDisk" and Name == "ReadsPerSecond"\r\n| extend Disk=tostring(todynamic(Tags)["vm.azm.ms/mountId"])\r\n| where ((strlen(Disk) == 2 and Disk contains ":") or (Disk == "/"))\r\n| extend Disco = strcat(Disk, " - ",_ResourceId )\r\n| make-series AVGReads = round(avg(Val),2) default = 0 on TimeGenerated from {TimeRange:start} to {TimeRange:end} step totimespan(\'00:30:00\') by Disco \r\n| project Disco, [\'Trend\'] = AVGReads; \r\n\r\nInsightsMetrics\r\n| where Origin == "vm.azm.ms"\r\n| extend RGName = tolower(split(_ResourceId, "/")[4])\r\n| where RGName in ({ResourceGroup})\r\n| where _ResourceId contains "{_ResourceId}" or "{_ResourceId}"=="All"\r\n| where Namespace == "LogicalDisk" and Name == "ReadsPerSecond"\r\n| extend Disk=tostring(todynamic(Tags)["vm.azm.ms/mountId"])\r\n| where ((strlen(Disk) == 2 and Disk contains ":") or (Disk == "/"))\r\n| extend Disco = strcat(Disk, " - ",_ResourceId )\r\n| summarize AVGReads=round(avg(Val),2) by Disco,_ResourceId,Disk\r\n| join (trend) on Disco\r\n| extend Status = case (\r\n AVGReads > 25, "Critical",\r\n AVGReads > 15, "Warning",\r\n AVGReads <= 15, "Healthy", "Unknown"\r\n)\r\n| project _ResourceId,Disk, ["Reads"]=AVGReads, Trend\r\n| sort by ["Reads"] desc '
+ size: 0
+ showAnalytics: true
+ timeContextFromParameter: 'TimeRange'
+ queryType: 0
+ resourceType: 'microsoft.operationalinsights/workspaces'
+ crossComponentResources: [
+ '{Workspace}'
+ ]
+ gridSettings: {
+ formatters: [
+ {
+ columnMatch: 'Trend'
+ formatter: 21
+ formatOptions: {
+ palette: 'green'
+ }
+ }
+ {
+ columnMatch: 'Status'
+ formatter: 18
+ formatOptions: {
+ thresholdsOptions: 'icons'
+ thresholdsGrid: [
+ {
+ operator: '=='
+ thresholdValue: 'Healthy'
+ representation: 'success'
+ text: '{1}'
+ }
+ {
+ operator: '=='
+ thresholdValue: 'Warning'
+ representation: '2'
+ text: '{1}'
+ }
+ {
+ operator: '=='
+ thresholdValue: 'Critical'
+ representation: 'critical'
+ text: '{1}'
+ }
+ {
+ operator: 'Default'
+ thresholdValue: null
+ representation: 'unknown'
+ text: '{1}'
+ }
+ ]
+ }
+ }
+ {
+ columnMatch: 'Average'
+ formatter: 8
+ formatOptions: {
+ min: 0
+ max: 100
+ palette: 'blue'
+ }
+ }
+ ]
+ filter: true
+ sortBy: [
+ {
+ itemKey: '$gen_link__ResourceId_0'
+ sortOrder: 2
+ }
+ ]
+ labelSettings: [
+ {
+ columnId: '_ResourceId'
+ label: 'Computer'
+ }
+ ]
+ }
+ sortBy: [
+ {
+ itemKey: '$gen_link__ResourceId_0'
+ sortOrder: 2
+ }
+ ]
+ }
+ customWidth: '50'
+ name: 'query - 9 - Copy - Copy - Copy'
+ styleSettings: {
+ showBorder: true
+ }
+ }
+ {
+ type: 3
+ content: {
+ version: 'KqlItem/1.0'
+ query: 'let TopDiscos= InsightsMetrics \r\n| where Origin == "vm.azm.ms"\r\n| extend RGName = tolower(split(_ResourceId, "/")[4])\r\n| where RGName in ({ResourceGroup})\r\n| where _ResourceId contains "{_ResourceId}" or "{_ResourceId}"=="All"\r\n| where Namespace == "LogicalDisk" and Name == "WritesPerSecond"\r\n| extend Disk=tostring(todynamic(Tags)["vm.azm.ms/mountId"])\r\n| where (strlen(Disk) ==2 and Disk contains ":") or Disk=="/"\r\n| extend Disco = strcat(Disk, " - ",Computer )\r\n| summarize MilliSeconds = avg(Val) by Disco\r\n| top 10 by MilliSeconds desc\r\n| project Disco; \r\nInsightsMetrics \r\n| where Origin == "vm.azm.ms"\r\n| where Namespace == "LogicalDisk" and Name == "WritesPerSecond"\r\n| extend Disk=tostring(todynamic(Tags)["vm.azm.ms/mountId"])\r\n| where (strlen(Disk) ==2 and Disk contains ":") or Disk=="/"\r\n| extend Disco = strcat(Disk, " - ",Computer )\r\n| where Disco in (TopDiscos) \r\n| summarize MilliSeconds = avg(Val) by Disco, bin(TimeGenerated, ({TimeRange:end} - {TimeRange:start})/100)\r\n| render timechart\r\n\r\n\r\n'
+ size: 0
+ showAnalytics: true
+ title: 'Disk Writes/sec - Top 10 Computers-Volume'
+ timeContextFromParameter: 'TimeRange'
+ queryType: 0
+ resourceType: 'microsoft.operationalinsights/workspaces'
+ crossComponentResources: [
+ '{Workspace}'
+ ]
+ visualization: 'timechart'
+ chartSettings: {
+ group: 'Disco'
+ createOtherGroup: 22
+ showLegend: true
+ }
+ }
+ customWidth: '50'
+ name: 'query - 6 - Copy - Copy'
+ styleSettings: {
+ showBorder: true
+ }
+ }
+ {
+ type: 3
+ content: {
+ version: 'KqlItem/1.0'
+ query: 'let trend = \r\nInsightsMetrics \r\n| where Origin == "vm.azm.ms" \r\n| extend RGName = tolower(split(_ResourceId, "/")[4])\r\n| where RGName in ({ResourceGroup})\r\n| where _ResourceId contains "{_ResourceId}" or "{_ResourceId}"=="All"\r\n| where Namespace == "LogicalDisk" and Name == "WritesPerSecond"\r\n| extend Disk=tostring(todynamic(Tags)["vm.azm.ms/mountId"])\r\n| where (strlen(Disk) ==2 and Disk contains ":") or Disk=="/"\r\n| extend Disco = strcat(Disk, " - ",_ResourceId )\r\n| make-series AVGWrites = round(avg(Val),2) default = 0 on TimeGenerated from {TimeRange:start} to {TimeRange:end} step totimespan(\'00:30:00\') by Disco \r\n| project Disco, [\'Trend\'] = AVGWrites; \r\n\r\nInsightsMetrics\r\n| where Origin == "vm.azm.ms"\r\n| extend RGName = tolower(split(_ResourceId, "/")[4])\r\n| where RGName in ({ResourceGroup})\r\n| where _ResourceId contains "{_ResourceId}" or "{_ResourceId}"=="All"\r\n| where Namespace == "LogicalDisk" and Name == "WritesPerSecond"\r\n| extend Disk=tostring(todynamic(Tags)["vm.azm.ms/mountId"])\r\n| where (strlen(Disk) ==2 and Disk contains ":") or Disk=="/"\r\n| extend Disco = strcat(Disk, " - ",_ResourceId )\r\n| summarize AVGWrites=round(avg(Val),2) by Disco,_ResourceId,Disk\r\n| join (trend) on Disco\r\n| extend Status = case (\r\n AVGWrites > 25, "Critical",\r\n AVGWrites > 15, "Warning",\r\n AVGWrites <= 15, "Healthy", "Unknown"\r\n)\r\n\r\n| project _ResourceId,Disk, ["Writes"]=AVGWrites, Trend\r\n| sort by ["Writes"] desc '
+ size: 0
+ showAnalytics: true
+ timeContextFromParameter: 'TimeRange'
+ queryType: 0
+ resourceType: 'microsoft.operationalinsights/workspaces'
+ crossComponentResources: [
+ '{Workspace}'
+ ]
+ gridSettings: {
+ formatters: [
+ {
+ columnMatch: 'Trend'
+ formatter: 21
+ formatOptions: {
+ palette: 'green'
+ }
+ }
+ {
+ columnMatch: 'Status'
+ formatter: 18
+ formatOptions: {
+ thresholdsOptions: 'icons'
+ thresholdsGrid: [
+ {
+ operator: '=='
+ thresholdValue: 'Healthy'
+ representation: 'success'
+ text: '{1}'
+ }
+ {
+ operator: '=='
+ thresholdValue: 'Warning'
+ representation: '2'
+ text: '{1}'
+ }
+ {
+ operator: '=='
+ thresholdValue: 'Critical'
+ representation: 'critical'
+ text: '{1}'
+ }
+ {
+ operator: 'Default'
+ thresholdValue: null
+ representation: 'unknown'
+ text: '{1}'
+ }
+ ]
+ }
+ }
+ {
+ columnMatch: 'Average'
+ formatter: 8
+ formatOptions: {
+ min: 0
+ max: 100
+ palette: 'blue'
+ }
+ }
+ ]
+ filter: true
+ sortBy: [
+ {
+ itemKey: '$gen_link__ResourceId_0'
+ sortOrder: 2
+ }
+ ]
+ labelSettings: [
+ {
+ columnId: '_ResourceId'
+ label: 'Computer'
+ }
+ ]
+ }
+ sortBy: [
+ {
+ itemKey: '$gen_link__ResourceId_0'
+ sortOrder: 2
+ }
+ ]
+ }
+ customWidth: '50'
+ name: 'query - 9 - Copy - Copy - Copy - Copy'
+ styleSettings: {
+ showBorder: true
+ }
+ }
+ ]
+ fallbackResourceIds: [
+ 'azure monitor'
+ ]
+ '$schema': 'https://github.com/Microsoft/Application-Insights-Workbooks/blob/master/schema/workbook.json'
+}
+resource workbookId_resource 'microsoft.insights/workbooks@2022-04-01' = {
+ name: workbookId
+ location: location
+ kind: 'shared'
+ properties: {
+ displayName: workbookDisplayName
+ serializedData: string(workbookContent)
+ version: '1.0'
+ sourceId: workbookSourceId
+ category: workbookType
+ }
+ dependsOn: []
+}
+output workbookId string = workbookId_resource.id
diff --git a/azure_jumpstart_ag/retail/artifacts/monitoring/grafana-cluster-global.json b/azure_jumpstart_ag/artifacts/monitoring/grafana-cluster-global.json
similarity index 100%
rename from azure_jumpstart_ag/retail/artifacts/monitoring/grafana-cluster-global.json
rename to azure_jumpstart_ag/artifacts/monitoring/grafana-cluster-global.json
diff --git a/azure_jumpstart_ag/retail/artifacts/monitoring/grafana-freezer-monitoring.json b/azure_jumpstart_ag/artifacts/monitoring/grafana-freezer-monitoring.json
similarity index 100%
rename from azure_jumpstart_ag/retail/artifacts/monitoring/grafana-freezer-monitoring.json
rename to azure_jumpstart_ag/artifacts/monitoring/grafana-freezer-monitoring.json
diff --git a/azure_jumpstart_ag/retail/artifacts/monitoring/grafana-node-exporter-full.json b/azure_jumpstart_ag/artifacts/monitoring/grafana-node-exporter-full.json
similarity index 100%
rename from azure_jumpstart_ag/retail/artifacts/monitoring/grafana-node-exporter-full.json
rename to azure_jumpstart_ag/artifacts/monitoring/grafana-node-exporter-full.json
diff --git a/azure_jumpstart_ag/retail/artifacts/monitoring/prometheus-additional-scrape-config.yaml b/azure_jumpstart_ag/artifacts/monitoring/prometheus-additional-scrape-config.yaml
similarity index 100%
rename from azure_jumpstart_ag/retail/artifacts/monitoring/prometheus-additional-scrape-config.yaml
rename to azure_jumpstart_ag/artifacts/monitoring/prometheus-additional-scrape-config.yaml
diff --git a/azure_jumpstart_ag/artifacts/settings/Bookmarks-manufacturing b/azure_jumpstart_ag/artifacts/settings/Bookmarks-manufacturing
new file mode 100644
index 0000000000..0bdfbe45d8
--- /dev/null
+++ b/azure_jumpstart_ag/artifacts/settings/Bookmarks-manufacturing
@@ -0,0 +1,125 @@
+{
+ "checksum": "d77f9db622cff666aa1ae0f899c3b4ec",
+ "roots": {
+ "bookmark_bar": {
+ "children": [
+ {
+ "children": [ {
+ "id": "18",
+ "name": "Control center Detroit",
+ "show_icon": false,
+ "source": "unknown",
+ "type": "url",
+ "url": "Flask-Detroit-URL"
+ }, {
+ "id": "19",
+ "name": "Control center Monterrey",
+ "show_icon": false,
+ "source": "unknown",
+ "type": "url",
+ "url": "Flask-Monterrey-URL"
+ }],
+ "id": "16",
+ "name": "Control centers",
+ "source": "unknown",
+ "type": "folder"
+ },
+ {
+ "children": [ {
+ "id": "16",
+ "name": "Influxdb Detroit",
+ "show_icon": false,
+ "source": "unknown",
+ "type": "url",
+ "url": "Influxdb-Detroit-URL"
+ }, {
+ "id": "17",
+ "name": "Influxdb Monterrey",
+ "show_icon": false,
+ "source": "unknown",
+ "type": "url",
+ "url": "Influxdb-Monterrey-URL"
+ }],
+ "id": "15",
+ "name": "Influxdb",
+ "source": "unknown",
+ "type": "folder"
+ },
+ {
+ "children": [ {
+ "id": "20",
+ "name": "Grafana",
+ "show_icon": false,
+ "source": "unknown",
+ "type": "url",
+ "url": "http://localhost:3000"
+ }],
+ "id": "17",
+ "name": "Grafana",
+ "source": "unknown",
+ "type": "folder"
+ },
+ {
+ "children": [ {
+ "id": "30",
+ "name": "Prometheus Detroit",
+ "show_icon": false,
+ "source": "unknown",
+ "type": "url",
+ "url": "Prometheus-Detroit-URL"
+ }, {
+ "id": "31",
+ "name": "Prometheus Monterrey",
+ "show_icon": false,
+ "source": "unknown",
+ "type": "url",
+ "url": "Prometheus-Monterrey-URL"
+ }],
+ "id": "29",
+ "name": "Prometheus",
+ "source": "unknown",
+ "type": "folder"
+ }, {
+ "id": "22",
+ "name": "ADX Dashboards",
+ "show_icon": false,
+ "source": "unknown",
+ "type": "url",
+ "url": "https://dataexplorer.azure.com/dashboards/"
+ }, {
+ "id": "23",
+ "name": "Azure Arc Jumpstart",
+ "show_icon": false,
+ "source": "unknown",
+ "type": "url",
+ "url": "https://aka.ms/ArcJumpstart/"
+ }, {
+ "id": "24",
+ "name": "Azure Portal",
+ "show_icon": false,
+ "source": "unknown",
+ "type": "url",
+ "url": "https://portal.azure.com/"
+ } ],
+ "id": "1",
+ "name": "Favorites bar",
+ "source": "unknown",
+ "type": "folder"
+ },
+ "other": {
+ "children": [ ],
+ "id": "25",
+ "name": "Other favorites",
+ "source": "unknown",
+ "type": "folder"
+ },
+ "synced": {
+ "children": [ ],
+ "id": "26",
+ "name": "Mobile favorites",
+ "source": "unknown",
+ "type": "folder"
+ }
+ },
+ "version": 1
+}
\ No newline at end of file
diff --git a/azure_jumpstart_ag/retail/artifacts/settings/Bookmarks b/azure_jumpstart_ag/artifacts/settings/Bookmarks-retail
similarity index 100%
rename from azure_jumpstart_ag/retail/artifacts/settings/Bookmarks
rename to azure_jumpstart_ag/artifacts/settings/Bookmarks-retail
diff --git a/azure_jumpstart_ag/retail/artifacts/settings/DockerDesktopSettings.json b/azure_jumpstart_ag/artifacts/settings/DockerDesktopSettings.json
similarity index 100%
rename from azure_jumpstart_ag/retail/artifacts/settings/DockerDesktopSettings.json
rename to azure_jumpstart_ag/artifacts/settings/DockerDesktopSettings.json
diff --git a/azure_jumpstart_ag/artifacts/settings/influxdb-configmap.yml b/azure_jumpstart_ag/artifacts/settings/influxdb-configmap.yml
new file mode 100644
index 0000000000..c2eb2e5342
--- /dev/null
+++ b/azure_jumpstart_ag/artifacts/settings/influxdb-configmap.yml
@@ -0,0 +1,8 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: dashboard-config
+data:
+ report_demo.json: |
+ [{"apiVersion":"influxdata.com/v2alpha1","kind":"Dashboard","metadata":{"name":"priceless-dubinsky-15a001"},"spec":{"charts":[{"colors":[{"id":"base","name":"laser","type":"background","hex":"#00C9FF"},{"id":"-OSk3ZHwI-9qzlZ2QfQPQ","name":"pineapple","type":"background","hex":"#FFB94A","value":80},{"id":"54eKypz7pFEJLV5zvkHci","name":"honeydew","type":"background","hex":"#7CE490","value":90}],"decimalPlaces":2,"height":1,"kind":"Single_Stat","name":"Overall Efficiency","queries":[{"query":"from(bucket: \"manufacturing\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"topic/productionline\")\n |> filter(fn: (r) => r[\"_field\"] == \"OverallEfficiency\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")"}],"staticLegend":{},"width":3},{"colors":[{"id":"0","name":"curacao","type":"min","hex":"#F95F53"},{"id":"3BQRuxy21foOTRuynaBbT","name":"pineapple","type":"threshold","hex":"#FFB94A","value":80},{"id":"UmDKq3fT8NFnXrLCy0T2x","name":"honeydew","type":"threshold","hex":"#7CE490","value":90},{"id":"1","name":"honeydew","type":"max","hex":"#7CE490","value":100}],"decimalPlaces":2,"height":2,"kind":"Gauge","name":"Availability","queries":[{"query":"from(bucket: \"manufacturing\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"topic/productionline\")\n |> filter(fn: (r) => r[\"_field\"] == \"Availability\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")"}],"staticLegend":{},"width":3,"yPos":1},{"axes":[{"base":"10","name":"x","scale":"linear"},{"base":"10","name":"y","scale":"linear"}],"colorizeRows":true,"colors":[{"id":"e9Cw-2YyDrKAIdLRHX-8r","name":"Delorean","type":"scale","hex":"#FD7A5D"},{"id":"QNncvwjZ-gxcEWK-n2Cdp","name":"Delorean","type":"scale","hex":"#5F1CF2"},{"id":"MHoU5w2iozIwXU4MI-v-z","name":"Delorean","type":"scale","hex":"#4CE09A"}],"geom":"step","height":3,"hoverDimension":"auto","kind":"Xy","legendColorizeRows":true,"legendOpacity":1,"legendOrientationThreshold":100000000,"name":"Downtime","opacity":1,"orientationThreshold":100000000,"position":"overlaid","queries":[{"query":"from(bucket: \"manufacturing\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"topic/productionline\")\n |> filter(fn: (r) => r[\"_field\"] == \"DownTime\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")"}],"staticLegend":{"colorizeRows":true,"opacity":1,"orientationThreshold":100000000,"widthRatio":1},"width":7,"widthRatio":1,"xCol":"_time","yCol":"_value","yPos":3},{"axes":[{"base":"10","name":"x","scale":"linear"},{"base":"10","name":"y","scale":"linear"}],"colorizeRows":true,"colors":[{"id":"DCWSO9dpLF_BrvtRxnAR3","name":"Nineteen Eighty Four","type":"scale","hex":"#31C0F6"},{"id":"f3dDOAwqo7-DyaeujwnXr","name":"Nineteen Eighty Four","type":"scale","hex":"#A500A5"},{"id":"JDrOgEW8LlH9kbSz9On2Y","name":"Nineteen Eighty Four","type":"scale","hex":"#FF7E27"}],"geom":"line","height":3,"hoverDimension":"auto","kind":"Xy","legendColorizeRows":true,"legendOpacity":1,"legendOrientationThreshold":100000000,"name":"Oil temperature","opacity":1,"orientationThreshold":100000000,"position":"overlaid","queries":[{"query":"from(bucket: \"manufacturing\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"topic/productionline\")\n |> filter(fn: (r) => r[\"_field\"] == \"TargetCutPerMinutes\" or r[\"_field\"] == \"CurrentCutPerMinutes\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")"}],"staticLegend":{"colorizeRows":true,"opacity":1,"orientationThreshold":100000000,"widthRatio":1},"width":7,"widthRatio":1,"xCol":"_time","yCol":"_value","yPos":6},{"colors":[{"id":"base","name":"laser","type":"text","hex":"#00C9FF"}],"decimalPlaces":2,"height":1,"kind":"Single_Stat","name":"Batch completed","queries":[{"query":"from(bucket: \"manufacturing\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"topic/productionline\")\n |> filter(fn: (r) => r[\"_field\"] == \"CompletedDoughs\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")\n\nfrom(bucket: \"manufacturing\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"topic/productionline\")\n |> filter(fn: (r) => r[\"_field\"] == \"CompletedDoughs\")\n |> aggregateWindow(every: v.windowPeriod, fn: last, createEmpty: false)\n |> yield(name: \"last\")"}],"staticLegend":{},"width":3,"xPos":3},{"colors":[{"id":"0","name":"fire","type":"min","hex":"#DC4E58"},{"id":"PDFCUrH43se0hP6kP6tNg","name":"thunder","type":"threshold","hex":"#FFD255","value":70},{"id":"GBy2gLjKsSc-v56Rwu0Va","name":"honeydew","type":"threshold","hex":"#7CE490","value":90},{"id":"1","name":"viridian","type":"max","hex":"#32B08C","value":100}],"decimalPlaces":2,"height":2,"kind":"Gauge","name":"Quality","queries":[{"query":"from(bucket: \"manufacturing\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"topic/productionline\")\n |> filter(fn: (r) => r[\"_field\"] == \"Quality\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")"}],"staticLegend":{},"width":3,"xPos":3,"yPos":1},{"colors":[{"id":"base","name":"laser","type":"text","hex":"#00C9FF"}],"decimalPlaces":2,"height":1,"kind":"Single_Stat","name":"Current Shift","queries":[{"query":"from(bucket: \"manufacturing\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"topic/productionline\")\n |> filter(fn: (r) => r[\"_field\"] == \"CurrentShift\")\n |> aggregateWindow(every: v.windowPeriod, fn: last, createEmpty: false)\n |> yield(name: \"last\")"}],"staticLegend":{},"width":3,"xPos":6},{"colors":[{"id":"0","name":"ruby","type":"min","hex":"#BF3D5E"},{"id":"CR2q8dxx6WKVcBfk6PyGM","name":"pineapple","type":"threshold","hex":"#FFB94A","value":80},{"id":"4fgeiiGAoga1-ctNbTrKW","name":"honeydew","type":"threshold","hex":"#7CE490","value":90},{"id":"1","name":"rainforest","type":"max","hex":"#4ED8A0","value":100}],"decimalPlaces":2,"height":2,"kind":"Gauge","name":"Performance","queries":[{"query":"from(bucket: \"manufacturing\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"topic/productionline\")\n |> filter(fn: (r) => r[\"_field\"] == \"Performance\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")"}],"staticLegend":{},"width":3,"xPos":6,"yPos":1},{"colors":[{"id":"base","name":"laser","type":"text","hex":"#00C9FF"}],"decimalPlaces":2,"height":1,"kind":"Single_Stat","name":"fryer Humidity","queries":[{"query":"from(bucket: \"manufacturing\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"topic/fryer\")\n |> filter(fn: (r) => r[\"_field\"] == \"Humidity\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")"}],"staticLegend":{},"width":2,"xPos":7,"yPos":3},{"axes":[{"base":"10","name":"x","scale":"linear"},{"base":"10","name":"y","scale":"linear"}],"colorizeRows":true,"colors":[{"id":"DCWSO9dpLF_BrvtRxnAR3","name":"Nineteen Eighty Four","type":"scale","hex":"#31C0F6"},{"id":"f3dDOAwqo7-DyaeujwnXr","name":"Nineteen Eighty Four","type":"scale","hex":"#A500A5"},{"id":"JDrOgEW8LlH9kbSz9On2Y","name":"Nineteen Eighty Four","type":"scale","hex":"#FF7E27"}],"geom":"line","height":2,"hoverDimension":"auto","kind":"Xy","legendColorizeRows":true,"legendOpacity":1,"legendOrientationThreshold":100000000,"name":"fryer Voltage","opacity":1,"orientationThreshold":100000000,"position":"overlaid","queries":[{"query":"from(bucket: \"manufacturing\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"topic/fryer\")\n |> filter(fn: (r) => r[\"_field\"] == \"Voltage\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")"}],"staticLegend":{"colorizeRows":true,"opacity":1,"orientationThreshold":100000000,"widthRatio":1},"width":5,"widthRatio":1,"xCol":"_time","xPos":7,"yCol":"_value","yPos":4},{"axes":[{"base":"10","name":"x","scale":"linear"},{"base":"10","name":"y","scale":"linear"}],"colorizeRows":true,"colors":[{"id":"3Klw7kgHH0KzcILU2Qlk8","name":"Solid Green","type":"scale","hex":"#34BB55"},{"id":"nGMbaMg0WHO20E9BllgMQ","name":"Solid Green","type":"scale","hex":"#34BB55"},{"id":"zVZGa94dsEPNsx6gnqts2","name":"Solid Green","type":"scale","hex":"#34BB55"}],"geom":"monotoneX","height":2,"hoverDimension":"auto","kind":"Xy","legendColorizeRows":true,"legendOpacity":1,"legendOrientationThreshold":100000000,"name":"fryer Tank Level","opacity":1,"orientationThreshold":100000000,"position":"overlaid","queries":[{"query":"from(bucket: \"manufacturing\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"topic/fryer\")\n |> filter(fn: (r) => r[\"_field\"] == \"Tank_Level\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")"}],"staticLegend":{"colorizeRows":true,"opacity":1,"orientationThreshold":100000000,"widthRatio":1},"width":5,"widthRatio":1,"xCol":"_time","xPos":7,"yCol":"_value","yPos":6},{"colors":[{"id":"base","name":"laser","type":"text","hex":"#00C9FF"}],"decimalPlaces":2,"height":1,"kind":"Single_Stat","name":"Current SKU","queries":[{"query":"from(bucket: \"manufacturing\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"topic/productionline\")\n |> filter(fn: (r) => r[\"_field\"] == \"Product\")\n |> aggregateWindow(every: v.windowPeriod, fn: last, createEmpty: false)\n |> yield(name: \"last\")"}],"staticLegend":{},"width":3,"xPos":9},{"colors":[{"id":"base","name":"laser","type":"text","hex":"#00C9FF"}],"decimalPlaces":2,"height":2,"kind":"Single_Stat","name":"Lost time classification","queries":[{"query":"from(bucket: \"manufacturing\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"topic/productionline\")\n |> filter(fn: (r) => r[\"_field\"] == \"LostTimeReason\")\n |> aggregateWindow(every: v.windowPeriod, fn: last, createEmpty: false)\n |> yield(name: \"last\")"}],"staticLegend":{},"width":3,"xPos":9,"yPos":1},{"colors":[{"id":"base","name":"laser","type":"text","hex":"#00C9FF"}],"decimalPlaces":2,"height":1,"kind":"Single_Stat","name":"fryer Temperature","queries":[{"query":"from(bucket: \"manufacturing\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"topic/fryer\")\n |> filter(fn: (r) => r[\"_field\"] == \"Temperature\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")"}],"staticLegend":{},"width":3,"xPos":9,"yPos":3}],"name":"Contoso Bakery Strawberry Donut production line"}}]
+
\ No newline at end of file
diff --git a/azure_jumpstart_ag/artifacts/settings/influxdb-import-dashboard.yml b/azure_jumpstart_ag/artifacts/settings/influxdb-import-dashboard.yml
new file mode 100644
index 0000000000..e3df9e2196
--- /dev/null
+++ b/azure_jumpstart_ag/artifacts/settings/influxdb-import-dashboard.yml
@@ -0,0 +1,34 @@
+
+apiVersion: batch/v1
+kind: Job
+metadata:
+ name: influxdb-import-dashboard
+spec:
+ template:
+ spec:
+ restartPolicy: Never
+ containers:
+ - name: influxdb-import-dashboard
+ image: influxdb:latest
+ command:
+ - influx
+ args:
+ - apply
+ - -f
+ - "/etc/config/report_demo.json"
+ - --org
+ - InfluxData
+ - --token
+ - secret-token
+ - --host
+ - http://influxPlaceholder:8086
+ - --force
+ - "yes"
+ volumeMounts:
+ - name: config-volume
+ mountPath: "/etc/config"
+ volumes:
+ - name: config-volume
+ configMap:
+ name: dashboard-config
+
diff --git a/azure_jumpstart_ag/artifacts/settings/influxdb-setup.yml b/azure_jumpstart_ag/artifacts/settings/influxdb-setup.yml
new file mode 100644
index 0000000000..472968b00e
--- /dev/null
+++ b/azure_jumpstart_ag/artifacts/settings/influxdb-setup.yml
@@ -0,0 +1,17 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: influxdb
+spec:
+ type: LoadBalancer
+ selector:
+ app: influxdb
+ ports:
+ - name: api
+ port: 9999
+ protocol: TCP
+ targetPort: 9999
+ - name: gui
+ port: 8086
+ protocol: TCP
+ targetPort: 8086
\ No newline at end of file
diff --git a/azure_jumpstart_ag/artifacts/settings/influxdb.yml b/azure_jumpstart_ag/artifacts/settings/influxdb.yml
new file mode 100644
index 0000000000..d18d9f6ed0
--- /dev/null
+++ b/azure_jumpstart_ag/artifacts/settings/influxdb.yml
@@ -0,0 +1,100 @@
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: contosoba
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+ name: contosoba-clusterrole
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: ClusterRole
+ name: cluster-admin
+subjects:
+- kind: ServiceAccount
+ name: contosoba
+ namespace: azure-iot-operations
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+ name: contosoba-role
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: Role
+ name: cluster-admin
+subjects:
+- kind: ServiceAccount
+ name: contosoba
+---
+apiVersion: apps/v1
+kind: StatefulSet
+metadata:
+ name: influxdb
+spec:
+ serviceName: "influxdb"
+ selector:
+ matchLabels:
+ app: influxdb
+ template:
+ metadata:
+ labels:
+ app: influxdb
+ spec:
+ serviceAccount: contosoba
+ containers:
+ - name: influxdb
+ image: influxdb:latest
+ resources:
+ limits:
+ memory: "1Gi"
+ cpu: "500m"
+ ports:
+ - name: api
+ containerPort: 9999
+ - name: gui
+ containerPort: 8086
+ volumeMounts:
+ - name: data
+ mountPath: /var/lib/influxdb2
+ volumeClaimTemplates:
+ - metadata:
+ name: data
+ spec:
+ accessModes:
+ - ReadWriteOnce
+ storageClassName: "local-path"
+ resources:
+ requests:
+ storage: 10Gi
+ volumeMode: Filesystem
+---
+apiVersion: batch/v1
+kind: Job
+metadata:
+ name: influxdb-setup
+spec:
+ template:
+ spec:
+ restartPolicy: Never
+ containers:
+ - name: create-credentials
+ image: influxdb:latest
+ command:
+ - influx
+ args:
+ - setup
+ - --host
+ - http://influxPlaceholder:8086
+ - --bucket
+ - manufacturing
+ - --org
+ - InfluxData
+ - --password
+ - influxAdminPwdPlaceHolder
+ - --username
+ - influxAdminPlaceHolder
+ - --token
+ - secret-token
+ - --force
diff --git a/azure_jumpstart_ag/artifacts/settings/mq_cloudConnector.yml b/azure_jumpstart_ag/artifacts/settings/mq_cloudConnector.yml
new file mode 100644
index 0000000000..6ffbf23210
--- /dev/null
+++ b/azure_jumpstart_ag/artifacts/settings/mq_cloudConnector.yml
@@ -0,0 +1,34 @@
+apiVersion: mq.iotoperations.azure.com/v1beta1
+kind: MqttBridgeTopicMap
+metadata:
+ name: my-topic-map
+ namespace: azure-iot-operations
+spec:
+ mqttBridgeConnectorRef: my-mqtt-bridge
+ routes:
+ - direction: local-to-remote
+ name: route-to-eventgrid
+ qos: 1
+ source: "topic/#"
+---
+apiVersion: mq.iotoperations.azure.com/v1beta1
+kind: MqttBridgeConnector
+metadata:
+ name: my-mqtt-bridge
+ namespace: azure-iot-operations
+spec:
+ image:
+ repository: mcr.microsoft.com/azureiotoperations/mqttbridge
+ tag: 0.1.0-preview
+ pullPolicy: IfNotPresent
+ protocol: v5
+ bridgeInstances: 1
+ clientIdPrefix: clusterName
+ logLevel: debug
+ remoteBrokerConnection:
+ endpoint: eventGridPlaceholder:8883
+ tls:
+ tlsEnabled: true
+ authentication:
+ systemAssignedManagedIdentity:
+ audience: https://eventgrid.azure.net
diff --git a/azure_jumpstart_ag/artifacts/settings/mqtt_explorer_settings.json b/azure_jumpstart_ag/artifacts/settings/mqtt_explorer_settings.json
new file mode 100644
index 0000000000..6c5d40f802
--- /dev/null
+++ b/azure_jumpstart_ag/artifacts/settings/mqtt_explorer_settings.json
@@ -0,0 +1,34 @@
+{
+ "ConnectionManager_connections": {
+ "mqtt.eclipse.org": {
+ "certValidation": false,
+ "clientId": "mqtt-explorer-640c948e",
+ "encryption": false,
+ "host": "detroitIpPlaceholder",
+ "id": "mqtt.eclipse.org",
+ "name": "detroit",
+ "port": 1883,
+ "protocol": "mqtt",
+ "subscriptions": [
+ "#",
+ "$SYS/#"
+ ],
+ "type": "mqtt"
+ },
+ "mqtt.eclipse.org2": {
+ "certValidation": false,
+ "clientId": "mqtt-explorer-640c948e",
+ "encryption": false,
+ "host": "monterreyIpPlaceholder",
+ "id": "mqtt.eclipse.org2",
+ "name": "monterrey",
+ "port": 1883,
+ "protocol": "mqtt",
+ "subscriptions": [
+ "#",
+ "$SYS/#"
+ ],
+ "type": "mqtt"
+ }
+ }
+}
\ No newline at end of file
diff --git a/azure_jumpstart_ag/artifacts/settings/mqtt_listener.yml b/azure_jumpstart_ag/artifacts/settings/mqtt_listener.yml
new file mode 100644
index 0000000000..5d82b71c8d
--- /dev/null
+++ b/azure_jumpstart_ag/artifacts/settings/mqtt_listener.yml
@@ -0,0 +1,42 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: mqtt-listener-deployment
+ labels:
+ app: mqtt-listener
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: mqtt-listener
+ template:
+ metadata:
+ labels:
+ app: mqtt-listener
+ spec:
+ containers:
+ - name: mqtt-listener
+ image: jumpstartprod.azurecr.io/mqtt-listener:latest
+ resources:
+ limits:
+ memory: "512Mi"
+ cpu: "500m"
+ env:
+ - name: MQTT_BROKER
+ value: "MQTTIpPlaceholder"
+ - name: MQTT_Port
+ value: "1883"
+ - name: MQTT_TOPIC1
+ value: "topic/weldingrobot"
+ - name: MQTT_TOPIC2
+ value: "topic/productionline"
+ - name: MQTT_TOPIC3
+ value: "topic/assemblyline"
+ - name: INFLUX_URL
+ value: "http://influxPlaceholder:8086"
+ - name: INFLUX_TOKEN
+ value: "secret-token"
+ - name: INFLUX_ORG
+ value: "InfluxData"
+ - name: INFLUX_BUCKET
+ value: "manufacturing"
diff --git a/azure_jumpstart_ag/artifacts/settings/mqtt_simulator.yml b/azure_jumpstart_ag/artifacts/settings/mqtt_simulator.yml
new file mode 100644
index 0000000000..36b04431e4
--- /dev/null
+++ b/azure_jumpstart_ag/artifacts/settings/mqtt_simulator.yml
@@ -0,0 +1,28 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: mqtt-simulator-deployment
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: mqtt-simulator
+ template:
+ metadata:
+ labels:
+ app: mqtt-simulator
+ spec:
+ containers:
+ - name: mqtt-simulator
+ image: agoraarmbladev.azurecr.io/mqtt-simulator:latest
+ resources:
+ limits:
+ cpu: "1"
+ memory: "500Mi"
+ env:
+ - name: MQTT_BROKER
+ value: "MQTTIpPlaceholder"
+ - name: MQTT_PORT
+ value: "1883"
+ - name: FRECUENCY
+ value: "5"
diff --git a/azure_jumpstart_ag/retail/artifacts/workflows/pos-app-build-canary.yml b/azure_jumpstart_ag/artifacts/workflows/pos-app-build-canary.yml
similarity index 98%
rename from azure_jumpstart_ag/retail/artifacts/workflows/pos-app-build-canary.yml
rename to azure_jumpstart_ag/artifacts/workflows/pos-app-build-canary.yml
index 8ce0628db0..c6f4eaab92 100644
--- a/azure_jumpstart_ag/retail/artifacts/workflows/pos-app-build-canary.yml
+++ b/azure_jumpstart_ag/artifacts/workflows/pos-app-build-canary.yml
@@ -16,7 +16,7 @@ jobs:
steps:
- name: 'Checkout repository'
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: 'Login to ACR'
uses: azure/docker-login@v1
@@ -69,7 +69,7 @@ jobs:
fi
- name: 'Checkout canary branch'
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
env:
latest_tag: ${{ steps.latestImageTag.outputs.latest_tag }}
canary_latest_tag: ${{ steps.canaryLatestImageTag.outputs.latest_tag }}
diff --git a/azure_jumpstart_ag/retail/artifacts/workflows/pos-app-build-production.yml b/azure_jumpstart_ag/artifacts/workflows/pos-app-build-production.yml
similarity index 98%
rename from azure_jumpstart_ag/retail/artifacts/workflows/pos-app-build-production.yml
rename to azure_jumpstart_ag/artifacts/workflows/pos-app-build-production.yml
index 3649a67638..4de810d7fb 100644
--- a/azure_jumpstart_ag/retail/artifacts/workflows/pos-app-build-production.yml
+++ b/azure_jumpstart_ag/artifacts/workflows/pos-app-build-production.yml
@@ -16,7 +16,7 @@ jobs:
steps:
- name: 'Checkout repository'
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: 'Login to ACR'
uses: azure/docker-login@v1
@@ -67,7 +67,7 @@ jobs:
docker push ${{ secrets.ACR_NAME }}.azurecr.io/$namespace/contoso-supermarket/pos:$latest_tag
- name: 'Checkout production branch'
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
env:
latest_tag: ${{ steps.latestImageTag.outputs.latest_tag }}
prod_latest_tag: ${{ steps.prodLatestImageTag.outputs.latest_tag }}
diff --git a/azure_jumpstart_ag/retail/artifacts/workflows/pos-app-build-staging.yml b/azure_jumpstart_ag/artifacts/workflows/pos-app-build-staging.yml
similarity index 98%
rename from azure_jumpstart_ag/retail/artifacts/workflows/pos-app-build-staging.yml
rename to azure_jumpstart_ag/artifacts/workflows/pos-app-build-staging.yml
index 850a3a09a1..3762bc6d3d 100644
--- a/azure_jumpstart_ag/retail/artifacts/workflows/pos-app-build-staging.yml
+++ b/azure_jumpstart_ag/artifacts/workflows/pos-app-build-staging.yml
@@ -23,7 +23,7 @@ jobs:
steps:
# checkout the repo
- name: 'Checkout repository'
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: 'Login to ACR'
uses: azure/docker-login@v1
@@ -92,7 +92,7 @@ jobs:
gh pr merge $pr_number --merge --delete-branch
- name: 'Checkout staging branch'
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
ref: 'staging'
diff --git a/azure_jumpstart_ag/retail/artifacts/workflows/pos-app-initial-images-build.yml b/azure_jumpstart_ag/artifacts/workflows/pos-app-initial-images-build.yml
similarity index 95%
rename from azure_jumpstart_ag/retail/artifacts/workflows/pos-app-initial-images-build.yml
rename to azure_jumpstart_ag/artifacts/workflows/pos-app-initial-images-build.yml
index 31b764be95..a3eb50fcfe 100644
--- a/azure_jumpstart_ag/retail/artifacts/workflows/pos-app-initial-images-build.yml
+++ b/azure_jumpstart_ag/artifacts/workflows/pos-app-initial-images-build.yml
@@ -14,7 +14,7 @@ jobs:
steps:
# checkout the repo
- name: 'Checkout repository'
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: 'Login to ACR'
uses: azure/docker-login@v1
@@ -26,7 +26,7 @@ jobs:
- name: 'Build and push pos v1.0 images'
env:
latest_tag: "v1.0"
- uses: nick-fields/retry@v2
+ uses: nick-fields/retry@v3
with:
timeout_minutes: 25
retry_on: error
@@ -45,7 +45,7 @@ jobs:
- name: 'Build and push cloudSync v1.0 images'
env:
latest_tag: "v1.0"
- uses: nick-fields/retry@v2
+ uses: nick-fields/retry@v3
with:
timeout_minutes: 25
retry_on: error
@@ -64,7 +64,7 @@ jobs:
- name: 'Build and push contosoAi v1.0 images'
env:
latest_tag: "v1.0"
- uses: nick-fields/retry@v2
+ uses: nick-fields/retry@v3
with:
timeout_minutes: 25
retry_on: error
@@ -83,7 +83,7 @@ jobs:
- name: 'Build and push queue monitoring backend v1.0 images'
env:
latest_tag: "v1.0"
- uses: nick-fields/retry@v2
+ uses: nick-fields/retry@v3
with:
timeout_minutes: 25
retry_on: error
@@ -102,7 +102,7 @@ jobs:
- name: 'Build and push queue monitoring frontend v1.0 images'
env:
latest_tag: "v1.0"
- uses: nick-fields/retry@v2
+ uses: nick-fields/retry@v3
with:
timeout_minutes: 25
retry_on: error
diff --git a/azure_jumpstart_ag/retail/artifacts/workflows/update-files.yml b/azure_jumpstart_ag/artifacts/workflows/update-files.yml
similarity index 99%
rename from azure_jumpstart_ag/retail/artifacts/workflows/update-files.yml
rename to azure_jumpstart_ag/artifacts/workflows/update-files.yml
index 0ed2336fb2..4cf5217752 100644
--- a/azure_jumpstart_ag/retail/artifacts/workflows/update-files.yml
+++ b/azure_jumpstart_ag/artifacts/workflows/update-files.yml
@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: Replace the correct ACR name
continue-on-error: true
diff --git a/azure_jumpstart_ag/manufacturing/.gitignore b/azure_jumpstart_ag/manufacturing/.gitignore
new file mode 100644
index 0000000000..6297a3b672
--- /dev/null
+++ b/azure_jumpstart_ag/manufacturing/.gitignore
@@ -0,0 +1,2 @@
+.azure
+js_rsa*
\ No newline at end of file
diff --git a/azure_jumpstart_ag/manufacturing/azure.yaml b/azure_jumpstart_ag/manufacturing/azure.yaml
new file mode 100644
index 0000000000..200ea17596
--- /dev/null
+++ b/azure_jumpstart_ag/manufacturing/azure.yaml
@@ -0,0 +1,20 @@
+# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json
+
+name: azure_jumpstart_ag
+metadata:
+ template: azure_jumpstart_agora@0.0.1-beta
+infra:
+ provider: "bicep"
+ path: "bicep"
+ module: "main.azd"
+hooks:
+ preprovision:
+ shell: pwsh
+ run: ./scripts/preprovision.ps1
+ continueOnError: false
+ interactive: true
+ postprovision:
+ shell: pwsh
+ run: ./scripts/postprovision.ps1
+ continueOnError: false
+ interactive: true
\ No newline at end of file
diff --git a/azure_jumpstart_ag/manufacturing/bicep/clientVm/clientVm.bicep b/azure_jumpstart_ag/manufacturing/bicep/clientVm/clientVm.bicep
new file mode 100644
index 0000000000..ca491c51fc
--- /dev/null
+++ b/azure_jumpstart_ag/manufacturing/bicep/clientVm/clientVm.bicep
@@ -0,0 +1,208 @@
+@description('The name of your Virtual Machine')
+param vmName string = 'Ag-VM-Client'
+
+@description('Username for the Virtual Machine')
+param windowsAdminUsername string = 'agora'
+
+@description('Password for Windows account. Password must have 3 of the following: 1 lower case character, 1 upper case character, 1 number, and 1 special character. The value must be between 12 and 123 characters long')
+@minLength(12)
+@maxLength(123)
+@secure()
+param windowsAdminPassword string
+
+@description('The Windows version for the VM. This will pick a fully patched image of this given Windows version')
+param windowsOSVersion string = '2022-datacenter-g2'
+
+@description('Location for all resources')
+param location string = resourceGroup().location
+
+
+@description('Name of the storage account')
+param aioStorageAccountName string = 'aiostg${namingGuid}'
+
+@description('Resource tag for Jumpstart Agora')
+param resourceTags object = {
+ Project: 'Jumpstart_Agora'
+}
+
+@description('Resource Id of the subnet in the virtual network')
+param subnetId string
+
+@description('Client id of the service principal')
+param spnClientId string
+
+@description('Azure service principal object id')
+param spnObjectId string
+
+@description('Client secret of the service principal')
+@secure()
+param spnClientSecret string
+param spnAuthority string = environment().authentication.loginEndpoint
+
+@description('Tenant id of the service principal')
+param spnTenantId string
+
+@description('Name for the environment Azure Log Analytics workspace')
+param workspaceName string
+
+@description('The base URL used for accessing artifacts and automation artifacts.')
+param templateBaseUrl string
+
+@description('Choice to deploy Bastion to connect to the client VM')
+param deployBastion bool = false
+
+@description('Storage account used for staging file artifacts')
+param storageAccountName string
+
+@description('The name of ESA container in Storage Account')
+param stcontainerName string
+
+@description('The login server name of the Azure Container Registry')
+param acrName string
+
+@description('The name of the Azure Data Explorer cluster')
+param adxClusterName string
+
+@description('Override default RDP port using this parameter. Default is 3389. No changes will be made to the client VM.')
+param rdpPort string = '3389'
+
+@description('Target GitHub account')
+param githubAccount string = 'microsoft'
+
+@description('Target GitHub branch')
+param githubBranch string = 'main'
+
+//@description('GitHub Personal access token for the user account')
+//@secure()
+//param githubPAT string
+
+@description('Random GUID')
+param namingGuid string
+
+@description('The custom location RPO ID')
+param customLocationRPOID string
+
+@description('The agora industry to be deployed')
+param industry string = 'retail'
+
+var encodedPassword = base64(windowsAdminPassword)
+var bastionName = 'Ag-Bastion'
+var publicIpAddressName = deployBastion == false ? '${vmName}-PIP' : '${bastionName}-PIP'
+var networkInterfaceName = '${vmName}-NIC'
+var osDiskType = 'Premium_LRS'
+var PublicIPNoBastion = {
+ id: publicIpAddress.id
+}
+
+resource networkInterface 'Microsoft.Network/networkInterfaces@2023-02-01' = {
+ name: networkInterfaceName
+ location: location
+ tags: resourceTags
+ properties: {
+ ipConfigurations: [
+ {
+ name: 'ipconfig1'
+ properties: {
+ subnet: {
+ id: subnetId
+ }
+ privateIPAllocationMethod: 'Dynamic'
+ publicIPAddress: deployBastion == false ? PublicIPNoBastion : null
+ }
+ }
+ ]
+ }
+}
+
+resource publicIpAddress 'Microsoft.Network/publicIpAddresses@2023-02-01' = if (deployBastion == false) {
+ name: publicIpAddressName
+ location: location
+ tags: resourceTags
+ properties: {
+ publicIPAllocationMethod: 'Static'
+ publicIPAddressVersion: 'IPv4'
+ idleTimeoutInMinutes: 4
+ }
+ sku: {
+ name: 'Basic'
+ }
+}
+
+resource vm 'Microsoft.Compute/virtualMachines@2022-11-01' = {
+ name: vmName
+ location: location
+ tags: resourceTags
+ properties: {
+ hardwareProfile: {
+ vmSize: 'Standard_D32s_v5'
+ }
+ storageProfile: {
+ osDisk: {
+ name: '${vmName}-OSDisk'
+ caching: 'ReadWrite'
+ createOption: 'FromImage'
+ managedDisk: {
+ storageAccountType: osDiskType
+ }
+ diskSizeGB: 256
+ }
+ imageReference: {
+ publisher: 'MicrosoftWindowsServer'
+ offer: 'WindowsServer'
+ sku: windowsOSVersion
+ version: 'latest'
+ }
+ dataDisks: [
+ {
+ diskSizeGB: 1024
+ lun: 0
+ createOption: 'Empty'
+ caching: 'ReadWrite'
+ managedDisk: {
+ storageAccountType: 'Premium_LRS'
+ }
+ }
+ ]
+ }
+ networkProfile: {
+ networkInterfaces: [
+ {
+ id: networkInterface.id
+ }
+ ]
+ }
+ osProfile: {
+ computerName: vmName
+ adminUsername: windowsAdminUsername
+ adminPassword: windowsAdminPassword
+ windowsConfiguration: {
+ provisionVMAgent: true
+ enableAutomaticUpdates: false
+ }
+ }
+ }
+}
+
+resource vmBootstrap 'Microsoft.Compute/virtualMachines/extensions@2022-11-01' = {
+ parent: vm
+ name: 'Bootstrap'
+ location: location
+ tags: {
+ displayName: 'config-choco'
+ }
+ properties: {
+ publisher: 'Microsoft.Compute'
+ type: 'CustomScriptExtension'
+ typeHandlerVersion: '1.10'
+ autoUpgradeMinorVersion: true
+ protectedSettings: {
+ fileUris: [
+ uri(templateBaseUrl, 'artifacts/PowerShell/Bootstrap.ps1')
+ ]
+ commandToExecute: 'powershell.exe -ExecutionPolicy Bypass -File Bootstrap.ps1 -adminUsername ${windowsAdminUsername} -adminPassword ${encodedPassword} -spnClientId ${spnClientId} -spnClientSecret ${spnClientSecret} -spnObjectId ${spnObjectId} -spnTenantId ${spnTenantId} -spnAuthority ${spnAuthority} -subscriptionId ${subscription().subscriptionId} -resourceGroup ${resourceGroup().name} -azureLocation ${location} -stagingStorageAccountName ${storageAccountName} -workspaceName ${workspaceName} -templateBaseUrl ${templateBaseUrl} -acrName ${acrName} -rdpPort ${rdpPort} -githubAccount ${githubAccount} -githubBranch ${githubBranch} -namingGuid ${namingGuid} -adxClusterName ${adxClusterName} -customLocationRPOID ${customLocationRPOID} -industry ${industry} -aioStorageAccountName ${aioStorageAccountName} -stcontainerName ${stcontainerName}'
+ }
+ }
+}
+
+output adminUsername string = windowsAdminUsername
+output publicIP string = deployBastion == false ? concat(publicIpAddress.properties.ipAddress) : ''
diff --git a/azure_jumpstart_ag/manufacturing/bicep/data/dataExplorer.bicep b/azure_jumpstart_ag/manufacturing/bicep/data/dataExplorer.bicep
new file mode 100644
index 0000000000..9c6d824bd7
--- /dev/null
+++ b/azure_jumpstart_ag/manufacturing/bicep/data/dataExplorer.bicep
@@ -0,0 +1,109 @@
+@description('The name of the Azure Data Explorer cluster')
+param adxClusterName string
+
+@description('The location of the Azure Data Explorer cluster')
+param location string = resourceGroup().location
+
+@description('Resource tag for Jumpstart Agora')
+param resourceTags object = {
+ Project: 'Jumpstart_azure_aio'
+}
+
+@description('The name of the Azure Data Explorer cluster Sku')
+param skuName string = 'Dev(No SLA)_Standard_E2a_v4'
+
+@description('The name of the Azure Data Explorer cluster Sku tier')
+param skuTier string = 'Basic'
+
+@description('The name of the Event Hub')
+param eventHubName string
+
+@description('The name of the Event Hub Namespace')
+param eventHubNamespaceName string
+
+@description('The resource id of the Event Hub')
+param eventHubResourceId string
+
+@description('The name of the Azure Data Explorer database')
+param adxDBName string = 'manufacturing'
+
+@description('The name of the Azure Data Explorer Event Hub consumer group for staging data')
+param stagingDataCGName string = 'mqttdataemulator'
+
+@description('# of nodes')
+@minValue(1)
+@maxValue(2)
+param skuCapacity int = 1
+
+
+resource adxCluster 'Microsoft.Kusto/clusters@2023-05-02' = {
+ name: adxClusterName
+ location: location
+ tags: resourceTags
+ sku: {
+ name: skuName
+ tier: skuTier
+ capacity: skuCapacity
+ }
+ identity: {
+ type: 'SystemAssigned'
+ }
+}
+
+resource manufacturingAdxDB 'Microsoft.Kusto/clusters/databases@2023-05-02' = {
+ parent: adxCluster
+ name: adxDBName
+ location: location
+ kind: 'ReadWrite'
+}
+
+resource assemblylineScript 'Microsoft.Kusto/clusters/databases/scripts@2023-05-02' = {
+ name: 'assemblylineScript'
+ parent: manufacturingAdxDB
+ properties: {
+ continueOnErrors: false
+ forceUpdateTag: 'string'
+ scriptContent: loadTextContent('script.kql')
+ }
+}
+
+resource azureEventHubsDataReceiverRole 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = {
+ name: 'a638d3c7-ab3a-418d-83e6-5f17a39d4fde'
+ scope: tenant()
+}
+
+resource eventHub 'Microsoft.EventHub/namespaces/eventhubs@2023-01-01-preview' existing = {
+ name: '${eventHubNamespaceName}/${eventHubName}'
+}
+
+resource eventHubRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
+ name: guid('AzureEventHubsDataReceiverRole', adxCluster.id, eventHubResourceId)
+ scope: eventHub
+ properties: {
+ roleDefinitionId: azureEventHubsDataReceiverRole.id
+ principalId: adxCluster.identity.principalId
+ }
+}
+
+resource stagingDataConnection 'Microsoft.Kusto/clusters/databases/dataConnections@2023-08-15' = {
+ name: 'stagingDataConnection'
+ kind: 'EventHub'
+ dependsOn: [
+ assemblylineScript
+ ]
+ location: location
+ parent: manufacturingAdxDB
+ properties: {
+ managedIdentityResourceId: adxCluster.id
+ eventHubResourceId: eventHubResourceId
+ consumerGroup: stagingDataCGName
+ tableName: 'staging'
+ dataFormat: 'MULTIJSON'
+ mappingRuleName: 'staging_mapping'
+ eventSystemProperties: []
+ compression: 'None'
+ databaseRouting: 'Single'
+ }
+}
+
+output adxEndpoint string = adxCluster.properties.uri
diff --git a/azure_jumpstart_ag/manufacturing/bicep/data/eventGrid.bicep b/azure_jumpstart_ag/manufacturing/bicep/data/eventGrid.bicep
new file mode 100644
index 0000000000..d5d7e8b994
--- /dev/null
+++ b/azure_jumpstart_ag/manufacturing/bicep/data/eventGrid.bicep
@@ -0,0 +1,184 @@
+@description('The name of the EventGrid namespace')
+param eventGridNamespaceName string = 'aioNamespace'
+
+@description('The location of the Azure Data Explorer cluster')
+param location string = resourceGroup().location
+
+@maxLength(5)
+@description('Random GUID')
+param namingGuid string
+
+@description('EventGrid Sku')
+param eventGridSku string = 'Standard'
+
+@description('EventGrid capacity')
+param eventGridCapacity int = 1
+
+@description('The name of the EventGrid client group')
+param eventGridClientGroupName string = '$all'
+
+@description('The name of the EventGrid namespace')
+param eventGridTopicSpaceName string = 'aiotopicSpace${namingGuid}'
+
+@description('The name of the EventGrid topic templates')
+param eventGridTopicTemplates array = [
+ '#'
+]
+
+@description('Resource tag for Jumpstart Agora')
+param resourceTags object = {
+ Project: 'Jumpstart_azure_aio'
+}
+
+@description('The name of the EventGrid publisher binding name')
+param publisherBindingName string = 'publisherBinding'
+
+@description('The name of the EventGrid subscription binding name')
+param subscriberBindingName string = 'subscriberBindingName'
+
+@description('The name of the EventHub topic subscription')
+param eventGridTopicSubscriptionName string = 'aioEventHubSubscription'
+
+@description('The name of the storage topic subscription')
+param storageTopicSubscriptionName string = 'aioStorageSubscription'
+
+@description('The name of the EventGrid topic')
+param eventGridTopicName string = 'aiotopic${namingGuid}'
+
+@description('The name of the EventGrid topic sku')
+param eventGridTopicSku string = 'Basic'
+
+@description('The resource Id of the event hub')
+param eventHubResourceId string
+
+@description('The resource Id of the storage account queue')
+param storageAccountResourceId string
+
+@description('The name of the storage account queue')
+param queueName string
+
+@description('The time to live of the storage account queue')
+param queueTTL int = 604800
+
+@description('The maximum number of client sessions per authentication name')
+param maximumClientSessionsPerAuthenticationName int = 100
+
+resource eventGrid 'Microsoft.EventGrid/namespaces@2023-12-15-preview' = {
+ name: eventGridNamespaceName
+ tags: resourceTags
+ location: location
+ sku: {
+ name: eventGridSku
+ capacity: eventGridCapacity
+ }
+ identity: {
+ type: 'SystemAssigned'
+ }
+ properties: {
+ topicSpacesConfiguration: {
+ state: 'Enabled'
+ maximumClientSessionsPerAuthenticationName: maximumClientSessionsPerAuthenticationName
+ clientAuthentication: {
+ alternativeAuthenticationNameSources: [
+ 'ClientCertificateSubject'
+ ]
+ }
+ routeTopicResourceId: eventGridTopic.id
+ }
+ }
+}
+
+resource eventGridTopicSpace 'Microsoft.EventGrid/namespaces/topicSpaces@2023-06-01-preview' = {
+ name: eventGridTopicSpaceName
+ parent: eventGrid
+ properties: {
+ topicTemplates: eventGridTopicTemplates
+ }
+}
+
+resource eventGridPubisherBinding 'Microsoft.EventGrid/namespaces/permissionBindings@2023-06-01-preview' = {
+ name: publisherBindingName
+ parent: eventGrid
+ properties: {
+ clientGroupName: eventGridClientGroupName
+ permission: 'Publisher'
+ topicSpaceName: eventGridTopicSpace.name
+ }
+}
+
+resource eventGridsubscriberBindingName 'Microsoft.EventGrid/namespaces/permissionBindings@2023-06-01-preview' = {
+ name: subscriberBindingName
+ parent: eventGrid
+ properties: {
+ clientGroupName: eventGridClientGroupName
+ permission: 'Subscriber'
+ topicSpaceName: eventGridTopicSpace.name
+ }
+}
+
+resource eventGridTopic 'Microsoft.EventGrid/topics@2023-06-01-preview' = {
+ name: eventGridTopicName
+ location: location
+ tags: resourceTags
+ sku: {
+ name: eventGridTopicSku
+ }
+ identity: {
+ type: 'SystemAssigned'
+ }
+ properties: {
+ inputSchema: 'CloudEventSchemaV1_0'
+ }
+}
+
+
+resource eventHubTopicSubscription 'Microsoft.EventGrid/topics/eventSubscriptions@2023-06-01-preview' = {
+ name: eventGridTopicSubscriptionName
+ parent:eventGridTopic
+ properties: {
+ destination: {
+ endpointType: 'EventHub'
+ properties: {
+ resourceId: eventHubResourceId
+ }
+ }
+ filter: {
+ enableAdvancedFilteringOnArrays: true
+ }
+ eventDeliverySchema: 'CloudEventSchemaV1_0'
+ }
+}
+
+resource storageTopicSubscription 'Microsoft.EventGrid/topics/eventSubscriptions@2023-06-01-preview' = {
+ name: storageTopicSubscriptionName
+ parent:eventGridTopic
+ properties: {
+ destination: {
+ endpointType: 'StorageQueue'
+ properties: {
+ resourceId: storageAccountResourceId
+ queueName: queueName
+ queueMessageTimeToLiveInSeconds: queueTTL
+ }
+ }
+ filter: {
+ enableAdvancedFilteringOnArrays: true
+ }
+ eventDeliverySchema: 'CloudEventSchemaV1_0'
+ }
+}
+
+resource azureEventGridDataSenderRole 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = {
+ name: 'd5a91429-5739-47e2-a06b-3470a27159e7'
+ scope: tenant()
+}
+
+resource eventGridTopicRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
+ name: guid('azureEventGridDataSenderRole', eventGrid.id, eventGridTopic.id)
+ scope: eventGridTopic
+ properties: {
+ roleDefinitionId: azureEventGridDataSenderRole.id
+ principalId: eventGrid.identity.principalId
+ }
+}
+
diff --git a/azure_jumpstart_ag/manufacturing/bicep/data/eventHub.bicep b/azure_jumpstart_ag/manufacturing/bicep/data/eventHub.bicep
new file mode 100644
index 0000000000..9492b09cc4
--- /dev/null
+++ b/azure_jumpstart_ag/manufacturing/bicep/data/eventHub.bicep
@@ -0,0 +1,61 @@
+@description('The name of the EventHub namespace')
+param eventHubNamespaceName string = 'aiohubns${uniqueString(resourceGroup().id)}'
+
+@description('The name of the EventHub')
+param eventHubName string = 'aioEventHub'
+
+@description('EventHub Sku')
+param eventHubSku string = 'Standard'
+
+@description('EventHub Tier')
+param eventHubTier string = 'Standard'
+
+@description('EventHub capacity')
+param eventHubCapacity int = 1
+
+@description('Resource tag for Jumpstart Agora')
+param resourceTags object = {
+ Project: 'Jumpstart_azure_aio'
+}
+
+@description('The location of the Azure Data Explorer cluster')
+param location string = resourceGroup().location
+
+@description('The name of the Azure Data Explorer Event Hub consumer group for mqttdataemulator')
+param stagingDataCGName string = 'mqttdataemulator'
+
+resource eventHubNamespace 'Microsoft.EventHub/namespaces@2023-01-01-preview' = {
+ name: eventHubNamespaceName
+ tags: resourceTags
+ location: location
+ sku: {
+ name: eventHubSku
+ capacity: eventHubCapacity
+ tier: eventHubTier
+ }
+}
+
+resource eventHub 'Microsoft.EventHub/namespaces/eventhubs@2023-01-01-preview' = {
+ name: eventHubName
+ parent: eventHubNamespace
+ properties: {
+ messageRetentionInDays: 1
+ }
+}
+
+resource eventHubAuthRule 'Microsoft.EventHub/namespaces/authorizationRules@2023-01-01-preview' = {
+ name: 'eventHubAuthRule'
+ parent: eventHubNamespace
+ properties: {
+ rights: [
+ 'Listen'
+ ]
+ }
+}
+
+resource weldingrobotCG 'Microsoft.EventHub/namespaces/eventhubs/consumergroups@2023-01-01-preview' = {
+ name: stagingDataCGName
+ parent: eventHub
+}
+
+output eventHubResourceId string = eventHub.id
diff --git a/azure_jumpstart_ag/manufacturing/bicep/data/keyVault.bicep b/azure_jumpstart_ag/manufacturing/bicep/data/keyVault.bicep
new file mode 100644
index 0000000000..c0b7ad3428
--- /dev/null
+++ b/azure_jumpstart_ag/manufacturing/bicep/data/keyVault.bicep
@@ -0,0 +1,71 @@
+@description('Azure Key Vault name')
+param akvNameSite1 string = 'aio-akv-01'
+
+@description('Azure Key Vault name')
+param akvNameSite2 string = 'aio-akv-02'
+
+@description('Azure Key Vault location')
+param location string = resourceGroup().location
+
+@description('Azure Key Vault SKU')
+param akvSku string = 'standard'
+
+@description('Azure Key Vault tenant ID')
+param tenantId string = subscription().tenantId
+
+@description('Secret name')
+param aioPlaceHolder string = 'azure-iot-operations'
+
+@description('Secret value')
+param aioPlaceHolderValue string = 'aioSecretValue'
+
+@description('Resource tag for Jumpstart Agora')
+param resourceTags object = {
+ Project: 'Jumpstart_azure_aio'
+}
+
+resource akv 'Microsoft.KeyVault/vaults@2023-02-01' = {
+ name: akvNameSite1
+ location: location
+ tags: resourceTags
+ properties: {
+ sku: {
+ name: akvSku
+ family: 'A'
+ }
+ accessPolicies: []
+ enableSoftDelete: false
+ tenantId: tenantId
+ }
+}
+
+resource aioSecretPlaceholder 'Microsoft.KeyVault/vaults/secrets@2023-02-01' = {
+ name: aioPlaceHolder
+ parent: akv
+ properties: {
+ value: aioPlaceHolderValue
+ }
+}
+
+resource akv2 'Microsoft.KeyVault/vaults@2023-02-01' = {
+ name: akvNameSite2
+ location: location
+ tags: resourceTags
+ properties: {
+ sku: {
+ name: akvSku
+ family: 'A'
+ }
+ accessPolicies: []
+ enableSoftDelete: false
+ tenantId: tenantId
+ }
+}
+
+resource aioSecretPlaceholder2 'Microsoft.KeyVault/vaults/secrets@2023-02-01' = {
+ name: aioPlaceHolder
+ parent: akv2
+ properties: {
+ value: aioPlaceHolderValue
+ }
+}
diff --git a/azure_jumpstart_ag/manufacturing/bicep/data/script.kql b/azure_jumpstart_ag/manufacturing/bicep/data/script.kql
new file mode 100644
index 0000000000..941aa67dba
--- /dev/null
+++ b/azure_jumpstart_ag/manufacturing/bicep/data/script.kql
@@ -0,0 +1,212 @@
+// Create a landing table for manufacturing events
+.create table assemblyline (date_time: datetime, plant_details: dynamic, shift: string, employees_on_shift: dynamic, cars_produced: dynamic, equipment_maintenance: dynamic, production_schedule: dynamic, actual_production: dynamic, equipment_telemetry: dynamic, performance_metrics: dynamic)
+
+// Create mapping from JSON ingestion to landing table
+.create table assemblyline ingestion json mapping "assemblyline_mapping"
+```
+[
+ {"column":"date_time","path":"$['date_time']","datatype":""},
+ {"column":"plant_details","path":"$['plant_details']","datatype":""},
+ {"column":"shift","path":"$['shift']","datatype":""},
+ {"column":"employees_on_shift","path":"$['employees_on_shift']","datatype":""},
+ {"column":"cars_produced","path":"$['cars_produced']","datatype":""},
+ {"column":"equipment_maintenance","path":"$['equipment_maintenance']","datatype":""},
+ {"column":"production_schedule","path":"$['production_schedule']","datatype":""},
+ {"column":"actual_production","path":"$['actual_production']","datatype":""},
+ {"column":"equipment_telemetry","path":"$['equipment_telemetry']","datatype":""},
+ {"column":"performance_metrics","path":"$['performance_metrics']","datatype":""}
+]
+```
+
+// create staging table, ingest base64 encoded data received from MQTT
+.create table staging (['id']: string, source: string, ['type']: string, data_base64: string, ['time']: dynamic, specversion: real, subject: string)
+
+.create table staging ingestion json mapping "staging_mapping"
+```
+[
+{"column":"id","path":"$['id']","datatype":"string"},
+{"column":"source","path":"$['source']","datatype":"string"},
+{"column":"type","path":"$['type']","datatype":"string"},
+{"column":"data_base64","path":"$['data_base64']","datatype":""},
+{"column":"time","path":"$['time']","datatype":""},
+{"column":"specversion","path":"$['specversion']","datatype":"real"},
+{"column":"subject","path":"$['subject']","datatype":"string"}]
+```
+
+// Modify the ingestion batching policy to ingest data frequently
+// THIS CONFIGURATION SHOULDN'T BE USED IN PRODUCTION: MaximumBatchingTimeSpan SHOULD BE AT LEAST 1 MINUTE
+.alter table staging policy ingestionbatching "{'MaximumBatchingTimeSpan': '0:01:00', 'MaximumNumberOfItems': 10000}"
+
+// Create table to store data from base64 to json format
+.create table weldingrobot (Timestamp: datetime, Heater_Outlet_Temp: real, Pump1_Flow_Totalizer: real, Pump2_Flow_Totalizer: real, Pump3_Flow_Totalizer: real, Pump1_Temperature_Flow: real, Pump2_Temperature_Flow: real, Pump3_Temperature_Flow: real, Pumps_Total_Flow: real, Pressure_Filter_Inlet: real, Pressure_Filter_Outlet: real, RobotPosition_J0: real, RobotPosition_J1: real, RobotPosition_J2: real, RobotPosition_J3: real, RobotPosition_J4: real, RobotPosition_J5: real, Tank_Level: real, Drive1_Current: real, Drive1_Frequency: int, Drive1_Speed: int, Drive1_Voltage: real, Drive2_Current: real, Drive2_Frequency: int, Drive2_Speed: int, Drive2_Voltage: real, Drive3_Current: real, Drive3_Frequency: int, Drive3_Speed: int, Drive3_Voltage: real, Cooler_Inlet_Temp: real, Cooler_Outlet_Temp: real, Dynamix_Ch1_Acceleration: real, Flow001: real, Pressure001: real, Pressure002: real, Heater_Inlet_Temp: real, Pump1_Conductivity: real, Valve_000_Pump1: boolean, Cooler_ON: boolean, Fan001_On: boolean, Heater_ON: boolean, Filter_Chg_Required: boolean, Filter_Reset: boolean, Filter_Override: boolean, UTC_Time: datetime, Current: real, Voltage: real, Temperature: real, Humidity: real, VacuumAlert: boolean, VacuumPressure: real, Oiltemperature: real, OiltemperatureTarget: real)
+
+// Create a table to store the data from the assemblybatteries topic
+.create table assemblybatteries (Timestamp: datetime, MakeupArea: string, Line: string, Product: string, Process: string, Batch: int, CurrentShift: string, CurrentCellAssemblyPerMinutes: int, TargetCellAssemblyPerMinutes: int, StartTime: datetime, FinishTime: datetime, Waste: real, WasteReason: string, LostTime: string, LostTimeReason: string, LostTimeTimeCount: int, ScheduledBatteries: int, CompletedBatteries: int, ScheduledBatteriesPerHour: int, Temperature: real, ImpactTest: int, VibrationTest: int, CellTest: int, DownTime: int, Thruput: int, OverallEfficiency: int, Availability: int, Performance: int, Quality: int, PlannedProductionTime: int, ActualRuntime: int, UnplannedDowntime: int, PlannedDowntime: int, PlannedQuantity: int, ActualQuantity: int, RejectedQuantity: int, OEE_GoalbyPlant: real, OEE_Seattle: real, OEE_Detroit: real, OEE_Hannover: real, OEE_USA: real, OEE_Mexico: real, OEE_GoalbyProduct: real, OEE_BatteryA: real, OEE_BatteryB: real, OEE_BatteryC: real, OEE_GoalbyShift: real, OEE_MorningShift: real, OEE_DayShift: real, OEE_NightShift: real)
+
+// Create a function to parse the data from the assemblybatteries topic
+.create-or-alter function Expand_assemblybatteries_Data()
+{
+ staging
+ | where subject == "topic/assemblybatteries"
+ | extend data = parse_json( base64_decode_tostring(data_base64) )
+ | project
+ Timestamp = todatetime(data.data.Timestamp),
+ MakeupArea = tostring(data.data.MakeupArea),
+ Line = tostring(data.data.Line),
+ Product = tostring(data.data.Product),
+ Process = tostring(data.data.Process),
+ Batch = toint(data.data.Batch),
+ CurrentShift = tostring(data.data.CurrentShift),
+ CurrentCellAssemblyPerMinutes = toint(data.data.CurrentCellAssemblyPerMinutes),
+ TargetCellAssemblyPerMinutes = toint(data.data.TargetCellAssemblyPerMinutes),
+ StartTime = todatetime(data.data.StartTime),
+ FinishTime = todatetime(data.data.FinishTime),
+ Waste = toreal(data.data.Waste),
+ WasteReason = tostring(data.data.WasteReason),
+ LostTime = tostring(data.data.LostTime),
+ LostTimeReason = tostring(data.data.LostTimeReason),
+ LostTimeTimeCount = toint(data.data.LostTimeTimeCount),
+ ScheduledBatteries = toint(data.data.ScheduledBatteries),
+ CompletedBatteries = toint(data.data.CompletedBatteries),
+ ScheduledBatteriesPerHour = toint(data.data.ScheduledBatteriesPerHour),
+ Temperature = toreal(data.data.DoughTemperature),
+ ImpactTest = toint(data.data.ImpactTest),
+ VibrationTest = toint(data.data.VibrationTest),
+ CellTest = toint(data.data.CellTest),
+ DownTime = toint(data.data.DownTime),
+ Thruput = toint(data.data.Thruput),
+ OverallEfficiency = toint(data.data.OverallEfficiency),
+ Availability = toint(data.data.Availability),
+ Performance = toint(data.data.Performance),
+ Quality = toint(data.data.Quality),
+ PlannedProductionTime = toint(data.data.PlannedProductionTime),
+ ActualRuntime = toint(data.data.ActualRuntime),
+ UnplannedDowntime = toint(data.data.UnplannedDowntime),
+ PlannedDowntime = toint(data.data.PlannedDowntime),
+ PlannedQuantity = toint(data.data.PlannedQuantity),
+ ActualQuantity = toint(data.data.ActualQuantity),
+ RejectedQuantity = toint(data.data.RejectedQuantity),
+ OEE_GoalbyPlant = toreal(data.data.OEE_GoalbyPlant),
+ OEE_Seattle = toreal(data.data.OEE_Seattle),
+ OEE_Detroit = toreal(data.data.OEE_Detroit),
+ OEE_Hannover = toreal(data.data.OEE_Hannover),
+ OEE_USA = toreal(data.data.OEE_USA),
+ OEE_Mexico = toreal(data.data.OEE_Mexico),
+ OEE_GoalbyProduct = toreal(data.data.OEE_GoalbyProduct),
+ OEE_BatteryA = toreal(data.data.OEE_BatteryA),
+ OEE_BatteryB = toreal(data.data.OEE_BatteryB),
+ OEE_BatteryC = toreal(data.data.OEE_BatteryC),
+ OEE_GoalbyShift = toreal(data.data.OEE_GoalbyShift),
+ OEE_MorningShift = toreal(data.data.OEE_MorningShift),
+ OEE_DayShift = toreal(data.data.OEE_DayShift),
+ OEE_NightShift = toreal(data.data.OEE_NightShift)
+}
+
+// Create policy
+.alter table assemblybatteries policy update @'[{"Source": "staging", "Query": "Expand_assemblybatteries_Data()", "IsEnabled": "True"}]'
+
+// Create function to decode base64 to json format
+.create-or-alter function Expand_weldingrobot_Data()
+{
+ staging
+ | where subject == "topic/weldingrobot"
+ | extend data = parse_json( base64_decode_tostring(data_base64) )
+ | project
+ Timestamp = todatetime(data.data.Timestamp),
+ Heater_Outlet_Temp = toreal(data.data.Heater_Outlet_Temp),
+ Pump1_Flow_Totalizer = toreal(data.data.Pump1_Flow_Totalizer),
+ Pump2_Flow_Totalizer = toreal(data.data.Pump2_Flow_Totalizer),
+ Pump3_Flow_Totalizer = toreal(data.data.Pump3_Flow_Totalizer),
+ Pump1_Temperature_Flow = toreal(data.data.Pump1_Temperature_Flow),
+ Pump2_Temperature_Flow = toreal(data.data.Pump2_Temperature_Flow),
+ Pump3_Temperature_Flow = toreal(data.data.Pump3_Temperature_Flow),
+ Pumps_Total_Flow = toreal(data.data.Pumps_Total_Flow),
+ Pressure_Filter_Inlet = toreal(data.data.Pressure_Filter_Inlet),
+ Pressure_Filter_Outlet = toreal(data.data.Pressure_Filter_Outlet),
+ RobotPosition_J0 = toreal(data.data.RobotPosition_J0),
+ RobotPosition_J1 = toreal(data.data.RobotPosition_J1),
+ RobotPosition_J2 = toreal(data.data.RobotPosition_J2),
+ RobotPosition_J3 = toreal(data.data.RobotPosition_J3),
+ RobotPosition_J4 = toreal(data.data.RobotPosition_J4),
+ RobotPosition_J5 = toreal(data.data.RobotPosition_J5),
+ Tank_Level = toreal(data.data.Tank_Level),
+ Drive1_Current = toreal(data.data.Drive1_Current),
+ Drive1_Frequency = toint(data.data.Drive1_Frequency),
+ Drive1_Speed = toint(data.data.Drive1_Speed),
+ Drive1_Voltage = toreal(data.data.Drive1_Voltage),
+ Drive2_Current = toreal(data.data.Drive2_Current),
+ Drive2_Frequency = toint(data.data.Drive2_Frequency),
+ Drive2_Speed = toint(data.data.Drive2_Speed),
+ Drive2_Voltage = toreal(data.data.Drive2_Voltage),
+ Drive3_Current = toreal(data.data.Drive3_Current),
+ Drive3_Frequency = toint(data.data.Drive3_Frequency),
+ Drive3_Speed = toint(data.data.Drive3_Speed),
+ Drive3_Voltage = toreal(data.data.Drive3_Voltage),
+ Cooler_Inlet_Temp = toreal(data.data.Cooler_Inlet_Temp),
+ Cooler_Outlet_Temp = toreal(data.data.Cooler_Outlet_Temp),
+ Dynamix_Ch1_Acceleration = toreal(data.data.Dynamix_Ch1_Acceleration),
+ Flow001 = toreal(data.data.Flow001),
+ Pressure001 = toreal(data.data.Pressure001),
+ Pressure002 = toreal(data.data.Pressure002),
+ Heater_Inlet_Temp = toreal(data.data.Heater_Inlet_Temp),
+ Pump1_Conductivity = toreal(data.data.Pump1_Conductivity),
+ Valve_000_Pump1 = toboolean(data.data.Valve_000_Pump1),
+ Cooler_ON = toboolean(data.data.Cooler_ON),
+ Fan001_On = toboolean(data.data.Fan001_On),
+ Heater_ON = toboolean(data.data.Heater_ON),
+ Filter_Chg_Required = toboolean(data.data.Filter_Chg_Required),
+ Filter_Reset = toboolean(data.data.Filter_Reset),
+ Filter_Override = toboolean(data.data.Filter_Override),
+ UTC_Time = todatetime(data.data.UTC_Time),
+ Current = toreal(data.data.Current),
+ Voltage = toreal(data.data.Voltage),
+ Temperature = toreal(data.data.Temperature),
+ Humidity = toreal(data.data.Humidity),
+ VacuumAlert = toboolean(data.data.VacuumAlert),
+ VacuumPressure = toreal(data.data.VacuumPressure),
+ Oiltemperature = toreal(data.data.Oiltemperature),
+ OiltemperatureTarget = toreal(data.data.OiltemperatureTarget)
+}
+
+// Create policy
+.alter table weldingrobot policy update @'[{"Source": "staging", "Query": "Expand_weldingrobot_Data()", "IsEnabled": "True"}]'
+
+// Function extract assemblyline telemtry from staging table to assemblyline table
+.create-or-alter function Expand_assemblyline_Data() {
+let combinedData = (
+ staging
+ | where subject == "topic/dataemulator"
+ | extend data = parse_json(data_base64)
+ | project
+ date_time = todatetime(data.date_time),
+ plant_details = data.plant_details,
+ shift = tostring(data.shift),
+ employees_on_shift = data.employees_on_shift,
+ cars_produced = data.cars_produced,
+ equipment_maintenance = data.equipment_maintenance,
+ production_schedule = data.production_schedule,
+ actual_production = data.actual_production,
+ equipment_telemetry = data.equipment_telemetry,
+ performance_metrics = data.performance_metrics
+ )
+ | union (
+ staging
+ | where subject == "topic/assemblyline"
+ | extend data = parse_json(base64_decode_tostring(data_base64))
+ | project
+ date_time = todatetime(data.data.date_time),
+ plant_details = data.data.plant_details,
+ shift = tostring(data.data.shift),
+ employees_on_shift = data.data.employees_on_shift,
+ cars_produced = data.data.cars_produced,
+ equipment_maintenance = data.data.equipment_maintenance,
+ production_schedule = data.data.production_schedule,
+ actual_production = data.data.actual_production,
+ equipment_telemetry = data.data.equipment_telemetry,
+ performance_metrics = data.data.performance_metrics
+ );
+ combinedData
+}
+
+// Create policy
+.alter table assemblyline policy update @'[{"Source": "staging", "Query": "Expand_assemblyline_Data()", "IsEnabled": "True"}]'
diff --git a/azure_jumpstart_ag/manufacturing/bicep/kubernetes/acr.bicep b/azure_jumpstart_ag/manufacturing/bicep/kubernetes/acr.bicep
new file mode 100644
index 0000000000..c864f1db59
--- /dev/null
+++ b/azure_jumpstart_ag/manufacturing/bicep/kubernetes/acr.bicep
@@ -0,0 +1,25 @@
+@description('The location of the Managed Cluster resource')
+param location string = resourceGroup().location
+
+@description('Resource tag for Jumpstart Agora')
+param resourceTags object = {
+ Project: 'Jumpstart_Agora'
+}
+
+@description('Name of the Azure Container Registry')
+param acrName string
+
+@description('Provide a tier of your Azure Container Registry.')
+param acrSku string = 'Basic'
+
+resource acr 'Microsoft.ContainerRegistry/registries@2023-01-01-preview' ={
+ name: acrName
+ location: location
+ tags: resourceTags
+ sku: {
+ name: acrSku
+ }
+ properties: {
+ adminUserEnabled: true
+ }
+}
diff --git a/azure_jumpstart_ag/manufacturing/bicep/main.azd.bicep b/azure_jumpstart_ag/manufacturing/bicep/main.azd.bicep
new file mode 100644
index 0000000000..f751cc8409
--- /dev/null
+++ b/azure_jumpstart_ag/manufacturing/bicep/main.azd.bicep
@@ -0,0 +1,238 @@
+targetScope = 'subscription'
+
+@description('Azure service principal client id')
+param spnClientId string = ''
+
+@description('Azure service principal client secret')
+@secure()
+param spnClientSecret string = newGuid()
+
+@description('Azure AD tenant id for your service principal')
+param spnTenantId string = ''
+
+@description('Azure service principal Object id')
+param spnObjectId string = ''
+
+@minLength(1)
+@maxLength(77)
+@description('Prefix for resource group, i.e. {name}-rg')
+param envName string = toLower(substring(newGuid(), 0, 5))
+
+resource rg 'Microsoft.Resources/resourceGroups@2020-06-01' = {
+ name: '${envName}-rg'
+ location: location
+}
+
+@description('Location for all resources')
+param location string = ''
+
+@maxLength(5)
+@description('Random GUID')
+param namingGuid string = toLower(substring(newGuid(), 0, 5))
+
+@description('Username for Windows account')
+param windowsAdminUsername string = 'Agora'
+
+@description('Password for Windows account. Password must have 3 of the following: 1 lower case character, 1 upper case character, 1 number, and 1 special character. The value must be between 12 and 123 characters long')
+@minLength(12)
+@maxLength(123)
+@secure()
+param windowsAdminPassword string
+
+@description('Name for your log analytics workspace')
+param logAnalyticsWorkspaceName string = 'Ag-Workspace-${namingGuid}'
+
+@description('Target GitHub account')
+param githubAccount string = 'microsoft'
+
+@description('Target GitHub branch')
+param githubBranch string = 'main'
+
+@description('Choice to deploy Bastion to connect to the client VM')
+param deployBastion bool = false
+
+@description('Name of the Cloud VNet')
+param virtualNetworkNameCloud string = 'Ag-Vnet-Prod'
+
+@description('Name of the Staging AKS subnet in the cloud virtual network')
+param subnetNameCloudAksStaging string = 'Ag-Subnet-Staging'
+
+@description('Name of the inner-loop AKS subnet in the cloud virtual network')
+param subnetNameCloudAksInnerLoop string = 'Ag-Subnet-InnerLoop'
+
+@description('Name of the storage queue')
+param storageQueueName string = 'aioqueue'
+
+@description('Name of the event hub')
+param eventHubName string = 'aiohub${namingGuid}'
+
+@description('Name of the event hub namespace')
+param eventHubNamespaceName string = 'aiohubns${namingGuid}'
+
+@description('Name of the event grid namespace')
+param eventGridNamespaceName string = 'aioeventgridns${namingGuid}'
+
+@description('The name of the Key Vault for site 1')
+param akvNameSite1 string = 'agakv1${namingGuid}'
+
+@description('The name of the Key Vault for site 2')
+param akvNameSite2 string = 'agakv2${namingGuid}'
+
+@description('Name of the storage account')
+param aioStorageAccountName string = 'aiostg${namingGuid}'
+
+@description('The name of ESA container in Storage Account')
+param stcontainerName string = 'esacontainer'
+
+@description('The name of the Azure Data Explorer cluster')
+param adxClusterName string = 'agadx${namingGuid}'
+
+@description('The custom location RPO ID')
+param customLocationRPOID string = ''
+
+@minLength(5)
+@maxLength(50)
+@description('Name of the Azure Container Registry')
+param acrName string = 'agacr${namingGuid}'
+
+@description('Override default RDP port using this parameter. Default is 3389. No changes will be made to the client VM.')
+param rdpPort string = '3389'
+
+@description('The agora industry to be deployed')
+param industry string = 'manufacturing'
+
+var templateBaseUrl = 'https://raw.githubusercontent.com/${githubAccount}/azure_arc/${githubBranch}/azure_jumpstart_ag/'
+
+module mgmtArtifactsAndPolicyDeployment 'mgmt/mgmtArtifacts.bicep' = {
+ name: 'mgmtArtifactsAndPolicyDeployment'
+ scope: rg
+ params: {
+ workspaceName: logAnalyticsWorkspaceName
+ location: location
+ }
+}
+
+module networkDeployment 'mgmt/network.bicep' = {
+ name: 'networkDeployment'
+ scope: rg
+ params: {
+ virtualNetworkNameCloud: virtualNetworkNameCloud
+ subnetNameCloudAksStaging: subnetNameCloudAksStaging
+ subnetNameCloudAksInnerLoop: subnetNameCloudAksInnerLoop
+ deployBastion: deployBastion
+ location: location
+ }
+}
+
+module storageAccountDeployment 'mgmt/storageAccount.bicep' = {
+ name: 'storageAccountDeployment'
+ scope: rg
+ params: {
+ location: location
+ }
+}
+
+module clientVmDeployment 'clientVm/clientVm.bicep' = {
+ name: 'clientVmDeployment'
+ scope: rg
+ params: {
+ windowsAdminUsername: windowsAdminUsername
+ windowsAdminPassword: windowsAdminPassword
+ spnClientId: spnClientId
+ spnClientSecret: spnClientSecret
+ spnObjectId: spnObjectId
+ spnTenantId: spnTenantId
+ workspaceName: logAnalyticsWorkspaceName
+ storageAccountName: storageAccountDeployment.outputs.storageAccountName
+ templateBaseUrl: templateBaseUrl
+ deployBastion: deployBastion
+ githubAccount: githubAccount
+ githubBranch: githubBranch
+ //githubPAT: githubPAT
+ location: location
+ subnetId: networkDeployment.outputs.innerLoopSubnetId
+ acrName: acrName
+ rdpPort: rdpPort
+ namingGuid: namingGuid
+ adxClusterName: adxClusterName
+ customLocationRPOID: customLocationRPOID
+ industry: industry
+ stcontainerName: stcontainerName
+ }
+}
+
+module eventHub 'data/eventHub.bicep' = {
+ name: 'eventHubDeployment'
+ scope: rg
+ params: {
+ eventHubName: eventHubName
+ eventHubNamespaceName: eventHubNamespaceName
+ location: location
+ }
+}
+
+module storageAccount 'storage/storageAccount.bicep' = {
+ name: 'aioStorageAccountDeployment'
+ scope: rg
+ params: {
+ storageAccountName: aioStorageAccountName
+ location: location
+ storageQueueName: storageQueueName
+ stcontainerName: stcontainerName
+ }
+}
+
+module eventGrid 'data/eventGrid.bicep' = {
+ name: 'eventGridDeployment'
+ scope: rg
+ params: {
+ eventGridNamespaceName: eventGridNamespaceName
+ eventHubResourceId: eventHub.outputs.eventHubResourceId
+ queueName: storageQueueName
+ storageAccountResourceId: storageAccount.outputs.storageAccountId
+ namingGuid: namingGuid
+ location: location
+ }
+}
+
+module keyVault 'data/keyVault.bicep' = {
+ name: 'keyVaultDeployment'
+ scope: rg
+ params: {
+ tenantId: spnTenantId
+ akvNameSite1: akvNameSite1
+ akvNameSite2: akvNameSite2
+ location: location
+ }
+}
+
+module acr 'kubernetes/acr.bicep' = {
+ name: 'acrDeployment'
+ scope: rg
+ params: {
+ acrName: acrName
+ location: location
+ }
+}
+
+module adx 'data/dataExplorer.bicep' = {
+ name: 'adxDeployment'
+ scope: rg
+ params: {
+ adxClusterName: adxClusterName
+ location: location
+ eventHubResourceId: eventHub.outputs.eventHubResourceId
+ eventHubName: eventHubName
+ eventHubNamespaceName: eventHubNamespaceName
+ }
+}
+
+output AZURE_TENANT_ID string = tenant().tenantId
+output AZURE_RESOURCE_GROUP string = rg.name
+
+output NAMING_GUID string = namingGuid
+output RDP_PORT string = rdpPort
+
+output ADX_CLUSTER_NAME string = adxClusterName
+output ACR_NAME string = acrName
+
diff --git a/azure_jumpstart_ag/manufacturing/bicep/main.azd.parameters.json b/azure_jumpstart_ag/manufacturing/bicep/main.azd.parameters.json
new file mode 100644
index 0000000000..81d476bcf8
--- /dev/null
+++ b/azure_jumpstart_ag/manufacturing/bicep/main.azd.parameters.json
@@ -0,0 +1,36 @@
+{
+ "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
+ "contentVersion": "1.0.0.0",
+ "parameters": {
+ "envName": {
+ "value": "${AZURE_ENV_NAME}"
+ },
+ "location": {
+ "value": "${AZURE_LOCATION}"
+ },
+ "spnClientId": {
+ "value": "${SPN_CLIENT_ID}"
+ },
+ "spnClientSecret": {
+ "value": "${SPN_CLIENT_SECRET}"
+ },
+ "spnTenantId": {
+ "value": "${SPN_TENANT_ID}"
+ },
+ "spnObjectId": {
+ "value": "${SPN_OBJECT_ID}"
+ },
+ "windowsAdminUsername": {
+ "value": "${JS_WINDOWS_ADMIN_USERNAME}"
+ },
+ "deployBastion": {
+ "value": "${JS_DEPLOY_BASTION}"
+ },
+ "rdpPort": {
+ "value": "${JS_RDP_PORT}"
+ },
+ "customLocationRPOID": {
+ "value": "${CUSTOM_LOCATION_RP_ID}"
+ }
+ }
+}
\ No newline at end of file
diff --git a/azure_jumpstart_ag/manufacturing/bicep/main.bicep b/azure_jumpstart_ag/manufacturing/bicep/main.bicep
new file mode 100644
index 0000000000..f5ad1458fe
--- /dev/null
+++ b/azure_jumpstart_ag/manufacturing/bicep/main.bicep
@@ -0,0 +1,212 @@
+@description('Azure service principal client id')
+param spnClientId string
+
+@description('Azure service principal client secret')
+@secure()
+param spnClientSecret string
+
+@description('Azure AD tenant id for your service principal')
+param spnTenantId string
+
+@description('Azure service principal Object id')
+param spnObjectId string
+
+@description('Location for all resources')
+param location string = resourceGroup().location
+
+@maxLength(5)
+@description('Random GUID')
+param namingGuid string = toLower(substring(newGuid(), 0, 5))
+
+@description('Username for Windows account')
+param windowsAdminUsername string
+
+@description('Password for Windows account. Password must have 3 of the following: 1 lower case character, 1 upper case character, 1 number, and 1 special character. The value must be between 12 and 123 characters long')
+@minLength(12)
+@maxLength(123)
+@secure()
+param windowsAdminPassword string
+
+@description('Name for your log analytics workspace')
+param logAnalyticsWorkspaceName string = 'Ag-Workspace-${namingGuid}'
+
+@description('Target GitHub account')
+param githubAccount string = 'microsoft'
+
+@description('Target GitHub branch')
+param githubBranch string = 'main'
+
+@description('Choice to deploy Bastion to connect to the client VM')
+param deployBastion bool = false
+
+@description('Name of the Cloud VNet')
+param virtualNetworkNameCloud string = 'Ag-Vnet-Prod'
+
+@description('Name of the Staging AKS subnet in the cloud virtual network')
+param subnetNameCloudAksStaging string = 'Ag-Subnet-Staging'
+
+@description('Name of the inner-loop AKS subnet in the cloud virtual network')
+param subnetNameCloudAksInnerLoop string = 'Ag-Subnet-InnerLoop'
+
+@description('Name of the storage queue')
+param storageQueueName string = 'aioqueue'
+
+@description('Name of the event hub')
+param eventHubName string = 'aiohub${namingGuid}'
+
+@description('Name of the event hub namespace')
+param eventHubNamespaceName string = 'aiohubns${namingGuid}'
+
+@description('Name of the event grid namespace')
+param eventGridNamespaceName string = 'aioeventgridns${namingGuid}'
+
+@description('The name of the Key Vault for site 1')
+param akvNameSite1 string = 'agakv1${namingGuid}'
+
+@description('The name of the Key Vault for site 2')
+param akvNameSite2 string = 'agakv2${namingGuid}'
+
+@description('The name of the Azure Data Explorer Event Hub consumer group for assemblybatteries')
+param stagingDataCGName string = 'mqttdataemulator'
+
+@description('Name of the storage account')
+param aioStorageAccountName string = 'aiostg${namingGuid}'
+
+@description('The name of ESA container in Storage Account')
+param stcontainerName string = 'esacontainer'
+
+@description('The name of the Azure Data Explorer cluster')
+param adxClusterName string = 'agadx${namingGuid}'
+
+@description('The custom location RPO ID')
+param customLocationRPOID string
+
+@minLength(5)
+@maxLength(50)
+@description('Name of the Azure Container Registry')
+param acrName string = 'agacr${namingGuid}'
+
+@description('Override default RDP port using this parameter. Default is 3389. No changes will be made to the client VM.')
+param rdpPort string = '3389'
+
+@description('The agora industry to be deployed')
+param industry string = 'manufacturing'
+
+var templateBaseUrl = 'https://raw.githubusercontent.com/${githubAccount}/azure_arc/${githubBranch}/azure_jumpstart_ag/'
+
+module mgmtArtifactsAndPolicyDeployment 'mgmt/mgmtArtifacts.bicep' = {
+ name: 'mgmtArtifactsAndPolicyDeployment'
+ params: {
+ workspaceName: logAnalyticsWorkspaceName
+ location: location
+ }
+}
+
+module networkDeployment 'mgmt/network.bicep' = {
+ name: 'networkDeployment'
+ params: {
+ virtualNetworkNameCloud: virtualNetworkNameCloud
+ subnetNameCloudAksStaging: subnetNameCloudAksStaging
+ subnetNameCloudAksInnerLoop: subnetNameCloudAksInnerLoop
+ deployBastion: deployBastion
+ location: location
+ }
+}
+
+module storageAccountDeployment 'mgmt/storageAccount.bicep' = {
+ name: 'storageAccountDeployment'
+ params: {
+ location: location
+ }
+}
+
+module clientVmDeployment 'clientVm/clientVm.bicep' = {
+ name: 'clientVmDeployment'
+ params: {
+ windowsAdminUsername: windowsAdminUsername
+ windowsAdminPassword: windowsAdminPassword
+ spnClientId: spnClientId
+ spnClientSecret: spnClientSecret
+ spnObjectId: spnObjectId
+ spnTenantId: spnTenantId
+ workspaceName: logAnalyticsWorkspaceName
+ storageAccountName: storageAccountDeployment.outputs.storageAccountName
+ templateBaseUrl: templateBaseUrl
+ deployBastion: deployBastion
+ githubAccount: githubAccount
+ githubBranch: githubBranch
+ //githubPAT: githubPAT
+ location: location
+ subnetId: networkDeployment.outputs.innerLoopSubnetId
+ acrName: acrName
+ rdpPort: rdpPort
+ namingGuid: namingGuid
+ adxClusterName: adxClusterName
+ customLocationRPOID: customLocationRPOID
+ industry: industry
+ aioStorageAccountName: aioStorageAccountName
+ stcontainerName: stcontainerName
+ }
+}
+
+module eventHub 'data/eventHub.bicep' = {
+ name: 'eventHubDeployment'
+ params: {
+ eventHubName: eventHubName
+ eventHubNamespaceName: eventHubNamespaceName
+ location: location
+ stagingDataCGName: stagingDataCGName
+ }
+}
+
+module storageAccount 'storage/storageAccount.bicep' = {
+ name: 'aioStorageAccountDeployment'
+ params: {
+ storageAccountName: aioStorageAccountName
+ location: location
+ storageQueueName: storageQueueName
+ stcontainerName: stcontainerName
+ }
+}
+
+module eventGrid 'data/eventGrid.bicep' = {
+ name: 'eventGridDeployment'
+ params: {
+ eventGridNamespaceName: eventGridNamespaceName
+ eventHubResourceId: eventHub.outputs.eventHubResourceId
+ queueName: storageQueueName
+ storageAccountResourceId: storageAccount.outputs.storageAccountId
+ namingGuid: namingGuid
+ location: location
+ }
+}
+
+module keyVault 'data/keyVault.bicep' = {
+ name: 'keyVaultDeployment'
+ params: {
+ tenantId: spnTenantId
+ akvNameSite1: akvNameSite1
+ akvNameSite2: akvNameSite2
+ location: location
+ }
+}
+
+module acr 'kubernetes/acr.bicep' = {
+ name: 'acrDeployment'
+ params: {
+ acrName: acrName
+ location: location
+ }
+}
+
+module adx 'data/dataExplorer.bicep' = {
+ name: 'adxDeployment'
+ params: {
+ adxClusterName: adxClusterName
+ location: location
+ eventHubResourceId: eventHub.outputs.eventHubResourceId
+ eventHubName: eventHubName
+ eventHubNamespaceName: eventHubNamespaceName
+ stagingDataCGName: stagingDataCGName
+ }
+}
diff --git a/azure_jumpstart_ag/manufacturing/bicep/main.parameters.json b/azure_jumpstart_ag/manufacturing/bicep/main.parameters.json
new file mode 100644
index 0000000000..c37a48de68
--- /dev/null
+++ b/azure_jumpstart_ag/manufacturing/bicep/main.parameters.json
@@ -0,0 +1,30 @@
+{
+ "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
+ "contentVersion": "1.0.0.0",
+ "parameters": {
+ "spnClientId": {
+ "value": ""
+ },
+ "spnClientSecret": {
+ "value": ""
+ },
+ "spnTenantId": {
+ "value": ""
+ },
+ "spnObjectId": {
+ "value": ""
+ },
+ "windowsAdminUsername": {
+ "value": ""
+ },
+ "windowsAdminPassword": {
+ "value": ""
+ },
+ "deployBastion": {
+ "value": false
+ },
+ "customLocationRPOID": {
+ "value": ""
+ }
+ }
+}
diff --git a/azure_jumpstart_ag/manufacturing/bicep/mgmt/VMInsightsDCR.bicep b/azure_jumpstart_ag/manufacturing/bicep/mgmt/VMInsightsDCR.bicep
new file mode 100644
index 0000000000..6e904cec50
--- /dev/null
+++ b/azure_jumpstart_ag/manufacturing/bicep/mgmt/VMInsightsDCR.bicep
@@ -0,0 +1,56 @@
+@description('This is the name of the AMA-VMI Data Collection Rule(DCR)')
+@metadata({ displayName: 'Name of the Data Collection Rule(DCR)' })
+param DcrName string
+
+@description('Workspace Location.')
+param WorkspaceLocation string
+
+@description('Workspace Resource ID.')
+param WorkspaceResourceId string
+
+resource MSVMI_PerfandDa_Dcr 'Microsoft.Insights/dataCollectionRules@2021-04-01' = {
+ name: 'MSVMI-PerfandDa-${DcrName}'
+ location: WorkspaceLocation
+ properties: {
+ description: 'Data collection rule for VM Insights.'
+ dataSources: {
+ performanceCounters: [
+ {
+ name: 'VMInsightsPerfCounters'
+ streams: ['Microsoft-InsightsMetrics']
+ scheduledTransferPeriod: 'PT1M'
+ samplingFrequencyInSeconds: 60
+ counterSpecifiers: ['\\VmInsights\\DetailedMetrics']
+ }
+ ]
+ extensions: [
+ {
+ streams: ['Microsoft-ServiceMap']
+ extensionName: 'DependencyAgent'
+ extensionSettings: {}
+ name: 'DependencyAgentDataSource'
+ }
+ ]
+ }
+ destinations: {
+ logAnalytics: [
+ {
+ workspaceResourceId: WorkspaceResourceId
+ name: 'VMInsightsPerf-Logs-Dest'
+ }
+ ]
+ }
+ dataFlows: [
+ {
+ streams: ['Microsoft-InsightsMetrics']
+ destinations: ['VMInsightsPerf-Logs-Dest']
+ }
+ {
+ streams: ['Microsoft-ServiceMap']
+ destinations: ['VMInsightsPerf-Logs-Dest']
+ }
+ ]
+ }
+}
+
+output id string = MSVMI_PerfandDa_Dcr.id
diff --git a/azure_jumpstart_ag/manufacturing/bicep/mgmt/mgmtArtifacts.bicep b/azure_jumpstart_ag/manufacturing/bicep/mgmt/mgmtArtifacts.bicep
new file mode 100644
index 0000000000..94d8e25876
--- /dev/null
+++ b/azure_jumpstart_ag/manufacturing/bicep/mgmt/mgmtArtifacts.bicep
@@ -0,0 +1,64 @@
+@description('Name for your log analytics workspace')
+param workspaceName string
+
+@description('Azure Region to deploy the Log Analytics Workspace')
+param location string = resourceGroup().location
+
+@description('Resource tag for Jumpstart Agora')
+param resourceTags object = {
+ Project: 'Jumpstart_Agora'
+}
+
+@description('SKU, leave default pergb2018')
+param sku string = 'pergb2018'
+
+@description('Suffix of Data Collection Rule for VM Insights: MSVMI-PerfandDa-"suffix"')
+param VMIDCRName string = 'Agora'
+
+var security = {
+ name: 'Security(${workspaceName})'
+ galleryName: 'Security'
+}
+
+resource workspace 'Microsoft.OperationalInsights/workspaces@2022-10-01' = {
+ name: workspaceName
+ location: location
+ tags: resourceTags
+ properties: {
+ sku: {
+ name: sku
+ }
+ }
+}
+
+resource securityGallery 'Microsoft.OperationsManagement/solutions@2015-11-01-preview' = {
+ name: security.name
+ location: location
+ tags: resourceTags
+ properties: {
+ workspaceResourceId: workspace.id
+ }
+ plan: {
+ name: security.name
+ promotionCode: ''
+ product: 'OMSGallery/${security.galleryName}'
+ publisher: 'Microsoft'
+ }
+}
+
+module policyDeploymentRGScope './policyAzureArcRGScope.bicep' = {
+ name: 'policyDeployment'
+ params: {
+ azureLocation: location
+ VMInsightsDCRId: VMI_DCR_Deployment.outputs.id
+ }
+}
+
+module VMI_DCR_Deployment './VMInsightsDCR.bicep' = {
+ name: 'VMI-DCR-Deployment-${uniqueString(VMIDCRName)}'
+ params: {
+ DcrName: VMIDCRName
+ WorkspaceLocation: location
+ WorkspaceResourceId: workspace.id
+ }
+}
diff --git a/azure_jumpstart_ag/manufacturing/bicep/mgmt/network.bicep b/azure_jumpstart_ag/manufacturing/bicep/mgmt/network.bicep
new file mode 100644
index 0000000000..dbf41bcbb4
--- /dev/null
+++ b/azure_jumpstart_ag/manufacturing/bicep/mgmt/network.bicep
@@ -0,0 +1,368 @@
+@description('Name of the Cloud VNet')
+param virtualNetworkNameCloud string
+
+@description('Name of the Staging AKS subnet in the cloud virtual network')
+param subnetNameCloudAksStaging string
+
+@description('Name of the inner-loop AKS subnet in the cloud virtual network')
+param subnetNameCloudAksInnerLoop string
+
+@description('Azure Region to deploy the Log Analytics Workspace')
+param location string = resourceGroup().location
+
+@description('Resource tag for Jumpstart Agora')
+param resourceTags object = {
+ Project: 'Jumpstart_Agora'
+}
+
+@description('Choice to deploy Bastion to connect to the client VM')
+param deployBastion bool = false
+
+@description('Name of the prod Network Security Group')
+param networkSecurityGroupNameCloud string = 'Ag-NSG-Prod'
+
+@description('Name of the Bastion Network Security Group')
+param bastionNetworkSecurityGroupName string = 'Ag-NSG-Bastion'
+
+var addressPrefixCloud = '10.16.0.0/16'
+var subnetAddressPrefixAksDev = '10.16.80.0/21'
+var subnetAddressPrefixInnerLoop = '10.16.64.0/21'
+var bastionSubnetIpPrefix = '10.16.3.64/26'
+var bastionSubnetName = 'AzureBastionSubnet'
+var bastionSubnetRef = '${cloudVirtualNetwork.id}/subnets/${bastionSubnetName}'
+var bastionName = 'Ag-Bastion'
+var bastionPublicIpAddressName = '${bastionName}-PIP'
+
+
+var bastionSubnet = [
+ {
+ name: 'AzureBastionSubnet'
+ properties: {
+ addressPrefix: bastionSubnetIpPrefix
+ networkSecurityGroup: {
+ id: bastionNetworkSecurityGroup.id
+ }
+ }
+ }
+]
+var cloudAKSDevSubnet = [
+ {
+ name: subnetNameCloudAksStaging
+ properties: {
+ addressPrefix: subnetAddressPrefixAksDev
+ privateEndpointNetworkPolicies: 'Enabled'
+ privateLinkServiceNetworkPolicies: 'Enabled'
+ networkSecurityGroup: {
+ id: networkSecurityGroupCloud.id
+ }
+ }
+ }
+]
+
+var cloudAKSInnerLoopSubnet = [
+ {
+ name: subnetNameCloudAksInnerLoop
+ properties: {
+ addressPrefix: subnetAddressPrefixInnerLoop
+ privateEndpointNetworkPolicies: 'Enabled'
+ privateLinkServiceNetworkPolicies: 'Enabled'
+ networkSecurityGroup: {
+ id: networkSecurityGroupCloud.id
+ }
+ }
+ }
+]
+
+resource cloudVirtualNetwork 'Microsoft.Network/virtualNetworks@2022-07-01' = {
+ name: virtualNetworkNameCloud
+ location: location
+ tags: resourceTags
+ properties: {
+ addressSpace: {
+ addressPrefixes: [
+ addressPrefixCloud
+ ]
+ }
+ subnets: (deployBastion == false) ? union (cloudAKSDevSubnet,cloudAKSInnerLoopSubnet) : union(cloudAKSDevSubnet,cloudAKSInnerLoopSubnet,bastionSubnet)
+ }
+}
+
+resource publicIpAddress 'Microsoft.Network/publicIPAddresses@2023-02-01' = if (deployBastion == true) {
+ name: bastionPublicIpAddressName
+ location: location
+ tags: resourceTags
+ properties: {
+ publicIPAllocationMethod: 'Static'
+ publicIPAddressVersion: 'IPv4'
+ idleTimeoutInMinutes: 4
+ }
+ sku: {
+ name: 'Standard'
+ }
+}
+
+resource networkSecurityGroupCloud 'Microsoft.Network/networkSecurityGroups@2023-02-01' = {
+ name: networkSecurityGroupNameCloud
+ location: location
+ tags: resourceTags
+ properties: {
+ securityRules: [
+ {
+ name: 'allow_k8s_80'
+ properties: {
+ priority: 1003
+ protocol: 'TCP'
+ access: 'Allow'
+ direction: 'Inbound'
+ sourceAddressPrefix: '*'
+ sourcePortRange: '*'
+ destinationAddressPrefix: '*'
+ destinationPortRange: '80'
+ }
+ }
+ {
+ name: 'allow_k8s_8080'
+ properties: {
+ priority: 1004
+ protocol: 'TCP'
+ access: 'Allow'
+ direction: 'Inbound'
+ sourceAddressPrefix: '*'
+ sourcePortRange: '*'
+ destinationAddressPrefix: '*'
+ destinationPortRange: '8080'
+ }
+ }
+ {
+ name: 'allow_k8s_443'
+ properties: {
+ priority: 1005
+ protocol: 'TCP'
+ access: 'Allow'
+ direction: 'Inbound'
+ sourceAddressPrefix: '*'
+ sourcePortRange: '*'
+ destinationAddressPrefix: '*'
+ destinationPortRange: '443'
+ }
+ }
+ {
+ name: 'allow_pos_5000'
+ properties: {
+ priority: 1006
+ protocol: 'TCP'
+ access: 'Allow'
+ direction: 'Inbound'
+ sourceAddressPrefix: '*'
+ sourcePortRange: '*'
+ destinationAddressPrefix: '*'
+ destinationPortRange: '5000'
+ }
+ }
+ {
+ name: 'allow_pos_81'
+ properties: {
+ priority: 1007
+ protocol: 'TCP'
+ access: 'Allow'
+ direction: 'Inbound'
+ sourceAddressPrefix: '*'
+ sourcePortRange: '*'
+ destinationAddressPrefix: '*'
+ destinationPortRange: '81'
+ }
+ }
+ {
+ name: 'allow_prometheus_9090'
+ properties: {
+ priority: 1008
+ protocol: 'TCP'
+ access: 'Allow'
+ direction: 'Inbound'
+ sourceAddressPrefix: '*'
+ sourcePortRange: '*'
+ destinationAddressPrefix: '*'
+ destinationPortRange: '9090'
+ }
+ }
+ {
+ name: 'allow_MQ_8883'
+ properties: {
+ priority: 1009
+ protocol: 'TCP'
+ access: 'Allow'
+ direction: 'Inbound'
+ sourceAddressPrefix: '*'
+ sourcePortRange: '*'
+ destinationAddressPrefix: '*'
+ destinationPortRange: '8883'
+ }
+ }
+ {
+ name: 'allow_MQ_1883'
+ properties: {
+ priority: 1010
+ protocol: 'TCP'
+ access: 'Allow'
+ direction: 'Inbound'
+ sourceAddressPrefix: '*'
+ sourcePortRange: '*'
+ destinationAddressPrefix: '*'
+ destinationPortRange: '1883'
+ }
+ }
+ ]
+ }
+}
+
+resource bastionNetworkSecurityGroup 'Microsoft.Network/networkSecurityGroups@2023-02-01' = if (deployBastion == true) {
+ name: bastionNetworkSecurityGroupName
+ location: location
+ tags: resourceTags
+ properties: {
+ securityRules: [
+ {
+ name: 'bastion_allow_https_inbound'
+ properties: {
+ priority: 1010
+ protocol: 'TCP'
+ access: 'Allow'
+ direction: 'Inbound'
+ sourceAddressPrefix: 'Internet'
+ sourcePortRange: '*'
+ destinationAddressPrefix: '*'
+ destinationPortRange: '443'
+ }
+ }
+ {
+ name: 'bastion_allow_gateway_manager_inbound'
+ properties: {
+ priority: 1011
+ protocol: 'TCP'
+ access: 'Allow'
+ direction: 'Inbound'
+ sourceAddressPrefix: 'GatewayManager'
+ sourcePortRange: '*'
+ destinationAddressPrefix: '*'
+ destinationPortRange: '443'
+ }
+ }
+ {
+ name: 'bastion_allow_load_balancer_inbound'
+ properties: {
+ priority: 1012
+ protocol: 'TCP'
+ access: 'Allow'
+ direction: 'Inbound'
+ sourceAddressPrefix: 'AzureLoadBalancer'
+ sourcePortRange: '*'
+ destinationAddressPrefix: '*'
+ destinationPortRange: '443'
+ }
+ }
+ {
+ name: 'bastion_allow_host_comms'
+ properties: {
+ priority: 1013
+ protocol: '*'
+ access: 'Allow'
+ direction: 'Inbound'
+ sourceAddressPrefix: 'VirtualNetwork'
+ sourcePortRange: '*'
+ destinationAddressPrefix: 'VirtualNetwork'
+ destinationPortRanges: [
+ '8080'
+ '5701'
+ ]
+ }
+ }
+ {
+ name: 'bastion_allow_ssh_rdp_outbound'
+ properties: {
+ priority: 1014
+ protocol: '*'
+ access: 'Allow'
+ direction: 'Outbound'
+ sourceAddressPrefix: '*'
+ sourcePortRange: '*'
+ destinationAddressPrefix: 'VirtualNetwork'
+ destinationPortRanges: [
+ '22'
+ '3389'
+ ]
+ }
+ }
+ {
+ name: 'bastion_allow_azure_cloud_outbound'
+ properties: {
+ priority: 1015
+ protocol: 'TCP'
+ access: 'Allow'
+ direction: 'Outbound'
+ sourceAddressPrefix: '*'
+ sourcePortRange: '*'
+ destinationAddressPrefix: 'AzureCloud'
+ destinationPortRange: '443'
+ }
+ }
+ {
+ name: 'bastion_allow_bastion_comms'
+ properties: {
+ priority: 1016
+ protocol: '*'
+ access: 'Allow'
+ direction: 'Outbound'
+ sourceAddressPrefix: 'VirtualNetwork'
+ sourcePortRange: '*'
+ destinationAddressPrefix: 'VirtualNetwork'
+ destinationPortRanges: [
+ '8080'
+ '5701'
+ ]
+ }
+ }
+ {
+ name: 'bastion_allow_get_session_info'
+ properties: {
+ priority: 1017
+ protocol: '*'
+ access: 'Allow'
+ direction: 'Outbound'
+ sourceAddressPrefix: '*'
+ sourcePortRange: '*'
+ destinationAddressPrefix: 'Internet'
+ destinationPortRanges: [
+ '80'
+ '443'
+ ]
+ }
+ }
+ ]
+ }
+}
+
+resource bastionHost 'Microsoft.Network/bastionHosts@2023-02-01' = if (deployBastion == true) {
+ name: bastionName
+ location: location
+ tags: resourceTags
+ properties: {
+ ipConfigurations: [
+ {
+ name: 'IpConf'
+ properties: {
+ publicIPAddress: {
+ id: publicIpAddress.id
+ }
+ subnet: {
+ id: bastionSubnetRef
+ }
+ }
+ }
+ ]
+ }
+}
+
+output vnetId string = cloudVirtualNetwork.id
+output devSubnetId string = cloudVirtualNetwork.properties.subnets[0].id
+output innerLoopSubnetId string = cloudVirtualNetwork.properties.subnets[1].id
+output virtualNetworkNameCloud string = cloudVirtualNetwork.name
diff --git a/azure_jumpstart_ag/manufacturing/bicep/mgmt/policyAzureArcRGScope.bicep b/azure_jumpstart_ag/manufacturing/bicep/mgmt/policyAzureArcRGScope.bicep
new file mode 100644
index 0000000000..7f550c7418
--- /dev/null
+++ b/azure_jumpstart_ag/manufacturing/bicep/mgmt/policyAzureArcRGScope.bicep
@@ -0,0 +1,125 @@
+@description('Location of your Azure resources')
+param azureLocation string
+
+@description('Resource ID of Data Collection Rule for VM Insights')
+param VMInsightsDCRId string
+
+var policies = [
+ {
+ name: '(Ag) Enable Azure Monitor for Hybrid VMs with AMA'
+ definitionId: '/providers/Microsoft.Authorization/policySetDefinitions/2b00397d-c309-49c4-aa5a-f0b2c5bc6321'
+ roleDefinition: [
+ '/subscriptions/${subscription().subscriptionId}/providers/Microsoft.Authorization/roleDefinitions/92aaf0da-9dab-42b6-94a3-d43ce8d16293'
+ '/subscriptions/${subscription().subscriptionId}/providers/Microsoft.Authorization/roleDefinitions/cd570a14-e51a-42ad-bac8-bafd67325302'
+ '/subscriptions/${subscription().subscriptionId}/providers/Microsoft.Authorization/roleDefinitions/749f88d5-cbae-40b8-bcfc-e573ddc772fa'
+ ]
+ scope: resourceGroup().id
+ parameters: {
+ dcrResourceId: {
+ value: VMInsightsDCRId
+ }
+ enableProcessesAndDependencies: {
+ value: true
+ }
+ }
+ }
+ {
+ name: '(Ag) Deploy Azure Security agent on Windows Arc machines'
+ definitionId: '/providers/Microsoft.Authorization/policyDefinitions/d01f3018-de9f-4d75-8dae-d12c1875da9f'
+ roleDefinition: '/subscriptions/${subscription().subscriptionId}/providers/Microsoft.Authorization/roleDefinitions/92aaf0da-9dab-42b6-94a3-d43ce8d16293'
+ parameters: {}
+ }
+ {
+ name: '(Ag) Deploy Azure Security agent on Linux Arc machines'
+ definitionId: '/providers/Microsoft.Authorization/policyDefinitions/2f47ec78-4301-4655-b78e-b29377030cdc'
+ roleDefinition: '/subscriptions/${subscription().subscriptionId}/providers/Microsoft.Authorization/roleDefinitions/92aaf0da-9dab-42b6-94a3-d43ce8d16293'
+ parameters: {}
+ }
+ {
+ name: '(Ag) Deploy MDE agent on Windows Arc machines'
+ definitionId: '/providers/Microsoft.Authorization/policyDefinitions/37c043a6-6d64-656d-6465-b362dfeb354a'
+ roleDefinition: '/subscriptions/${subscription().subscriptionId}/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c'
+ parameters: {}
+ }
+ {
+ name: '(Ag) Deploy MDE agent on Linux Arc machines'
+ definitionId: '/providers/Microsoft.Authorization/policyDefinitions/4eb909e7-6d64-656d-6465-2eeb297a1625'
+ roleDefinition: '/subscriptions/${subscription().subscriptionId}/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c'
+ parameters: {}
+ }
+]
+
+resource policies_name 'Microsoft.Authorization/policyAssignments@2022-06-01' = [for item in policies: {
+ name: item.name
+ location: azureLocation
+ identity: {
+ type: 'SystemAssigned'
+ }
+ properties: {
+ policyDefinitionId: item.definitionId
+ parameters: item.parameters
+ }
+}]
+
+resource policy_AMA_role_0 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
+ name: guid( policies[0].name, policies[0].roleDefinition[0],resourceGroup().id)
+ properties: {
+ roleDefinitionId: policies[0].roleDefinition[0]
+ principalId: policies_name[0].identity.principalId
+ principalType: 'ServicePrincipal'
+ }
+}
+
+resource policy_AMA_role_1 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
+ name: guid( policies[0].name, policies[0].roleDefinition[1],resourceGroup().id)
+ properties: {
+ roleDefinitionId: policies[0].roleDefinition[1]
+ principalId: policies_name[0].identity.principalId
+ principalType: 'ServicePrincipal'
+ }
+}
+
+resource policy_AMA_role_2 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
+ name: guid( policies[0].name, policies[0].roleDefinition[2],resourceGroup().id)
+ properties: {
+ roleDefinitionId: policies[0].roleDefinition[2]
+ principalId: policies_name[0].identity.principalId
+ principalType: 'ServicePrincipal'
+ }
+}
+
+resource policy_arc_windows_azure_security_agent 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
+ name: guid( policies[1].name, policies[1].roleDefinition,resourceGroup().id)
+ properties: {
+ roleDefinitionId: policies[1].roleDefinition
+ principalId: policies_name[1].identity.principalId
+ principalType: 'ServicePrincipal'
+ }
+}
+
+resource policy_arc_linux_azure_security_agent 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
+ name: guid( policies[2].name, policies[2].roleDefinition,resourceGroup().id)
+ properties: {
+ roleDefinitionId: policies[2].roleDefinition
+ principalId: policies_name[2].identity.principalId
+ principalType: 'ServicePrincipal'
+ }
+}
+
+resource policy_arc_windows_mde 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
+ name: guid( policies[3].name, policies[3].roleDefinition,resourceGroup().id)
+ properties: {
+ roleDefinitionId: policies[3].roleDefinition
+ principalId: policies_name[3].identity.principalId
+ principalType: 'ServicePrincipal'
+ }
+}
+
+resource policy_arc_linux_mde 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
+ name: guid( policies[4].name, policies[4].roleDefinition,resourceGroup().id)
+ properties: {
+ roleDefinitionId: policies[4].roleDefinition
+ principalId: policies_name[4].identity.principalId
+ principalType: 'ServicePrincipal'
+ }
+}
diff --git a/azure_jumpstart_ag/manufacturing/bicep/mgmt/storageAccount.bicep b/azure_jumpstart_ag/manufacturing/bicep/mgmt/storageAccount.bicep
new file mode 100644
index 0000000000..b36fbbb9cf
--- /dev/null
+++ b/azure_jumpstart_ag/manufacturing/bicep/mgmt/storageAccount.bicep
@@ -0,0 +1,33 @@
+@description('Storage Account type')
+@allowed([
+ 'Standard_LRS'
+ 'Standard_GRS'
+ 'Standard_ZRS'
+ 'Premium_LRS'
+])
+param storageAccountType string = 'Standard_LRS'
+
+@description('Location for all resources.')
+param location string = resourceGroup().location
+
+@description('Resource tag for Jumpstart Agora')
+param resourceTags object = {
+ Project: 'Jumpstart_Agora'
+}
+
+var storageAccountName = 'agora${uniqueString(resourceGroup().id)}'
+
+resource storageAccount 'Microsoft.Storage/storageAccounts@2022-09-01' = {
+ name: storageAccountName
+ location: location
+ tags: resourceTags
+ sku: {
+ name: storageAccountType
+ }
+ kind: 'StorageV2'
+ properties: {
+ supportsHttpsTrafficOnly: true
+ }
+}
+
+output storageAccountName string = storageAccountName
diff --git a/azure_jumpstart_ag/manufacturing/bicep/storage/storageAccount.bicep b/azure_jumpstart_ag/manufacturing/bicep/storage/storageAccount.bicep
new file mode 100644
index 0000000000..35d095bce9
--- /dev/null
+++ b/azure_jumpstart_ag/manufacturing/bicep/storage/storageAccount.bicep
@@ -0,0 +1,48 @@
+@description('Storage account name')
+param storageAccountName string
+
+@description('Storage account location')
+param location string = resourceGroup().location
+
+@description('Storage account kind')
+param kind string = 'StorageV2'
+
+@description('Storage account sku')
+param skuName string = 'Standard_LRS'
+
+param storageQueueName string = 'aioQueue'
+
+@description('The name of ESA container in Storage Account')
+param stcontainerName string
+
+resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
+ name: storageAccountName
+ location: location
+ kind: kind
+ sku: {
+ name: skuName
+ }
+ properties: {
+ supportsHttpsTrafficOnly: true
+ }
+}
+
+resource storageQueueServices 'Microsoft.Storage/storageAccounts/queueServices@2023-01-01' = {
+ parent: storageAccount
+ name: 'default'
+}
+
+resource storageQueue 'Microsoft.Storage/storageAccounts/queueServices/queues@2023-01-01' = {
+ parent: storageQueueServices
+ name: storageQueueName
+}
+
+resource storageAccountName_default_container 'Microsoft.Storage/storageAccounts/blobServices/containers@2021-04-01' = {
+ name: '${storageAccountName}/default/${stcontainerName}'
+ dependsOn: [
+ storageAccount
+ ]
+}
+
+output queueName string = storageQueueName
+output storageAccountId string = storageAccount.id
diff --git a/azure_jumpstart_ag/manufacturing/scripts/postprovision.ps1 b/azure_jumpstart_ag/manufacturing/scripts/postprovision.ps1
new file mode 100644
index 0000000000..2ab82ed85f
--- /dev/null
+++ b/azure_jumpstart_ag/manufacturing/scripts/postprovision.ps1
@@ -0,0 +1,86 @@
+if ($null -ne $env:AZURE_RESOURCE_GROUP){
+ $resourceGroup = $env:AZURE_RESOURCE_GROUP
+ $adxClusterName = $env:ADX_CLUSTER_NAME
+ Select-AzSubscription -SubscriptionId $env:AZURE_SUBSCRIPTION_ID | out-null
+ $rdpPort = $env:JS_RDP_PORT
+ $deployBastion = $env:JS_DEPLOY_BASTION
+}
+
+########################################################################
+# ADX Dashboards
+########################################################################
+
+Write-Host "Importing Azure Data Explorer dashboards..."
+
+# Get the ADX/Kusto cluster info
+$kustoCluster = Get-AzKustoCluster -ResourceGroupName $resourceGroup -Name $adxClusterName
+$adxEndPoint = $kustoCluster.Uri
+
+# Update the dashboards files with the new ADX cluster name and URI
+$templateBaseUrl = "https://raw.githubusercontent.com/microsoft/azure_arc/main/azure_jumpstart_ag/"
+$ordersDashboardBody = (Invoke-WebRequest -Method Get -Uri "$templateBaseUrl/artifacts/adx_dashboards/adx-dashboard-orders-payload.json").Content -replace '{{ADX_CLUSTER_URI}}', $adxEndPoint -replace '{{ADX_CLUSTER_NAME}}', $adxClusterName
+$iotSensorsDashboardBody = (Invoke-WebRequest -Method Get -Uri "$templateBaseUrl/artifacts/adx_dashboards/adx-dashboard-iotsensor-payload.json") -replace '{{ADX_CLUSTER_URI}}', $adxEndPoint -replace '{{ADX_CLUSTER_NAME}}', $adxClusterName
+
+# Get access token to make REST API call to Azure Data Explorer Dashabord API. Replace double quotes surrounding access token
+$token = (az account get-access-token --scope "https://rtd-metadata.azurewebsites.net/user_impersonation openid profile offline_access" --query "accessToken") -replace "`"", ""
+
+# Prepare authorization header with access token
+$httpHeaders = @{"Authorization" = "Bearer $token"; "Content-Type" = "application/json" }
+
+# Make REST API call to the dashboard endpoint.
+$dashboardApi = "https://dashboards.kusto.windows.net/dashboards"
+
+# Import orders dashboard report
+$httpResponse = Invoke-WebRequest -Method Post -Uri $dashboardApi -Body $ordersDashboardBody -Headers $httpHeaders
+if ($httpResponse.StatusCode -ne 200){
+ Write-Host "ERROR: Failed import orders dashboard report into Azure Data Explorer" -ForegroundColor Red
+}
+
+# Import IoT Sensor dashboard report
+$httpResponse = Invoke-WebRequest -Method Post -Uri $dashboardApi -Body $iotSensorsDashboardBody -Headers $httpHeaders
+if ($httpResponse.StatusCode -ne 200){
+ Write-Host "ERROR: Failed import IoT Sensor dashboard report into Azure Data Explorer" -ForegroundColor Red
+}
+
+
+########################################################################
+# RDP Port
+########################################################################
+
+# Configure NSG Rule for RDP (if needed)
+If ($rdpPort -ne "3389" -and !$deployBastion) {
+
+ Write-Host "Configuring NSG Rule for RDP..."
+ $nsg = Get-AzNetworkSecurityGroup -ResourceGroupName $resourceGroup -Name Ag-NSG-Prod
+
+ Add-AzNetworkSecurityRuleConfig `
+ -NetworkSecurityGroup $nsg `
+ -Name "RDP-$rdpPort" `
+ -Description "Allow RDP" `
+ -Access Allow `
+ -Protocol Tcp `
+ -Direction Inbound `
+ -Priority 100 `
+ -SourceAddressPrefix * `
+ -SourcePortRange * `
+ -DestinationAddressPrefix * `
+ -DestinationPortRange $rdpPort `
+ | Out-Null
+
+ Set-AzNetworkSecurityGroup -NetworkSecurityGroup $nsg | Out-Null
+ # az network nsg rule create -g $resourceGroup --nsg-name Ag-NSG-Prod --name "RDC-$rdpPort" --priority 100 --source-address-prefixes * --destination-port-ranges $rdpPort --access Allow --protocol Tcp
+}
+
+
+# Client VM IP address
+if(!$deployBastion){
+ $ip = (Get-AzPublicIpAddress -ResourceGroupName $resourceGroup -Name "Ag-VM-Client-PIP").IpAddress
+ Write-Host "You can now connect to the client VM using the following command: " -NoNewline
+ Write-Host "mstsc /v:$($ip):$($rdpPort)" -ForegroundColor Green -BackgroundColor Black
+ Write-Host "Remember to use the Windows admin user name [$env:JS_WINDOWS_ADMIN_USERNAME] and the password you specified."
+}else{
+ Write-Host "You can now connect to the client VM using the Azure Bastion service." -ForegroundColor Green
+ Write-Host "Remember to use the Windows admin user name [$env:JS_WINDOWS_ADMIN_USERNAME] and the password you specified."
+}
+
+
diff --git a/azure_jumpstart_ag/manufacturing/scripts/predown.ps1 b/azure_jumpstart_ag/manufacturing/scripts/predown.ps1
new file mode 100644
index 0000000000..6c23a3078e
--- /dev/null
+++ b/azure_jumpstart_ag/manufacturing/scripts/predown.ps1
@@ -0,0 +1,6 @@
+########################################################################
+# Delete service principal
+########################################################################
+$spnObjectId = $env:SPN_OBJECT_ID
+Remove-AzRoleAssignment -ObjectId $spnObjectId -RoleDefinitionName "Owner"
+Remove-AzADServicePrincipal -ObjectId $spnObjectId
\ No newline at end of file
diff --git a/azure_jumpstart_ag/manufacturing/scripts/preprovision.ps1 b/azure_jumpstart_ag/manufacturing/scripts/preprovision.ps1
new file mode 100644
index 0000000000..4ff2ec9955
--- /dev/null
+++ b/azure_jumpstart_ag/manufacturing/scripts/preprovision.ps1
@@ -0,0 +1,227 @@
+########################################################################
+# Connect to Azure
+########################################################################
+
+Write-Host "Connecting to Azure..."
+
+# Install Azure module if not already installed
+if (-not (Get-Command -Name Get-AzContext)) {
+ Write-Host "Installing Azure module..."
+ Install-Module -Name Az -AllowClobber -Scope CurrentUser -ErrorAction Stop
+}
+
+# If not signed in, run the Connect-AzAccount cmdlet
+if (-not (Get-AzContext)) {
+ Write-Host "Logging in to Azure..."
+ If (-not (Connect-AzAccount -SubscriptionId $env:AZURE_SUBSCRIPTION_ID -ErrorAction Stop)){
+ Throw "Unable to login to Azure. Please check your credentials and try again."
+ }
+}
+
+# Write-Host "Getting Azure Tenant Id..."
+$tenantId = (Get-AzSubscription -SubscriptionId $env:AZURE_SUBSCRIPTION_ID).TenantId
+
+# Write-Host "Setting Azure context..."
+$context = Set-AzContext -SubscriptionId $env:AZURE_SUBSCRIPTION_ID -Tenant $tenantId -ErrorAction Stop
+
+# Write-Host "Setting az subscription..."
+$azLogin = az account set --subscription $env:AZURE_SUBSCRIPTION_ID
+
+
+########################################################################
+# Check for available capacity in region
+########################################################################
+#region Functions
+Function Get-AzAvailableCores ($location, $skuFriendlyNames, $minCores = 0) {
+ # using az command because there is currently a bug in various versions of PowerShell that affects Get-AzVMUsage
+ $usage = (az vm list-usage --location $location --output json --only-show-errors) | ConvertFrom-Json
+
+ $usage = $usage |
+ Where-Object {$_.localname -match $skuFriendlyNames}
+
+ $enhanced = $usage |
+ ForEach-Object {
+ $_ | Add-Member -MemberType NoteProperty -Name available -Value 0 -Force -PassThru
+ $_.available = $_.limit - $_.currentValue
+ }
+
+ $enhanced = $enhanced |
+ ForEach-Object {
+ $_ | Add-Member -MemberType NoteProperty -Name usableLocation -Value $false -Force -PassThru
+ If ($_.available -ge $minCores) {
+ $_.usableLocation = $true
+ }
+ else {
+ $_.usableLocation = $false
+ }
+ }
+
+ $enhanced
+
+}
+
+Function Get-AzAvailableLocations ($location, $skuFriendlyNames, $minCores = 0) {
+ $allLocations = get-AzLocation
+ $geographyGroup = ($allLocations | Where-Object {$_.location -eq $location}).GeographyGroup
+ $locations = $allLocations | Where-Object { `
+ $_.GeographyGroup -eq $geographyGroup `
+ -and $_.Location -ne $location `
+ -and $_.RegionCategory -eq "Recommended" `
+ -and $_.PhysicalLocation -ne ""
+ }
+
+ $usableLocations = $locations |
+ ForEach-Object {
+ $available = Get-AzAvailableCores -location $_.location -skuFriendlyNames $skuFriendlyNames -minCores $minCores |
+ Where-Object {$_.localName -ne "Total Regional vCPUs"}
+ If ($available.usableLocation) {
+ $_ | Add-Member -MemberType NoteProperty -Name TotalCores -Value $available.limit -Force
+ $_ | Add-Member -MemberType NoteProperty -Name AvailableCores -Value $available.available -Force
+ $_ | Add-Member -MemberType NoteProperty -Name usableLocation -Value $available.usableLocation -Force -PassThru
+ }
+ }
+
+ $usableLocations
+}
+
+Function Get-AzAvailablePublicIpAddress ($location, $subscriptionId, $minPublicIP = 0) {
+
+ $accessToken = az account get-access-token --query accessToken -o tsv
+ $headers = @{
+ "Authorization" = "Bearer $accessToken"
+ }
+
+ $uri = "https://management.azure.com/subscriptions/$subscriptionId/providers/Microsoft.Network/locations/$location/usages?api-version=2023-02-01"
+
+ $publicIpCount = (Get-AzPublicIpAddress | where-object {$_.location -eq $location} | measure-object).count
+ $response = Invoke-RestMethod -Uri $uri -Headers $headers -Method Get
+
+ $limit = ($response.value | where-object { $_.name.value -eq "PublicIPAddresses"}).limit
+
+ $availableIP = $limit - $publicIpCount
+
+ $availableIP
+
+}
+
+#endregion Functions
+
+$location = $env:AZURE_LOCATION
+$subscriptionId = $env:AZURE_SUBSCRIPTION_ID
+$minCores = 32
+$minPublicIP = 10
+$skuFriendlyNames = "Standard DSv5 Family vCPUs|Total Regional vCPUs"
+
+Write-Host "`nChecking for available capacity in $location region..."
+
+$available = Get-AzAvailableCores -location $location -skuFriendlyNames $skuFriendlyNames -minCores $minCores
+
+If ($available.usableLocation -contains $false) {
+ Write-Host "`n`u{274C} There is not enough VM capacity in the $location region to deploy the Jumpstart environment." -ForegroundColor Red
+
+ Write-Host "`nChecking other regions in the same geography with enough capacity ($minCores cores)...`n"
+
+ $locations = Get-AzAvailableLocations -location $location -skuFriendlyNames $skuFriendlyNames -minCores $minCores |
+ Format-Table Location, DisplayName, TotalCores, AvailableCores, UsableLocation -AutoSize | Out-String
+
+ Write-Host $locations
+
+ Write-Host "Please run ``azd env --new`` to create a new environment and select the new location.`n"
+
+ $message = "Not enough capacity in $location region."
+ Throw $message
+
+} else {
+ $availableIP = Get-AzAvailablePublicIpAddress -location $location -subscriptionId $subscriptionId -minPublicIP $minPublicIP
+
+ If ($availableIP -le $minPublicIP) {
+ $requiredIp = $minPublicIP - $availableIP
+ Write-Host "`n`u{274C} There is not enough Public IP in the $location region to deploy the Jumpstart environment. Need addtional $requiredIp Public IP." -ForegroundColor Red
+
+ $message = "Not enough capacity in $location region."
+ Throw $message
+ } else {
+ Write-Host "`n`u{2705} There is enough VM and Public IP capacity in the $location region to deploy the Jumpstart environment.`n"
+ }
+}
+
+########################################################################
+# Get Windows Admin Username and Password
+########################################################################
+$JS_WINDOWS_ADMIN_USERNAME = 'agora'
+if ($promptOutput = Read-Host "Enter the Windows Admin Username [$JS_WINDOWS_ADMIN_USERNAME]") { $JS_WINDOWS_ADMIN_USERNAME = $promptOutput }
+
+# set the env variable
+azd env set JS_WINDOWS_ADMIN_USERNAME -- $JS_WINDOWS_ADMIN_USERNAME
+
+########################################################################
+# Use Azure Bastion?
+########################################################################
+$promptOutput = Read-Host "Configure Azure Bastion for accessing Agora host [Y/N]?"
+$JS_DEPLOY_BASTION = $false
+if ($promptOutput -like 'y')
+{
+ $JS_DEPLOY_BASTION = $true
+}
+
+# set the env variable
+azd env set JS_DEPLOY_BASTION $JS_DEPLOY_BASTION
+
+########################################################################
+# RDP Port
+########################################################################
+$JS_RDP_PORT = '3389'
+If ($env:JS_RDP_PORT) {
+ $JS_RDP_PORT = $env:JS_RDP_PORT
+}
+if ($promptOutput -notlike 'y') {
+ if ($promptOutput = Read-Host "Enter the RDP Port for remote desktop connection [$JS_RDP_PORT]")
+ {
+ $JS_RDP_PORT = $promptOutput
+ }
+}
+# set the env variable
+azd env set JS_RDP_PORT $JS_RDP_PORT
+
+########################################################################
+# Get custom locations RP Id
+########################################################################
+$customLocationRPOID=(Get-AzADServicePrincipal -DisplayName 'Custom Locations RP').Id
+
+# Set environment variables
+azd env set CUSTOM_LOCATION_RP_ID $customLocationRPOID
+
+
+########################################################################
+# Create Azure Service Principal
+########################################################################
+Write-Host "Checking for existing stored Azure service principal..."
+if ($null -ne $env:SPN_CLIENT_ID) {
+ Write-Host "Re-using existing service principal..."
+} else {
+ Write-Host "Attempting to create new service principal with scope /subscriptions/$($env:AZURE_SUBSCRIPTION_ID)..."
+ $user = (Get-AzContext).Account.Id.split("@")[0]
+ $uniqueSpnName = "$user-jumpstart-spn-$(Get-Random -Minimum 1000 -Maximum 9999)"
+ try {
+ $spn = New-AzADServicePrincipal -DisplayName $uniqueSpnName -Role "Owner" -Scope "/subscriptions/$($env:AZURE_SUBSCRIPTION_ID)" -ErrorAction Stop
+ $SPN_CLIENT_ID = $spn.AppId
+ $SPN_CLIENT_SECRET = $spn.PasswordCredentials.SecretText
+ $SPN_TENANT_ID = (Get-AzContext).Tenant.Id
+ $SPN_OBJECT_ID = $spn.Id
+ # Set environment variables
+ azd env set SPN_CLIENT_ID -- $SPN_CLIENT_ID
+ azd env set SPN_CLIENT_SECRET -- $SPN_CLIENT_SECRET
+ azd env set SPN_TENANT_ID -- $SPN_TENANT_ID
+ azd env set SPN_OBJECT_ID -- $SPN_OBJECT_ID
+ }
+ catch {
+
+ If ($error[0].ToString() -match "Forbidden"){
+ Throw "You do not have permission to create a service principal. Please contact your Azure subscription administrator to grant you the Owner role on the subscription."
+ }
+ else {
+ Throw "An error occurred creating the service principal. Error:" + $error[0].ToString()
+ }
+ }
+
+}
diff --git a/azure_jumpstart_ag/retail/artifacts/PowerShell/AgLogonScript.ps1 b/azure_jumpstart_ag/retail/artifacts/PowerShell/AgLogonScript.ps1
deleted file mode 100644
index d0e36abb92..0000000000
--- a/azure_jumpstart_ag/retail/artifacts/PowerShell/AgLogonScript.ps1
+++ /dev/null
@@ -1,1873 +0,0 @@
-# Script runtime environment: Level-0 Azure virtual machine ("Client VM")
-
-$ProgressPreference = "SilentlyContinue"
-Set-PSDebug -Strict
-
-#####################################################################
-# Initialize the environment
-#####################################################################
-$AgConfig = Import-PowerShellDataFile -Path $Env:AgConfigPath
-$AgToolsDir = $AgConfig.AgDirectories["AgToolsDir"]
-$AgIconsDir = $AgConfig.AgDirectories["AgIconDir"]
-$AgAppsRepo = $AgConfig.AgDirectories["AgAppsRepo"]
-$configMapDir = $agConfig.AgDirectories["AgConfigMapDir"]
-$websiteUrls = $AgConfig.URLs
-$githubAccount = $Env:githubAccount
-$githubBranch = $Env:githubBranch
-$githubUser = $Env:githubUser
-$githubPat = $Env:GITHUB_TOKEN
-$resourceGroup = $Env:resourceGroup
-$azureLocation = $Env:azureLocation
-$spnClientId = $Env:spnClientId
-$spnClientSecret = $Env:spnClientSecret
-$spnTenantId = $Env:spnTenantId
-$adminUsername = $Env:adminUsername
-$acrName = $Env:acrName.ToLower()
-$cosmosDBName = $Env:cosmosDBName
-$cosmosDBEndpoint = $Env:cosmosDBEndpoint
-$templateBaseUrl = $Env:templateBaseUrl
-$appClonedRepo = "https://github.com/$githubUser/jumpstart-agora-apps"
-$appUpstreamRepo = "https://github.com/microsoft/jumpstart-agora-apps"
-$adxClusterName = $Env:adxClusterName
-$namingGuid = $Env:namingGuid
-$appsRepo = "jumpstart-agora-apps"
-$adminPassword = $Env:adminPassword
-$gitHubAPIBaseUri = $websiteUrls["githubAPI"]
-$workflowStatus = ""
-
-Start-Transcript -Path ($AgConfig.AgDirectories["AgLogsDir"] + "\AgLogonScript.log")
-Write-Header "Executing Jumpstart Agora automation scripts"
-$startTime = Get-Date
-
-# Disable Windows firewall
-Set-NetFirewallProfile -Profile Domain, Public, Private -Enabled False
-
-# Force TLS 1.2 for connections to prevent TLS/SSL errors
-[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
-
-
-#####################################################################
-# Setup Azure CLI
-#####################################################################
-Write-Host "[$(Get-Date -Format t)] INFO: Configuring Azure CLI (Step 1/17)" -ForegroundColor DarkGreen
-$cliDir = New-Item -Path ($AgConfig.AgDirectories["AgLogsDir"] + "\.cli\") -Name ".Ag" -ItemType Directory
-
-if (-not $($cliDir.Parent.Attributes.HasFlag([System.IO.FileAttributes]::Hidden))) {
- $folder = Get-Item $cliDir.Parent.FullName -ErrorAction SilentlyContinue
- $folder.Attributes += [System.IO.FileAttributes]::Hidden
-}
-
-$Env:AZURE_CONFIG_DIR = $cliDir.FullName
-
-Write-Host "[$(Get-Date -Format t)] INFO: Logging into Az CLI using the service principal and secret provided at deployment" -ForegroundColor Gray
-az login --service-principal --username $Env:spnClientID --password=$Env:spnClientSecret --tenant $Env:spnTenantId | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\AzCLI.log")
-
-# Making extension install dynamic
-if ($AgConfig.AzCLIExtensions.Count -ne 0) {
- Write-Host "[$(Get-Date -Format t)] INFO: Installing Azure CLI extensions: " ($AgConfig.AzCLIExtensions -join ', ') -ForegroundColor Gray
- az config set extension.use_dynamic_install=yes_without_prompt --only-show-errors
- # Installing Azure CLI extensions
- foreach ($extension in $AgConfig.AzCLIExtensions) {
- az extension add --name $extension --system --only-show-errors
- }
-}
-
-Write-Host "[$(Get-Date -Format t)] INFO: Az CLI configuration complete!" -ForegroundColor Green
-Write-Host
-
-#####################################################################
-# Setup Azure PowerShell and register providers
-#####################################################################
-Write-Host "[$(Get-Date -Format t)] INFO: Configuring Azure PowerShell (Step 2/17)" -ForegroundColor DarkGreen
-$azurePassword = ConvertTo-SecureString $Env:spnClientSecret -AsPlainText -Force
-$psCred = New-Object System.Management.Automation.PSCredential($Env:spnClientID , $azurePassword)
-Connect-AzAccount -Credential $psCred -TenantId $Env:spnTenantId -ServicePrincipal | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\AzPowerShell.log")
-$subscriptionId = (Get-AzSubscription).Id
-
-# Install PowerShell modules
-if ($AgConfig.PowerShellModules.Count -ne 0) {
- Write-Host "[$(Get-Date -Format t)] INFO: Installing PowerShell modules: " ($AgConfig.PowerShellModules -join ', ') -ForegroundColor Gray
- foreach ($module in $AgConfig.PowerShellModules) {
- Install-Module -Name $module -Force | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\AzPowerShell.log")
- }
-}
-
-# Register Azure providers
-if ($AgConfig.AzureProviders.Count -ne 0) {
- Write-Host "[$(Get-Date -Format t)] INFO: Registering Azure providers in the current subscription: " ($AgConfig.AzureProviders -join ', ') -ForegroundColor Gray
- foreach ($provider in $AgConfig.AzureProviders) {
- Register-AzResourceProvider -ProviderNamespace $provider | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\AzPowerShell.log")
- }
-}
-Write-Host "[$(Get-Date -Format t)] INFO: Azure PowerShell configuration and resource provider registration complete!" -ForegroundColor Green
-Write-Host
-
-#############################################################
-# Install Windows Terminal, WSL2, and Ubuntu
-#############################################################
-Write-Host "[$(Get-Date -Format t)] INFO: Installing dev tools (Step 3/17)" -ForegroundColor DarkGreen
-
-$DevToolsInstallationJob = Invoke-Command -ScriptBlock {
-
-$AgConfig = $using:AgConfig
-$websiteUrls = $using:websiteUrls
-$AgToolsDir = $using:AgToolsDir
-$adminUsername = $using:adminUsername
-
-
-If ($PSVersionTable.PSVersion.Major -ge 7) { Write-Error "This script needs be run by version of PowerShell prior to 7.0" }
-$downloadDir = "C:\WinTerminal"
-$frameworkPkgPath = "$downloadDir\Microsoft.VCLibs.x64.14.00.Desktop.appx"
-$WindowsTerminalKitPath = "$downloadDir\Microsoft.WindowsTerminal.PreinstallKit.zip"
-$windowsTerminalPath = "$downloadDir\WindowsTerminal"
-$filenamePattern = "*PreinstallKit.zip"
-$terminalDownloadUri = ((Invoke-RestMethod -Method GET -Uri $websiteUrls["windowsTerminal"]).assets | Where-Object name -like $filenamePattern ).browser_download_url | Select-Object -First 1
-
-# Download C++ Runtime framework packages for Desktop Bridge and Windows Terminal latest release
-Write-Host "[$(Get-Date -Format t)] INFO: Downloading binaries." -ForegroundColor Gray
-
-$ProgressPreference = 'SilentlyContinue'
-
-Invoke-WebRequest -Uri $websiteUrls["vcLibs"] -OutFile ( New-Item -Path $frameworkPkgPath -Force ) | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Tools.log")
-Invoke-WebRequest -Uri $terminalDownloadUri -OutFile ( New-Item -Path $windowsTerminalKitPath -Force ) | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Tools.log")
-
-$ProgressPreference = 'Continue'
-
-# Extract Windows Terminal PreinstallKit
-Write-Host "[$(Get-Date -Format t)] INFO: Expanding Windows Terminal PreinstallKit." -ForegroundColor Gray
-Expand-Archive $WindowsTerminalKitPath $windowsTerminalPath | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Tools.log")
-
-# Install WSL latest kernel update
-Write-Host "[$(Get-Date -Format t)] INFO: Installing WSL." -ForegroundColor Gray
-msiexec /i "$AgToolsDir\wsl_update_x64.msi" /qn | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Tools.log")
-
-# Install C++ Runtime framework packages for Desktop Bridge and Windows Terminal latest release
-Write-Host "[$(Get-Date -Format t)] INFO: Installing Windows Terminal" -ForegroundColor Gray
-Add-AppxPackage -Path $frameworkPkgPath | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Tools.log")
-
-# Install the Windows Terminal prereqs
-foreach ($file in Get-ChildItem $windowsTerminalPath -Filter *x64*.appx) {
- Add-AppxPackage -Path $file.FullName | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Tools.log")
-}
-
-# Install Windows Terminal
-foreach ($file in Get-ChildItem $windowsTerminalPath -Filter *.msixbundle) {
- Add-AppxPackage -Path $file.FullName | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Tools.log")
-}
-
-# Configure Windows Terminal
-Set-Location $Env:LOCALAPPDATA\Packages\Microsoft.WindowsTerminal*\LocalState
-
-# Launch Windows Terminal for default settings.json to be created
-$action = New-ScheduledTaskAction -Execute $((Get-Command wt.exe).Source)
-$trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddSeconds(1)
-$null = Register-ScheduledTask -Action $action -Trigger $trigger -TaskName WindowsTerminalInit
-
-# Give process time to initiate and create settings file
-Start-Sleep 10
-
-# Stop Windows Terminal process
-Get-Process WindowsTerminal | Stop-Process
-
-Unregister-ScheduledTask -TaskName WindowsTerminalInit -Confirm:$false
-
-$settings = Get-Content .\settings.json | ConvertFrom-Json
-$settings.profiles.defaults.elevate
-
-# Configure the default profile setting "Run this profile as Administrator" to "true"
-$settings.profiles.defaults | Add-Member -Name elevate -MemberType NoteProperty -Value $true -Force
-
-$settings | ConvertTo-Json -Depth 8 | Set-Content .\settings.json
-
-# Install Ubuntu
-Write-Host "[$(Get-Date -Format t)] INFO: Installing Ubuntu" -ForegroundColor Gray
-Add-AppxPackage -Path "$AgToolsDir\Ubuntu.appx" | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Tools.log")
-
-# Setting WSL environment variables
-$userenv = [System.Environment]::GetEnvironmentVariable("Path", "User")
-[System.Environment]::SetEnvironmentVariable("PATH", $userenv + ";C:\Users\$adminUsername\Ubuntu", "User")
-
-# Initializing the wsl ubuntu app without requiring user input
-$ubuntu_path = "c:/users/$adminUsername/AppData/Local/Microsoft/WindowsApps/ubuntu"
-Invoke-Expression -Command "$ubuntu_path install --root" | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Tools.log")
-
-# Create Windows Terminal shortcut
-$WshShell = New-Object -comObject WScript.Shell
-$WinTerminalPath = (Get-ChildItem "C:\Program Files\WindowsApps" -Recurse | Where-Object { $_.name -eq "wt.exe" }).FullName
-$Shortcut = $WshShell.CreateShortcut("$Env:USERPROFILE\Desktop\Windows Terminal.lnk")
-$Shortcut.TargetPath = $WinTerminalPath
-$shortcut.WindowStyle = 3
-$shortcut.Save()
-
-#############################################################
-# Install VSCode extensions
-#############################################################
-Write-Host "[$(Get-Date -Format t)] INFO: Installing VSCode extensions: " + ($AgConfig.VSCodeExtensions -join ', ') -ForegroundColor Gray
-# Install VSCode extensions
-foreach ($extension in $AgConfig.VSCodeExtensions) {
- code --install-extension $extension 2>&1 | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Tools.log")
-}
-
-#############################################################
-# Install Docker Desktop
-#############################################################
-Write-Host "[$(Get-Date -Format t)] INFO: Installing Docker Desktop." -ForegroundColor DarkGreen
-# Download and Install Docker Desktop
-$arguments = 'install --quiet --accept-license'
-Start-Process "$AgToolsDir\DockerDesktopInstaller.exe" -Wait -ArgumentList $arguments
-Get-ChildItem "$Env:USERPROFILE\Desktop\Docker Desktop.lnk" | Remove-Item -Confirm:$false
-Copy-Item "$AgToolsDir\settings.json" -Destination "$Env:USERPROFILE\AppData\Roaming\Docker\settings.json" -Force
-Start-Process "C:\Program Files\Docker\Docker\Docker Desktop.exe"
-Start-Sleep -Seconds 15
-Get-Process | Where-Object { $_.name -like "Docker Desktop" } | Stop-Process -Force
-# Cleanup
-Remove-Item $downloadDir -Recurse -Force
-
-} -JobName step3 -ThrottleLimit 16 -AsJob -ComputerName .
-
-Write-Host "[$(Get-Date -Format t)] INFO: Dev Tools installation initiated in background job." -ForegroundColor Green
-
-$DevToolsInstallationJob
-
-Write-Host
-
-#####################################################################
-# Configure Jumpstart Agora Apps repository
-#####################################################################
-Write-Host "INFO: Forking and preparing Apps repository locally (Step 4/17)" -ForegroundColor DarkGreen
-Set-Location $AgAppsRepo
-Write-Host "INFO: Checking if the $appsRepo repository is forked" -ForegroundColor Gray
-$retryCount = 0
-$maxRetries = 5
-do {
- $forkExists = $false
- try {
- $response = Invoke-RestMethod -Uri "$gitHubAPIBaseUri/repos/$githubUser/$appsRepo"
- if ($response) {
- write-host "INFO: Fork exists....Proceeding" -ForegroundColor Gray
- $forkExists = $true
- }
- }
- catch {
- if ($retryCount -lt $maxRetries) {
- Write-Host "ERROR: $githubUser/$appsRepo Fork doesn't exist, please fork https://github.com/microsoft/jumpstart-agora-apps to proceed (attempt $retryCount/$maxRetries) . . . waiting 60 seconds" -ForegroundColor Red
- $retryCount++
- $forkExists = $false
- start-sleep -Seconds 60
- }
- else {
- Write-Host "[$(Get-Date -Format t)] ERROR: Retry limit reached, $githubUser/$appsRepo Fork doesn't exist. Exiting." -ForegroundColor Red
- exit
- }
- }
-} until ($forkExists -eq $true)
-
-Write-Host "INFO: Checking if the GitHub access token is valid." -ForegroundColor Gray
-do {
- $response = gh auth status 2>&1
- if ($response -match "authentication failed") {
- write-host "ERROR: The GitHub Personal access token is not valid" -ForegroundColor Red
- Write-Host "INFO: Please try to re-generate the personal access token and provide it here (https://aka.ms/AgoraPreReqs): "
- do {
- $githubPAT = Read-Host "GitHub personal access token"
- } while ($githubPAT -eq "")
- }
-} until (
- $response -notmatch "authentication failed"
-)
-
-Write-Host "INFO: The GitHub Personal access token is valid. Proceeding." -ForegroundColor DarkGreen
-$Env:GITHUB_TOKEN = $githubPAT.Trim()
-[System.Environment]::SetEnvironmentVariable('GITHUB_TOKEN', $githubPAT.Trim(), [System.EnvironmentVariableTarget]::Machine)
-
-Write-Host "INFO: Checking if the personal access token is assigned on the $githubUser/$appsRepo Fork" -ForegroundColor Gray
-$headers = @{
- Authorization = "token $githubPat"
- "Content-Type" = "application/json"
-}
-$retryCount = 0
-$maxRetries = 5
-$uri = "$gitHubAPIBaseUri/repos/$githubUser/$appsRepo/actions/secrets"
-do {
- try {
- $response=Invoke-RestMethod -Uri $uri -Method Get -Headers $headers
- Write-Host "INFO: Personal access token is assigned on $githubUser/$appsRepo fork" -ForegroundColor DarkGreen
- $PatAssigned = $true
- }
- catch {
- if ($retryCount -lt $maxRetries) {
- Write-Host "ERROR: Personal access token is not assigned on $githubUser/$appsRepo fork. Please assign the personal access token to your fork (https://aka.ms/AgoraPreReqs) (attempt $retryCount/$maxRetries).....waiting 60 seconds" -ForegroundColor Red
- $PatAssigned = $false
- $retryCount++
- start-sleep -Seconds 60
- }
- else{
- Write-Host "[$(Get-Date -Format t)] ERROR: Retry limit reached, the personal access token is not assigned to $githubUser/$appsRepo. Exiting." -ForegroundColor Red
- exit
- }
- }
-} until ($PatAssigned -eq $true)
-
-
-Write-Host "INFO: Cloning the GitHub repository locally" -ForegroundColor Gray
-git clone "https://$githubPat@github.com/$githubUser/$appsRepo.git" "$AgAppsRepo\$appsRepo"
-Set-Location "$AgAppsRepo\$appsRepo"
-
-Write-Host "INFO: Verifying 'Administration' permissions" -ForegroundColor Gray
-$retryCount = 0
-$maxRetries = 5
-
-$body = @{
- required_status_checks = $null
- enforce_admins = $false
- required_pull_request_reviews = @{
- required_approving_review_count = 0
- }
- dismiss_stale_reviews = $true
- restrictions = $null
-} | ConvertTo-Json
-
-do {
- try {
- $response = Invoke-WebRequest -Uri "$gitHubAPIBaseUri/repos/$githubUser/$appsRepo/branches/main/protection" -Method Put -Headers $headers -Body $body -ContentType "application/json"
- }
- catch {
- if ($retryCount -lt $maxRetries) {
- Write-Host "ERROR: The GitHub Personal access token doesn't seem to have 'Administration' write permissions, please assign the right permissions (https://aka.ms/AgoraPreReqs) (attempt $retryCount/$maxRetries)...waiting 60 seconds" -ForegroundColor Red
- $retryCount++
- start-sleep -Seconds 60
- }
- else {
- Write-Host "[$(Get-Date -Format t)] ERROR: Retry limit reached, the personal access token doesn't have 'Administration' write permissions assigned. Exiting." -ForegroundColor Red
- exit
- }
- }
-} until ($response)
-Write-Host "INFO: 'Administration' write permissions verified" -ForegroundColor DarkGreen
-
-
-Write-Host "INFO: Checking if there are existing branch protection policies" -ForegroundColor Gray
-$protectedBranches = Invoke-RestMethod -Uri "$gitHubAPIBaseUri/repos/$githubUser/$appsRepo/branches?protected=true" -Method GET -Headers $headers
-foreach ($branch in $protectedBranches) {
- $branchName = $branch.name
- $deleteProtectionUrl = "$gitHubAPIBaseUri/repos/$githubUser/$appsRepo/branches/$branchName/protection"
- Invoke-RestMethod -Uri $deleteProtectionUrl -Headers $headers -Method Delete
- Write-Host "INFO: Deleted protection policy for branch: $branchName" -ForegroundColor Gray
-}
-
-Write-Host "INFO: Pulling latests changes to GitHub repository" -ForegroundColor Gray
-git config --global user.email "dev@agora.com"
-git config --global user.name "Agora Dev"
-git remote add upstream "$appUpstreamRepo.git"
-git fetch upstream
-git checkout main
-git reset --hard upstream/main
-git push origin main -f
-git pull
-git remote remove upstream
-git remote add upstream "$appClonedRepo.git"
-
-Write-Host "INFO: Creating GitHub workflows" -ForegroundColor Gray
-New-Item -ItemType Directory ".github/workflows" -Force
-$githubApiUrl = "$gitHubAPIBaseUri/repos/$githubAccount/azure_arc/contents/azure_jumpstart_ag/retail/artifacts/workflows?ref=$githubBranch"
-$response = Invoke-RestMethod -Uri $githubApiUrl
-$fileUrls = $response | Where-Object { $_.type -eq "file" } | Select-Object -ExpandProperty download_url
-$fileUrls | ForEach-Object {
- $fileName = $_.Substring($_.LastIndexOf("/") + 1)
- $outputFile = Join-Path "$AgAppsRepo\$appsRepo\.github\workflows" $fileName
- Invoke-RestMethod -Uri $_ -OutFile $outputFile
-}
-git add .
-git commit -m "Pushing GitHub Actions to apps fork"
-git push
-Start-Sleep -Seconds 20
-
-Write-Host "INFO: Verifying 'Secrets' permissions" -ForegroundColor Gray
-$retryCount = 0
-$maxRetries = 5
-do {
- $response = gh secret set "test" -b "test" 2>&1
- if ($response -match "error") {
- if ($retryCount -eq $maxRetries) {
- Write-Host "[$(Get-Date -Format t)] ERROR: Retry limit reached, the personal access token doesn't have 'Secrets' write permissions assigned. Exiting." -ForegroundColor Red
- exit
- }
- else {
- $retryCount++
- write-host "ERROR: The GitHub Personal access token doesn't seem to have 'Secrets' write permissions, please assign the right permissions (https://aka.ms/AgoraPreReqs) (attempt $retryCount/$maxRetries)...waiting 60 seconds" -ForegroundColor Red
- Start-Sleep -Seconds 60
- }
- }
-} while ($response -match "error" -or $retryCount -ge $maxRetries)
-gh secret delete test
-Write-Host "INFO: 'Secrets' write permissions verified" -ForegroundColor DarkGreen
-
-Write-Host "INFO: Verifying 'Actions' permissions" -ForegroundColor Gray
-$retryCount = 0
-$maxRetries = 5
-do {
- $response = gh workflow enable update-files.yml 2>&1
- if ($response -match "failed") {
- if ($retryCount -eq $maxRetries) {
- Write-Host "[$(Get-Date -Format t)] ERROR: Retry limit reached, the personal access token doesn't have 'Actions' write permissions assigned. Exiting." -ForegroundColor Red
- exit
- }
- else {
- $retryCount++
- write-host "ERROR: The GitHub Personal access token doesn't seem to have 'Actions' write permissions, please assign the right permissions (https://aka.ms/AgoraPreReqs) (attempt $retryCount/$maxRetries)...waiting 60 seconds" -ForegroundColor Red
- Start-Sleep -Seconds 60
- }
- }
-} while ($response -match "failed" -or $retryCount -ge $maxRetries)
-Write-Host "INFO: 'Actions' write permissions verified" -ForegroundColor DarkGreen
-
-write-host "INFO: Creating GitHub secrets" -ForegroundColor Gray
-Write-Host "INFO: Getting Cosmos DB access key" -ForegroundColor Gray
-Write-Host "INFO: Adding GitHub secrets to apps fork" -ForegroundColor Gray
-gh api -X PUT "/repos/$githubUser/$appsRepo/actions/permissions/workflow" -F can_approve_pull_request_reviews=true
-gh repo set-default "$githubUser/$appsRepo"
-gh secret set "SPN_CLIENT_ID" -b $spnClientID
-gh secret set "SPN_CLIENT_SECRET" -b $spnClientSecret
-gh secret set "ACR_NAME" -b $acrName
-gh secret set "PAT_GITHUB" -b $githubPat
-gh secret set "COSMOS_DB_ENDPOINT" -b $cosmosDBEndpoint
-gh secret set "SPN_TENANT_ID" -b $spnTenantId
-
-Write-Host "INFO: Updating ACR name and Cosmos DB endpoint in all branches" -ForegroundColor Gray
-gh workflow run update-files.yml
-while ($workflowStatus.status -ne "completed") {
- Write-Host "INFO: Waiting for update-files workflow to complete" -ForegroundColor Gray
- Start-Sleep -Seconds 10
- $workflowStatus = (gh run list --workflow=update-files.yml --json status) | ConvertFrom-Json
-}
-Write-Host "INFO: Starting Contoso supermarket pos application v1.0 image build" -ForegroundColor Gray
-gh workflow run pos-app-initial-images-build.yml
-
-Write-Host "INFO: Creating GitHub branches to $appsRepo fork" -ForegroundColor Gray
-$branches = $AgConfig.GitBranches
-foreach ($branch in $branches) {
- try {
- $response = Invoke-RestMethod -Uri "$gitHubAPIBaseUri/repos/$githubUser/$appsRepo/branches/$branch"
- if ($response) {
- if ($branch -ne "main") {
- Write-Host "INFO: branch $branch already exists! Deleting and recreating the branch" -ForegroundColor Gray
- git push origin --delete $branch
- git branch -d $branch
- git fetch origin
- git checkout main
- git pull origin main
- git checkout -b $branch main
- git pull origin main
- git push --set-upstream origin $branch
- }
- }
- }
- catch {
- Write-Host "INFO: Creating $branch branch" -ForegroundColor Gray
- git fetch origin
- git checkout main
- git pull origin main
- git checkout -b $branch main
- git pull origin main
- git push --set-upstream origin $branch
- }
-}
-Write-Host "INFO: Cleaning up any other branches" -ForegroundColor Gray
-$existingBranches = gh api "repos/$githubUser/$appsRepo/branches" | ConvertFrom-Json
-$branches = $AgConfig.GitBranches
-foreach ($branch in $existingBranches) {
- if ($branches -notcontains $branch.name){
- $branchToDelete = $branch.name
- git push origin --delete $branchToDelete
- }
-}
-
-Write-Host "INFO: Switching to main branch" -ForegroundColor Gray
-git checkout main
-
-Write-Host "INFO: Adding branch protection policies for all branches" -ForegroundColor Gray
-foreach ($branch in $branches) {
- Write-Host "INFO: Adding branch protection policies for $branch branch" -ForegroundColor Gray
- $headers = @{
- "Authorization" = "Bearer $githubPat"
- "Accept" = "application/vnd.github+json"
- }
- $body = @{
- required_status_checks = $null
- enforce_admins = $false
- required_pull_request_reviews = @{
- required_approving_review_count = 0
- }
- dismiss_stale_reviews = $true
- restrictions = $null
- } | ConvertTo-Json
-
- Invoke-WebRequest -Uri "$gitHubAPIBaseUri/repos/$githubUser/$appsRepo/branches/$branch/protection" -Method Put -Headers $headers -Body $body -ContentType "application/json"
-}
-Write-Host "INFO: GitHub repo configuration complete!" -ForegroundColor Green
-Write-Host
-
-#####################################################################
-# Azure IoT Hub resources preparation
-#####################################################################
-Write-Host "[$(Get-Date -Format t)] INFO: Creating Azure IoT resources (Step 5/17)" -ForegroundColor DarkGreen
-if ($githubUser -ne "microsoft") {
- $iotHubHostName = $Env:iotHubHostName
- $iotHubName = $iotHubHostName.replace(".azure-devices.net", "")
- $sites = $AgConfig.SiteConfig.Values
- Write-Host "[$(Get-Date -Format t)] INFO: Create an Azure IoT device for each site" -ForegroundColor Gray
- foreach ($site in $sites) {
- foreach ($device in $site.IoTDevices) {
- $deviceId = "$device-$($site.FriendlyName)"
- Add-AzIotHubDevice -ResourceGroupName $resourceGroup -IotHubName $iotHubName -DeviceId $deviceId -EdgeEnabled | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\IoT.log")
- }
- }
- Write-Host "[$(Get-Date -Format t)] INFO: Azure IoT Hub configuration complete!" -ForegroundColor Green
- Write-Host
-}
-else {
- Write-Host "[$(Get-Date -Format t)] ERROR: You have to fork the jumpstart-agora-apps repository!" -ForegroundColor Red
-}
-
-### BELOW IS AN ALTERNATIVE APPROACH TO IMPORT DASHBOARD USING README INSTRUCTIONS
-$adxDashBoardsDir = $AgConfig.AgDirectories["AgAdxDashboards"]
-$dataEmulatorDir = $AgConfig.AgDirectories["AgDataEmulator"]
-$kustoCluster = Get-AzKustoCluster -ResourceGroupName $resourceGroup -Name $adxClusterName
-if ($null -ne $kustoCluster) {
- $adxEndPoint = $kustoCluster.Uri
- if ($null -ne $adxEndPoint -and $adxEndPoint -ne "") {
- $ordersDashboardBody = (Invoke-WebRequest -Method Get -Uri "$templateBaseUrl/artifacts/adx_dashboards/adx-dashboard-orders-payload.json").Content -replace '{{ADX_CLUSTER_URI}}', $adxEndPoint -replace '{{ADX_CLUSTER_NAME}}', $adxClusterName
- Set-Content -Path "$adxDashBoardsDir\adx-dashboard-orders-payload.json" -Value $ordersDashboardBody -Force -ErrorAction Ignore
- $iotSensorsDashboardBody = (Invoke-WebRequest -Method Get -Uri "$templateBaseUrl/artifacts/adx_dashboards/adx-dashboard-iotsensor-payload.json") -replace '{{ADX_CLUSTER_URI}}', $adxEndPoint -replace '{{ADX_CLUSTER_NAME}}', $adxClusterName
- Set-Content -Path "$adxDashBoardsDir\adx-dashboard-iotsensor-payload.json" -Value $iotSensorsDashboardBody -Force -ErrorAction Ignore
- }
- else {
- Write-Host "[$(Get-Date -Format t)] ERROR: Unable to find Azure Data Explorer endpoint from the cluster resource in the resource group."
- }
-}
-
-# Download DataEmulator.zip into Agora folder and unzip
-$emulatorPath = "$dataEmulatorDir\DataEmulator.zip"
-Invoke-WebRequest -Method Get -Uri "$templateBaseUrl/artifacts/data_emulator/DataEmulator.zip" -OutFile $emulatorPath
-
-# Unzip DataEmulator.zip to copy DataEmulator exe and config file to generate sample data for dashboards
-if (Test-Path -Path $emulatorPath) {
- Expand-Archive -Path "$emulatorPath" -DestinationPath "$dataEmulatorDir" -ErrorAction SilentlyContinue -Force
-}
-
-# Download products.json and stores.json file to use in Data Emulator
-$productsJsonPath = "$dataEmulatorDir\products.json"
-Invoke-WebRequest -Method Get -Uri "$templateBaseUrl/artifacts/data_emulator/products.json" -OutFile $productsJsonPath
-if (!(Test-Path -Path $productsJsonPath)) {
- Write-Host "Unabled to download products.json file. Please download manually from GitHub into the data_emulator folder."
-}
-
-$storesJsonPath = "$dataEmulatorDir\stores.json"
-Invoke-WebRequest -Method Get -Uri "$templateBaseUrl/artifacts/data_emulator/stores.json" -OutFile $storesJsonPath
-if (!(Test-Path -Path $storesJsonPath)) {
- Write-Host "Unabled to download stores.json file. Please download manually from GitHub into the data_emulator folder."
-}
-
-# Download icon file
-$iconPath = "$AgIconsDir\emulator.ico"
-Invoke-WebRequest -Method Get -Uri "$templateBaseUrl/artifacts/icons/emulator.ico" -OutFile $iconPath
-if (!(Test-Path -Path $iconPath)) {
- Write-Host "Unabled to download emulator.ico file. Please download manually from GitHub into the icons folder."
-}
-
-# Create desktop shortcut
-$shortcutLocation = "$Env:Public\Desktop\Data Emulator.lnk"
-$wScriptShell = New-Object -ComObject WScript.Shell
-$shortcut = $wScriptShell.CreateShortcut($shortcutLocation)
-$shortcut.TargetPath = "$dataEmulatorDir\DataEmulator.exe"
-$shortcut.IconLocation = "$iconPath, 0"
-$shortcut.WindowStyle = 7
-$shortcut.Save()
-
-#####################################################################
-# Configure L1 virtualization infrastructure
-#####################################################################
-Write-Host "[$(Get-Date -Format t)] INFO: Configuring L1 virtualization infrastructure (Step 6/17)" -ForegroundColor DarkGreen
-$password = ConvertTo-SecureString $AgConfig.L1Password -AsPlainText -Force
-$Credentials = New-Object System.Management.Automation.PSCredential($AgConfig.L1Username, $password)
-
-# Turn the .kube folder to a shared folder where all Kubernetes kubeconfig files will be copied to
-$kubeFolder = "$Env:USERPROFILE\.kube"
-New-Item -ItemType Directory $kubeFolder -Force | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\L1Infra.log")
-New-SmbShare -Name "kube" -Path "$Env:USERPROFILE\.kube" -FullAccess "Everyone" | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\L1Infra.log")
-
-# Enable Enhanced Session Mode on Host
-Write-Host "[$(Get-Date -Format t)] INFO: Enabling Enhanced Session Mode on Hyper-V host" -ForegroundColor Gray
-Set-VMHost -EnableEnhancedSessionMode $true | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\L1Infra.log")
-
-# Create Internal Hyper-V switch for the L1 nested virtual machines
-New-VMSwitch -Name $AgConfig.L1SwitchName -SwitchType Internal | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\L1Infra.log")
-$ifIndex = (Get-NetAdapter -Name ("vEthernet (" + $AgConfig.L1SwitchName + ")")).ifIndex
-New-NetIPAddress -IPAddress $AgConfig.L1DefaultGateway -PrefixLength 24 -InterfaceIndex $ifIndex | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\L1Infra.log")
-New-NetNat -Name $AgConfig.L1SwitchName -InternalIPInterfaceAddressPrefix $AgConfig.L1NatSubnetPrefix | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\L1Infra.log")
-
-#####################################################################
-# Deploying the nested L1 virtual machines
-#####################################################################
-Write-Host "[$(Get-Date -Format t)] INFO: Fetching Windows 11 IoT Enterprise VM image from Azure storage. This may take a few minutes." -ForegroundColor Yellow
-# azcopy cp $AgConfig.PreProdVHDBlobURL $AgConfig.AgDirectories["AgVHDXDir"] --recursive=true --check-length=false --log-level=ERROR | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\L1Infra.log")
-azcopy cp $AgConfig.ProdVHDBlobURL $AgConfig.AgDirectories["AgVHDXDir"] --recursive=true --check-length=false --log-level=ERROR | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\L1Infra.log")
-
-# Create three virtual machines from the base VHDX image
-$vhdxPath = Get-ChildItem $AgConfig.AgDirectories["AgVHDXDir"] -Filter *.vhdx | Select-Object -ExpandProperty FullName
-foreach ($site in $AgConfig.SiteConfig.GetEnumerator()) {
- if ($site.Value.Type -eq "AKSEE") {
- # Create disks for each site host
- Write-Host "[$(Get-Date -Format t)] INFO: Creating $($site.Name) disk." -ForegroundColor Gray
- $destVhdxPath = "$($AgConfig.AgDirectories["AgVHDXDir"])\$($site.Name)Disk.vhdx"
- $destPath = $AgConfig.AgDirectories["AgVHDXDir"]
- New-VHD -ParentPath $vhdxPath -Path $destVhdxPath -Differencing | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\L1Infra.log")
-
- # Create a new virtual machine and attach the existing virtual hard disk
- Write-Host "[$(Get-Date -Format t)] INFO: Creating and configuring $($site.Name) virtual machine." -ForegroundColor Gray
-
- New-VM -Name $site.Name `
- -Path $destPath `
- -MemoryStartupBytes $AgConfig.L1VMMemory `
- -BootDevice VHD `
- -VHDPath $destVhdxPath `
- -Generation 2 `
- -Switch $AgConfig.L1SwitchName | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\L1Infra.log")
-
- # Set up the virtual machine before coping all AKS Edge Essentials automation files
- Set-VMProcessor -VMName $site.Name `
- -Count $AgConfig.L1VMNumVCPU `
- -ExposeVirtualizationExtensions $true | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\L1Infra.log")
-
- Get-VMNetworkAdapter -VMName $site.Name | Set-VMNetworkAdapter -MacAddressSpoofing On | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\L1Infra.log")
- Enable-VMIntegrationService -VMName $site.Name -Name "Guest Service Interface" | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\L1Infra.log")
-
- # Start the virtual machine
- Start-VM -Name $site.Name | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\L1Infra.log")
- }
-}
-
-Start-Sleep -Seconds 20
-# Create an array with VM names
-$VMnames = (Get-VM).Name
-
-$sourcePath = "$PsHome\Profile.ps1"
-$destinationPath = "C:\Deployment\Profile.ps1"
-$maxRetries = 3
-
-foreach ($VM in $VMNames) {
- $retryCount = 0
- $copySucceeded = $false
-
- while (-not $copySucceeded -and $retryCount -lt $maxRetries) {
- try {
- Copy-VMFile $VM -SourcePath $sourcePath -DestinationPath $destinationPath -CreateFullPath -FileSource Host -Force -ErrorAction Stop
- $copySucceeded = $true
- Write-Host "File copied to $VM successfully."
- } catch {
- $retryCount++
- Write-Host "Attempt $retryCount : File copy to $VM failed. Retrying..."
- Start-Sleep -Seconds 30 # Wait for 30 seconds before retrying
- }
- }
-
- if (-not $copySucceeded) {
- Write-Host "File copy to $VM failed after $maxRetries attempts."
- }
-}
-
-########################################################################
-# Prepare L1 nested virtual machines for AKS Edge Essentials bootstrap
-########################################################################
-foreach ($site in $AgConfig.SiteConfig.GetEnumerator()) {
- if ($site.Value.Type -eq "AKSEE") {
- Write-Host "[$(Get-Date -Format t)] INFO: Renaming computer name of $($site.Name)" -ForegroundColor Gray
- $ErrorActionPreference = "SilentlyContinue"
- Invoke-Command -VMName $site.Name -Credential $Credentials -ScriptBlock {
- $site = $using:site
- (gwmi win32_computersystem).Rename($site.Name)
- } | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\L1Infra.log")
- $ErrorActionPreference = "Continue"
- Stop-VM -Name $site.Name -Force -Confirm:$false
- Start-VM -Name $site.Name
- }
-}
-
-foreach ($VM in $VMNames) {
- $VMStatus = Get-VMIntegrationService -VMName $VM -Name Heartbeat
- while ($VMStatus.PrimaryStatusDescription -ne "OK") {
- $VMStatus = Get-VMIntegrationService -VMName $VM -Name Heartbeat
- write-host "[$(Get-Date -Format t)] INFO: Waiting for $VM to finish booting." -ForegroundColor Gray
- Start-Sleep -Seconds 5
- }
-}
-
-Write-Host "[$(Get-Date -Format t)] INFO: Fetching the latest two AKS Edge Essentials releases." -ForegroundColor Gray
-$latestReleaseTag = (Invoke-WebRequest $websiteUrls["aksEEReleases"] | ConvertFrom-Json)[0].tag_name
-$beforeLatestReleaseTag = (Invoke-WebRequest $websiteUrls["aksEEReleases"] | ConvertFrom-Json)[1].tag_name
-$AKSEEReleasesTags = ($latestReleaseTag,$beforeLatestReleaseTag)
-$AKSEESchemaVersions = @()
-
-for ($i = 0; $i -lt $AKSEEReleasesTags.Count; $i++) {
- $releaseTag = (Invoke-WebRequest $websiteUrls["aksEEReleases"] | ConvertFrom-Json)[$i].tag_name
- $AKSEEReleaseDownloadUrl = "https://github.com/Azure/AKS-Edge/archive/refs/tags/$releaseTag.zip"
- $output = Join-Path $AgToolsDir "$releaseTag.zip"
- Invoke-WebRequest $AKSEEReleaseDownloadUrl -OutFile $output
- Expand-Archive $output -DestinationPath $AgToolsDir -Force
- $AKSEEReleaseConfigFilePath = "$AgToolsDir\AKS-Edge-$releaseTag\tools\aksedge-config.json"
- $jsonContent = Get-Content -Raw -Path $AKSEEReleaseConfigFilePath | ConvertFrom-Json
- $schemaVersion = $jsonContent.SchemaVersion
- $AKSEESchemaVersions += $schemaVersion
- # Clean up the downloaded release files
- Remove-Item -Path $output -Force
- Remove-Item -Path "$AgToolsDir\AKS-Edge-$releaseTag" -Force -Recurse
-}
-
-Invoke-Command -VMName $VMnames -Credential $Credentials -ScriptBlock {
- $hostname = hostname
- $ProgressPreference = "SilentlyContinue"
- ###########################################
- # Preparing environment folders structure
- ###########################################
- Write-Host "[$(Get-Date -Format t)] INFO: Preparing folder structure on $hostname." -ForegroundColor Gray
- $deploymentFolder = "C:\Deployment" # Deployment folder is already pre-created in the VHD image
- $logsFolder = "$deploymentFolder\Logs"
- $kubeFolder = "$Env:USERPROFILE\.kube"
-
- # Set up an array of folders
- $folders = @($logsFolder, $kubeFolder)
-
- # Loop through each folder and create it
- foreach ($Folder in $folders) {
- New-Item -ItemType Directory $Folder -Force
- }
-} | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\L1Infra.log")
-
-$subscriptionId = (Get-AzSubscription).Id
-Invoke-Command -VMName $VMnames -Credential $Credentials -ScriptBlock {
- # Start logging
- $hostname = hostname
- $ProgressPreference = "SilentlyContinue"
- $deploymentFolder = "C:\Deployment" # Deployment folder is already pre-created in the VHD image
- $logsFolder = "$deploymentFolder\Logs"
- Start-Transcript -Path $logsFolder\AKSEEBootstrap.log
- $AgConfig = $using:AgConfig
- $AgToolsDir = $using:AgToolsDir
- $websiteUrls = $using:websiteUrls
-
- ##########################################
- # Deploying AKS Edge Essentials clusters
- ##########################################
- $deploymentFolder = "C:\Deployment" # Deployment folder is already pre-created in the VHD image
- $logsFolder = "$deploymentFolder\Logs"
-
- # Assigning network adapter IP address
- $NetIPAddress = $AgConfig.SiteConfig[$Env:COMPUTERNAME].NetIPAddress
- $DefaultGateway = $AgConfig.SiteConfig[$Env:COMPUTERNAME].DefaultGateway
- $PrefixLength = $AgConfig.SiteConfig[$Env:COMPUTERNAME].PrefixLength
- $DNSClientServerAddress = $AgConfig.SiteConfig[$Env:COMPUTERNAME].DNSClientServerAddress
- Write-Host "[$(Get-Date -Format t)] INFO: Configuring networking interface on $hostname with IP address $NetIPAddress." -ForegroundColor Gray
- $AdapterName = (Get-NetAdapter -Name Ethernet*).Name
- $ifIndex = (Get-NetAdapter -Name $AdapterName).ifIndex
- New-NetIPAddress -IPAddress $NetIPAddress -DefaultGateway $DefaultGateway -PrefixLength $PrefixLength -InterfaceIndex $ifIndex | Out-Null
- Set-DNSClientServerAddress -InterfaceIndex $ifIndex -ServerAddresses $DNSClientServerAddress | Out-Null
-
- ###########################################
- # Validating internet connectivity
- ###########################################
- $timeElapsed = 0
- do {
- Write-Host "[$(Get-Date -Format t)] INFO: Waiting for internet connection to be healthy on $hostname." -ForegroundColor Gray
- Start-Sleep -Seconds 5
- $timeElapsed = $timeElapsed + 10
- } until ((Test-Connection bing.com -Count 1 -ErrorAction SilentlyContinue) -or ($timeElapsed -eq 60))
-
- # Fetching latest AKS Edge Essentials msi file
- Write-Host "[$(Get-Date -Format t)] INFO: Fetching latest AKS Edge Essentials install file on $hostname." -ForegroundColor Gray
- Invoke-WebRequest $websiteUrls["aksEEk3s"] -OutFile $deploymentFolder\AKSEEK3s.msi
-
- # Fetching required GitHub artifacts from Jumpstart repository
- Write-Host "[$(Get-Date -Format t)] INFO: Fetching GitHub artifacts" -ForegroundColor Gray
- $repoName = "azure_arc" # While testing, change to your GitHub fork's repository name
- $githubApiUrl = "https://api.github.com/repos/$using:githubAccount/$repoName/contents/azure_jumpstart_ag/retail/artifacts/L1Files?ref=$using:githubBranch"
- $response = Invoke-RestMethod -Uri $githubApiUrl
- $fileUrls = $response | Where-Object { $_.type -eq "file" } | Select-Object -ExpandProperty download_url
- $fileUrls | ForEach-Object {
- $fileName = $_.Substring($_.LastIndexOf("/") + 1)
- $outputFile = Join-Path $deploymentFolder $fileName
- Invoke-RestMethod -Uri $_ -OutFile $outputFile
- }
-
- ###############################################################################
- # Setting up replacement parameters for AKS Edge Essentials config json file
- ###############################################################################
- Write-Host "[$(Get-Date -Format t)] INFO: Building AKS Edge Essentials config json file on $hostname." -ForegroundColor Gray
- $AKSEEConfigFilePath = "$deploymentFolder\ScalableCluster.json"
- $AdapterName = (Get-NetAdapter -Name Ethernet*).Name
- $namingGuid = $using:namingGuid
- $arcClusterName = $AgConfig.SiteConfig[$Env:COMPUTERNAME].ArcClusterName + "-$namingGuid"
-
- # Fetch schemaVersion release from the AgConfig file
- $AKSEESchemaVersionUseLatest = $AgConfig.SiteConfig[$Env:COMPUTERNAME].AKSEEReleaseUseLatest
- if($AKSEESchemaVersionUseLatest){
- $SchemaVersion = $using:AKSEESchemaVersions[0]
- }
- else {
- $SchemaVersion = $using:AKSEESchemaVersions[1]
- }
-
- $replacementParams = @{
- "SchemaVersion-null" = $SchemaVersion
- "ServiceIPRangeStart-null" = $AgConfig.SiteConfig[$Env:COMPUTERNAME].ServiceIPRangeStart
- "1000" = $AgConfig.SiteConfig[$Env:COMPUTERNAME].ServiceIPRangeSize
- "ControlPlaneEndpointIp-null" = $AgConfig.SiteConfig[$Env:COMPUTERNAME].ControlPlaneEndpointIp
- "Ip4GatewayAddress-null" = $AgConfig.SiteConfig[$Env:COMPUTERNAME].DefaultGateway
- "2000" = $AgConfig.SiteConfig[$Env:COMPUTERNAME].PrefixLength
- "DnsServer-null" = $AgConfig.SiteConfig[$Env:COMPUTERNAME].DNSClientServerAddress
- "Ethernet-Null" = $AdapterName
- "Ip4Address-null" = $AgConfig.SiteConfig[$Env:COMPUTERNAME].LinuxNodeIp4Address
- "ClusterName-null" = $arcClusterName
- "Location-null" = $using:azureLocation
- "ResourceGroupName-null" = $using:resourceGroup
- "SubscriptionId-null" = $using:subscriptionId
- "TenantId-null" = $using:spnTenantId
- "ClientId-null" = $using:spnClientId
- "ClientSecret-null" = $using:spnClientSecret
- }
-
- ###################################################
- # Preparing AKS Edge Essentials config json file
- ###################################################
- $content = Get-Content $AKSEEConfigFilePath
- foreach ($key in $replacementParams.Keys) {
- $content = $content -replace $key, $replacementParams[$key]
- }
- Set-Content "$deploymentFolder\Config.json" -Value $content
-}
-Write-Host "[$(Get-Date -Format t)] INFO: Initial L1 virtualization infrastructure configuration complete." -ForegroundColor Green
-Write-Host
-
-Write-Host "[$(Get-Date -Format t)] INFO: Installing AKS Edge Essentials (Step 7/17)" -ForegroundColor DarkGreen
-foreach ($VMName in $VMNames) {
- $Session = New-PSSession -VMName $VMName -Credential $Credentials
- Write-Host "[$(Get-Date -Format t)] INFO: Rebooting $VMName." -ForegroundColor Gray
- Invoke-Command -Session $Session -ScriptBlock {
- $Action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-ExecutionPolicy Bypass -File C:\Deployment\AKSEEBootstrap.ps1"
- $Trigger = New-ScheduledTaskTrigger -AtStartup
- Register-ScheduledTask -TaskName "Startup Scan" -Action $Action -Trigger $Trigger -User $Env:USERNAME -Password 'Agora123!!' -RunLevel Highest | Out-Null
- Restart-Computer -Force -Confirm:$false
- } | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\L1AKSInfra.log")
- Remove-PSSession $Session | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\L1AKSInfra.log")
-}
-
-Write-Host "[$(Get-Date -Format t)] INFO: Sleeping for three (3) minutes to allow for AKS EE installs to complete." -ForegroundColor Gray
-Start-Sleep -Seconds 180 # Give some time for the AKS EE installs to complete. This will take a few minutes.
-
-#####################################################################
-# Monitor until the kubeconfig files are detected and copied over
-#####################################################################
-$elapsedTime = Measure-Command {
- foreach ($VMName in $VMNames) {
- $path = "C:\Users\Administrator\.kube\config-" + $VMName.ToLower()
- $user = $AgConfig.L1Username
- [securestring]$secStringPassword = ConvertTo-SecureString $AgConfig.L1Password -AsPlainText -Force
- $credential = New-Object System.Management.Automation.PSCredential($user, $secStringPassword)
- Start-Sleep 5
- while (!(Invoke-Command -VMName $VMName -Credential $credential -ScriptBlock { Test-Path $using:path })) {
- Start-Sleep 30
- Write-Host "[$(Get-Date -Format t)] INFO: Waiting for AKS Edge Essentials kubeconfig to be available on $VMName." -ForegroundColor Gray
- }
-
- Write-Host "[$(Get-Date -Format t)] INFO: $VMName's kubeconfig is ready - copying over config-$VMName" -ForegroundColor DarkGreen
- $destinationPath = $Env:USERPROFILE + "\.kube\config-" + $VMName
- $s = New-PSSession -VMName $VMName -Credential $credential
- Copy-Item -FromSession $s -Path $path -Destination $destinationPath
- $file = Get-Item $destinationPath
- if ($file.Length -eq 0) {
- Write-Host "[$(Get-Date -Format t)] ERROR: Kubeconfig on $VMName is corrupt. This error is unrecoverable. Exiting." -ForegroundColor White -BackgroundColor Red
- exit 1
- }
- }
-}
-
-# Display the elapsed time in seconds it took for kubeconfig files to show up in folder
-Write-Host "[$(Get-Date -Format t)] INFO: Waiting on kubeconfig files took $($elapsedTime.ToString("g"))." -ForegroundColor Gray
-
-#####################################################################
-# Merging kubeconfig files on the L0 virtual machine
-#####################################################################
-Write-Host "[$(Get-Date -Format t)] INFO: All three kubeconfig files are present. Merging kubeconfig files for use with kubectx." -ForegroundColor Gray
-$kubeconfigpath = ""
-foreach ($VMName in $VMNames) {
- $kubeconfigpath = $kubeconfigpath + "$Env:USERPROFILE\.kube\config-" + $VMName.ToLower() + ";"
-}
-$Env:KUBECONFIG = $kubeconfigpath
-kubectl config view --merge --flatten > "$Env:USERPROFILE\.kube\config-raw" | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\L1AKSInfra.log")
-kubectl config get-clusters --kubeconfig="$Env:USERPROFILE\.kube\config-raw" | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\L1AKSInfra.log")
-Rename-Item -Path "$Env:USERPROFILE\.kube\config-raw" -NewName "$Env:USERPROFILE\.kube\config"
-$Env:KUBECONFIG = "$Env:USERPROFILE\.kube\config"
-
-# Print a message indicating that the merge is complete
-Write-Host "[$(Get-Date -Format t)] INFO: All three kubeconfig files merged successfully." -ForegroundColor Gray
-
-# Validate context switching using kubectx & kubectl
-foreach ($cluster in $VMNames) {
- Write-Host "[$(Get-Date -Format t)] INFO: Testing connectivity to kube api on $cluster cluster." -ForegroundColor Gray
- kubectx $cluster.ToLower()
- kubectl get nodes -o wide
-}
-Write-Host "[$(Get-Date -Format t)] INFO: AKS Edge Essentials installs are complete!" -ForegroundColor Green
-Write-Host
-
-#####################################################################
-# Setup Azure Container registry on cloud AKS staging environment
-#####################################################################
-az aks get-credentials --resource-group $Env:resourceGroup --name $Env:aksStagingClusterName --admin | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\ClusterSecrets.log")
-kubectx staging="$Env:aksStagingClusterName-admin" | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\ClusterSecrets.log")
-
-# Attach ACR to staging cluster
-Write-Host "[$(Get-Date -Format t)] INFO: Attaching Azure Container Registry to AKS staging cluster." -ForegroundColor Gray
-az aks update -n $Env:aksStagingClusterName -g $Env:resourceGroup --attach-acr $acrName | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\ClusterSecrets.log")
-
-#####################################################################
-# Creating Kubernetes namespaces on clusters
-#####################################################################
-Write-Host "[$(Get-Date -Format t)] INFO: Creating namespaces on clusters (Step 8/17)" -ForegroundColor DarkGreen
-foreach ($cluster in $AgConfig.SiteConfig.GetEnumerator()) {
- $clusterName = $cluster.Name.ToLower()
- kubectx $clusterName | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\ClusterSecrets.log")
- foreach ($namespace in $AgConfig.Namespaces) {
- Write-Host "[$(Get-Date -Format t)] INFO: Creating namespace $namespace on $clusterName" -ForegroundColor Gray
- kubectl create namespace $namespace | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\ClusterSecrets.log")
- }
-}
-
-#####################################################################
-# Setup Azure Container registry pull secret on clusters
-#####################################################################
-Write-Host "[$(Get-Date -Format t)] INFO: Configuring secrets on clusters (Step 9/17)" -ForegroundColor DarkGreen
-foreach ($cluster in $AgConfig.SiteConfig.GetEnumerator()) {
- $clusterName = $cluster.Name.ToLower()
- foreach ($namespace in $AgConfig.Namespaces) {
- if ($namespace -eq "contoso-supermarket" -or $namespace -eq "images-cache"){
- Write-Host "[$(Get-Date -Format t)] INFO: Configuring Azure Container registry on $clusterName"
- kubectx $clusterName | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\ClusterSecrets.log")
- kubectl create secret docker-registry acr-secret `
- --namespace $namespace `
- --docker-server="$acrName.azurecr.io" `
- --docker-username="$Env:spnClientId" `
- --docker-password="$Env:spnClientSecret" | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\ClusterSecrets.log")
- }
- }
-}
-
-#####################################################################
-# Create secrets for GitHub actions
-#####################################################################
-Write-Host "[$(Get-Date -Format t)] INFO: Creating Kubernetes secrets" -ForegroundColor Gray
-$cosmosDBKey = $(az cosmosdb keys list --name $cosmosDBName --resource-group $resourceGroup --query primaryMasterKey --output tsv)
-foreach ($cluster in $AgConfig.SiteConfig.GetEnumerator()) {
- $clusterName = $cluster.Name.ToLower()
- Write-Host "[$(Get-Date -Format t)] INFO: Creating Kubernetes secrets on $clusterName" -ForegroundColor Gray
- foreach ($namespace in $AgConfig.Namespaces) {
- if ($namespace -eq "contoso-supermarket" -or $namespace -eq "images-cache"){
- kubectx $cluster.Name.ToLower() | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\ClusterSecrets.log")
- kubectl create secret generic postgrespw --from-literal=POSTGRES_PASSWORD='Agora123!!' --namespace $namespace | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\ClusterSecrets.log")
- kubectl create secret generic cosmoskey --from-literal=COSMOS_KEY=$cosmosDBKey --namespace $namespace | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\ClusterSecrets.log")
- kubectl create secret generic github-token --from-literal=token=$githubPat --namespace $namespace | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\ClusterSecrets.log")
- }
- }
-}
-Write-Host "[$(Get-Date -Format t)] INFO: Cluster secrets configuration complete." -ForegroundColor Green
-Write-Host
-
-#####################################################################
-# Cache contoso-supermarket images on all clusters
-#####################################################################
-Write-Host "[$(Get-Date -Format t)] INFO: Caching contoso-supermarket images on all clusters" -ForegroundColor Gray
-while ($workflowStatus.status -ne "completed") {
- Write-Host "INFO: Waiting for pos-app-initial-images-build workflow to complete" -ForegroundColor Gray
- Start-Sleep -Seconds 10
- $workflowStatus = (gh run list --workflow=pos-app-initial-images-build.yml --json status) | ConvertFrom-Json
-}
-foreach ($cluster in $AgConfig.SiteConfig.GetEnumerator()) {
- $branch = $cluster.Name.ToLower()
- $context = $cluster.Name.ToLower()
- $applicationName = "contoso-supermarket"
- $imageTag = "v1.0"
- $imagePullSecret = "acr-secret"
- $namespace = "images-cache"
- if ($branch -eq "chicago") {
- $branch = "canary"
- }
- if ($branch -eq "seattle") {
- $branch = "production"
- }
- Save-K8sImage -applicationName $applicationName -imageName "contosoai" -imageTag $imageTag -namespace $namespace -imagePullSecret $imagePullSecret -branch $branch -acrName $acrName -context $context
- Save-K8sImage -applicationName $applicationName -imageName "pos" -imageTag $imageTag -namespace $namespace -imagePullSecret $imagePullSecret -branch $branch -acrName $acrName -context $context
- Save-K8sImage -applicationName $applicationName -imageName "pos-cloudsync" -imageTag $imageTag -namespace $namespace -imagePullSecret $imagePullSecret -branch $branch -acrName $acrName -context $context
- Save-K8sImage -applicationName $applicationName -imageName "queue-monitoring-backend" -imageTag $imageTag -namespace $namespace -imagePullSecret $imagePullSecret -branch $branch -acrName $acrName -context $context
- Save-K8sImage -applicationName $applicationName -imageName "queue-monitoring-frontend" -imageTag $imageTag -namespace $namespace -imagePullSecret $imagePullSecret -branch $branch -acrName $acrName -context $context
-}
-
-#####################################################################
-# Connect the AKS Edge Essentials clusters and hosts to Azure Arc
-#####################################################################
-Write-Host "[$(Get-Date -Format t)] INFO: Connecting AKS Edge clusters to Azure with Azure Arc (Step 10/17)" -ForegroundColor DarkGreen
-
-# Running pre-checks to ensure that the aksedge ConfigMap is present on all clusters
-$maxRetries = 5
-$retryInterval = 30 # seconds
-$retryCount = 0
-foreach ($cluster in $AgConfig.SiteConfig.GetEnumerator()) {
- $clusterName = $cluster.Name.ToLower()
- if ($clusterName -ne "staging") {
- while ($retryCount -lt $maxRetries) {
- kubectx $clusterName
- $configMap = kubectl get configmap -n aksedge aksedge
- if ($null -eq $configMap) {
- $retryCount++
- Write-Host "Retry ${retryCount}/${maxRetries}: aksedge ConfigMap not found on $clusterName. Retrying in $retryInterval seconds..." | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\ArcConnectivity.log")
- Start-Sleep -Seconds $retryInterval
- }
- else {
- # ConfigMap found, continue with the rest of the script
- Write-Host "aksedge ConfigMap found on $clusterName. Continuing with the script..." | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\ArcConnectivity.log")
- break # Exit the loop
- }
- }
-
- if ($retryCount -eq $maxRetries) {
- Write-Host "[$(Get-Date -Format t)] ERROR: aksedge ConfigMap not found on $clusterName. Exiting..." -ForegroundColor White -BackgroundColor Red | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\ArcConnectivity.log")
- exit 1 # Exit the script
- }
- }
-}
-
-foreach ($VM in $VMNames) {
- $secret = $Env:spnClientSecret
- $clientId = $Env:spnClientId
- $tenantId = $Env:spnTenantId
- $location = $Env:azureLocation
- $resourceGroup = $Env:resourceGroup
-
- Invoke-Command -VMName $VM -Credential $Credentials -ScriptBlock {
- # Install prerequisites
- . C:\Deployment\Profile.ps1
- $hostname = hostname
- $ProgressPreference = "SilentlyContinue"
- Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force
- Install-Module Az.Resources -Repository PSGallery -Force -AllowClobber -ErrorAction Stop
- Install-Module Az.Accounts -Repository PSGallery -Force -AllowClobber -ErrorAction Stop
- Install-Module Az.ConnectedKubernetes -Repository PSGallery -Force -AllowClobber -ErrorAction Stop
- Install-Module Az.ConnectedMachine -Force -AllowClobber -ErrorAction Stop
-
- # Connect servers to Arc
- $azurePassword = ConvertTo-SecureString $using:secret -AsPlainText -Force
- $psCred = New-Object System.Management.Automation.PSCredential($using:clientId, $azurePassword)
- Connect-AzAccount -Credential $psCred -TenantId $using:tenantId -ServicePrincipal
- Write-Host "[$(Get-Date -Format t)] INFO: Arc-enabling $hostname server." -ForegroundColor Gray
- Redo-Command -ScriptBlock { Connect-AzConnectedMachine -ResourceGroupName $using:resourceGroup -Name "Ag-$hostname-Host" -Location $using:location }
-
- # Connect clusters to Arc
- $deploymentPath = "C:\Deployment\config.json"
- Write-Host "[$(Get-Date -Format t)] INFO: Arc-enabling $hostname AKS Edge Essentials cluster." -ForegroundColor Gray
-
- kubectl get svc
-
- $retryCount = 5 # Number of times to retry the operation
- $retryDelay = 30 # Delay in seconds between retries
-
- for ($retry = 1; $retry -le $retryCount; $retry++) {
- $return = Connect-AksEdgeArc -JsonConfigFilePath $deploymentPath
- if ($return -ne "OK") {
- Write-Output "Failed to onboard AKS Edge Essentials cluster to Azure Arc. Retrying (Attempt $retry of $retryCount)..."
- if ($retry -lt $retryCount) {
- Start-Sleep -Seconds $retryDelay # Wait before retrying
- }
- else {
- Write-Output "Exceeded maximum retry attempts. Exiting."
- break # Exit the loop after the maximum number of retries
- }
- } else {
- Write-Output "Successfully onboarded AKS Edge Essentials cluster to Azure Arc."
- break # Exit the loop if the connection is successful
- }
- }
-
-
- } 2>&1 | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\ArcConnectivity.log")
-}
-
-#####################################################################
-# Tag Azure Arc resources
-#####################################################################
-$arcResourceTypes = $AgConfig.ArcServerResourceType, $AgConfig.ArcK8sResourceType
-$Tag = @{$AgConfig.TagName = $AgConfig.TagValue }
-
-# Iterate over the Arc resources and tag it
-foreach ($arcResourceType in $arcResourceTypes) {
- $arcResources = Get-AzResource -ResourceType $arcResourceType -ResourceGroupName $Env:resourceGroup
- foreach ($arcResource in $arcResources) {
- Update-AzTag -ResourceId $arcResource.Id -Tag $Tag -Operation Merge | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\ArcConnectivity.log")
- }
-}
-
-Write-Host "[$(Get-Date -Format t)] INFO: AKS Edge Essentials clusters and hosts have been registered with Azure Arc!" -ForegroundColor Green
-Write-Host
-
-
-#####################################################################
-# Installing flux extension on clusters
-#####################################################################
-Write-Host "[$(Get-Date -Format t)] INFO: Installing flux extension on clusters (Step 11/17)" -ForegroundColor DarkGreen
-
-$resourceTypes = @($AgConfig.ArcK8sResourceType, $AgConfig.AksResourceType)
-$resources = Get-AzResource -ResourceGroupName $Env:resourceGroup | Where-Object { $_.ResourceType -in $resourceTypes }
-
-$jobs = @()
-
-foreach ($resource in $resources) {
-
- $resourceName = $resource.Name
- $resourceType = $resource.Type
-
- Write-Host "[$(Get-Date -Format t)] INFO: Installing flux extension on $resourceName" -ForegroundColor Gray
-
- $job = Start-Job -Name $resourceName -ScriptBlock {
- param($resourceName, $resourceType)
-
- $retryCount = 10
- $retryDelaySeconds = 60
-
- switch ($resourceType)
- {
- 'Microsoft.Kubernetes/connectedClusters' {$ClusterType = 'ConnectedClusters'}
- 'Microsoft.ContainerService/managedClusters' {$ClusterType = 'ManagedClusters'}
- }
-
- if($clusterType -eq 'ConnectedClusters'){
- # Check if cluster is connected to Azure Arc control plane
- $ConnectivityStatus = (Get-AzConnectedKubernetes -ResourceGroupName $Env:resourceGroup -ClusterName $resourceName).ConnectivityStatus
-
- if (-not ($ConnectivityStatus -eq 'Connected')) {
-
- for ($attempt = 1; $attempt -le $retryCount; $attempt++) {
-
-
- $ConnectivityStatus = (Get-AzConnectedKubernetes -ResourceGroupName $Env:resourceGroup -ClusterName $resourceName).ConnectivityStatus
-
- # Check the condition
- if ($ConnectivityStatus -eq 'Connected') {
- # Condition is true, break out of the loop
- break
- }
-
- # Wait for a specific duration before re-evaluating the condition
- Start-Sleep -Seconds $retryDelaySeconds
-
-
- if ($attempt -lt $retryCount) {
- Write-Host "Retrying in $retryDelaySeconds seconds..."
- Start-Sleep -Seconds $retryDelaySeconds
- }
- else {
- $ProvisioningState = "Timed out after $($retryDelaySeconds * $retryCount) seconds while waiting for cluster to become connected to Azure Arc control plane. Current status: $ConnectivityStatus"
- break # Max retry attempts reached, exit the loop
- }
-
- }
- }
- }
-
- $extension = az k8s-extension list --cluster-name $resourceName --resource-group $Env:resourceGroup --cluster-type $ClusterType --output json | ConvertFrom-Json
- $extension = $extension | Where-Object extensionType -eq 'microsoft.flux'
-
- if ($extension.ProvisioningState -ne 'Succeeded' -and ($ConnectivityStatus -eq 'Connected' -or $clusterType -eq "ManagedClusters")) {
-
- for ($attempt = 1; $attempt -le $retryCount; $attempt++) {
-
- try {
-
- if ($extension) {
-
- az k8s-extension delete --name "flux" --cluster-name $resourceName --resource-group $Env:resourceGroup --cluster-type $ClusterType --force --yes
-
- }
-
- az k8s-extension create --name "flux" --extension-type "microsoft.flux" --cluster-name $resourceName --resource-group $Env:resourceGroup --cluster-type $ClusterType --output json | ConvertFrom-Json -OutVariable extension
-
- break # Command succeeded, exit the loop
- }
-
- catch {
- Write-Warning "An error occurred: $($_.Exception.Message)"
-
- if ($attempt -lt $retryCount) {
- Write-Host "Retrying in $retryDelaySeconds seconds..."
- Start-Sleep -Seconds $retryDelaySeconds
- }
- else {
- Write-Error "Failed to execute the command after $retryCount attempts."
- $ProvisioningState = $($_.Exception.Message)
- break # Max retry attempts reached, exit the loop
- }
-
- }
-
- }
-
- }
-
- $ProvisioningState = $extension.ProvisioningState
-
- [PSCustomObject]@{
- ResourceName = $resourceName
- ResourceType = $resourceType
- ProvisioningState = $ProvisioningState
- }
-
- } -ArgumentList $resourceName, $resourceType
-
- $jobs += $job
-}
-
-# Wait for all jobs to complete
-$FluxExtensionJobs = $jobs | Wait-Job | Receive-Job -Keep
-
-$jobs | Format-Table Name,PSBeginTime,PSEndTime -AutoSize
-
-# Clean up jobs
-$jobs | Remove-Job
-
-# Abort if Flux-extension fails on any cluster
-if ($FluxExtensionJobs | Where-Object ProvisioningState -ne 'Succeeded') {
-
- throw "One or more Flux-extension deployments failed - aborting"
-
-}
-
-#####################################################################
-# Deploying nginx on AKS cluster
-#####################################################################
-Write-Host "[$(Get-Date -Format t)] INFO: Deploying nginx on AKS cluster (Step 12/17)" -ForegroundColor DarkGreen
-kubectx $AgConfig.SiteConfig.Staging.FriendlyName.ToLower() | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Nginx.log")
-helm repo add $AgConfig.nginx.RepoName $AgConfig.nginx.RepoURL | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Nginx.log")
-helm repo update | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Nginx.log")
-
-helm install $AgConfig.nginx.ReleaseName $AgConfig.nginx.ChartName `
- --create-namespace `
- --namespace $AgConfig.nginx.Namespace `
- --set controller.service.annotations."service\.beta\.kubernetes\.io/azure-load-balancer-health-probe-request-path"=/healthz | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Nginx.log")
-
-#####################################################################
-# Configuring applications on the clusters using GitOps
-#####################################################################
-Write-Host "[$(Get-Date -Format t)] INFO: Configuring GitOps (Step 13/17)" -ForegroundColor DarkGreen
-
-Write-Host "[$(Get-Date -Format t)] INFO: Cleaning up images-cache namespace on all clusters" -ForegroundColor Gray
-# Cleaning up images-cache namespace on all clusters
-foreach ($cluster in $AgConfig.SiteConfig.GetEnumerator()) {
- Start-Job -Name images-cache-cleanup -ScriptBlock {
- $cluster = $using:cluster
- $clusterName = $cluster.Name.ToLower()
- Write-Host "[$(Get-Date -Format t)] INFO: Deleting images-cache namespace on cluster $clusterName" -ForegroundColor Gray
- kubectl delete namespace "images-cache" --context $clusterName
- }
-}
-
-# TODO - this looks app-specific so should perhaps be moved to the app loop
-while ($workflowStatus.status -ne "completed") {
- Write-Host "INFO: Waiting for pos-app-initial-images-build workflow to complete" -ForegroundColor Gray
- Start-Sleep -Seconds 10
- $workflowStatus = (gh run list --workflow=pos-app-initial-images-build.yml --json status) | ConvertFrom-Json
-}
-
-foreach ($cluster in $AgConfig.SiteConfig.GetEnumerator()) {
- Start-Job -Name gitops -ScriptBlock {
-
- Function Get-GitHubFiles ($githubApiUrl, $folderPath, [Switch]$excludeFolders) {
- # Force TLS 1.2 for connections to prevent TLS/SSL errors
- [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
-
- $response = Invoke-RestMethod -Uri $githubApiUrl
- $fileUrls = $response | Where-Object { $_.type -eq "file" } | Select-Object -ExpandProperty download_url
- $fileUrls | ForEach-Object {
- $fileName = $_.Substring($_.LastIndexOf("/") + 1)
- $outputFile = Join-Path $folderPath $fileName
- Invoke-RestMethod -Uri $_ -OutFile $outputFile
- }
-
- If (-not $excludeFolders) {
- $response | Where-Object { $_.type -eq "dir" } | ForEach-Object {
- $folderName = $_.name
- $path = Join-Path $folderPath $folderName
- New-Item $path -ItemType Directory -Force -ErrorAction Continue
- Get-GitHubFiles -githubApiUrl $_.url -folderPath $path
- }
- }
- }
-
- $AgConfig = $using:AgConfig
- $cluster = $using:cluster
- $site = $cluster.Value
- $siteName = $site.FriendlyName.ToLower()
- $namingGuid = $using:namingGuid
- $resourceGroup = $using:resourceGroup
- $appClonedRepo = $using:appClonedRepo
- $appsRepo = $using:appsRepo
-
- $AgConfig.AppConfig.GetEnumerator() | sort-object -Property @{Expression = { $_.value.Order }; Ascending = $true } | ForEach-Object {
- $app = $_
- $store = $cluster.value.Branch.ToLower()
- $clusterName = $cluster.value.ArcClusterName + "-$namingGuid"
- $branch = $cluster.value.Branch.ToLower()
- $configName = $app.value.GitOpsConfigName.ToLower()
- $clusterType = $cluster.value.Type
- $namespace = $app.value.Namespace
- $appName = $app.Value.KustomizationName
- $appPath = $app.Value.KustomizationPath
- $retryCount = 0
- $maxRetries = 2
-
- Write-Host "[$(Get-Date -Format t)] INFO: Creating GitOps config for $configName on $($cluster.Value.ArcClusterName+"-$namingGuid")" -ForegroundColor Gray
- if ($clusterType -eq "AKS") {
- $type = "managedClusters"
- $clusterName = $cluster.value.ArcClusterName
- }
- else {
- $type = "connectedClusters"
- }
- if ($branch -eq "main") {
- $store = "dev"
- }
-
- # Wait for Kubernetes API server to become available
- $apiServer = kubectl config view --context $cluster.Name.ToLower() --minify -o jsonpath='{.clusters[0].cluster.server}'
- $apiServerAddress = $apiServer -replace '.*https://| .*$'
- $apiServerFqdn = ($apiServerAddress -split ":")[0]
- $apiServerPort = ($apiServerAddress -split ":")[1]
-
- do {
- $result = Test-NetConnection -ComputerName $apiServerFqdn -Port $apiServerPort -WarningAction SilentlyContinue
- if ($result.TcpTestSucceeded) {
- break
- }
- else {
- Start-Sleep -Seconds 5
- }
- } while ($true)
- If ($app.Value.ConfigMaps){
- # download the config files
- foreach ($configMap in $app.value.ConfigMaps.GetEnumerator()){
- $repoPath = $configMap.value.RepoPath
- $configPath = "$configMapDir\$appPath\config\$($configMap.Name)\$branch"
- $iotHubName = $Env:iotHubHostName.replace(".azure-devices.net", "")
- $gitHubUser = $Env:gitHubUser
- $githubBranch = $Env:githubBranch
-
- New-Item -Path $configPath -ItemType Directory -Force | Out-Null
-
- $githubApiUrl = "https://api.github.com/repos/$gitHubUser/$appsRepo/$($repoPath)?ref=$branch"
- Get-GitHubFiles -githubApiUrl $githubApiUrl -folderPath $configPath
-
- # replace the IoT Hub name and the SAS Tokens with the deployment specific values
- # this is a one-off for the broker, but needs to be generalized if/when another app needs it
- If ($configMap.Name -eq "mqtt-broker-config"){
- $configFile = "$configPath\mosquitto.conf"
- $update = (Get-Content $configFile -Raw)
- $update = $update -replace "Ag-IotHub-\w*", $iotHubName
-
- foreach ($device in $site.IoTDevices) {
- $deviceId = "$device-$($site.FriendlyName)"
- $deviceSASToken = $(az iot hub generate-sas-token --device-id $deviceId --hub-name $iotHubName --resource-group $resourceGroup --duration (60 * 60 * 24 * 30) --query sas -o tsv --only-show-errors)
- $update = $update -replace "Chicago", $site.FriendlyName
- $update = $update -replace "SharedAccessSignature.*$($device).*",$deviceSASToken
- }
-
- $update | Set-Content $configFile
- }
-
- # create the namespace if needed
- If (-not (kubectl get namespace $namespace --context $siteName)){
- kubectl create namespace $namespace --context $siteName
- }
- # create the configmap
- kubectl create configmap $configMap.name --from-file=$configPath --namespace $namespace --context $siteName
- }
- }
-
- az k8s-configuration flux create `
- --cluster-name $clusterName `
- --resource-group $resourceGroup `
- --name $configName `
- --cluster-type $type `
- --url $appClonedRepo `
- --branch $branch `
- --sync-interval 5s `
- --kustomization name=$appName path=$appPath/$store prune=true retry_interval=1m `
- --timeout 10m `
- --namespace $namespace `
- --only-show-errors `
- 2>&1 | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\GitOps-$clusterName.log")
-
- do {
- $configStatus = $(az k8s-configuration flux show --name $configName --cluster-name $clusterName --cluster-type $type --resource-group $resourceGroup -o json 2>$null) | convertFrom-JSON
- if ($configStatus.ComplianceState -eq "Compliant") {
- Write-Host "[$(Get-Date -Format t)] INFO: GitOps configuration $configName is ready on $clusterName" -ForegroundColor DarkGreen | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\GitOps-$clusterName.log")
- }
- else {
- if ($configStatus.ComplianceState -ne "Non-compliant") {
- Start-Sleep -Seconds 20
- }
- elseif ($configStatus.ComplianceState -eq "Non-compliant" -and $retryCount -lt $maxRetries) {
- Start-Sleep -Seconds 20
- $configStatus = $(az k8s-configuration flux show --name $configName --cluster-name $clusterName --cluster-type $type --resource-group $resourceGroup -o json 2>$null) | convertFrom-JSON
- if ($configStatus.ComplianceState -eq "Non-compliant" -and $retryCount -lt $maxRetries) {
- $retryCount++
- Write-Host "[$(Get-Date -Format t)] INFO: Attempting to re-install $configName on $clusterName" -ForegroundColor Gray | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\GitOps-$clusterName.log")
- Write-Host "[$(Get-Date -Format t)] INFO: Deleting $configName on $clusterName" -ForegroundColor Gray | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\GitOps-$clusterName.log")
- az k8s-configuration flux delete `
- --resource-group $resourceGroup `
- --cluster-name $clusterName `
- --cluster-type $type `
- --name $configName `
- --force `
- --yes `
- --only-show-errors `
- 2>&1 | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\GitOps-$clusterName.log")
-
- Start-Sleep -Seconds 10
- Write-Host "[$(Get-Date -Format t)] INFO: Re-creating $configName on $clusterName" -ForegroundColor Gray | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\GitOps-$clusterName.log")
-
- az k8s-configuration flux create `
- --cluster-name $clusterName `
- --resource-group $resourceGroup `
- --name $configName `
- --cluster-type $type `
- --url $appClonedRepo `
- --branch $branch `
- --sync-interval 5s `
- --kustomization name=$appName path=$appPath/$store prune=true `
- --timeout 30m `
- --namespace $namespace `
- --only-show-errors `
- 2>&1 | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\GitOps-$clusterName.log")
- }
- }
- elseif ($configStatus.ComplianceState -eq "Non-compliant" -and $retryCount -eq $maxRetries) {
- Write-Host "[$(Get-Date -Format t)] ERROR: GitOps configuration $configName has failed on $clusterName. Exiting..." -ForegroundColor White -BackgroundColor Red | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\GitOps-$clusterName.log")
- break
- }
- }
- } until ($configStatus.ComplianceState -eq "Compliant")
- }
- }
-}
-
-while ($(Get-Job -Name gitops).State -eq 'Running') {
- #Write-Host "[$(Get-Date -Format t)] INFO: Waiting for GitOps configuration to complete on all clusters...waiting 60 seconds" -ForegroundColor Gray
- Receive-Job -Name gitops -WarningAction SilentlyContinue
- Start-Sleep -Seconds 60
-}
-
-Get-Job -name gitops | Remove-Job
-Write-Host "[$(Get-Date -Format t)] INFO: GitOps configuration complete." -ForegroundColor Green
-Write-Host
-
-#####################################################################
-# Deploy Kubernetes Prometheus Stack for Observability
-#####################################################################
-$AgMonitoringDir = $AgConfig.AgDirectories["AgMonitoringDir"]
-$observabilityNamespace = $AgConfig.Monitoring["Namespace"]
-$observabilityDashboards = $AgConfig.Monitoring["Dashboards"]
-$adminPassword = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($adminPassword))
-
-# Set Prod Grafana API endpoint
-$grafanaDS = $AgConfig.Monitoring["ProdURL"] + "/api/datasources"
-
-# Installing Grafana
-Write-Host "[$(Get-Date -Format t)] INFO: Installing and Configuring Observability components (Step 14/17)" -ForegroundColor DarkGreen
-Write-Host "[$(Get-Date -Format t)] INFO: Installing Grafana." -ForegroundColor Gray
-$latestRelease = (Invoke-WebRequest -Uri $websiteUrls["grafana"] | ConvertFrom-Json).tag_name.replace('v', '')
-Start-Process msiexec.exe -Wait -ArgumentList "/I $AgToolsDir\grafana-$latestRelease.windows-amd64.msi /quiet" | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Observability.log")
-
-# Update Prometheus Helm charts
-helm repo add prometheus-community $websiteUrls["prometheus"] | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Observability.log")
-helm repo update | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Observability.log")
-
-# Update Grafana Icons
-Copy-Item -Path $AgIconsDir\contoso.png -Destination "C:\Program Files\GrafanaLabs\grafana\public\img"
-Copy-Item -Path $AgIconsDir\contoso.svg -Destination "C:\Program Files\GrafanaLabs\grafana\public\img\grafana_icon.svg"
-
-Get-ChildItem -Path 'C:\Program Files\GrafanaLabs\grafana\public\build\*.js' -Recurse -File | ForEach-Object {
-(Get-Content $_.FullName) -replace 'className:u,src:"public/img/grafana_icon.svg"', 'className:u,src:"public/img/contoso.png"' | Set-Content $_.FullName
-}
-
-# Reset Grafana UI
-Get-ChildItem -Path 'C:\Program Files\GrafanaLabs\grafana\public\build\*.js' -Recurse -File | ForEach-Object {
-(Get-Content $_.FullName) -replace 'Welcome to Grafana', 'Welcome to Grafana for Contoso Supermarket Production' | Set-Content $_.FullName
-}
-
-# Reset Grafana Password
-$Env:Path += ';C:\Program Files\GrafanaLabs\grafana\bin'
-grafana-cli --homepath "C:\Program Files\GrafanaLabs\grafana" admin reset-admin-password $adminPassword | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Observability.log")
-
-# Get Grafana Admin credentials
-$adminCredentials = $AgConfig.Monitoring["AdminUser"] + ':' + $adminPassword
-$adminEncodedcredentials = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($adminCredentials))
-
-$adminHeaders = @{
- "Authorization" = ("Basic " + $adminEncodedcredentials)
- "Content-Type" = "application/json"
-}
-
-# Get Contoso User credentials
-$userCredentials = $adminUsername + ':' + $adminPassword
-$userEncodedcredentials = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($userCredentials))
-
-$userHeaders = @{
- "Authorization" = ("Basic " + $userEncodedcredentials)
- "Content-Type" = "application/json"
-}
-
-# Download dashboards
-foreach ($dashboard in $observabilityDashboards.'grafana.com') {
- $grafanaDBPath = "$AgMonitoringDir\grafana-$dashboard.json"
- $dashboardmetadata = Invoke-RestMethod -Uri https://grafana.com/api/dashboards/$dashboard/revisions
- $dashboardversion = $dashboardmetadata.items | Sort-Object revision | Select-Object -Last 1 | Select-Object -ExpandProperty revision
- Invoke-WebRequest https://grafana.com/api/dashboards/$dashboard/revisions/$dashboardversion/download -OutFile $grafanaDBPath
-}
-
-$observabilityDashboardstoImport = @()
-$observabilityDashboardstoImport += $observabilityDashboards.'grafana.com'
-$observabilityDashboardstoImport += $observabilityDashboards.'custom'
-
-Write-Host "[$(Get-Date -Format t)] INFO: Creating Prod Grafana User" -ForegroundColor Gray
-# Add Contoso Operator User
-$grafanaUserBody = @{
- name = $AgConfig.Monitoring["User"] # Display Name
- email = $AgConfig.Monitoring["Email"]
- login = $adminUsername
- password = $adminPassword
-} | ConvertTo-Json
-
-# Make HTTP request to the API to create user
-$retryCount = 10
-$retryDelay = 30
-do {
- try {
- Invoke-RestMethod -Method Post -Uri "$($AgConfig.Monitoring["ProdURL"])/api/admin/users" -Headers $adminHeaders -Body $grafanaUserBody | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Observability.log")
- $retryCount = 0
- }
- catch {
- $retryCount--
- if ($retryCount -gt 0) {
- Write-Host "[$(Get-Date -Format t)] INFO: Retrying in $retryDelay seconds..." -ForegroundColor Gray
- Start-Sleep -Seconds $retryDelay
- }
- }
-} while ($retryCount -gt 0)
-
-# Deploying Kube Prometheus Stack for stores
-$AgConfig.SiteConfig.GetEnumerator() | ForEach-Object {
- Write-Host "[$(Get-Date -Format t)] INFO: Deploying Kube Prometheus Stack for $($_.Value.FriendlyName) environment" -ForegroundColor Gray
- kubectx $_.Value.FriendlyName.ToLower() | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Observability.log")
-
- # Wait for Kubernetes API server to become available
- $apiServer = kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}'
- $apiServerAddress = $apiServer -replace '.*https://| .*$'
- $apiServerFqdn = ($apiServerAddress -split ":")[0]
- $apiServerPort = ($apiServerAddress -split ":")[1]
-
- do {
- $result = Test-NetConnection -ComputerName $apiServerFqdn -Port $apiServerPort -WarningAction SilentlyContinue
- if ($result.TcpTestSucceeded) {
- Write-Host "[$(Get-Date -Format t)] INFO: Kubernetes API server $apiServer is available" -ForegroundColor Gray
- break
- }
- else {
- Write-Host "[$(Get-Date -Format t)] INFO: Kubernetes API server $apiServer is not yet available. Retrying in 10 seconds..." -ForegroundColor Gray
- Start-Sleep -Seconds 10
- }
- } while ($true)
-
- # Install Prometheus Operator
- $helmSetValue = $_.Value.HelmSetValue -replace 'adminPasswordPlaceholder', $adminPassword
- helm install prometheus prometheus-community/kube-prometheus-stack --set $helmSetValue --namespace $observabilityNamespace --create-namespace --values "$AgMonitoringDir\$($_.Value.HelmValuesFile)" | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Observability.log")
-
- Do {
- Write-Host "[$(Get-Date -Format t)] INFO: Waiting for $($_.Value.FriendlyName) monitoring service to provision.." -ForegroundColor Gray
- Start-Sleep -Seconds 45
- $monitorIP = $(if (kubectl get $_.Value.HelmService --namespace $observabilityNamespace --output=jsonpath='{.status.loadBalancer}' | Select-String "ingress" -Quiet) { "Ready!" }Else { "Nope" })
- } while ($monitorIP -eq "Nope" )
- # Get Load Balancer IP
- $monitorLBIP = kubectl --namespace $observabilityNamespace get $_.Value.HelmService --output=jsonpath='{.status.loadBalancer.ingress[0].ip}'
-
- if ($_.Value.IsProduction) {
- Write-Host "[$(Get-Date -Format t)] INFO: Add $($_.Value.FriendlyName) Data Source to Grafana"
- # Request body with information about the data source to add
- $grafanaDSBody = @{
- name = $_.Value.FriendlyName.ToLower()
- type = 'prometheus'
- url = ("http://" + $monitorLBIP + ":9090")
- access = 'proxy'
- basicAuth = $false
- isDefault = $true
- } | ConvertTo-Json
-
- # Make HTTP request to the API
- Invoke-RestMethod -Method Post -Uri $grafanaDS -Headers $adminHeaders -Body $grafanaDSBody | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Observability.log")
- }
-
- # Add Contoso Operator User
- if (!$_.Value.IsProduction) {
- Write-Host "[$(Get-Date -Format t)] INFO: Creating $($_.Value.FriendlyName) Grafana User" -ForegroundColor Gray
- $grafanaUserBody = @{
- name = $AgConfig.Monitoring["User"] # Display Name
- email = $AgConfig.Monitoring["Email"]
- login = $adminUsername
- password = $adminPassword
- } | ConvertTo-Json
-
- # Make HTTP request to the API to create user
- $retryCount = 10
- $retryDelay = 30
-
- do {
- try {
- Invoke-RestMethod -Method Post -Uri "http://$monitorLBIP/api/admin/users" -Headers $adminHeaders -Body $grafanaUserBody | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Observability.log")
- $retryCount = 0
- }
- catch {
- $retryCount--
- if ($retryCount -gt 0) {
- Write-Host "[$(Get-Date -Format t)] INFO: Retrying in $retryDelay seconds..." -ForegroundColor Gray
- Start-Sleep -Seconds $retryDelay
- }
- }
- } while ($retryCount -gt 0)
- }
-
- Write-Host "[$(Get-Date -Format t)] INFO: Importing dashboards for $($_.Value.FriendlyName) environment" -ForegroundColor Gray
- # Add dashboards
- foreach ($dashboard in $observabilityDashboardstoImport) {
- $grafanaDBPath = "$AgMonitoringDir\grafana-$dashboard.json"
- # Replace the datasource
- $replacementParams = @{
- "\$\{DS_PROMETHEUS}" = $_.Value.GrafanaDataSource
- }
- $content = Get-Content $grafanaDBPath
- foreach ($key in $replacementParams.Keys) {
- $content = $content -replace $key, $replacementParams[$key]
- }
- # Set dashboard JSON
- $dashboardObject = $content | ConvertFrom-Json
- # Best practice is to generate a random UID, such as a GUID
- $dashboardObject.uid = [guid]::NewGuid().ToString()
-
- # Need to set this to null to let Grafana generate a new ID
- $dashboardObject.id = $null
- # Set dashboard title
- $dashboardObject.title = $_.Value.FriendlyName + ' - ' + $dashboardObject.title
- # Request body with dashboard to add
- $grafanaDBBody = @{
- dashboard = $dashboardObject
- overwrite = $true
- } | ConvertTo-Json -Depth 8
-
- if ($_.Value.IsProduction) {
- # Set Grafana Dashboard endpoint
- $grafanaDBURI = $AgConfig.Monitoring["ProdURL"] + "/api/dashboards/db"
- $grafanaDBStarURI = $AgConfig.Monitoring["ProdURL"] + "/api/user/stars/dashboard"
- }
- else {
- # Set Grafana Dashboard endpoint
- $grafanaDBURI = "http://$monitorLBIP/api/dashboards/db"
- $grafanaDBStarURI = "http://$monitorLBIP/api/user/stars/dashboard"
- }
-
- # Make HTTP request to the API
- $dashboardID=(Invoke-RestMethod -Method Post -Uri $grafanaDBURI -Headers $adminHeaders -Body $grafanaDBBody).id
-
- Invoke-RestMethod -Method Post -Uri "$grafanaDBStarURI/$dashboardID" -Headers $userHeaders | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Observability.log")
-
- }
-
-}
-Write-Host
-
-##############################################################
-# Creating bookmarks
-##############################################################
-Write-Host "[$(Get-Date -Format t)] INFO: Creating Microsoft Edge Bookmarks in Favorites Bar (Step 15/17)" -ForegroundColor DarkGreen
-$bookmarksFileName = "$AgToolsDir\Bookmarks"
-$edgeBookmarksPath = "$Env:LOCALAPPDATA\Microsoft\Edge\User Data\Default"
-
-foreach ($cluster in $AgConfig.SiteConfig.GetEnumerator()) {
- kubectx $cluster.Name.ToLower() | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Bookmarks.log")
- $services = kubectl get services --all-namespaces -o json | ConvertFrom-Json
-
- # Matching url: pos - customer
- $matchingServices = $services.items | Where-Object {
- $_.spec.ports.port -contains 5000 -and
- $_.spec.type -eq "LoadBalancer"
- }
- $posIps = $matchingServices.status.loadBalancer.ingress.ip
-
- foreach ($posIp in $posIps) {
- $output = "http://$posIp" + ':5000'
- $output | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Bookmarks.log")
-
- # Replace matching value in the Bookmarks file
- $content = Get-Content -Path $bookmarksFileName
- $newContent = $content -replace ("POS-" + $cluster.Name + "-URL-Customer"), $output
- $newContent | Set-Content -Path $bookmarksFileName
-
- Start-Sleep -Seconds 2
- }
-
- # Matching url: pos - manager
- $matchingServices = $services.items | Where-Object {
- $_.spec.ports.port -contains 81 -and
- $_.spec.type -eq "LoadBalancer"
- }
- $posIps = $matchingServices.status.loadBalancer.ingress.ip
-
- foreach ($posIp in $posIps) {
- $output = "http://$posIp" + ':81'
- $output | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Bookmarks.log")
-
- # Replace matching value in the Bookmarks file
- $content = Get-Content -Path $bookmarksFileName
- $newContent = $content -replace ("POS-" + $cluster.Name + "-URL-Manager"), $output
- $newContent | Set-Content -Path $bookmarksFileName
-
- Start-Sleep -Seconds 2
- }
-
- # Matching url: prometheus-grafana
- if ($cluster.Name -eq "Staging" -or $cluster.Name -eq "Dev") {
- $matchingServices = $services.items | Where-Object {
- $_.metadata.name -eq 'prometheus-grafana'
- }
- $grafanaIps = $matchingServices.status.loadBalancer.ingress.ip
-
- foreach ($grafanaIp in $grafanaIps) {
- $output = "http://$grafanaIp"
- $output | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Bookmarks.log")
-
- # Replace matching value in the Bookmarks file
- $content = Get-Content -Path $bookmarksFileName
- $newContent = $content -replace ("Grafana-" + $cluster.Name + "-URL"), $output
- $newContent | Set-Content -Path $bookmarksFileName
-
- Start-Sleep -Seconds 2
- }
- }
-
- # Matching url: prometheus
- $matchingServices = $services.items | Where-Object {
- $_.spec.ports.port -contains 9090 -and
- $_.spec.type -eq "LoadBalancer"
- }
- $prometheusIps = $matchingServices.status.loadBalancer.ingress.ip
-
- foreach ($prometheusIp in $prometheusIps) {
- $output = "http://$prometheusIp" + ':9090'
- $output | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Bookmarks.log")
-
- # Replace matching value in the Bookmarks file
- $content = Get-Content -Path $bookmarksFileName
- $newContent = $content -replace ("Prometheus-" + $cluster.Name + "-URL"), $output
- $newContent | Set-Content -Path $bookmarksFileName
-
- Start-Sleep -Seconds 2
- }
-}
-
-# Matching url: Agora apps forked repo
-$output = $appClonedRepo
-$output | Out-File -Append -FilePath ($AgConfig.AgDirectories["AgLogsDir"] + "\Bookmarks.log")
-
-# Replace matching value in the Bookmarks file
-$content = Get-Content -Path $bookmarksFileName
-$newContent = $content -replace "Agora-Apps-Repo-Clone-URL", $output
-$newContent = $newContent -replace "Agora-Apps-Repo-Your-Fork", "Agora Apps Repo - $githubUser"
-$newContent | Set-Content -Path $bookmarksFileName
-
-Start-Sleep -Seconds 2
-
-Copy-Item -Path $bookmarksFileName -Destination $edgeBookmarksPath -Force
-
-##############################################################
-# Pinning important directories to Quick access
-##############################################################
-Write-Host "[$(Get-Date -Format t)] INFO: Pinning important directories to Quick access (Step 16/17)" -ForegroundColor DarkGreen
-$quickAccess = new-object -com shell.application
-$quickAccess.Namespace($AgConfig.AgDirectories.AgDir).Self.InvokeVerb("pintohome")
-$quickAccess.Namespace($AgConfig.AgDirectories.AgLogsDir).Self.InvokeVerb("pintohome")
-
-
-##############################################################
-# Cleanup
-##############################################################
-Write-Host "[$(Get-Date -Format t)] INFO: Cleaning up scripts and uploading logs (Step 17/17)" -ForegroundColor DarkGreen
-# Creating Hyper-V Manager desktop shortcut
-Write-Host "[$(Get-Date -Format t)] INFO: Creating Hyper-V desktop shortcut." -ForegroundColor Gray
-Copy-Item -Path "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Administrative Tools\Hyper-V Manager.lnk" -Destination "C:\Users\All Users\Desktop" -Force
-
-
-Write-Host "[$(Get-Date -Format t)] INFO: Cleaning up images-cache job" -ForegroundColor Gray
-while ($(Get-Job -Name images-cache-cleanup).State -eq 'Running') {
- Write-Host "[$(Get-Date -Format t)] INFO: Waiting for images-cache job to complete on all clusters...waiting 60 seconds" -ForegroundColor Gray
- Receive-Job -Name images-cache-cleanup -WarningAction SilentlyContinue
- Start-Sleep -Seconds 60
-}
-Get-Job -name images-cache-cleanup | Remove-Job
-
-# Removing the LogonScript Scheduled Task
-Write-Host "[$(Get-Date -Format t)] INFO: Removing scheduled logon task so it won't run on next login." -ForegroundColor Gray
-Unregister-ScheduledTask -TaskName "AgLogonScript" -Confirm:$false
-
-# Executing the deployment logs bundle PowerShell script in a new window
-Write-Host "[$(Get-Date -Format t)] INFO: Uploading Log Bundle." -ForegroundColor Gray
-$Env:AgLogsDir = $AgConfig.AgDirectories["AgLogsDir"]
-Invoke-Expression 'cmd /c start Powershell -Command {
-$RandomString = -join ((48..57) + (97..122) | Get-Random -Count 6 | % {[char]$_})
-Write-Host "Sleeping for 5 seconds before creating deployment logs bundle..."
-Start-Sleep -Seconds 5
-Write-Host "`n"
-Write-Host "Creating deployment logs bundle"
-7z a $Env:AgLogsDir\LogsBundle-"$RandomString".zip $Env:AgLogsDir\*.log
-}'
-
-Write-Host "[$(Get-Date -Format t)] INFO: Changing Wallpaper" -ForegroundColor Gray
-$imgPath = $AgConfig.AgDirectories["AgDir"] + "\wallpaper.png"
-$code = @'
-using System.Runtime.InteropServices;
-namespace Win32{
-
- public class Wallpaper{
- [DllImport("user32.dll", CharSet=CharSet.Auto)]
- static extern int SystemParametersInfo (int uAction , int uParam , string lpvParam , int fuWinIni) ;
-
- public static void SetWallpaper(string thePath){
- SystemParametersInfo(20,0,thePath,3);
- }
- }
-}
-'@
-Add-Type $code
-[Win32.Wallpaper]::SetWallpaper($imgPath)
-
-Write-Host "[$(Get-Date -Format t)] INFO: Starting Docker Desktop" -ForegroundColor Green
-Start-Process "C:\Program Files\Docker\Docker\Docker Desktop.exe"
-
-$endTime = Get-Date
-$timeSpan = New-TimeSpan -Start $starttime -End $endtime
-Write-Host
-Write-Host "[$(Get-Date -Format t)] INFO: Deployment is complete. Deployment time was $($timeSpan.Hours) hour and $($timeSpan.Minutes) minutes. Enjoy the Agora experience!" -ForegroundColor Green
-Write-Host
-
-Stop-Transcript
\ No newline at end of file
diff --git a/azure_jumpstart_ag/retail/bicep/clientVm/clientVm.bicep b/azure_jumpstart_ag/retail/bicep/clientVm/clientVm.bicep
index 156ee2b863..5892caa99f 100644
--- a/azure_jumpstart_ag/retail/bicep/clientVm/clientVm.bicep
+++ b/azure_jumpstart_ag/retail/bicep/clientVm/clientVm.bicep
@@ -84,6 +84,9 @@ param adxClusterName string
@description('Random GUID')
param namingGuid string
+@description('The agora industry to be deployed')
+param industry string = 'retail'
+
var encodedPassword = base64(windowsAdminPassword)
var bastionName = 'Ag-Bastion'
var publicIpAddressName = deployBastion == false ? '${vmName}-PIP' : '${bastionName}-PIP'
@@ -198,7 +201,7 @@ resource vmBootstrap 'Microsoft.Compute/virtualMachines/extensions@2022-11-01' =
fileUris: [
uri(templateBaseUrl, 'artifacts/PowerShell/Bootstrap.ps1')
]
- commandToExecute: 'powershell.exe -ExecutionPolicy Bypass -File Bootstrap.ps1 -adminUsername ${windowsAdminUsername} -adminPassword ${encodedPassword} -spnClientId ${spnClientId} -spnClientSecret ${spnClientSecret} -spnTenantId ${spnTenantId} -spnAuthority ${spnAuthority} -subscriptionId ${subscription().subscriptionId} -resourceGroup ${resourceGroup().name} -azureLocation ${location} -stagingStorageAccountName ${storageAccountName} -workspaceName ${workspaceName} -templateBaseUrl ${templateBaseUrl} -githubUser ${githubUser} -aksStagingClusterName ${aksStagingClusterName} -iotHubHostName ${iotHubHostName} -acrName ${acrName} -cosmosDBName ${cosmosDBName} -cosmosDBEndpoint ${cosmosDBEndpoint} -rdpPort ${rdpPort} -githubAccount ${githubAccount} -githubBranch ${githubBranch} -githubPAT ${githubPAT} -adxClusterName ${adxClusterName} -namingGuid ${namingGuid}'
+ commandToExecute: 'powershell.exe -ExecutionPolicy Bypass -File Bootstrap.ps1 -adminUsername ${windowsAdminUsername} -adminPassword ${encodedPassword} -spnClientId ${spnClientId} -spnClientSecret ${spnClientSecret} -spnTenantId ${spnTenantId} -spnAuthority ${spnAuthority} -subscriptionId ${subscription().subscriptionId} -resourceGroup ${resourceGroup().name} -azureLocation ${location} -stagingStorageAccountName ${storageAccountName} -workspaceName ${workspaceName} -templateBaseUrl ${templateBaseUrl} -githubUser ${githubUser} -aksStagingClusterName ${aksStagingClusterName} -iotHubHostName ${iotHubHostName} -acrName ${acrName} -cosmosDBName ${cosmosDBName} -cosmosDBEndpoint ${cosmosDBEndpoint} -rdpPort ${rdpPort} -githubAccount ${githubAccount} -githubBranch ${githubBranch} -githubPAT ${githubPAT} -adxClusterName ${adxClusterName} -namingGuid ${namingGuid} -industry ${industry}'
}
}
}
diff --git a/azure_jumpstart_ag/retail/bicep/main.azd.bicep b/azure_jumpstart_ag/retail/bicep/main.azd.bicep
index 7b60867233..cb6680b118 100644
--- a/azure_jumpstart_ag/retail/bicep/main.azd.bicep
+++ b/azure_jumpstart_ag/retail/bicep/main.azd.bicep
@@ -23,7 +23,7 @@ param location string = ''
param namingGuid string = toLower(substring(newGuid(), 0, 5))
@description('Username for Windows account')
-param windowsAdminUsername string = ''
+param windowsAdminUsername string = 'Agora'
@description('Password for Windows account. Password must have 3 of the following: 1 lower case character, 1 upper case character, 1 number, and 1 special character. The value must be between 12 and 123 characters long')
@minLength(12)
@@ -87,7 +87,10 @@ param acrName string = 'agacr${namingGuid}'
@description('Override default RDP port using this parameter. Default is 3389. No changes will be made to the client VM.')
param rdpPort string = '3389'
-var templateBaseUrl = 'https://raw.githubusercontent.com/${githubAccount}/azure_arc/${githubBranch}/azure_jumpstart_ag/retail/'
+@description('The agora industry to be deployed')
+param industry string = 'retail'
+
+var templateBaseUrl = 'https://raw.githubusercontent.com/${githubAccount}/azure_arc/${githubBranch}/azure_jumpstart_ag/'
targetScope = 'subscription'
@@ -135,8 +138,8 @@ module kubernetesDeployment 'kubernetes/aks.bicep' = {
spnClientId: spnClientId
spnClientSecret: spnClientSecret
location: location
- acrName: acrName
sshRSAPublicKey: sshRSAPublicKey
+ acrName: acrName
}
}
@@ -167,6 +170,7 @@ module clientVmDeployment 'clientVm/clientVm.bicep' = {
rdpPort: rdpPort
adxClusterName: adxClusterName
namingGuid: namingGuid
+ industry: industry
}
}
diff --git a/azure_jumpstart_ag/retail/bicep/main.bicep b/azure_jumpstart_ag/retail/bicep/main.bicep
index a39a4c2bcc..cd8a34fb62 100644
--- a/azure_jumpstart_ag/retail/bicep/main.bicep
+++ b/azure_jumpstart_ag/retail/bicep/main.bicep
@@ -80,7 +80,10 @@ param acrName string = 'agacr${namingGuid}'
@description('Override default RDP port using this parameter. Default is 3389. No changes will be made to the client VM.')
param rdpPort string = '3389'
-var templateBaseUrl = 'https://raw.githubusercontent.com/${githubAccount}/azure_arc/${githubBranch}/azure_jumpstart_ag/retail/'
+@description('The agora industry to be deployed')
+param industry string = 'retail'
+
+var templateBaseUrl = 'https://raw.githubusercontent.com/${githubAccount}/azure_arc/${githubBranch}/azure_jumpstart_ag/'
module mgmtArtifactsAndPolicyDeployment 'mgmt/mgmtArtifacts.bicep' = {
name: 'mgmtArtifactsAndPolicyDeployment'
@@ -148,6 +151,7 @@ module clientVmDeployment 'clientVm/clientVm.bicep' = {
rdpPort: rdpPort
adxClusterName: adxClusterName
namingGuid: namingGuid
+ industry: industry
}
}
diff --git a/azure_jumpstart_ag/retail/scripts/preprovision.ps1 b/azure_jumpstart_ag/retail/scripts/preprovision.ps1
index 43f7e24bf5..7d1be08c4b 100644
--- a/azure_jumpstart_ag/retail/scripts/preprovision.ps1
+++ b/azure_jumpstart_ag/retail/scripts/preprovision.ps1
@@ -85,7 +85,7 @@ Function Get-AzAvailableLocations ($location, $skuFriendlyNames, $minCores = 0)
}
Function Get-AzAvailablePublicIpAddress ($location, $subscriptionId, $minPublicIP = 0) {
-
+
$accessToken = az account get-access-token --query accessToken -o tsv
$headers = @{
"Authorization" = "Bearer $accessToken"
diff --git a/azure_jumpstart_hcibox/bicep/main.parameters.json b/azure_jumpstart_hcibox/bicep/main.parameters.json
index 25a48972db..4f19e63e35 100644
--- a/azure_jumpstart_hcibox/bicep/main.parameters.json
+++ b/azure_jumpstart_hcibox/bicep/main.parameters.json
@@ -12,7 +12,7 @@
"value": ""
},
"spnProviderId": {
- "value": ""
},
"windowsAdminUsername": {
"value": "arcdemo"