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"