diff --git a/azure_edge_iot_ops_jumpstart/aio_manufacturing/.gitignore b/azure_edge_iot_ops_jumpstart/aio_manufacturing/.gitignore new file mode 100644 index 0000000000..347ea51b72 --- /dev/null +++ b/azure_edge_iot_ops_jumpstart/aio_manufacturing/.gitignore @@ -0,0 +1 @@ +.azure \ No newline at end of file diff --git a/azure_edge_iot_ops_jumpstart/aio_manufacturing/azure.yaml b/azure_edge_iot_ops_jumpstart/aio_manufacturing/azure.yaml new file mode 100644 index 0000000000..60048b2c12 --- /dev/null +++ b/azure_edge_iot_ops_jumpstart/aio_manufacturing/azure.yaml @@ -0,0 +1,25 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json + +name: azure_jumpstart_aio +metadata: + template: azure_jumpstart_aio@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 + predown: + shell: pwsh + run: ./scripts/predown.ps1 + continueOnError: false + interactive: true \ No newline at end of file diff --git a/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/artifacts/PowerShell/Bootstrap.ps1 b/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/artifacts/PowerShell/Bootstrap.ps1 new file mode 100644 index 0000000000..ca82d7787c --- /dev/null +++ b/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/artifacts/PowerShell/Bootstrap.ps1 @@ -0,0 +1,277 @@ +param ( + [string]$adminUsername, + [string]$adminPassword, + [string]$spnClientId, + [string]$spnClientSecret, + [string]$spnTenantId, + [string]$spnObjectId, + [string]$subscriptionId, + [string]$location, + [string]$templateBaseUrl, + [string]$resourceGroup, + [string]$windowsNode, + [string]$kubernetesDistribution, + [string]$customLocationRPOID, + [string]$githubAccount, + [string]$githubBranch, + [string]$adxClusterName, + [string]$rdpPort +) + +[System.Environment]::SetEnvironmentVariable('adminUsername', $adminUsername, [System.EnvironmentVariableTarget]::Machine) +[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('spnTenantId', $spnTenantId, [System.EnvironmentVariableTarget]::Machine) +[System.Environment]::SetEnvironmentVariable('spnObjectId', $spnObjectId, [System.EnvironmentVariableTarget]::Machine) +[System.Environment]::SetEnvironmentVariable('resourceGroup', $resourceGroup, [System.EnvironmentVariableTarget]::Machine) +[System.Environment]::SetEnvironmentVariable('location', $location, [System.EnvironmentVariableTarget]::Machine) +[System.Environment]::SetEnvironmentVariable('subscriptionId', $subscriptionId, [System.EnvironmentVariableTarget]::Machine) +[System.Environment]::SetEnvironmentVariable('templateBaseUrl', $templateBaseUrl, [System.EnvironmentVariableTarget]::Machine) +[System.Environment]::SetEnvironmentVariable('kubernetesDistribution', $kubernetesDistribution, [System.EnvironmentVariableTarget]::Machine) +[System.Environment]::SetEnvironmentVariable('windowsNode', $windowsNode, [System.EnvironmentVariableTarget]::Machine) +[System.Environment]::SetEnvironmentVariable('customLocationRPOID', $customLocationRPOID, [System.EnvironmentVariableTarget]::Machine) +[System.Environment]::SetEnvironmentVariable('githubAccount', $githubAccount, [System.EnvironmentVariableTarget]::Machine) +[System.Environment]::SetEnvironmentVariable('githubBranch', $githubBranch, [System.EnvironmentVariableTarget]::Machine) +[System.Environment]::SetEnvironmentVariable('adxClusterName', $adxClusterName, [System.EnvironmentVariableTarget]::Machine) + +############################################################## +# Change RDP Port +############################################################## +Write-Host "RDP port number from configuration is $rdpPort" +if (($rdpPort -ne $null) -and ($rdpPort -ne "") -and ($rdpPort -ne "3389")) { + Write-Host "Configuring RDP port number to $rdpPort" + $TSPath = 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server' + $RDPTCPpath = $TSPath + '\Winstations\RDP-Tcp' + Set-ItemProperty -Path $TSPath -name 'fDenyTSConnections' -Value 0 + + # RDP port + $portNumber = (Get-ItemProperty -Path $RDPTCPpath -Name 'PortNumber').PortNumber + Write-Host "Current RDP PortNumber: $portNumber" + if (!($portNumber -eq $rdpPort)) { + Write-Host Setting RDP PortNumber to $rdpPort + Set-ItemProperty -Path $RDPTCPpath -name 'PortNumber' -Value $rdpPort + Restart-Service TermService -force + } + + #Setup firewall rules + if ($rdpPort -eq 3389) { + netsh advfirewall firewall set rule group="remote desktop" new Enable=Yes + } + else { + $systemroot = get-content env:systemroot + netsh advfirewall firewall add rule name="Remote Desktop - Custom Port" dir=in program=$systemroot\system32\svchost.exe service=termservice action=allow protocol=TCP localport=$RDPPort enable=yes + } + + Write-Host "RDP port configuration complete." +} + + +############################################################## +# Download configuration data file and declaring directories +############################################################## +$ConfigurationDataFile = "C:\Temp\aioConfig.psd1" +Invoke-WebRequest ($templateBaseUrl + "artifacts/PowerShell/aioConfig.psd1") -OutFile $ConfigurationDataFile + +$aioConfig = Import-PowerShellDataFile -Path $ConfigurationDataFile +$aioDirectory = $aioConfig.aioDirectories["aioDir"] +$aioToolsDir = $aioConfig.aioDirectories["aioToolsDir"] +$aioPowerShellDir = $aioConfig.aioDirectories["aioPowerShellDir"] +$aioDataExplorer = $aioConfig.aioDirectories["aioDataExplorer"] +$websiteUrls = $aioConfig.URLs +$mqttExplorerReleasesUrl = $websiteUrls["mqttExplorerReleases"] + +function BITSRequest { + Param( + [Parameter(Mandatory = $True)] + [hashtable]$Params + ) + $url = $Params['Uri'] + $filename = $Params['Filename'] + $download = Start-BitsTransfer -Source $url -Destination $filename -Asynchronous + $ProgressPreference = "Continue" + while ($download.JobState -ne "Transferred") { + if ($download.JobState -eq "TransientError") { + Get-BitsTransfer $download.name | Resume-BitsTransfer -Asynchronous + } + [int] $dlProgress = ($download.BytesTransferred / $download.BytesTotal) * 100; + Write-Progress -Activity "Downloading File $filename..." -Status "$dlProgress% Complete:" -PercentComplete $dlProgress; + } + Complete-BitsTransfer $download.JobId + Write-Progress -Activity "Downloading File $filename..." -Status "Ready" -Completed + $ProgressPreference = "SilentlyContinue" +} + + +############################################################## +# Creating aio paths +############################################################## +Write-Output "Creating aio paths" +foreach ($path in $aioConfig.aioDirectories.values) { + Write-Output "Creating path $path" + New-Item -ItemType Directory $path -Force +} + +Start-Transcript -Path ($aioConfig.aioDirectories["aioLogsDir"] + "\Bootstrap.log") + +$ErrorActionPreference = "SilentlyContinue" + +############################################################## +# Get latest Grafana OSS release +############################################################## +$latestRelease = (Invoke-RestMethod -Uri $websiteUrls["grafana"]).tag_name.replace('v', '') + +############################################################## +# Download artifacts +############################################################## +[System.Environment]::SetEnvironmentVariable('aioConfigPath', "$aioPowerShellDir\aioConfig.psd1", [System.EnvironmentVariableTarget]::Machine) +Invoke-WebRequest ($templateBaseUrl + "artifacts/PowerShell/LogonScript.ps1") -OutFile "$aioPowerShellDir\LogonScript.ps1" +Invoke-WebRequest ($templateBaseUrl + "artifacts/PowerShell/aioConfig.psd1") -OutFile "$aioPowerShellDir\aioConfig.psd1" +Invoke-WebRequest ($templateBaseUrl + "artifacts/Settings/mq_bridge_eventgrid.yml") -OutFile "$aioToolsDir\mq_bridge_eventgrid.yml" +Invoke-WebRequest ($templateBaseUrl + "artifacts/Settings/mqtt_simulator.yml") -OutFile "$aioToolsDir\mqtt_simulator.yml" +Invoke-WebRequest ($templateBaseUrl + "artifacts/Settings/mq_cloudConnector.yml") -OutFile "$aioToolsDir\mq_cloudConnector.yml" +Invoke-WebRequest ($templateBaseUrl + "artifacts/Settings/influxdb.yml") -OutFile "$aioToolsDir\influxdb.yml" +Invoke-WebRequest ($templateBaseUrl + "artifacts/Settings/influxdb_setup.yml") -OutFile "$aioToolsDir\influxdb_setup.yml" +Invoke-WebRequest ($templateBaseUrl + "artifacts/Settings/influxdb-configmap.yml") -OutFile "$aioToolsDir\influxdb-configmap.yml" +Invoke-WebRequest ($templateBaseUrl + "artifacts/Settings/influxdb-import-dashboard.yml") -OutFile "$aioToolsDir\influxdb-import-dashboard.yml" +Invoke-WebRequest ($templateBaseUrl + "artifacts/Settings/mqtt_listener.yml") -OutFile "$aioToolsDir\mqtt_listener.yml" +Invoke-WebRequest ($templateBaseUrl + "artifacts/Settings/mqtt_explorer_settings.json") -OutFile "$aioToolsDir\mqtt_explorer_settings.json" +Invoke-WebRequest ($templateBaseUrl + "artifacts/Settings/Bookmarks") -OutFile "$aioToolsDir\Bookmarks" +Invoke-WebRequest ($templateBaseUrl + "artifacts/adx_dashboard/dashboard.json") -OutFile "$aioDataExplorer\dashboard.json" + +#Invoke-WebRequest "https://raw.githubusercontent.com/microsoft/azure_arc/main/img/jumpstart_wallpaper.png" -OutFile "$aioDirectory\wallpaper.png" + +Invoke-WebRequest "https://raw.githubusercontent.com/azure/arc_jumpstart_docs/canary/img/wallpaper/jumpstart_title_wallpaper_dark.png" -OutFile "$aioDirectory\wallpaper.png" + +############################################################## +# Testing connectivity to required URLs +############################################################## + +Function Test-Url($url, $maxRetries = 3, $retryDelaySeconds = 5) { + $retryCount = 0 + do { + try { + $response = Invoke-WebRequest -Uri $url -Method Head -UseBasicParsing + $statusCode = $response.StatusCode + + if ($statusCode -eq 200) { + Write-Host "$url is reachable." + break # Break out of the loop if website is reachable + } + else { + Write-Host "$url is unreachable. Status code: $statusCode" + } + } + catch { + Write-Host "An error occurred while testing the website: $url - $_" + } + + $retryCount++ + if ($retryCount -le $maxRetries) { + Write-Host "Retrying in $retryDelaySeconds seconds..." + Start-Sleep -Seconds $retryDelaySeconds + } + } while ($retryCount -le $maxRetries) + + if ($retryCount -gt $maxRetries) { + Write-Host "Exceeded maximum number of retries. Exiting..." + exit 1 # Stop script execution if maximum retries reached + } +} + +foreach ($url in $websiteUrls.Values) { + $maxRetries = 3 + $retryDelaySeconds = 5 + + Test-Url $url -maxRetries $maxRetries -retryDelaySeconds $retryDelaySeconds +} + +############################################################## +# Install Chocolatey packages +############################################################## +$maxRetries = 3 +$retryDelay = 30 # seconds + +$retryCount = 0 +$success = $false + +while (-not $success -and $retryCount -lt $maxRetries) { + try { + Write-Host "Installing Chocolatey packages" + try { + choco config get cacheLocation + } + catch { + Write-Output "Chocolatey not detected, trying to install now" + Invoke-Expression ((New-Object System.Net.WebClient).DownloadString($aioConfig.URLs.chocoInstallScript)) + } + + Write-Host "Chocolatey packages specified" + + foreach ($app in $aioConfig.ChocolateyPackagesList) { + Write-Host "Installing $app" + & choco install $app /y -Force | Write-Output + } + + # If the command succeeds, set $success to $true to exit the loop + $success = $true + } + catch { + # If an exception occurs, increment the retry count + $retryCount++ + + # If the maximum number of retries is not reached yet, display an error message + if ($retryCount -lt $maxRetries) { + Write-Host "Attempt $retryCount failed. Retrying in $retryDelay seconds..." + Start-Sleep -Seconds $retryDelay + } + else { + Write-Host "All attempts failed. Exiting..." + exit 1 # Stop script execution if maximum retries reached + } + } +} + +############################################################## +# Install Azure CLI (64-bit not available via Chocolatey) +############################################################## +$ProgressPreference = 'SilentlyContinue' +Invoke-WebRequest -Uri https://aka.ms/installazurecliwindowsx64 -OutFile .\AzureCLI.msi +Start-Process msiexec.exe -Wait -ArgumentList '/I AzureCLI.msi /quiet' +Remove-Item .\AzureCLI.msi + +# Enable VirtualMachinePlatform feature, the vm reboot will be done in DSC extension +Enable-WindowsOptionalFeature -Online -FeatureName VirtualMachinePlatform -NoRestart + +# Disable Microsoft Edge sidebar +$RegistryPath = 'HKLM:\SOFTWARE\Policies\Microsoft\Edge' +$Name = 'HubsSidebarEnabled' +$Value = '00000000' +# Create the key if it does not exist +If (-NOT (Test-Path $RegistryPath)) { + New-Item -Path $RegistryPath -Force | Out-Null +} +New-ItemProperty -Path $RegistryPath -Name $Name -Value $Value -PropertyType DWORD -Force + +# Disable Microsoft Edge first-run Welcome screen +$RegistryPath = 'HKLM:\SOFTWARE\Policies\Microsoft\Edge' +$Name = 'HideFirstRunExperience' +$Value = '00000001' +# Create the key if it does not exist +If (-NOT (Test-Path $RegistryPath)) { + New-Item -Path $RegistryPath -Force | Out-Null +} +New-ItemProperty -Path $RegistryPath -Name $Name -Value $Value -PropertyType DWORD -Force + +# Creating scheduled task for LogonScript.ps1 +$Trigger = New-ScheduledTaskTrigger -AtLogOn +$Action = New-ScheduledTaskAction -Execute "PowerShell.exe" -Argument "$aioPowerShellDir\LogonScript.ps1" +Register-ScheduledTask -TaskName "LogonScript" -Trigger $Trigger -User $adminUsername -Action $Action -RunLevel "Highest" -Force + +# Disabling Windows Server Manager Scheduled Task +Get-ScheduledTask -TaskName ServerManager | Disable-ScheduledTask + +# Clean up Bootstrap.log +Stop-Transcript +$logSuppress = Get-Content ($aioConfig.aioDirectories["aioLogsDir"] + "\Bootstrap.log") | Where-Object { $_ -notmatch "Host Application: powershell.exe" } +$logSuppress | Set-Content ($aioConfig.aioDirectories["aioLogsDir"] + "\Bootstrap.log") -Force \ No newline at end of file diff --git a/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/artifacts/PowerShell/LogonScript.ps1 b/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/artifacts/PowerShell/LogonScript.ps1 new file mode 100644 index 0000000000..9818054f09 --- /dev/null +++ b/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/artifacts/PowerShell/LogonScript.ps1 @@ -0,0 +1,713 @@ +$ProgressPreference = "SilentlyContinue" +Set-PSDebug -Strict + +##################################################################### +# Initialize the environment +##################################################################### +$aioConfig = Import-PowerShellDataFile -Path $Env:aioConfigPath +$aioTempDir = $aioConfig.aioDirectories["aioTempDir"] +$aioToolsDir = $aioConfig.aioDirectories["aioToolsDir"] +$aioDataExplorerDir = $aioConfig.aioDirectories["aioDataExplorer"] +$websiteUrls = $aioConfig.URLs +$aksEEReleasesUrl = $websiteUrls["aksEEReleases"] +$mqttuiReleasesUrl = $websiteUrls["mqttuiReleases"] +$mqttExplorerReleasesUrl = $websiteUrls["mqttExplorerReleases"] +$resourceGroup = $Env:resourceGroup +$location = $Env:location +$spnClientId = $Env:spnClientId +$spnClientSecret = $Env:spnClientSecret +$spnTenantId = $Env:spnTenantId +$spnObjectId = $Env:spnObjectId +$subscriptionId = $Env:subscriptionId +$customLocationRPOID = $Env:customLocationRPOID +$adminUsername = $Env:adminUsername +$adminPassword = $Env:adminPassword +$githubAccount = $Env:githubAccount +$githubBranch = $Env:githubBranch +$adxClusterName = $Env:adxClusterName +$aioNamespace = "azure-iot-operations" +$aideuserConfig = $aioConfig.AKSEEConfig["aideuserConfig"] +$aksedgeConfig = $aioConfig.AKSEEConfig["aksedgeConfig"] +$aksEdgeNodes = $aioConfig.AKSEEConfig["Nodes"] +$aksEdgeDeployModules = $aioConfig.AKSEEConfig["aksEdgeDeployModules"] +$AksEdgeRemoteDeployVersion = $aioConfig.AKSEEConfig["AksEdgeRemoteDeployVersion"] +$clusterLogSize = $aioConfig.AKSEEConfig["clusterLogSize"] + + +Start-Transcript -Path ($aioConfig.aioDirectories["aioLogsDir"] + "\LogonScript.log") +$startTime = Get-Date + +New-Variable -Name AksEdgeRemoteDeployVersion -Value $AksEdgeRemoteDeployVersion -Option Constant -ErrorAction SilentlyContinue + +if (! [Environment]::Is64BitProcess) { + Write-Host "[$(Get-Date -Format t)] Error: Run this in 64bit Powershell session" -ForegroundColor Red + exit -1 +} + +if ($env:kubernetesDistribution -eq "k8s") { + $productName = "AKS Edge Essentials - K8s" + $networkplugin = "calico" +} +else { + $productName = "AKS Edge Essentials - K3s" + $networkplugin = "flannel" +} + +############################################################## +# AKS EE setup +############################################################## +Write-Host "[$(Get-Date -Format t)] INFO: Fetching the latest AKS Edge Essentials release." -ForegroundColor DarkGreen +$latestReleaseTag = (Invoke-WebRequest $aksEEReleasesUrl | ConvertFrom-Json)[0].tag_name +$AKSEEReleaseDownloadUrl = "https://github.com/Azure/AKS-Edge/archive/refs/tags/$latestReleaseTag.zip" +$output = Join-Path $aioTempDir "$latestReleaseTag.zip" +Invoke-WebRequest $AKSEEReleaseDownloadUrl -OutFile $output +Expand-Archive $output -DestinationPath $aioTempDir -Force +$AKSEEReleaseConfigFilePath = "$aioTempDir\AKS-Edge-$latestReleaseTag\tools\aksedge-config.json" +$jsonContent = Get-Content -Raw -Path $AKSEEReleaseConfigFilePath | ConvertFrom-Json +$schemaVersionAksEdgeConfig = $jsonContent.SchemaVersion +# Clean up the downloaded release files +Remove-Item -Path $output -Force +Remove-Item -Path "$aioTempDir\AKS-Edge-$latestReleaseTag" -Force -Recurse + +# Create AKSEE configuration files +Write-host "[$(Get-Date -Format t)] INFO: Creating AKS Edge Essentials configuration files" -ForegroundColor DarkGreen + +$aideuserConfig.AksEdgeProduct = $productName +$aideuserConfig.Azure.Location = $location +$aideuserConfig.Azure.SubscriptionId = $subscriptionId +$aideuserConfig.Azure.TenantId = $spnTenantId +$aideuserConfig.Azure.ResourceGroupName = $resourceGroup +$aideuserConfig = $aideuserConfig | ConvertTo-Json -Depth 20 + + +$aksedgeConfig.SchemaVersion = $schemaVersionAksEdgeConfig +$aksedgeConfig.Network.NetworkPlugin = $networkplugin + +if ($env:windowsNode -eq $true) { + $aksedgeConfig.Machines += @{ + 'LinuxNode' = $aksEdgeNodes["LinuxNode"] + 'WindowsNode' = $aksEdgeNodes["WindowsNode"] + } +} +else { + $aksedgeConfig.Machines += @{ + 'LinuxNode' = $aksEdgeNodes["LinuxNode"] + } +} + +$aksedgeConfig = $aksedgeConfig | ConvertTo-Json -Depth 20 + +Set-ExecutionPolicy Bypass -Scope Process -Force +# Download the AksEdgeDeploy modules from Azure/AksEdge +$url = "https://github.com/Azure/AKS-Edge/archive/$aksEdgeDeployModules.zip" +$zipFile = "$aksEdgeDeployModules.zip" +$installDir = "$aioToolsDir\AksEdgeScript" +$workDir = "$installDir\AKS-Edge-main" + +if (-not (Test-Path -Path $installDir)) { + Write-Host "Creating $installDir..." + New-Item -Path "$installDir" -ItemType Directory | Out-Null +} + +Push-Location $installDir + +Write-Host "`n" +Write-Host "[$(Get-Date -Format t)] INFO: Installing AKS Edge Essentials, this will take a few minutes." -ForegroundColor DarkGreen +Write-Host "`n" + +try { + function download2() { $ProgressPreference = "SilentlyContinue"; Invoke-WebRequest -Uri $url -OutFile $installDir\$zipFile } + download2 +} +catch { + Write-Host "[$(Get-Date -Format t)] ERROR: Downloading Aide Powershell Modules failed" -ForegroundColor Red + Stop-Transcript | Out-Null + Pop-Location + exit -1 +} + +if (!(Test-Path -Path "$workDir")) { + Expand-Archive -Path $installDir\$zipFile -DestinationPath "$installDir" -Force +} + +$aidejson = (Get-ChildItem -Path "$workDir" -Filter aide-userconfig.json -Recurse).FullName +Set-Content -Path $aidejson -Value $aideuserConfig -Force +$aksedgejson = (Get-ChildItem -Path "$workDir" -Filter aksedge-config.json -Recurse).FullName +Set-Content -Path $aksedgejson -Value $aksedgeConfig -Force + +$aksedgeShell = (Get-ChildItem -Path "$workDir" -Filter AksEdgeShell.ps1 -Recurse).FullName +. $aksedgeShell + +# Download, install and deploy AKS EE +Write-Host "[$(Get-Date -Format t)] INFO: Step 2: Download, install and deploy AKS Edge Essentials" -ForegroundColor DarkGray +# invoke the workflow, the json file already stored above. +$retval = Start-AideWorkflow -jsonFile $aidejson +# report error via Write-Error for Intune to show proper status +if ($retval) { + Write-Host "[$(Get-Date -Format t)] INFO: Deployment Successful. " -ForegroundColor Green +} +else { + Write-Host -Message "[$(Get-Date -Format t)] Error: Deployment failed" -Category OperationStopped + Stop-Transcript | Out-Null + Pop-Location + exit -1 +} + +if ($env:windowsNode -eq $true) { + # Get a list of all nodes in the cluster + $nodes = kubectl get nodes -o json | ConvertFrom-Json + + # Loop through each node and check the OSImage field + foreach ($node in $nodes.items) { + $os = $node.status.nodeInfo.osImage + if ($os -like '*windows*') { + # If the OSImage field contains "windows", assign the "worker" role + kubectl label nodes $node.metadata.name node-role.kubernetes.io/worker=worker + } + } +} + +Write-Host "`n" +Write-Host "[$(Get-Date -Format t)] INFO: Checking kubernetes nodes" -ForegroundColor DarkGray +Write-Host "`n" +kubectl get nodes -o wide +Write-Host "`n" + +# az version +az -v + +Write-Host "[$(Get-Date -Format t)] INFO: Configuring cluster log size" -ForegroundColor DarkGray +Invoke-AksEdgeNodeCommand "sudo find /var/log -type f -exec truncate -s ${clusterLogSize} {} +" +Write-Host "`n" + +##################################################################### +# Setup Azure CLI +##################################################################### +Write-Host "[$(Get-Date -Format t)] INFO: Configuring Azure CLI" -ForegroundColor DarkGreen +$cliDir = New-Item -Path ($aioConfig.aioDirectories["aioLogsDir"] + "\.cli\") -Name ".aio" -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 DarkGray +az login --service-principal --username $spnClientID --password $spnClientSecret --tenant $spnTenantId +az account set --subscription $subscriptionId + +# Installing Azure CLI extensions +az extension add --name connectedk8s --version 1.3.17 + +# Making extension install dynamic +if ($aioConfig.AzCLIExtensions.Count -ne 0) { + Write-Host "[$(Get-Date -Format t)] INFO: Installing Azure CLI extensions: " ($aioConfig.AzCLIExtensions -join ', ') -ForegroundColor DarkGray + az config set extension.use_dynamic_install=yes_without_prompt --only-show-errors + # Installing Azure CLI extensions + foreach ($extension in $aioConfig.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" -ForegroundColor DarkGreen +$azurePassword = ConvertTo-SecureString $spnClientSecret -AsPlainText -Force +$psCred = New-Object System.Management.Automation.PSCredential($spnClientID , $azurePassword) +Connect-AzAccount -Credential $psCred -TenantId $spnTenantId -ServicePrincipal +Set-AzContext -Subscription $subscriptionId + +# Install PowerShell modules +if ($aioConfig.PowerShellModules.Count -ne 0) { + Write-Host "[$(Get-Date -Format t)] INFO: Installing PowerShell modules: " ($aioConfig.PowerShellModules -join ', ') -ForegroundColor DarkGray + Install-PackageProvider -Name NuGet -Confirm:$false -Force + foreach ($module in $aioConfig.PowerShellModules) { + Install-Module -Name $module -Force -Confirm:$false + } +} + +# Register Azure providers +if ($aioConfig.AzureProviders.Count -ne 0) { + Write-Host "[$(Get-Date -Format t)] INFO: Registering Azure providers in the current subscription: " ($aioConfig.AzureProviders -join ', ') -ForegroundColor DarkGray + foreach ($provider in $aioConfig.AzureProviders) { + Register-AzResourceProvider -ProviderNamespace $provider + } +} +Write-Host "[$(Get-Date -Format t)] INFO: Azure PowerShell configuration and resource provider registration complete!" -ForegroundColor Green +Write-Host + +##################################################################### +# Onboarding cluster to Azure Arc +##################################################################### + +# Onboarding the cluster to Azure Arc +Write-Host "[$(Get-Date -Format t)] INFO: Onboarding the AKS Edge Essentials cluster to Azure Arc..." -ForegroundColor DarkGreen +Write-Host "`n" + +$kubectlMonShell = Start-Process -PassThru PowerShell { for (0 -lt 1) { kubectl get pod -A | Sort-Object -Descending; Start-Sleep -Seconds 5; Clear-Host } } + +#Tag +$clusterId = $(kubectl get configmap -n aksedge aksedge -o jsonpath="{.data.clustername}") + +$guid = ([System.Guid]::NewGuid()).ToString().subString(0, 5).ToLower() +$arcClusterName = "aio-$guid" + + +if ($env:kubernetesDistribution -eq "k8s") { + az connectedk8s connect --name $arcClusterName ` + --resource-group $resourceGroup ` + --location $location ` + --distribution aks_edge_k8s ` + --tags "Project=jumpstart_azure_arc_k8s" "ClusterId=$clusterId" ` + --correlation-id "d009f5dd-dba8-4ac7-bac9-b54ef3a6671a" +} +else { + az connectedk8s connect --name $arcClusterName ` + --resource-group $resourceGroup ` + --location $location ` + --distribution aks_edge_k3s ` + --tags "Project=jumpstart_azure_arc_k8s" "ClusterId=$clusterId" ` + --correlation-id "d009f5dd-dba8-4ac7-bac9-b54ef3a6671a" +} + +Write-Host "`n" +Write-Host "[$(Get-Date -Format t)] INFO: Create Azure Monitor for containers Kubernetes extension instance" -ForegroundColor DarkGray +Write-Host "`n" + +# Deploying Azure log-analytics workspace +$workspaceName = ($arcClusterName).ToLower() +$workspaceResourceId = az monitor log-analytics workspace create ` + --resource-group $resourceGroup ` + --workspace-name "$workspaceName-law" ` + --query id -o tsv + +# Deploying Azure Monitor for containers Kubernetes extension instance +Write-Host "`n" +az k8s-extension create --name "azuremonitor-containers" ` + --cluster-name $arcClusterName ` + --resource-group $resourceGroup ` + --cluster-type connectedClusters ` + --extension-type Microsoft.AzureMonitor.Containers ` + --configuration-settings logAnalyticsWorkspaceResourceID=$workspaceResourceId + +# 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 +az connectedk8s enable-features --name $arcClusterName ` + --resource-group $resourceGroup ` + --features cluster-connect custom-locations ` + --custom-locations-oid $customLocationRPOID ` + --only-show-errors + + +############################################################## +# Preparing cluster for aio +############################################################## + +# Kill the open PowerShell monitoring kubectl get pods +Stop-Process -Id $kubectlMonShell.Id +$kubectlMonShell = Start-Process -PassThru PowerShell { for (0 -lt 1) { kubectl get pod -n azure-iot-operations | Sort-Object -Descending; Start-Sleep -Seconds 5; Clear-Host } } + + +Write-Host "`n" +Write-Host "[$(Get-Date -Format t)] INFO: Preparing AKSEE cluster 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" + } + else { + Write-Host "iptable rule exists, skip configuring iptable rules..." + } +} +catch { + Write-Host "Error: iptable rule update failed" -ForegroundColor Red +} + +############################################################## +# Install Azure edge CLI +############################################################## +Write-Host "`n" +Write-Host "[$(Get-Date -Format t)] INFO: Installing the Azure IoT Ops CLI extension" -ForegroundColor DarkGray +Write-Host "`n" +#az extension add --source ([System.Net.HttpWebRequest]::Create('https://aka.ms/aziotopscli-latest').GetResponse().ResponseUri.AbsoluteUri) -y +############################################################## +# Deploy aio +############################################################## +Write-Host "`n" +Write-Host "[$(Get-Date -Format t)] INFO: Deploying AIO to the cluster" -ForegroundColor DarkGray +Write-Host "`n" + +$keyVaultId = (az keyvault list -g $resourceGroup --resource-type vault --query "[0].id" -o tsv) +$retryCount = 0 +$maxRetries = 5 +$aioStatus = "notDeployed" + +do { + az iot ops init --cluster $arcClusterName -g $resourceGroup --kv-id $keyVaultId --sp-app-id $spnClientID --sp-object-id $spnObjectId --sp-secret $spnClientSecret --mq-service-type loadBalancer --mq-insecure true --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" + $retryCount++ + }else{ + $aioStatus = "deployed" + } +} until ($aioStatus -eq "deployed" -or $retryCount -eq $maxRetries) + +$retryCount = 0 +$maxRetries = 5 + +do { + $output = az iot ops check --as-object + $output = $output | ConvertFrom-Json + $mqServiceStatus = ($output.postDeployment | Where-Object { $_.name -eq "evalBrokerListeners" }).status + if ($mqServiceStatus -ne "Success") { + az iot ops init --cluster $arcClusterName -g $resourceGroup --kv-id $keyVaultId --sp-app-id $spnClientID --sp-object-id $spnObjectId --sp-secret $spnClientSecret --mq-service-type loadBalancer --mq-insecure true --only-show-errors + $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 "[$(Get-Date -Format t)] INFO: Started Event Grid role assignment process" -ForegroundColor DarkGray +$extensionPrincipalId = (az k8s-extension show --cluster-name $arcClusterName --name "mq" --resource-group $resourceGroup --cluster-type "connectedClusters" --output json | ConvertFrom-Json).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) + +az role assignment create --assignee $extensionPrincipalId --role "EventGrid TopicSpaces Publisher" --resource-group $resourceGroup --only-show-errors +az role assignment create --assignee $extensionPrincipalId --role "EventGrid TopicSpaces Subscriber" --resource-group $resourceGroup --only-show-errors +az role assignment create --assignee-object-id $extensionPrincipalId --role "EventGrid Data Sender" --scope $eventGridTopicId --assignee-principal-type ServicePrincipal +az role assignment create --assignee-object-id $spnObjectId --role "EventGrid Data Sender" --scope $eventGridTopicId --assignee-principal-type ServicePrincipal +az role assignment create --assignee $extensionPrincipalId --role "EventGrid TopicSpaces Subscriber" --scope $eventGridNamespaceId --only-show-errors +az role assignment create --assignee $extensionPrincipalId --role 'EventGrid TopicSpaces Publisher' --scope $eventGridNamespaceId --only-show-errors + + +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 load balancer +$mqconfigfile = "$aioToolsDir\mq_cloudConnector.yml" +$mqListenerService = "aio-mq-dmqtt-frontend" +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 $mqconfigfile) -replace 'eventGridPlaceholder', $eventGridHostName | Set-Content -Path $mqconfigfile +kubectl apply -f $mqconfigfile -n $aioNamespace + +############################################################## +# Deploy the simulator +############################################################## +Write-Host "[$(Get-Date -Format t)] INFO: Deploying the simulator" -ForegroundColor DarkGray +$simulatorYaml = "$aioToolsDir\mqtt_simulator.yml" + +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 +) + +(Get-Content $simulatorYaml ) -replace 'MQTTIpPlaceholder', $mqttIp | Set-Content $simulatorYaml +netsh interface portproxy add v4tov4 listenport=1883 listenaddress=0.0.0.0 connectport=1883 connectaddress=$mqttIp +kubectl apply -f $aioToolsDir\mqtt_simulator.yml -n $aioNamespace + +############################################################## +# Deploy OT Inspector (InfluxDB) +############################################################## +$listenerYaml = "$aioToolsDir\mqtt_listener.yml" +$influxdb_setupYaml = "$aioToolsDir\influxdb_setup.yml" +$influxdbYaml = "$aioToolsDir\influxdb.yml" +$influxImportYaml = "$aioToolsDir\influxdb-import-dashboard.yml" +$mqttExplorerSettings = "$aioToolsDir\mqtt_explorer_settings.json" + +do { + $simulatorPod = kubectl get pods -n $aioNamespace -o json | ConvertFrom-Json + $matchingPods = $simulatorPod.items | Where-Object { + $_.metadata.name -match "mqtt-simulator-deployment" -and + $_.status.phase -notmatch "running" + } + Write-Host "[$(Get-Date -Format t)] INFO: Waiting for the simulator to be deployed...Waiting for 20 seconds" -ForegroundColor DarkGray + Start-Sleep -Seconds 20 +} while ( + $matchingPods.Count -ne 0 +) + +kubectl apply -f $influxdb_setupYaml -n $aioNamespace + +do { + $influxIp = kubectl get service "influxdb" -n $aioNamespace -o jsonpath="{.status.loadBalancer.ingress[0].ip}" + Write-Host "[$(Get-Date -Format t)] INFO: Waiting for InfluxDB IP address to be assigned...Waiting for 10 seconds" -ForegroundColor DarkGray + Start-Sleep -Seconds 10 +} while ( + $null -eq $influxIp +) + +(Get-Content $listenerYaml ) -replace 'MQTTIpPlaceholder', $mqttIp | Set-Content $listenerYaml +(Get-Content $mqttExplorerSettings ) -replace 'MQTTIpPlaceholder', $mqttIp | Set-Content $mqttExplorerSettings +(Get-Content $listenerYaml ) -replace 'influxPlaceholder', $influxIp | Set-Content $listenerYaml +(Get-Content $influxdbYaml ) -replace 'influxPlaceholder', $influxIp | Set-Content $influxdbYaml +(Get-Content $influxdbYaml ) -replace 'influxAdminPwdPlaceHolder', $adminPassword | Set-Content $influxdbYaml +(Get-Content $influxdbYaml ) -replace 'influxAdminPlaceHolder', $adminUsername | Set-Content $influxdbYaml +(Get-Content $influxImportYaml ) -replace 'influxPlaceholder', $influxIp | Set-Content $influxImportYaml + +kubectl apply -f $aioToolsDir\influxdb.yml -n $aioNamespace + +do { + $influxPod = kubectl get pods -n $aioNamespace -o json | ConvertFrom-Json + $matchingPods = $influxPod.items | Where-Object { + $_.metadata.name -match "influxdb-0" -and + $_.status.phase -notmatch "running" + } + Write-Host "[$(Get-Date -Format t)] INFO: Waiting for the influx pods to be deployed...Waiting for 20 seconds" -ForegroundColor DarkGray + Start-Sleep -Seconds 20 +} while ( + $matchingPods.Count -ne 0 +) + +kubectl apply -f $aioToolsDir\mqtt_listener.yml -n $aioNamespace +do { + $listenerPod = kubectl get pods -n $aioNamespace -o json | ConvertFrom-Json + $matchingPods = $listenerPod.items | Where-Object { + $_.metadata.name -match "mqtt-listener-deployment" -and + $_.status.phase -notmatch "running" + } + Write-Host "[$(Get-Date -Format t)] INFO: Waiting for the mqtt listener pods to be deployed...Waiting for 20 seconds" -ForegroundColor DarkGray + Start-Sleep -Seconds 20 +} while ( + $matchingPods.Count -ne 0 +) + +kubectl apply -f $aioToolsDir\influxdb-import-dashboard.yml -n $aioNamespace +kubectl apply -f $aioToolsDir\influxdb-configmap.yml -n $aioNamespace + +############################################################## +# Install MQTT Explorer +############################################################## +Write-Host "`n" +Write-Host "[$(Get-Date -Format t)] INFO: Installing MQTT Explorer." -ForegroundColor DarkGreen +Write-Host "`n" +$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" +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 "$env:USERPROFILE\AppData\Roaming\MQTT-Explorer\settings.json" -Force + +############################################################## +# Creating bookmarks +############################################################## +Write-Host "`n" +Write-Host "[$(Get-Date -Format t)] INFO: Creating Microsoft Edge Bookmarks in Favorites Bar" -ForegroundColor DarkGreen +Write-Host "`n" +$bookmarksFileName = "$aioToolsDir\Bookmarks" +$edgeBookmarksPath = "$Env:LOCALAPPDATA\Microsoft\Edge\User Data\Default" + +# Replace matching value in the Bookmarks file +$content = Get-Content -Path $bookmarksFileName +$influxGrafanaUrl = "http://$influxIp"+":8086" +$newContent = $content -replace ("Grafana-influx-URL"), $influxGrafanaUrl +$newContent | Set-Content -Path $bookmarksFileName +Start-Sleep -Seconds 2 + +Copy-Item -Path $bookmarksFileName -Destination $edgeBookmarksPath -Force + +######################################################################## +# ADX Dashboards +######################################################################## +Write-Host "[$(Get-Date -Format t)] INFO: Creating the Azure Data Explorer dashboard..." + +# Get the ADX/Kusto cluster info +$kustoCluster = Get-AzKustoCluster -ResourceGroupName $resourceGroup -Name $adxClusterName +$adxEndPoint = $kustoCluster.Uri +(Get-content "$aioDataExplorerDir/dashboard.json").Replace('{{ADX_CLUSTER_URI}}', $adxEndPoint) | Set-Content "$aioDataExplorerDir/dashboard.json" + +############################################################## +# Arc-enabling the Windows server host +############################################################## +Write-Host "`n" +Write-Host "[$(Get-Date -Format t)] INFO: Configure the OS to allow Azure Arc Agent to be deploy on an Azure VM" -ForegroundColor DarkGray +Set-Service WindowsAzureGuestAgent -StartupType Disabled -Verbose +Stop-Service WindowsAzureGuestAgent -Force -Verbose +New-NetFirewallRule -Name BlockAzureIMDS -DisplayName "Block access to Azure IMDS" -Enabled True -Profile Any -Direction Outbound -Action Block -RemoteAddress 169.254.169.254 + +## Azure Arc agent Installation +Write-Host "`n" +Write-Host "[$(Get-Date -Format t)] INFO: Onboarding the Azure VM to Azure Arc..." -ForegroundColor DarkGray + +# Download the package +function download1() { $ProgressPreference = "SilentlyContinue"; Invoke-WebRequest -Uri https://aka.ms/AzureConnectedMachineAgent -OutFile AzureConnectedMachineAgent.msi } +download1 + +# Install the package +msiexec /i AzureConnectedMachineAgent.msi /l*v installationlog.txt /qn | Out-String + +#Tag +$clusterName = "$env:computername-$env:kubernetesDistribution" + +# Run connect command +& "$env:ProgramFiles\AzureConnectedMachineAgent\azcmagent.exe" connect ` + --service-principal-id $spnClientId ` + --service-principal-secret $spnClientSecret ` + --resource-group $resourceGroup ` + --tenant-id $spnTenantId ` + --location $location ` + --subscription-id $subscriptionId ` + --tags "Project=jumpstart_azure_arc_servers" "AKSEE=$clusterName"` + --correlation-id "d009f5dd-dba8-4ac7-bac9-b54ef3a6671a" + +############################################################## +# Install MQTTUI +############################################################## +Write-Host "[$(Get-Date -Format t)] INFO: Installing MQTTUI" -ForegroundColor DarkGray +$latestReleaseTag = (Invoke-WebRequest $mqttuiReleasesUrl | ConvertFrom-Json)[0].tag_name +$versionToDownload = $latestReleaseTag.Split("v")[1] +$mqttuiReleaseDownloadUrl = ((Invoke-WebRequest $mqttuiReleasesUrl | ConvertFrom-Json)[0].assets | Where-object { $_.name -like "mqttui-v${versionToDownload}-aarch64-pc-windows-msvc.zip" }).browser_download_url +$output = Join-Path $aioToolsDir "$latestReleaseTag.zip" +Invoke-WebRequest $mqttuiReleaseDownloadUrl -OutFile $output +Expand-Archive $output -DestinationPath "$aioToolsDir\mqttui" -Force +$mqttuiPath = "$aioToolsDir\mqttui\" +$currentPathVariable = [Environment]::GetEnvironmentVariable("PATH", [EnvironmentVariableTarget]::Machine) +$newPathVariable = $currentPathVariable + ";" + $mqttuiPath +$newPathVariable +[Environment]::SetEnvironmentVariable("PATH", $newPathVariable, [EnvironmentVariableTarget]::Machine) +Remove-Item -Path $output -Force + +############################################################## +# Install pip packages +############################################################## +Write-Host "Installing pip packages" +foreach ($package in $aioConfig.PipPackagesList) { + Write-Host "Installing $package" + & pip install -q $package +} + +############################################################# +# Install VSCode extensions +############################################################# +Write-Host "[$(Get-Date -Format t)] INFO: Installing VSCode extensions: " + ($aioConfig.VSCodeExtensions -join ', ') -ForegroundColor DarkGray +# Install VSCode extensions +foreach ($extension in $aioConfig.VSCodeExtensions) { + code --install-extension $extension 2>&1 | Out-Null +} + +############################################################## +# Pinning important directories to Quick access +############################################################## +Write-Host "[$(Get-Date -Format t)] INFO: Pinning important directories to Quick access" -ForegroundColor DarkGreen +$quickAccess = new-object -com shell.application +$quickAccess.Namespace($aioConfig.aioDirectories.aioDir).Self.InvokeVerb("pintohome") +$quickAccess.Namespace($aioConfig.aioDirectories.aioLogsDir).Self.InvokeVerb("pintohome") + +# Changing to Client VM wallpaper +$imgPath = Join-Path $aioConfig.aioDirectories["aioDir"] "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 +Stop-Process -Id $kubectlMonShell.Id + +# Removing the LogonScript Scheduled Task so it won't run on next reboot +Unregister-ScheduledTask -TaskName "LogonScript" -Confirm:$false +Start-Sleep -Seconds 5 + +$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." -ForegroundColor Green +Write-Host + +Stop-Transcript diff --git a/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/artifacts/PowerShell/aioConfig.psd1 b/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/artifacts/PowerShell/aioConfig.psd1 new file mode 100644 index 0000000000..5faa1e6583 --- /dev/null +++ b/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/artifacts/PowerShell/aioConfig.psd1 @@ -0,0 +1,164 @@ +@{ + # This is the PowerShell datafile used to provide configuration information for the aio environment. Product keys and password are not encrypted and will be available on host during installation. + + # Directory paths + aioDirectories = @{ + aioDir = "C:\AIO" + aioLogsDir = "C:\AIO\Logs" + aioPowerShellDir = "C:\AIO\PowerShell" + aioToolsDir = "C:\Tools" + aioTempDir = "C:\Temp" + aioConfigMapDir = "C:\AIO\ConfigMaps" + aioAppsRepo = "C:\AIO\AppsRepo" + aioDataExplorer = "C:\AIO\DataExplorer" + aioMonitoringDir = "C:\AIO\Monitoring" + aioInfluxMountPath = "C:\AIO\InfluxDB" + } + + # 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' + 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' + 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' + mqttuiReleases = 'https://api.github.com/repos/EdJoPaTo/mqttui/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", + "Microsoft.ExtendedLocation", + "Microsoft.IoTOperationsOrchestrator", + "Microsoft.IoTOperationsMQ", + "Microsoft.IoTOperationsDataProcessor" + ) + + # 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', + 'kubernetes-cli', + 'vcredist140', + 'azcopy10', + 'vscode', + 'git', + '7zip', + 'kubectx', + 'putty.install', + 'kubernetes-helm', + 'mqtt-explorer', + 'python' + ) + + # Pip packages list + PipPackagesList = @( + 'paho-mqtt' + ) + + # VSCode extensions + VSCodeExtensions = @( + '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' + ) + + + AKSEEConfig = @{ + AksEdgeRemoteDeployVersion = "1.0.230221.1200" + aksEdgeDeployModules = "main" + clusterLogSize = "1024" + + aideuserConfig = @{ + SchemaVersion = "1.1" + Version = "1.0" + AksEdgeProduct = "" + AksEdgeProductUrl = "" + Azure = @{ + SubscriptionId = "" + TenantId = "" + ResourceGroupName = "" + Location = "" + } + AksEdgeConfigFile = "aksedge-config.json" + } + + Nodes = @{ + 'LinuxNode' = @{ + 'CpuCount' = 6 + 'MemoryInMB' = 16384 + 'DataSizeInGB' = 50 + } + 'WindowsNode' = @{ + 'CpuCount' = 2 + 'MemoryInMB' = 4096 + } + } + + aksedgeConfig = @{ + SchemaVersion = "" + Version = "1.0" + DeploymentType = "SingleMachineCluster" + Init =@{ + ServiceIPRangeSize = 10 + } + Network = @{ + NetworkPlugin = "" + InternetDisabled = $false + } + User = @{ + AcceptEula = $true + AcceptOptionalTelemetry = $true + } + Machines = @() + } + } + + # Universal resource tag and resource types + TagName = 'Project' + TagValue = 'Jumpstart_aio' + ArcServerResourceType = 'Microsoft.HybridCompute/machines' + ArcK8sResourceType = 'Microsoft.Kubernetes/connectedClusters' + + # Microsoft Edge startup settings variables + EdgeSettingRegistryPath = 'HKLM:\SOFTWARE\Policies\Microsoft\Edge' + EdgeSettingValueTrue = '00000001' + EdgeSettingValueFalse = '00000000' +} diff --git a/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/artifacts/Settings/Bookmarks b/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/artifacts/Settings/Bookmarks new file mode 100644 index 0000000000..cc555abf90 --- /dev/null +++ b/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/artifacts/Settings/Bookmarks @@ -0,0 +1,54 @@ +{ + "checksum": "d77f9db622cff666aa1ae0f899c3b4ec", + "roots": { + "bookmark_bar": { + "children": [ { + "children": [ { + "id": "16", + "name": "AIO - Grafana", + "show_icon": false, + "source": "unknown", + "type": "url", + "url": "Grafana-influx-URL" + }], + "id": "15", + "name": "Grafana", + "source": "unknown", + "type": "folder" + }, { + "id": "23", + "name": "Azure Arc Jumpstart", + "show_icon": false, + "source": "unknown", + "type": "url", + "url": "https://azurearcjumpstart.io/" + }, { + "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 +} diff --git a/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/artifacts/Settings/DSCInstallWindowsFeatures.zip b/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/artifacts/Settings/DSCInstallWindowsFeatures.zip new file mode 100644 index 0000000000..bc61f0f7e6 Binary files /dev/null and b/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/artifacts/Settings/DSCInstallWindowsFeatures.zip differ diff --git a/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/artifacts/Settings/influxdb-configmap.yml b/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/artifacts/Settings/influxdb-configmap.yml new file mode 100644 index 0000000000..c2eb2e5342 --- /dev/null +++ b/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/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_edge_iot_ops_jumpstart/aio_manufacturing/bicep/artifacts/Settings/influxdb-import-dashboard.yml b/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/artifacts/Settings/influxdb-import-dashboard.yml new file mode 100644 index 0000000000..e3df9e2196 --- /dev/null +++ b/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/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_edge_iot_ops_jumpstart/aio_manufacturing/bicep/artifacts/Settings/influxdb.yml b/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/artifacts/Settings/influxdb.yml new file mode 100644 index 0000000000..d18d9f6ed0 --- /dev/null +++ b/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/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_edge_iot_ops_jumpstart/aio_manufacturing/bicep/artifacts/Settings/influxdb_setup.yml b/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/artifacts/Settings/influxdb_setup.yml new file mode 100644 index 0000000000..472968b00e --- /dev/null +++ b/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/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_edge_iot_ops_jumpstart/aio_manufacturing/bicep/artifacts/Settings/mq_cloudConnector.yml b/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/artifacts/Settings/mq_cloudConnector.yml new file mode 100644 index 0000000000..58bd54859b --- /dev/null +++ b/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/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: e4kpreview.azurecr.io/mqttbridge + tag: 0.1.0-preview-rc6 + pullPolicy: IfNotPresent + protocol: v5 + bridgeInstances: 1 + clientIdPrefix: factory-gateway- + logLevel: debug + remoteBrokerConnection: + endpoint: eventGridPlaceholder:8883 + tls: + tlsEnabled: true + authentication: + systemAssignedManagedIdentity: + audience: https://eventgrid.azure.net diff --git a/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/artifacts/Settings/mqtt_explorer_settings.json b/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/artifacts/Settings/mqtt_explorer_settings.json new file mode 100644 index 0000000000..758283680d --- /dev/null +++ b/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/artifacts/Settings/mqtt_explorer_settings.json @@ -0,0 +1,19 @@ +{ + "ConnectionManager_connections": { + "mqtt.eclipse.org": { + "certValidation": false, + "clientId": "mqtt-explorer-640c948e", + "encryption": false, + "host": "MQTTIpPlaceholder", + "id": "mqtt.eclipse.org", + "name": "aio", + "port": 1883, + "protocol": "mqtt", + "subscriptions": [ + "#", + "$SYS/#" + ], + "type": "mqtt" + } + } +} \ No newline at end of file diff --git a/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/artifacts/Settings/mqtt_listener.yml b/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/artifacts/Settings/mqtt_listener.yml new file mode 100644 index 0000000000..54ec3df88c --- /dev/null +++ b/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/artifacts/Settings/mqtt_listener.yml @@ -0,0 +1,40 @@ +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/productionline" + - name: MQTT_TOPIC2 + value: "topic/fryer" + - 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_edge_iot_ops_jumpstart/aio_manufacturing/bicep/artifacts/Settings/mqtt_simulator.yml b/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/artifacts/Settings/mqtt_simulator.yml new file mode 100644 index 0000000000..e5be1c5383 --- /dev/null +++ b/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/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: jumpstartprod.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_edge_iot_ops_jumpstart/aio_manufacturing/bicep/artifacts/adx_dashboard/dashboard.json b/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/artifacts/adx_dashboard/dashboard.json new file mode 100644 index 0000000000..0e156e89d4 --- /dev/null +++ b/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/artifacts/adx_dashboard/dashboard.json @@ -0,0 +1,671 @@ +{ + "$schema": "https://dataexplorer.azure.com/static/d/schema/48/dashboard.json", + "id": "9336e3c4-726d-441b-8c61-4e83deb1ad38", + "eTag": "145e214f-b68b-4804-805b-1ddc58b69d9c", + "schema_version": "48", + "title": "Contoso donut factory", + "autoRefresh": { "enabled": false }, + "baseQueries": [], + "tiles": [ + { + "id": "01caf441-74f3-4853-8f93-90fb8a4f8f77", + "title": "Voltage", + "description": "", + "visualType": "card", + "pageId": "674ee921-2d76-4c1c-a4fd-ed42dd861e9f", + "layout": { "x": 9, "y": 0, "width": 4, "height": 4 }, + "query": { + "kind": "inline", + "dataSource": { + "kind": "inline", + "dataSourceId": "27da065d-b46f-4658-b525-556227c5c6b3" + }, + "usedVariables": [], + "text": "fryer\n| top 1 by Timestamp desc\n| project Voltage\n| extend Voltage=round(Voltage, 5)\n\n" + }, + "visualOptions": { + "multiStat__textSize": "large", + "multiStat__valueColumn": null, + "colorRulesDisabled": false, + "colorRules": [ + { + "id": "3f1451c9-cbc7-492e-bb2a-aef83558a2a6", + "ruleType": "colorByCondition", + "applyToColumn": null, + "hideText": false, + "applyTo": "cells", + "conditions": [ + { "operator": "<", "column": null, "values": ["110"] } + ], + "chainingOperator": "and", + "visualType": "stat", + "colorStyle": "bold", + "color": "red", + "tag": "", + "icon": "warning", + "ruleName": "" + } + ], + "colorStyle": "light" + } + }, + { + "id": "0bbe1af8-eccc-40d7-8fdc-2748ef7a3b9d", + "title": "Pump 1 Flow Totalizer", + "description": "", + "visualType": "timechart", + "pageId": "674ee921-2d76-4c1c-a4fd-ed42dd861e9f", + "layout": { "x": 0, "y": 0, "width": 9, "height": 7 }, + "query": { + "kind": "inline", + "dataSource": { + "kind": "inline", + "dataSourceId": "27da065d-b46f-4658-b525-556227c5c6b3" + }, + "usedVariables": ["_endTime", "_startTime"], + "text": "fryer\n| where Timestamp < _endTime\n| make-series avg(Pump1_Flow_Totalizer) on Timestamp in range (_startTime, now(), 1m) \n\n\n" + }, + "visualOptions": { + "multipleYAxes": { + "base": { + "id": "-1", + "label": "", + "columns": [], + "yAxisMaximumValue": null, + "yAxisMinimumValue": null, + "yAxisScale": "linear", + "horizontalLines": [] + }, + "additional": [], + "showMultiplePanels": false + }, + "hideLegend": false, + "xColumnTitle": "", + "xColumn": null, + "yColumns": null, + "seriesColumns": null, + "xAxisScale": "linear", + "verticalLine": "", + "crossFilterDisabled": false, + "drillthroughDisabled": false, + "crossFilter": [], + "drillthrough": [] + } + }, + { + "id": "4f1afc67-bf20-4085-b8f3-c979519d8b70", + "title": "Location", + "description": "", + "visualType": "map", + "pageId": "674ee921-2d76-4c1c-a4fd-ed42dd861e9f", + "layout": { "x": 0, "y": 7, "width": 9, "height": 8 }, + "query": { + "kind": "inline", + "dataSource": { + "kind": "inline", + "dataSourceId": "27da065d-b46f-4658-b525-556227c5c6b3" + }, + "usedVariables": [], + "text": "print latitude=26.13263, longitude=-80.40270" + }, + "visualOptions": { + "map__type": "bubble", + "map__latitudeColumn": "latitude", + "map__longitudeColumn": "longitude", + "map__labelColumn": null, + "map__sizeColumn": null, + "map__sizeDisabled": true, + "map__geoType": "numeric", + "map__geoPointColumn": null + } + }, + { + "id": "d7b0eb26-7212-4aa8-9909-22a982b71c13", + "title": "New tile", + "hideTitle": true, + "description": "", + "visualType": "multistat", + "pageId": "674ee921-2d76-4c1c-a4fd-ed42dd861e9f", + "layout": { "x": 9, "y": 4, "width": 13, "height": 6 }, + "query": { + "kind": "inline", + "dataSource": { + "kind": "inline", + "dataSourceId": "27da065d-b46f-4658-b525-556227c5c6b3" + }, + "usedVariables": [], + "text": "fryer\n| top 1 by Timestamp desc\n| project Drive1_Current, Drive1_Frequency, Drive1_Voltage, Drive1_Speed, Drive2_Current, Drive2_Frequency, Drive2_Voltage, Drive2_Speed\n | evaluate narrow()\n| project Column, Value\n|extend Value=round(todouble(Value),4)\n" + }, + "visualOptions": { + "multiStat__textSize": "auto", + "multiStat__valueColumn": null, + "colorRulesDisabled": false, + "colorRules": [], + "colorStyle": "light", + "multiStat__displayOrientation": "horizontal", + "multiStat__labelColumn": "Column", + "multiStat__slot": { "width": 4, "height": 2 } + } + }, + { + "id": "fd8b01b8-df4c-493d-bd07-86f62afc102c", + "title": "Tank Level", + "description": "", + "visualType": "card", + "pageId": "674ee921-2d76-4c1c-a4fd-ed42dd861e9f", + "layout": { "x": 13, "y": 0, "width": 4, "height": 4 }, + "query": { + "kind": "inline", + "dataSource": { + "kind": "inline", + "dataSourceId": "27da065d-b46f-4658-b525-556227c5c6b3" + }, + "usedVariables": [], + "text": "fryer\n| top 1 by Timestamp desc\n| project Tank_Level\n| extend Tank_Level =round(Tank_Level, 5)\n" + }, + "visualOptions": { + "multiStat__textSize": "auto", + "multiStat__valueColumn": null, + "colorRulesDisabled": false, + "colorRules": [ + { + "id": "a97b0ca3-5b23-4753-b701-dd907ad0b83f", + "ruleType": "colorByCondition", + "applyToColumn": null, + "hideText": false, + "applyTo": "cells", + "conditions": [ + { "operator": ">", "column": null, "values": ["61"] } + ], + "chainingOperator": "and", + "visualType": "stat", + "colorStyle": "bold", + "color": "green", + "tag": "", + "icon": "completed", + "ruleName": "" + } + ], + "colorStyle": "light" + } + }, + { + "id": "ae9ab735-9128-438c-a2d2-cbd0245a50c5", + "title": "Temperature", + "description": "", + "visualType": "card", + "pageId": "674ee921-2d76-4c1c-a4fd-ed42dd861e9f", + "layout": { "x": 17, "y": 0, "width": 4, "height": 4 }, + "query": { + "kind": "inline", + "dataSource": { + "kind": "inline", + "dataSourceId": "27da065d-b46f-4658-b525-556227c5c6b3" + }, + "usedVariables": [], + "text": "fryer\n| top 1 by Timestamp desc\n| project Temperature\n| extend Temperature =round(Temperature, 5)\n" + }, + "visualOptions": { + "multiStat__textSize": "auto", + "multiStat__valueColumn": null, + "colorRulesDisabled": false, + "colorRules": [ + { + "id": "9e8bfc42-e489-42b2-8e97-4d902abec9c6", + "ruleType": "colorByCondition", + "applyToColumn": null, + "hideText": false, + "applyTo": "cells", + "conditions": [ + { "operator": "<", "column": "Temperature", "values": ["80"] } + ], + "chainingOperator": "and", + "visualType": "stat", + "colorStyle": "bold", + "color": "yellow", + "tag": "", + "icon": "warning", + "ruleName": "" + } + ], + "colorStyle": "light" + } + }, + { + "id": "297a171f-3d7e-46a0-858a-282d7ded0fda", + "title": "Operation", + "description": "", + "visualType": "table", + "pageId": "674ee921-2d76-4c1c-a4fd-ed42dd861e9f", + "layout": { "x": 9, "y": 10, "width": 13, "height": 5 }, + "query": { + "kind": "inline", + "dataSource": { + "kind": "inline", + "dataSourceId": "27da065d-b46f-4658-b525-556227c5c6b3" + }, + "usedVariables": [], + "text": "fryer\n| top 1 by Timestamp desc\n| project Filter_Chg_Required, Cooler_ON, Fan001_On, Heater_ON\n | evaluate narrow()\n| project Column, Value\n" + }, + "visualOptions": { + "table__enableRenderLinks": true, + "colorRules": [], + "colorRulesDisabled": true, + "colorStyle": "light", + "crossFilterDisabled": false, + "drillthroughDisabled": false, + "crossFilter": [], + "drillthrough": [], + "table__renderLinks": [] + } + }, + { + "id": "a19d64d3-68cf-4cbd-94ce-1f5162631706", + "title": "Overall OEE", + "description": "test bsdkjsdflkfj slkdfjkflsdkfjsl", + "visualType": "card", + "pageId": "4181f6f2-80d3-4896-bb38-dfded715c13c", + "layout": { "x": 0, "y": 0, "width": 5, "height": 3 }, + "query": { + "kind": "inline", + "dataSource": { + "kind": "inline", + "dataSourceId": "27da065d-b46f-4658-b525-556227c5c6b3" + }, + "usedVariables": [], + "text": "productionline\n| top 1 by Timestamp desc\n| project OverallEfficiency\n | evaluate narrow()\n| project Column, Value\n" + }, + "visualOptions": { + "multiStat__textSize": "auto", + "multiStat__valueColumn": "Value", + "colorRulesDisabled": false, + "colorRules": [ + { + "id": "52b565a0-7469-4f2b-9d54-966fb379a012", + "ruleType": "colorByCondition", + "applyToColumn": null, + "hideText": false, + "applyTo": "cells", + "conditions": [ + { "operator": ">", "column": null, "values": ["90"] } + ], + "chainingOperator": "and", + "visualType": "stat", + "colorStyle": "bold", + "color": "green", + "tag": "", + "icon": "completed", + "ruleName": "" + }, + { + "id": "6e549f9a-51b8-475b-af85-74185dbe1e4e", + "ruleType": "colorByCondition", + "applyToColumn": null, + "hideText": false, + "applyTo": "cells", + "conditions": [ + { "operator": "<=", "column": "Value", "values": ["90"] } + ], + "chainingOperator": "and", + "visualType": "stat", + "colorStyle": "bold", + "color": "yellow", + "tag": "", + "icon": "warning", + "ruleName": "" + } + ], + "colorStyle": "light" + } + }, + { + "id": "6aa3d2b5-e44a-4b5b-b7a2-d0f3b534e645", + "title": "Availability", + "description": "", + "visualType": "card", + "pageId": "4181f6f2-80d3-4896-bb38-dfded715c13c", + "layout": { "x": 0, "y": 6, "width": 5, "height": 3 }, + "query": { + "kind": "inline", + "dataSource": { + "kind": "inline", + "dataSourceId": "27da065d-b46f-4658-b525-556227c5c6b3" + }, + "usedVariables": [], + "text": "productionline\n| top 1 by Timestamp desc\n| project Availability\n | evaluate narrow()\n| project Column, Value\n" + }, + "visualOptions": { + "multiStat__textSize": "auto", + "multiStat__valueColumn": "Value", + "colorRulesDisabled": false, + "colorRules": [ + { + "id": "a2e46842-671e-4741-9801-bae57a755165", + "ruleType": "colorByCondition", + "applyToColumn": null, + "hideText": false, + "applyTo": "cells", + "conditions": [ + { "operator": ">", "column": "Value", "values": ["90"] } + ], + "chainingOperator": "and", + "visualType": "stat", + "colorStyle": "bold", + "color": "green", + "tag": "", + "icon": "completed", + "ruleName": "" + }, + { + "id": "51e4a581-7475-456a-a8d7-4227cd4bd58e", + "ruleType": "colorByCondition", + "applyToColumn": null, + "hideText": false, + "applyTo": "cells", + "conditions": [ + { "operator": "<=", "column": "Value", "values": ["90"] } + ], + "chainingOperator": "and", + "visualType": "stat", + "colorStyle": "bold", + "color": "yellow", + "tag": "", + "icon": "warning", + "ruleName": "" + } + ], + "colorStyle": "light" + } + }, + { + "id": "69a83a1b-3c31-413f-ba5f-7a220c6eff62", + "title": "Performance", + "description": "", + "visualType": "card", + "pageId": "4181f6f2-80d3-4896-bb38-dfded715c13c", + "layout": { "x": 0, "y": 3, "width": 5, "height": 3 }, + "query": { + "kind": "inline", + "dataSource": { + "kind": "inline", + "dataSourceId": "27da065d-b46f-4658-b525-556227c5c6b3" + }, + "usedVariables": [], + "text": "productionline\n| top 1 by Timestamp desc\n| project Performance\n | evaluate narrow()\n| project Column, Value\n" + }, + "visualOptions": { + "multiStat__textSize": "auto", + "multiStat__valueColumn": "Value", + "colorRulesDisabled": false, + "colorRules": [ + { + "id": "8f852e83-58ec-4a73-a0a0-84fa46ce8a2c", + "ruleType": "colorByCondition", + "applyToColumn": null, + "hideText": false, + "applyTo": "cells", + "conditions": [ + { "operator": ">", "column": "Value", "values": ["90"] } + ], + "chainingOperator": "and", + "visualType": "stat", + "colorStyle": "bold", + "color": "green", + "tag": "", + "icon": "completed", + "ruleName": "" + }, + { + "id": "0b932c1a-c43d-45b3-a005-6a66ea8fbe52", + "ruleType": "colorByCondition", + "applyToColumn": null, + "hideText": false, + "applyTo": "cells", + "conditions": [ + { "operator": "<=", "column": "Value", "values": ["90"] } + ], + "chainingOperator": "and", + "visualType": "stat", + "colorStyle": "bold", + "color": "yellow", + "tag": "", + "icon": "warning", + "ruleName": "" + } + ], + "colorStyle": "light" + } + }, + { + "id": "0afc4d3f-7b34-49d9-afbf-c444f2473e19", + "title": "OEE by shift", + "description": "", + "visualType": "pie", + "pageId": "4181f6f2-80d3-4896-bb38-dfded715c13c", + "layout": { "x": 5, "y": 0, "width": 10, "height": 7 }, + "query": { + "kind": "inline", + "dataSource": { + "kind": "inline", + "dataSourceId": "27da065d-b46f-4658-b525-556227c5c6b3" + }, + "usedVariables": [], + "text": "productionline\n| top 1 by Timestamp desc\n| project OEEMorningShift, OEEDayShift, OEENightShift\n | evaluate narrow()\n| project Column, Value\n|extend Value=round(todouble(Value),4)\n" + }, + "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": "fbd3a80d-a453-4d95-a1b3-520214c82b9e", + "title": "OEE by product", + "description": "", + "visualType": "bar", + "pageId": "4181f6f2-80d3-4896-bb38-dfded715c13c", + "layout": { "x": 15, "y": 0, "width": 9, "height": 7 }, + "query": { + "kind": "inline", + "dataSource": { + "kind": "inline", + "dataSourceId": "27da065d-b46f-4658-b525-556227c5c6b3" + }, + "usedVariables": [], + "text": "productionline\n| top 1 by Timestamp desc\n| project OEEDonuts, OEECakes, OEEBreads, OEEGoalbyProduct\n | evaluate narrow()\n| project Column, Value\n|extend Value=round(todouble(Value),4)\n" + }, + "visualOptions": { + "multipleYAxes": { + "base": { + "id": "-1", + "label": "", + "columns": [], + "yAxisMaximumValue": null, + "yAxisMinimumValue": null, + "yAxisScale": "linear", + "horizontalLines": [ + { "id": "0ae79ce1-d6b5-4949-98cd-fb59c6ad4290", "value": 1 } + ] + }, + "additional": [], + "showMultiplePanels": false + }, + "hideLegend": false, + "xColumnTitle": "", + "xColumn": null, + "yColumns": null, + "seriesColumns": null, + "xAxisScale": "linear", + "verticalLine": "", + "crossFilterDisabled": false, + "drillthroughDisabled": false, + "crossFilter": [], + "drillthrough": [] + } + }, + { + "id": "ae11bbf9-ddbf-4c5e-95be-d0ad0106a528", + "title": "OEE per Plant", + "description": "", + "visualType": "stackedcolumn", + "pageId": "4181f6f2-80d3-4896-bb38-dfded715c13c", + "layout": { "x": 5, "y": 7, "width": 19, "height": 5 }, + "query": { + "kind": "inline", + "dataSource": { + "kind": "inline", + "dataSourceId": "27da065d-b46f-4658-b525-556227c5c6b3" + }, + "usedVariables": [], + "text": "productionline\n| top 1 by Timestamp desc\n| project OEESeattle, OEEMiami, OEEBoston\n | evaluate narrow()\n| project Column, Value\n|extend Value=round(todouble(Value),4)\n\n" + }, + "visualOptions": { + "multipleYAxes": { + "base": { + "id": "-1", + "label": "OEE", + "columns": [], + "yAxisMaximumValue": null, + "yAxisMinimumValue": null, + "yAxisScale": "linear", + "horizontalLines": [] + }, + "additional": [], + "showMultiplePanels": false + }, + "hideLegend": false, + "xColumnTitle": "", + "xColumn": null, + "yColumns": null, + "seriesColumns": null, + "xAxisScale": "linear", + "verticalLine": "", + "crossFilterDisabled": false, + "drillthroughDisabled": false, + "crossFilter": [], + "drillthrough": [] + } + }, + { + "id": "29bc3ac1-7dca-4dfd-bf0b-1960062caaa0", + "title": "Quality", + "description": "", + "visualType": "multistat", + "pageId": "4181f6f2-80d3-4896-bb38-dfded715c13c", + "layout": { "x": 5, "y": 12, "width": 9, "height": 7 }, + "query": { + "kind": "inline", + "dataSource": { + "kind": "inline", + "dataSourceId": "27da065d-b46f-4658-b525-556227c5c6b3" + }, + "usedVariables": [], + "text": "productionline\n| top 1 by Timestamp desc\n| project DoughTemperature, Weight, Height, OvenTemp\n | evaluate narrow()\n| project Column, Value\n|extend Value=round(todouble(Value),4)" + }, + "visualOptions": { + "multiStat__textSize": "auto", + "multiStat__valueColumn": null, + "colorRulesDisabled": true, + "colorRules": [], + "colorStyle": "light", + "multiStat__displayOrientation": "horizontal", + "multiStat__labelColumn": null, + "multiStat__slot": { "width": 2, "height": 2 } + } + }, + { + "id": "bb790952-7b2f-4627-82ff-91cacd90626b", + "title": "Current Production", + "description": "", + "visualType": "table", + "pageId": "4181f6f2-80d3-4896-bb38-dfded715c13c", + "layout": { "x": 14, "y": 12, "width": 9, "height": 7 }, + "query": { + "kind": "inline", + "dataSource": { + "kind": "inline", + "dataSourceId": "27da065d-b46f-4658-b525-556227c5c6b3" + }, + "usedVariables": [], + "text": "productionline\n| top 1 by Timestamp desc\n| project MakeupArea, Line, Product, CurrentShift, Batch\n | evaluate narrow()\n| project Column, Value\n" + }, + "visualOptions": { + "table__enableRenderLinks": true, + "colorRules": [], + "colorRulesDisabled": true, + "colorStyle": "light", + "crossFilterDisabled": false, + "drillthroughDisabled": false, + "crossFilter": [], + "drillthrough": [], + "table__renderLinks": [] + } + }, + { + "id": "248dcd85-e575-4972-8555-0256eadb6273", + "title": "New tile", + "description": "", + "visualType": "multistat", + "pageId": "4181f6f2-80d3-4896-bb38-dfded715c13c", + "layout": { "x": 0, "y": 9, "width": 5, "height": 15 }, + "query": { + "kind": "inline", + "dataSource": { + "kind": "inline", + "dataSourceId": "27da065d-b46f-4658-b525-556227c5c6b3" + }, + "usedVariables": [], + "text": "productionline\n| top 1 by Timestamp desc\n| project WasteReason, Waste, UnplannedDowntime, ActualRuntime, RejectedQuantity\n | evaluate narrow()\n| project Column, Value" + }, + "visualOptions": { + "multiStat__textSize": "small", + "multiStat__valueColumn": "Value", + "colorRulesDisabled": true, + "colorRules": [], + "colorStyle": "light", + "multiStat__displayOrientation": "horizontal", + "multiStat__labelColumn": null, + "multiStat__slot": { "width": 1, "height": 5 } + } + } + ], + "parameters": [ + { + "kind": "duration", + "id": "b5b194fa-fbcf-40b9-85c6-c70384508d01", + "displayName": "Time range", + "description": "", + "beginVariableName": "_startTime", + "endVariableName": "_endTime", + "defaultValue": { "kind": "dynamic", "count": 6, "unit": "hours" }, + "showOnPages": { "kind": "all" } + } + ], + "dataSources": [ + { + "id": "27da065d-b46f-4658-b525-556227c5c6b3", + "name": "fryer", + "clusterUri": "{{ADX_CLUSTER_URI}}", + "database": "donutPlant", + "queryResultsCacheMaxAge": 5000, + "kind": "manual-kusto", + "scopeId": "kusto" + } + ], + "pages": [ + { "name": "fryer", "id": "674ee921-2d76-4c1c-a4fd-ed42dd861e9f" }, + { "name": "OEE", "id": "4181f6f2-80d3-4896-bb38-dfded715c13c" } + ] +} diff --git a/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/data/dataExplorer.bicep b/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/data/dataExplorer.bicep new file mode 100644 index 0000000000..ef776a46b6 --- /dev/null +++ b/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/data/dataExplorer.bicep @@ -0,0 +1,143 @@ +@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 Azure Data Explorer POS database') +param aioDBName string = 'donutPlant' + +@description('The name of the Azure Data Explorer Event Hub connection') +param aioEventHubConnectionName string = 'donutPlant-eh-messages' + +@description('The name of the Azure Data Explorer Event Hub table') +param tableName string = 'donutPlant' + +@description('The name of the Azure Data Explorer Event Hub data format') +param dataFormat string = 'multijson' + +@description('The name of the Azure Data Explorer Event Hub consumer group') +param eventHubConsumerGroupName string = 'cgadx' + +@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('# 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 stagingScript 'Microsoft.Kusto/clusters/databases/scripts@2023-05-02' = { + name: 'stagingScript' + parent: aiodonutPlantDB + properties: { + continueOnErrors: false + forceUpdateTag: 'string' + scriptContent: loadTextContent('staging.kql') + } +} +resource donutPlantScript 'Microsoft.Kusto/clusters/databases/scripts@2023-05-02' = { + name: 'donutPlantScript' + parent: aiodonutPlantDB + dependsOn: [ + stagingScript + ] + properties: { + continueOnErrors: false + forceUpdateTag: 'string' + scriptContent: loadTextContent('fryer.kql') + } +} + +resource productionLineScript 'Microsoft.Kusto/clusters/databases/scripts@2023-05-02' = { + name: 'productionLineScript' + parent: aiodonutPlantDB + dependsOn: [ + donutPlantScript + ] + properties: { + continueOnErrors: false + forceUpdateTag: 'string' + scriptContent: loadTextContent('productionline.kql') + } +} + +resource aiodonutPlantDB 'Microsoft.Kusto/clusters/databases@2023-05-02' = { + parent: adxCluster + name: aioDBName + location: location + kind: 'ReadWrite' +} + +resource adxEventHubConnection 'Microsoft.Kusto/clusters/databases/dataConnections@2023-08-15' = { + name: aioEventHubConnectionName + kind: 'EventHub' + dependsOn: [ + donutPlantScript + ] + location: location + parent: aiodonutPlantDB + properties: { + managedIdentityResourceId: adxCluster.id + eventHubResourceId: eventHubResourceId + consumerGroup: eventHubConsumerGroupName + tableName: tableName + dataFormat: dataFormat + eventSystemProperties: [] + compression: 'None' + databaseRouting: 'Single' + } +} + +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 + } +} + +output adxEndpoint string = adxCluster.properties.uri diff --git a/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/data/eventGrid.bicep b/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/data/eventGrid.bicep new file mode 100644 index 0000000000..598cc1ae2e --- /dev/null +++ b/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/data/eventGrid.bicep @@ -0,0 +1,185 @@ +@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-06-01-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_edge_iot_ops_jumpstart/aio_manufacturing/bicep/data/eventHub.bicep b/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/data/eventHub.bicep new file mode 100644 index 0000000000..34bf1d19e8 --- /dev/null +++ b/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/data/eventHub.bicep @@ -0,0 +1,70 @@ +@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') +param eventHubConsumerGroupName string = 'aioConsumerGroup' + +@description('The name of the Azure Data Explorer Event Hub production line consumer group') +param eventHubConsumerGroupNamePl string = 'aioConsumerGroupPl' + +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 eventHubConsumerGroup 'Microsoft.EventHub/namespaces/eventhubs/consumergroups@2023-01-01-preview' = { + name: eventHubConsumerGroupName + parent: eventHub +} + +resource eventHubConsumerGroupPl 'Microsoft.EventHub/namespaces/eventhubs/consumergroups@2023-01-01-preview' = { + name: eventHubConsumerGroupNamePl + parent: eventHub +} + + +output eventHubResourceId string = eventHub.id diff --git a/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/data/fryer.kql b/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/data/fryer.kql new file mode 100644 index 0000000000..e1a1649754 --- /dev/null +++ b/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/data/fryer.kql @@ -0,0 +1,124 @@ +// Create table to store data from base64 to json format +.create table fryer (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 function to decode base64 to json format +.create-or-alter function Expand_fryer_Data() { + donutPlant + | where subject == "topic/fryer" + | 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 to run function every 1 minute +.alter table fryer policy update @'[{"Source": "donutPlant", "Query": "Expand_fryer_Data()", "IsEnabled": "True"}]' + +.alter table fryer policy ingestionbatching "{'MaximumBatchingTimeSpan': '0:01:00', 'MaximumNumberOfItems': 10000}" + diff --git a/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/data/keyVault.bicep b/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/data/keyVault.bicep new file mode 100644 index 0000000000..7b54e5d0a1 --- /dev/null +++ b/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/data/keyVault.bicep @@ -0,0 +1,45 @@ +@description('Azure Key Vault name') +param akvName string = 'aio-akv-01' + +@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: akvName + 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 + } +} diff --git a/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/data/productionline.kql b/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/data/productionline.kql new file mode 100644 index 0000000000..8feb86d42a --- /dev/null +++ b/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/data/productionline.kql @@ -0,0 +1,119 @@ +// Create a table to store the data from the productionline topic +.create table productionline ( +Timestamp: datetime, +MakeupArea: string, +Line: string, +Product: string, +Process: string, +Batch: int, +CurrentShift: string, +CurrentCutPerMinutes: int, +TargetCutPerMinutes: int, +StartTime: datetime, +FinishTime: datetime, +Waste: real, +WasteReason: string, +LostTime: string, +LostTimeReason: string, +LostTimeTimeCount: int, +ScheduledDoughs: int, +CompletedDoughs: int, +ScheduledDoughsPerHour: int, +DoughTemperature: real, +Weight: real, +Height: real, +OvenTemp: real, +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, +OEEGoalbyPlant: real, +OEESeattle: real, +OEEMiami: real, +OEEBoston: real, +OEEMexicoCity: real, +OEEGoalbyCountry: real, +OEEUSA: real, +OEEMexico: real, +OEEGoalbyProduct: real, +OEEDonuts: real, +OEECakes: real, +OEEBreads: real, +OEEGoalbyShift: real, +OEEMorningShift: real, +OEEDayShift: real, +OEENightShift: real) + +// Create a function to parse the data from the productionline topic +.create-or-alter function Expand_productionline_Data() { + donutPlant + | where subject == "topic/productionline" + | 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), + CurrentCutPerMinutes = toint(data.data.CurrentCutPerMinutes), + TargetCutPerMinutes = toint(data.data.TargetCutPerMinutes), + 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), + ScheduledDoughs = toint(data.data.ScheduledDoughs), + CompletedDoughs = toint(data.data.CompletedDoughs), + ScheduledDoughsPerHour = toint(data.data.ScheduledDoughsPerHour), + DoughTemperature = toreal(data.data.DoughTemperature), + Weight = toreal(data.data.Weight), + Height = toreal(data.data.Height), + OvenTemp = toreal(data.data.OvenTemp), + 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), + OEEGoalbyPlant = toreal(data.data.OEEGoalbyPlant), + OEESeattle = toreal(data.data.OEESeattle), + OEEMiami = toreal(data.data.OEEMiami), + OEEBoston = toreal(data.data.OEEBoston), + OEEMexicoCity = toreal(data.data.OEEMexicoCity), + OEEGoalbyCountry = toreal(data.data.OEEGoalbyCountry), + OEEUSA = toreal(data.data.OEEUSA), + OEEMexico = toreal(data.data.OEEMexico), + OEEGoalbyProduct = toreal(data.data.OEEGoalbyProduct), + OEEDonuts = toreal(data.data.OEEDonuts), + OEECakes = toreal(data.data.OEECakes), + OEEBreads = toreal(data.data.OEEBreads), + OEEGoalbyShift = toreal(data.data.OEEGoalbyShift), + OEEMorningShift = toreal(data.data.OEEMorningShift), + OEEDayShift = toreal(data.data.OEEDayShift), + OEENightShift = toreal(data.data.OEENightShift) +} + +// Create a policy to update the productionline table +.alter table productionline policy update @'[{"Source": "donutPlant", "Query": "Expand_productionline_Data()", "IsEnabled": "True"}]' + +.alter table productionline policy ingestionbatching "{'MaximumBatchingTimeSpan': '0:01:00', 'MaximumNumberOfItems': 10000}" diff --git a/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/data/staging.kql b/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/data/staging.kql new file mode 100644 index 0000000000..319a882838 --- /dev/null +++ b/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/data/staging.kql @@ -0,0 +1,4 @@ +// create main table, in this table lands the data in base64 format +.create table donutPlant (['id']: guid, source: string, ['type']: string, data_base64: string, ['time']: datetime, specversion: real, subject: string) + +.alter table donutPlant policy ingestionbatching "{'MaximumBatchingTimeSpan': '0:01:00', 'MaximumNumberOfItems': 10000}" diff --git a/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/main.azd.bicep b/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/main.azd.bicep new file mode 100644 index 0000000000..b31d7a0757 --- /dev/null +++ b/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/main.azd.bicep @@ -0,0 +1,381 @@ +@description('The name of you Virtual Machine.') +param vmName string = 'AIO-Demo' + +@description('Kubernetes distribution') +@allowed([ + 'k8s' + 'k3s' +]) +param kubernetesDistribution string = 'k3s' + +@description('Username for the Virtual Machine.') +param windowsAdminUsername string = 'arcdemo' + +@description('Windows password for the Virtual Machine') +@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.') +@allowed([ + 'westus3' + 'eastus2' + 'westeurope' + 'centraluseuap' + 'eastus2euap' +]) +param location string + +@description('Choice to deploy Bastion to connect to the client VM') +param deployBastion bool + +@description('the Azure Bastion host name') +param bastionHostName string = 'AIO-Demo-Bastion' + +@description('The size of the VM') +param vmSize string = 'Standard_D8s_v3' + +@description('Unique SPN app ID') +param spnClientId string + +@description('Unique SPN object ID') +param spnObjectId string + +@description('Unique SPN password') +@minLength(12) +@maxLength(123) +@secure() +param spnClientSecret string + +@description('Unique SPN tenant ID') +param spnTenantId string + +@description('Azure subscription ID') +param subscriptionId string = subscription().subscriptionId + +@description('Target GitHub account') +param githubAccount string = 'microsoft' + +@description('Target GitHub branch') +param githubBranch string = 'main' + +@description('Name of the VNET') +param virtualNetworkName string = 'AIO-Demo-VNET' + +@description('Name of the subnet in the virtual network') +param subnetName string = 'Subnet' + +@description('Name of the Network Security Group') +param networkSecurityGroupName string = 'AIO-Demo-NSG' + +param resourceTags object = { + Project: 'jumpstart_azure_aio' +} + +@maxLength(5) +@description('Random GUID') +param namingGuid string = toLower(substring(newGuid(), 0, 5)) + +@description('Deploy Windows Node for AKS Edge Essentials') +param windowsNode bool = false + +@description('Name of the storage account') +param aioStorageAccountName string = 'aiostg${namingGuid}' + +@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 Azure Data Explorer cluster') +param adxClusterName string = 'aioadx${namingGuid}' + +@description('The custom location RPO ID') +param customLocationRPOID string + +@description('The name of the Azure Key Vault') +param akvName string = 'aioakv${namingGuid}' + +@description('The name of the Azure Data Explorer Event Hub consumer group') +param eventHubConsumerGroupName string = 'cgadx${namingGuid}' + +@description('The name of the Azure Data Explorer Event Hub production line consumer group') +param eventHubConsumerGroupNamePl string = 'cgadxpl${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_edge_iot_ops_jumpstart/aio_manufacturing/bicep/' +var publicIpAddressName = '${vmName}-PIP' +var networkInterfaceName = '${vmName}-NIC' +var bastionSubnetName = 'AzureBastionSubnet' +var subnetRef = resourceId('Microsoft.Network/virtualNetworks/subnets', virtualNetworkName, subnetName) +var bastionSubnetRef = resourceId('Microsoft.Network/virtualNetworks/subnets', virtualNetworkName, bastionSubnetName) +var osDiskType = 'Premium_LRS' +var subnetAddressPrefix = '10.1.0.0/24' +var addressPrefix = '10.1.0.0/16' +var bastionName = concat(bastionHostName) +var bastionSubnetIpPrefix = '10.1.1.64/26' +var PublicIPNoBastion = { + id: publicIpAddress.id +} + +resource networkInterface 'Microsoft.Network/networkInterfaces@2022-07-01' = { + name: networkInterfaceName + location: location + properties: { + ipConfigurations: [ + { + name: 'ipconfig1' + properties: { + subnet: { + id: subnetRef + } + privateIPAllocationMethod: 'Dynamic' + publicIPAddress: ((!deployBastion) ? PublicIPNoBastion : null) + } + } + ] + networkSecurityGroup: { + id: networkSecurityGroup.id + } + } +} + +resource networkSecurityGroup 'Microsoft.Network/networkSecurityGroups@2019-02-01' = { + name: networkSecurityGroupName + location: location +} + +resource networkSecurityGroupName_allow_RDP_3389 'Microsoft.Network/networkSecurityGroups/securityRules@2022-05-01' = if (deployBastion) { + parent: networkSecurityGroup + name: 'allow_RDP_3389' + properties: { + priority: 1001 + protocol: 'TCP' + access: 'Allow' + direction: 'Inbound' + sourceAddressPrefix: bastionSubnetIpPrefix + sourcePortRange: '*' + destinationAddressPrefix: '*' + destinationPortRange: '3389' + } +} + +resource virtualNetwork 'Microsoft.Network/virtualNetworks@2022-07-01' = { + name: virtualNetworkName + location: location + properties: { + addressSpace: { + addressPrefixes: [ + addressPrefix + ] + } + subnets: [ + { + name: subnetName + properties: { + addressPrefix: subnetAddressPrefix + privateEndpointNetworkPolicies: 'Enabled' + privateLinkServiceNetworkPolicies: 'Enabled' + } + } + { + name: 'AzureBastionSubnet' + properties: { + addressPrefix: bastionSubnetIpPrefix + } + } + ] + } +} + +resource publicIpAddress 'Microsoft.Network/publicIpAddresses@2022-07-01' = { + name: publicIpAddressName + location: location + properties: { + publicIPAllocationMethod: 'Static' + publicIPAddressVersion: 'IPv4' + idleTimeoutInMinutes: 4 + } + sku: { + name: ((!deployBastion) ? 'Basic' : 'Standard') + } +} + +resource vm 'Microsoft.Compute/virtualMachines@2022-11-01' = { + name: vmName + location: location + tags: resourceTags + properties: { + hardwareProfile: { + vmSize: vmSize + } + storageProfile: { + osDisk: { + name: '${vmName}-OSDisk' + caching: 'ReadWrite' + createOption: 'fromImage' + managedDisk: { + storageAccountType: osDiskType + } + } + imageReference: { + publisher: 'MicrosoftWindowsServer' + offer: 'WindowsServer' + sku: windowsOSVersion + version: 'latest' + } + } + networkProfile: { + networkInterfaces: [ + { + id: networkInterface.id + } + ] + } + osProfile: { + computerName: vmName + adminUsername: windowsAdminUsername + adminPassword: windowsAdminPassword + windowsConfiguration: { + provisionVMAgent: true + enableAutomaticUpdates: false + } + } + } +} + +resource Bootstrap 'Microsoft.Compute/virtualMachines/extensions@2022-11-01' = { + parent: vm + name: 'Bootstrap' + location: location + tags: { + displayName: 'Run Bootstrap' + } + properties: { + publisher: 'Microsoft.Compute' + type: 'CustomScriptExtension' + typeHandlerVersion: '1.10' + autoUpgradeMinorVersion: true + protectedSettings: { + fileUris: [ + uri(templateBaseUrl, 'artifacts/PowerShell/Bootstrap.ps1') + ] + commandToExecute: 'powershell.exe -ExecutionPolicy Unrestricted -File Bootstrap.ps1 -adminUsername ${windowsAdminUsername} -adminPassword ${windowsAdminPassword} -spnClientId ${spnClientId} -spnClientSecret ${spnClientSecret} -spnTenantId ${spnTenantId} -subscriptionId ${subscriptionId} -resourceGroup ${resourceGroup().name} -location ${location} -kubernetesDistribution ${kubernetesDistribution} -windowsNode ${windowsNode} -templateBaseUrl ${templateBaseUrl} -customLocationRPOID ${customLocationRPOID} -spnObjectId ${spnObjectId} -gitHubAccount ${githubAccount} -githubBranch ${githubBranch} -adxClusterName ${adxClusterName} -rdpPort ${rdpPort}' + } + } +} + +resource InstallWindowsFeatures 'Microsoft.Compute/virtualMachines/extensions@2022-11-01' = { + parent: vm + name: 'InstallWindowsFeatures' + dependsOn: [ + Bootstrap + ] + location: location + properties: { + publisher: 'Microsoft.Powershell' + type: 'DSC' + typeHandlerVersion: '2.77' + autoUpgradeMinorVersion: true + settings: { + wmfVersion: 'latest' + configuration: { + url: uri(templateBaseUrl, 'artifacts/Settings/DSCInstallWindowsFeatures.zip') + script: 'DSCInstallWindowsFeatures.ps1' + function: 'InstallWindowsFeatures' + } + } + } +} + +resource bastion 'Microsoft.Network/bastionHosts@2022-07-01' = if (deployBastion) { + name: bastionName + location: location + properties: { + ipConfigurations: [ + { + name: 'IpConf' + properties: { + subnet: { + id: bastionSubnetRef + } + publicIPAddress: { + id: publicIpAddress.id + } + } + } + ] + } + dependsOn: [ + virtualNetwork + + ] +} + +module storageAccount 'storage/storageAccount.bicep' = { + name: 'storageAccount' + params: { + storageAccountName: aioStorageAccountName + location: location + storageQueueName: storageQueueName + } +} + +module eventHub 'data/eventHub.bicep' = { + name: 'eventHub' + params: { + eventHubName: eventHubName + eventHubNamespaceName: eventHubNamespaceName + location: location + eventHubConsumerGroupName: eventHubConsumerGroupName + eventHubConsumerGroupNamePl: eventHubConsumerGroupNamePl + } +} + +module eventGrid 'data/eventGrid.bicep' = { + name: 'eventGrid' + params: { + eventGridNamespaceName: eventGridNamespaceName + eventHubResourceId: eventHub.outputs.eventHubResourceId + queueName: storageQueueName + storageAccountResourceId: storageAccount.outputs.storageAccountId + namingGuid: namingGuid + location: location + } +} + +module adxCluster 'data/dataExplorer.bicep' = { + name: 'dataExplorer' + params: { + adxClusterName: adxClusterName + location: location + eventHubResourceId: eventHub.outputs.eventHubResourceId + eventHubConsumerGroupName: eventHubConsumerGroupName + eventHubName: eventHubName + eventHubNamespaceName: eventHubNamespaceName + } +} + +module keyVault 'data/keyVault.bicep' = { + name: 'keyVault' + params: { + tenantId: spnTenantId + akvName: akvName + location: location + } +} + +output windowsAdminUsername string = windowsAdminUsername +output publicIP string = concat(publicIpAddress.properties.ipAddress) +output adxEndpoint string = adxCluster.outputs.adxEndpoint diff --git a/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/main.azd.parameters.json b/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/main.azd.parameters.json new file mode 100644 index 0000000000..455ef571b3 --- /dev/null +++ b/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/main.azd.parameters.json @@ -0,0 +1,39 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "envName": { + "value": "${AZURE_ENV_NAME}" + }, + "kubernetesDistribution": { + "value": "k3s" + }, + "windowsAdminUsername": { + "value": "${JS_WINDOWS_ADMIN_USERNAME}" + }, + "spnClientId": { + "value": "${SPN_CLIENT_ID}" + }, + "spnObjectId": { + "value": "${SPN_OBJECT_ID}" + }, + "spnClientSecret": { + "value": "${SPN_CLIENT_SECRET}" + }, + "spnTenantId": { + "value": "${SPN_TENANT_ID}" + }, + "location": { + "value": "${AZURE_LOCATION}" + }, + "deployBastion": { + "value": false + }, + "customLocationRPOID": { + "value": "${CUSTOM_LOCATION_RP_ID}" + }, + "rdpPort": { + "value": "${JS_RDP_PORT}" + } + } +} \ No newline at end of file diff --git a/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/main.bicep b/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/main.bicep new file mode 100644 index 0000000000..694136268d --- /dev/null +++ b/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/main.bicep @@ -0,0 +1,381 @@ +@description('The name of you Virtual Machine.') +param vmName string = 'AIO-Demo' + +@description('Kubernetes distribution') +@allowed([ + 'k8s' + 'k3s' +]) +param kubernetesDistribution string = 'k3s' + +@description('Username for the Virtual Machine.') +param windowsAdminUsername string = 'arcdemo' + +@description('Windows password for the Virtual Machine') +@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.') +@allowed([ + 'westus3' + 'eastus2' + 'westeurope' + 'westus2' + 'northeurope' +]) +param location string + +@description('Choice to deploy Bastion to connect to the client VM') +param deployBastion bool + +@description('the Azure Bastion host name') +param bastionHostName string = 'AIO-Demo-Bastion' + +@description('The size of the VM') +param vmSize string = 'Standard_D8s_v3' + +@description('Unique SPN app ID') +param spnClientId string + +@description('Unique SPN object ID') +param spnObjectId string + +@description('Unique SPN password') +@minLength(12) +@maxLength(123) +@secure() +param spnClientSecret string + +@description('Unique SPN tenant ID') +param spnTenantId string + +@description('Azure subscription ID') +param subscriptionId string = subscription().subscriptionId + +@description('Target GitHub account') +param githubAccount string = 'microsoft' + +@description('Target GitHub branch') +param githubBranch string = 'main' + +@description('Name of the VNET') +param virtualNetworkName string = 'AIO-Demo-VNET' + +@description('Name of the subnet in the virtual network') +param subnetName string = 'Subnet' + +@description('Name of the Network Security Group') +param networkSecurityGroupName string = 'AIO-Demo-NSG' + +param resourceTags object = { + Project: 'jumpstart_azure_aio' +} + +@maxLength(5) +@description('Random GUID') +param namingGuid string = toLower(substring(newGuid(), 0, 5)) + +@description('Deploy Windows Node for AKS Edge Essentials') +param windowsNode bool = false + +@description('Name of the storage account') +param aioStorageAccountName string = 'aiostg${namingGuid}' + +@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 Azure Data Explorer cluster') +param adxClusterName string = 'aioadx${namingGuid}' + +@description('The custom location RPO ID') +param customLocationRPOID string + +@description('The name of the Azure Key Vault') +param akvName string = 'aioakv${namingGuid}' + +@description('The name of the Azure Data Explorer Event Hub consumer group') +param eventHubConsumerGroupName string = 'cgadx${namingGuid}' + +@description('The name of the Azure Data Explorer Event Hub production line consumer group') +param eventHubConsumerGroupNamePl string = 'cgadxpl${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_edge_iot_ops_jumpstart/aio_manufacturing/bicep/' +var publicIpAddressName = '${vmName}-PIP' +var networkInterfaceName = '${vmName}-NIC' +var bastionSubnetName = 'AzureBastionSubnet' +var subnetRef = resourceId('Microsoft.Network/virtualNetworks/subnets', virtualNetworkName, subnetName) +var bastionSubnetRef = resourceId('Microsoft.Network/virtualNetworks/subnets', virtualNetworkName, bastionSubnetName) +var osDiskType = 'Premium_LRS' +var subnetAddressPrefix = '10.1.0.0/24' +var addressPrefix = '10.1.0.0/16' +var bastionName = concat(bastionHostName) +var bastionSubnetIpPrefix = '10.1.1.64/26' +var PublicIPNoBastion = { + id: publicIpAddress.id +} + +resource networkInterface 'Microsoft.Network/networkInterfaces@2022-07-01' = { + name: networkInterfaceName + location: location + properties: { + ipConfigurations: [ + { + name: 'ipconfig1' + properties: { + subnet: { + id: subnetRef + } + privateIPAllocationMethod: 'Dynamic' + publicIPAddress: ((!deployBastion) ? PublicIPNoBastion : null) + } + } + ] + networkSecurityGroup: { + id: networkSecurityGroup.id + } + } +} + +resource networkSecurityGroup 'Microsoft.Network/networkSecurityGroups@2019-02-01' = { + name: networkSecurityGroupName + location: location +} + +resource networkSecurityGroupName_allow_RDP_3389 'Microsoft.Network/networkSecurityGroups/securityRules@2022-05-01' = if (deployBastion) { + parent: networkSecurityGroup + name: 'allow_RDP_3389' + properties: { + priority: 1001 + protocol: 'TCP' + access: 'Allow' + direction: 'Inbound' + sourceAddressPrefix: bastionSubnetIpPrefix + sourcePortRange: '*' + destinationAddressPrefix: '*' + destinationPortRange: '3389' + } +} + +resource virtualNetwork 'Microsoft.Network/virtualNetworks@2022-07-01' = { + name: virtualNetworkName + location: location + properties: { + addressSpace: { + addressPrefixes: [ + addressPrefix + ] + } + subnets: [ + { + name: subnetName + properties: { + addressPrefix: subnetAddressPrefix + privateEndpointNetworkPolicies: 'Enabled' + privateLinkServiceNetworkPolicies: 'Enabled' + } + } + { + name: 'AzureBastionSubnet' + properties: { + addressPrefix: bastionSubnetIpPrefix + } + } + ] + } +} + +resource publicIpAddress 'Microsoft.Network/publicIpAddresses@2022-07-01' = { + name: publicIpAddressName + location: location + properties: { + publicIPAllocationMethod: 'Static' + publicIPAddressVersion: 'IPv4' + idleTimeoutInMinutes: 4 + } + sku: { + name: ((!deployBastion) ? 'Basic' : 'Standard') + } +} + +resource vm 'Microsoft.Compute/virtualMachines@2022-11-01' = { + name: vmName + location: location + tags: resourceTags + properties: { + hardwareProfile: { + vmSize: vmSize + } + storageProfile: { + osDisk: { + name: '${vmName}-OSDisk' + caching: 'ReadWrite' + createOption: 'fromImage' + managedDisk: { + storageAccountType: osDiskType + } + } + imageReference: { + publisher: 'MicrosoftWindowsServer' + offer: 'WindowsServer' + sku: windowsOSVersion + version: 'latest' + } + } + networkProfile: { + networkInterfaces: [ + { + id: networkInterface.id + } + ] + } + osProfile: { + computerName: vmName + adminUsername: windowsAdminUsername + adminPassword: windowsAdminPassword + windowsConfiguration: { + provisionVMAgent: true + enableAutomaticUpdates: false + } + } + } +} + +resource Bootstrap 'Microsoft.Compute/virtualMachines/extensions@2022-11-01' = { + parent: vm + name: 'Bootstrap' + location: location + tags: { + displayName: 'Run Bootstrap' + } + properties: { + publisher: 'Microsoft.Compute' + type: 'CustomScriptExtension' + typeHandlerVersion: '1.10' + autoUpgradeMinorVersion: true + protectedSettings: { + fileUris: [ + uri(templateBaseUrl, 'artifacts/PowerShell/Bootstrap.ps1') + ] + commandToExecute: 'powershell.exe -ExecutionPolicy Unrestricted -File Bootstrap.ps1 -adminUsername ${windowsAdminUsername} -adminPassword ${windowsAdminPassword} -spnClientId ${spnClientId} -spnClientSecret ${spnClientSecret} -spnTenantId ${spnTenantId} -subscriptionId ${subscriptionId} -resourceGroup ${resourceGroup().name} -location ${location} -kubernetesDistribution ${kubernetesDistribution} -windowsNode ${windowsNode} -templateBaseUrl ${templateBaseUrl} -customLocationRPOID ${customLocationRPOID} -spnObjectId ${spnObjectId} -gitHubAccount ${githubAccount} -githubBranch ${githubBranch} -adxClusterName ${adxClusterName} -rdpPort ${rdpPort}' + } + } +} + +resource InstallWindowsFeatures 'Microsoft.Compute/virtualMachines/extensions@2022-11-01' = { + parent: vm + name: 'InstallWindowsFeatures' + dependsOn: [ + Bootstrap + ] + location: location + properties: { + publisher: 'Microsoft.Powershell' + type: 'DSC' + typeHandlerVersion: '2.77' + autoUpgradeMinorVersion: true + settings: { + wmfVersion: 'latest' + configuration: { + url: uri(templateBaseUrl, 'artifacts/Settings/DSCInstallWindowsFeatures.zip') + script: 'DSCInstallWindowsFeatures.ps1' + function: 'InstallWindowsFeatures' + } + } + } +} + +resource bastion 'Microsoft.Network/bastionHosts@2022-07-01' = if (deployBastion) { + name: bastionName + location: location + properties: { + ipConfigurations: [ + { + name: 'IpConf' + properties: { + subnet: { + id: bastionSubnetRef + } + publicIPAddress: { + id: publicIpAddress.id + } + } + } + ] + } + dependsOn: [ + virtualNetwork + + ] +} + +module storageAccount 'storage/storageAccount.bicep' = { + name: 'storageAccount' + params: { + storageAccountName: aioStorageAccountName + location: location + storageQueueName: storageQueueName + } +} + +module eventHub 'data/eventHub.bicep' = { + name: 'eventHub' + params: { + eventHubName: eventHubName + eventHubNamespaceName: eventHubNamespaceName + location: location + eventHubConsumerGroupName: eventHubConsumerGroupName + eventHubConsumerGroupNamePl: eventHubConsumerGroupNamePl + } +} + +module eventGrid 'data/eventGrid.bicep' = { + name: 'eventGrid' + params: { + eventGridNamespaceName: eventGridNamespaceName + eventHubResourceId: eventHub.outputs.eventHubResourceId + queueName: storageQueueName + storageAccountResourceId: storageAccount.outputs.storageAccountId + namingGuid: namingGuid + location: location + } +} + +module adxCluster 'data/dataExplorer.bicep' = { + name: 'dataExplorer' + params: { + adxClusterName: adxClusterName + location: location + eventHubResourceId: eventHub.outputs.eventHubResourceId + eventHubConsumerGroupName: eventHubConsumerGroupName + eventHubName: eventHubName + eventHubNamespaceName: eventHubNamespaceName + } +} + +module keyVault 'data/keyVault.bicep' = { + name: 'keyVault' + params: { + tenantId: spnTenantId + akvName: akvName + location: location + } +} + +output windowsAdminUsername string = windowsAdminUsername +output publicIP string = concat(publicIpAddress.properties.ipAddress) +output adxEndpoint string = adxCluster.outputs.adxEndpoint diff --git a/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/main.parameters.example.json b/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/main.parameters.example.json new file mode 100644 index 0000000000..b82381f0d0 --- /dev/null +++ b/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/main.parameters.example.json @@ -0,0 +1,36 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "kubernetesDistribution": { + "value": "k3s" + }, + "windowsAdminUsername": { + "value": "arcdemo" + }, + "windowsAdminPassword": { + "value": "ArcPassword123!!" + }, + "spnClientId": { + "value": "XXXXX-XXXXX-XXXXX" + }, + "spnObjectId": { + "value": "XXXXX-XXXXX-XXXXX" + }, + "spnClientSecret": { + "value": "XXXXX-XXXXX-XXXXX" + }, + "spnTenantId": { + "value": "XXXXX-XXXXX-XXXXX" + }, + "location": { + "value": "eastus2" + }, + "deployBastion": { + "value": false + }, + "customLocationRPOID": { + "value": "XXXXX-XXXXX-XXXXX" + } + } +} \ No newline at end of file diff --git a/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/main.parameters.json b/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/main.parameters.json new file mode 100644 index 0000000000..b82381f0d0 --- /dev/null +++ b/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/main.parameters.json @@ -0,0 +1,36 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "kubernetesDistribution": { + "value": "k3s" + }, + "windowsAdminUsername": { + "value": "arcdemo" + }, + "windowsAdminPassword": { + "value": "ArcPassword123!!" + }, + "spnClientId": { + "value": "XXXXX-XXXXX-XXXXX" + }, + "spnObjectId": { + "value": "XXXXX-XXXXX-XXXXX" + }, + "spnClientSecret": { + "value": "XXXXX-XXXXX-XXXXX" + }, + "spnTenantId": { + "value": "XXXXX-XXXXX-XXXXX" + }, + "location": { + "value": "eastus2" + }, + "deployBastion": { + "value": false + }, + "customLocationRPOID": { + "value": "XXXXX-XXXXX-XXXXX" + } + } +} \ No newline at end of file diff --git a/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/storage/storageAccount.bicep b/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/storage/storageAccount.bicep new file mode 100644 index 0000000000..ffa8f62af9 --- /dev/null +++ b/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep/storage/storageAccount.bicep @@ -0,0 +1,39 @@ +@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' + +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 +} + + +output queueName string = storageQueueName +output storageAccountId string = storageAccount.id diff --git a/azure_edge_iot_ops_jumpstart/aio_manufacturing/scripts/postprovision.ps1 b/azure_edge_iot_ops_jumpstart/aio_manufacturing/scripts/postprovision.ps1 new file mode 100644 index 0000000000..6ae74d1986 --- /dev/null +++ b/azure_edge_iot_ops_jumpstart/aio_manufacturing/scripts/postprovision.ps1 @@ -0,0 +1,64 @@ +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 +} + + +######################################################################## +# 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/${githubAccount}/azure_arc/${githubBranch}/azure_edge_iot_ops_jumpstart/aio_manufacturing/bicep" +$dashboardBody = (Invoke-WebRequest -Method Get -Uri "$templateBaseUrl/artifacts/adx_dashboards/dashboard.json").Content -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 $dashboardBody -Headers $httpHeaders +if ($httpResponse.StatusCode -ne 200){ + Write-Host "ERROR: Failed import orders dashboard report into Azure Data Explorer" -ForegroundColor Red +} + + +######################################################################## +# RDP Port +######################################################################## + +# Configure NSG Rule for RDP (if needed) +If ($rdpPort -ne "3389") { + + Write-Host "Configuring NSG Rule for RDP..." + $nsg = Get-AzNetworkSecurityGroup -ResourceGroupName $resourceGroup -Name AKS-EE-Demo-NSG + + 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 +} diff --git a/azure_edge_iot_ops_jumpstart/aio_manufacturing/scripts/predown.ps1 b/azure_edge_iot_ops_jumpstart/aio_manufacturing/scripts/predown.ps1 new file mode 100644 index 0000000000..6c23a3078e --- /dev/null +++ b/azure_edge_iot_ops_jumpstart/aio_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_edge_iot_ops_jumpstart/aio_manufacturing/scripts/preprovision.ps1 b/azure_edge_iot_ops_jumpstart/aio_manufacturing/scripts/preprovision.ps1 new file mode 100644 index 0000000000..b6658bf224 --- /dev/null +++ b/azure_edge_iot_ops_jumpstart/aio_manufacturing/scripts/preprovision.ps1 @@ -0,0 +1,208 @@ +######################################################################## +# 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 DSv3 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 = 'arcdemo' +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 + + +######################################################################## +# RDP Port +######################################################################## +$JS_RDP_PORT = '3389' +If ($env:JS_RDP_PORT) { + $JS_RDP_PORT = $env:JS_RDP_PORT +} +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')[0].Id + +# Set environment variables +azd env set CUSTOM_LOCATION_RP_ID $customLocationRPOID + +######################################################################## +# Create Azure Service Principal +######################################################################## +Write-Host "Creating Azure Service Principal..." + +$user = $context.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 +} +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." + } + elseif ($error[0].ToString() -match "credentials") { + Throw "Please run Connect-AzAccount to sign and run 'azd up' again." + } + else { + Throw "An error occurred creating the service principal. Please try again." + } +} + +$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 \ No newline at end of file