├── .gitignore ├── 2020-01 DFWSMUG ├── Check Agent Status.ps1 ├── Create Update Deployment.ps1 ├── Full Update Deployment Results.kql ├── Reset Linux Agent.sh ├── Reset Windows Agent.ps1 ├── SoftwareUpdateConfigurationRunContext.json ├── Start Runbook on Hybrid Worker from Azure Runbook.ps1 └── Update Management DFWSMUG.pdf ├── 2021-08 PowerShell Southampton ├── Connect-RunAsAccount.ps1 ├── Create-AzureDeployment.ps1 ├── DeploymentResults.kusto ├── Get-UpdateJobStates.ps1 ├── Install Log Analytics Agent.ps1 ├── PostTask-DomainControllerGroupB.ps1 ├── PostTask-StopAzVM.ps1 ├── PreTask-CheckStarts.ps1 ├── PreTask-StartAzVM.ps1 ├── PreTask-StopServices.ps1 ├── Send-Notification.ps1 ├── Stop-LinuxServices.ps1 ├── Stop-WindowsServices.ps1 └── Update Management Southampton.pdf ├── 2021-09 New York PowerShell User Group ├── Move-OutlookItems.ps1 ├── New-DevServer.ps1 ├── Outlook-FreeTime.ps1 ├── SeleniumForm.ps1 └── Spell Check Comments.ps1 ├── 2022-08 DFWSMUG ├── Azure Arc Functions.ps1 ├── Azure Arc Script.ps1 ├── Azure VM Functions.ps1 ├── Azure VM Script.ps1 ├── Combined Execution.ps1 └── VSCodeExt.ps1 ├── 2022-12 RTPSUG ├── 01 Azure VM Script.ps1 ├── 02 Azure VM Script Linux.ps1 ├── 03 Azure Arc Script.ps1 ├── 04 Azure Arc Script Linux.ps1 ├── 05 Get-SystemInfo.ps1 ├── 06 Azure VM Functions.ps1 ├── 07 VM Executions.ps1 ├── 08 Azure Arc Functions.ps1 ├── 09 Arc Executions.ps1 ├── Combined Execution.ps1 └── Managing Hybrid Infrastructure.pdf ├── 2023-04 PSHSummit ├── LightingDemo │ ├── Move-OutlookItems.ps1 │ └── Outlook-FreeTime.ps1 └── Managing Hybrid Infrastructure │ ├── AzRemoteCommand │ ├── ARM │ │ ├── Get-ArcScriptWrapper.ps1 │ │ ├── Get-ArcScriptWrapperArm.ps1 │ │ └── Invoke-ArcCommandArm.ps1 │ ├── AzRemoteCommand.psd1 │ ├── AzRemoteCommand.psm1 │ ├── Public │ │ ├── Get-ArcScriptStatus.ps1 │ │ ├── Get-ArcScriptWrapper.ps1 │ │ ├── Get-AzRemoteCommandOutput.ps1 │ │ ├── Get-AzRemoteCommandStatus.ps1 │ │ ├── Get-VmScriptStatus.ps1 │ │ ├── Invoke-ArcCommand.ps1 │ │ ├── Invoke-AzRemoteCommand.ps1 │ │ ├── Invoke-VmCommand.ps1 │ │ └── Write-StringtoBlob.ps1 │ └── Resources │ │ ├── pwshinstall.ps1 │ │ └── pwshinstall.sh │ ├── Demo │ ├── Demo01 - Run Command on VM.ps1 │ ├── Demo02 - Run Command on Arc.ps1 │ ├── Demo03 - Output VM results to Blob.ps1 │ ├── Demo04 - Write to Blob.ps1 │ ├── Demo05 - Arc Script Wrapper.ps1 │ ├── Demo06 - Write Command Output to Blob.ps1 │ ├── Demo07 - Output Arc results to Blob.ps1 │ ├── Demo08 - Get return data from Everything.ps1 │ ├── Demo09 - Functions.ps1 │ └── Demo10 - All Together.ps1 │ ├── Managing Hybrid Infrastructure - PSHSummit.pdf │ └── Scripts │ ├── Get-SystemInfo.ps1 │ └── Get-WinSystemInfo.ps1 ├── 2024-04 DFWSMUG ├── AzurePester │ ├── ManualDeployment.ps1 │ ├── PesterTester.ps1 │ ├── azstorage.yml │ └── azuredeploy.json ├── DataDriven.Tests.ps1 ├── RunTests.ps1 ├── ServerConfig.Tests.ps1 ├── Taking Pester Beyond the Code DFWSMUG.pdf └── TheHardWay.ps1 ├── 2025-04 PSHSummit ├── Building Resilient Automations │ ├── Building Resilient Automations.pdf │ ├── ErrorHandling-No.ps1 │ ├── ErrorHandling-Yes.ps1 │ ├── FileDownload.ps1 │ ├── FileUpload.ps1 │ └── NewUserDemo.ps1 └── IaC Automated Assurance │ ├── .github │ └── workflows │ │ ├── azure-analyze.yaml │ │ ├── bicep-diff.yml │ │ └── bicep-linter.yml │ ├── .ps-rule │ ├── Bicep.Naming.Rule.ps1 │ ├── Org.Tag.Rule.ps1 │ └── Suppression.Rule.yaml │ ├── IaC Automated Assurance.pdf │ ├── Linter │ └── main.bicep │ ├── PSRule │ ├── dev.bicepparam │ ├── main.bicep │ └── prod.bicepparam │ ├── PesterTests │ ├── Bicep.Test.ps1 │ └── Functions │ │ ├── ConvertFrom-BicepFile.ps1 │ │ ├── Invoke-BicepTests.ps1 │ │ ├── New-TestResults.ps1 │ │ ├── Test-BicepLinter.ps1 │ │ ├── Test-BicepResourceOrder.ps1 │ │ └── Write-Failure.ps1 │ ├── WhatIf │ ├── Compare-BicepWhatIf.ps1 │ ├── demo01.bicep │ ├── demo02.bicep │ └── dev.bicepparam │ ├── main.bicep │ └── ps-rule.yaml └── README.md /.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdowst/Presentation-Materials/7c5577ab08f8cae96555df27df12d103fea04d32/.gitignore -------------------------------------------------------------------------------- /2020-01 DFWSMUG/Check Agent Status.ps1: -------------------------------------------------------------------------------- 1 | # Query log analytics group for nonAzure Windows machines 2 | $query = @' 3 | Heartbeat 4 | | where OSType == "Windows" 5 | | summarize arg_max(TimeGenerated, *) by SourceComputerId 6 | '@ 7 | $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $WorkspaceId -Query $query 8 | $Computers = $queryResults.Results | Select-Object -ExpandProperty Computer 9 | 10 | # Confirm all machines are Hybrid Runbook Workers and reporting 11 | Get-AzAutomationHybridWorkerGroup -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName | 12 | Where-Object{ $_.GroupType -eq 'System'} | Select-Object -ExpandProperty RunbookWorker | 13 | Where-Object{ $Computers -contains $_.Name } -------------------------------------------------------------------------------- /2020-01 DFWSMUG/Create Update Deployment.ps1: -------------------------------------------------------------------------------- 1 | # Create the schedule object 2 | $startTime = Get-Date '21:00:00' 3 | $AzAutomationSchedule = @{ 4 | ResourceGroupName = $ResourceGroupName 5 | AutomationAccountName = $AutomationAccountName 6 | Name = 'Daily Defender Updates' 7 | StartTime = $startTime 8 | DayInterval = 1 9 | ForUpdateConfiguration = $true 10 | } 11 | $schedule = New-AzAutomationSchedule @AzAutomationSchedule 12 | 13 | $WorkspaceObject = Get-AzOperationalInsightsWorkspace -ResourceGroupName $ResourceGroupName -Name $WorkspaceName 14 | 15 | # Create Non Azure Query Object 16 | $NonAzureQuery = [Microsoft.Azure.Commands.Automation.Model.UpdateManagement.NonAzureQueryProperties]::new() 17 | $NonAzureQuery.FunctionAlias = 'NonAzure_Windows' 18 | $NonAzureQuery.WorkspaceResourceId = $WorkspaceObject.ResourceId 19 | 20 | # Create the update deployment 21 | $duration = New-TimeSpan -Hours 2 22 | $UpdateConfiguration = @{ 23 | ResourceGroupName = $ResourceGroupName 24 | AutomationAccountName = $AutomationAccountName 25 | Schedule = $schedule 26 | NonAzureQuery = $NonAzureQuery 27 | Windows = $true 28 | IncludedUpdateClassification = 'Definition' 29 | Duration = $duration 30 | } 31 | New-AzAutomationSoftwareUpdateConfiguration @UpdateConfiguration 32 | -------------------------------------------------------------------------------- /2020-01 DFWSMUG/Full Update Deployment Results.kql: -------------------------------------------------------------------------------- 1 | let timeAgo = ago(1d); 2 | // Get the Parent Jobs 3 | let ParentJobs = AzureDiagnostics 4 | | where TimeGenerated > timeAgo 5 | | where RunbookName_s == "Patch-MicrosoftOMSComputers" and StreamType_s == "Verbose" and Category == "JobStreams" 6 | | where ResultDescription contains "Getting SoftwareUpdateConfigurationMachines" 7 | | extend ScheduleName = substring(ResultDescription,indexof(ResultDescription, "SoftwareUpdateConfigurationName")+32, indexof(ResultDescription, "ShouldResolveStaticMachines")-indexof(ResultDescription, "SoftwareUpdateConfigurationName")-34) 8 | | project TimeGenerated, ScheduleName, ParentJobId_g = JobId_g 9 | | join kind= inner ( 10 | AzureDiagnostics 11 | | where TimeGenerated > timeAgo 12 | | where RunbookName_s == "Patch-MicrosoftOMSComputers" and StreamType_s == "Verbose" and Category == "JobStreams" 13 | | where ResultDescription contains "Wait-AutomationJob" 14 | | extend jobId = substring(ResultDescription,indexof(ResultDescription, "JobId")+6, 36) 15 | | summarize arg_max(TimeGenerated, *) by jobId 16 | | project TimeGenerated, jobId, ParentJobId_g = JobId_g 17 | ) on ParentJobId_g 18 | | project ScheduleName, ParentJobId_g, jobId; 19 | // Get any jobs that are still running 20 | let RunningJobs = AzureDiagnostics 21 | | where TimeGenerated >= timeAgo and ResourceProvider == "MICROSOFT.AUTOMATION" and RunbookName_s == "Patch-MicrosoftOMSComputer" 22 | | where RunOn_s != "" 23 | | summarize arg_max(TimeGenerated, *) by JobId_g 24 | | where ResultDescription == "Job is started" 25 | | extend RunOn = substring(RunOn_s,0,strlen(RunOn_s)-37) 26 | | extend timeAgo = now() - TimeGenerated 27 | | extend timeAgoMinutes = round(timeAgo/1m,0) 28 | | project jobId = JobId_g, EndDateTimeUtc = datetime(null), DurationInMinutes = timeAgoMinutes, MachineName = RunOn, StartDateTimeUtc = TimeGenerated, Status = "In Progress",StatusDescription = "",RebootRequired = false,InitialRequiredUpdatesCount = toreal(0),TotalUpdatesInstalled = toreal(0),TotalUpdatesFailed = toreal(0),InstallPercentage = toreal(0); 29 | // Get the jobs that have completed 30 | let CompletedJobs = AzureDiagnostics 31 | | where TimeGenerated >= timeAgo and ResourceProvider == "MICROSOFT.AUTOMATION" and RunbookName_s == "Patch-MicrosoftOMSComputer" 32 | | where resultDescription_Summary_Status_s !in ("","InProgress") 33 | | project jobId = JobId_g, EndDateTimeUtc = resultDescription_Summary_EndDateTimeUtc_t,DurationInMinutes = resultDescription_Summary_DurationInMinutes_d,MachineName = resultDescription_Summary_MachineName_s,StartDateTimeUtc = resultDescription_Summary_StartDateTimeUtc_t,Status = resultDescription_Summary_Status_s,StatusDescription = resultDescription_Summary_StatusDescription_s,RebootRequired = resultDescription_Summary_RebootRequired_b,InitialRequiredUpdatesCount = resultDescription_Summary_InitialRequiredUpdatesCount_d,TotalUpdatesInstalled = resultDescription_Summary_TotalUpdatesInstalled_d,TotalUpdatesFailed = resultDescription_Summary_TotalUpdatesFailed_d,InstallPercentage = resultDescription_Summary_InstallPercentage_d; 34 | // Get any suspended jobs 35 | let SuspendStatus = AzureDiagnostics 36 | | where TimeGenerated > timeAgo 37 | | where RunbookName_s == "Patch-MicrosoftOMSComputers" and StreamType_s == "Verbose" and Category == "JobStreams" 38 | | where ResultDescription contains "Status = FailedToStart" 39 | | extend jobId = substring(ResultDescription,indexof(ResultDescription, "ChildJobId")+13, 36) 40 | | project TimeGenerated, jobId, ParentJobId_g = JobId_g 41 | | join kind= leftouter ( 42 | AzureDiagnostics 43 | | where TimeGenerated > timeAgo 44 | | where RunbookName_s == "Patch-MicrosoftOMSComputers" and StreamType_s == "Output" and Category == "JobStreams" 45 | | project resultDescription_ChildJobs_s 46 | | extend RunOn_object = parsejson(resultDescription_ChildJobs_s) 47 | | mvexpand RunOn_object 48 | | project RunOn = tostring(RunOn_object.RunOn), jobId = tostring(RunOn_object.JobId) 49 | ) on jobId 50 | | extend MachineName = substring(RunOn,0,strlen(RunOn)-37) 51 | | join kind= leftouter ( 52 | AzureDiagnostics 53 | | where TimeGenerated > timeAgo 54 | | where RunbookName_s == "Patch-MicrosoftOMSComputers" and StreamType_s == "Verbose" and Category == "JobStreams" 55 | | where ResultDescription contains "message=Created SUCR" 56 | | extend ConfigName = substring(ResultDescription,indexof(ResultDescription, "-SoftwareUpdateConfigurationName")+33, strlen(ResultDescription) - indexof(ResultDescription, "SoftwareUpdateConfigurationName")-33) 57 | | project ScheduleName = substring(ConfigName,0,indexof(ConfigName, " -")), ParentJobId_g = JobId_g 58 | ) on ParentJobId_g 59 | | project jobId, EndDateTimeUtc = TimeGenerated,DurationInMinutes = toreal(0),MachineName,StartDateTimeUtc = TimeGenerated,Status = "FailedToStart",StatusDescription = "Job was suspended.",RebootRequired = false,InitialRequiredUpdatesCount = toreal(0),TotalUpdatesInstalled = toreal(0),TotalUpdatesFailed = toreal(0),InstallPercentage = toreal(0); 60 | // Join all results on the parent jobs 61 | union CompletedJobs, RunningJobs, SuspendStatus 62 | | join kind= leftouter ( 63 | ParentJobs 64 | ) on jobId 65 | | project MachineName, ScheduleName, Status, DurationInMinutes, StartDateTimeUtc, EndDateTimeUtc, StatusDescription, RebootRequired, InitialRequiredUpdatesCount, TotalUpdatesInstalled, TotalUpdatesFailed, InstallPercentage 66 | | sort by Status desc -------------------------------------------------------------------------------- /2020-01 DFWSMUG/Reset Linux Agent.sh: -------------------------------------------------------------------------------- 1 | # Get the latest version of the agent script 2 | find -type f -name 'onboard_agent.sh' -exec rm -f {} \; 3 | wget https://raw.githubusercontent.com/Microsoft/OMS-Agent-for-Linux/master/installer/scripts/onboard_agent.sh 4 | 5 | # Uninstall Existing Agent (Skip if new install) 6 | sudo sh onboard_agent.sh --purge 7 | 8 | # Rename the worker configuration files (Skip if new install) 9 | mv -f /home/nxautomation/state/worker.conf /home/nxautomation/state/worker.conf_old 10 | mv -f /home/nxautomation/state/worker_diy.crt /home/nxautomation/state/worker_diy.crt_old 11 | mv -f /home/nxautomation/state/worker_diy.key /home/nxautomation/state/worker_diy.key_old 12 | 13 | # Remove from Automation Account through PowerShell of Portal 14 | 15 | # Reinstall Agent 16 | sudo sh onboard_agent.sh -w -s -d opinsights.azure.com 17 | -------------------------------------------------------------------------------- /2020-01 DFWSMUG/Reset Windows Agent.ps1: -------------------------------------------------------------------------------- 1 | # Server stop Health Service and clear cache and Hybrid Worker Configuration 2 | Stop-Service -Name HealthService 3 | Remove-Item -Path 'C:\Program Files\Microsoft Monitoring Agent\Agent\Health Service State' -Recurse 4 | Remove-Item -Path "HKLM:\software\microsoft\hybridrunbookworker" -Recurse -Force 5 | 6 | # Remove the Hybrid Worker from the Automation Account 7 | $ResourceGroupName = '' 8 | $AutomationAccountName = '' 9 | $Server = '' 10 | Get-AzAutomationHybridWorkerGroup -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName | 11 | Where-Object{ $_.RunbookWorker.Name -contains $Server} | Remove-AzAutomationHybridWorkerGroup 12 | 13 | # Restart the Health Service 14 | Start-Service -Name HealthService -------------------------------------------------------------------------------- /2020-01 DFWSMUG/SoftwareUpdateConfigurationRunContext.json: -------------------------------------------------------------------------------- 1 | { 2 | "SoftwareUpdateConfigurationName": "Pre-Example", 3 | "SoftwareUpdateConfigurationRunId": "3bf1f669-4117-4288-8f5f-7aa02ab0f878", 4 | "SoftwareUpdateConfigurationSettings": { 5 | "OperatingSystem": 1, 6 | "WindowsConfiguration": { 7 | "UpdateCategories": 16, 8 | "ExcludedKBNumbers": null, 9 | "IncludedKBNumbers": null, 10 | "RebootSetting": "Never" 11 | }, 12 | "LinuxConfiguration": null, 13 | "Targets": null, 14 | "NonAzureComputerNames": [ 15 | "ServerTest02" 16 | ], 17 | "AzureVirtualMachines": [], 18 | "Duration": "00:30:00", 19 | "PSComputerName": "localhost", 20 | "PSShowComputerName": true, 21 | "PSSourceJobInstanceId": "3bf1f669-4117-4288-8f5f-7aa02ab0f878" 22 | } 23 | } -------------------------------------------------------------------------------- /2020-01 DFWSMUG/Start Runbook on Hybrid Worker from Azure Runbook.ps1: -------------------------------------------------------------------------------- 1 | $hybridWorker = 'HBW01' 2 | $runbook = 'Stop-Services' 3 | # Get the current job Id 4 | $CurrentJobId = $PSPrivateMetadata.JobId.Guid 5 | 6 | # Get the Service Principal connection details for the Connection name 7 | $servicePrincipalConnection = Get-AutomationConnection -Name "AzureRunAsConnection" 8 | 9 | # Connect to Azure 10 | $params = @{ 11 | TenantId = $servicePrincipalConnection.TenantId 12 | CertificateThumbprint = $servicePrincipalConnection.CertificateThumbprint 13 | ApplicationId = $servicePrincipalConnection.ApplicationId 14 | } 15 | Add-AzureRmAccount -ServicePrincipal @params | Out-Null 16 | Set-AzureRmContext -SubscriptionId $servicePrincipalConnection.SubscriptionId | Out-Null 17 | 18 | #Get Automation account and resource group names 19 | $AutomationAccounts = Find-AzureRmResource -ResourceType Microsoft.Automation/AutomationAccounts 20 | foreach ($item in $AutomationAccounts) { 21 | # Loop through each Automation account to find this job 22 | $Job = Get-AzureRmAutomationJob -ResourceGroupName $item.ResourceGroupName -AutomationAccountName $item.Name -Id $CurrentJobId -ErrorAction SilentlyContinue 23 | if ($Job) { 24 | $AutomationAccountName = $item.Name 25 | $ResourceGroupName = $item.ResourceGroupName 26 | break 27 | } 28 | } 29 | 30 | # Start the runbook on the hybrid worker 31 | $jobId = Start-AutomationRunbook -Name $runbook -Parameters @{ "args" = $args } -RunOn $hybridWorker -ErrorAction Stop 32 | 33 | # wait for the job to finish 34 | Wait-AutomationJob -Id $jobId -TimeoutInMinutes 10 35 | 36 | #In this case, we want to terminate the patch job if any run fails. 37 | #This logic might not hold for all cases - you might want to allow success as long as at least 1 run succeeds 38 | $JobOutput = Get-AzAutomationJobOutput -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Id $jobId -stream Any 39 | foreach($summary in $JobOutput){ 40 | if ($summary.Type -eq "Error"){ 41 | #We must throw in order to fail the patch deployment. 42 | throw $summary.Summary 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /2020-01 DFWSMUG/Update Management DFWSMUG.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdowst/Presentation-Materials/7c5577ab08f8cae96555df27df12d103fea04d32/2020-01 DFWSMUG/Update Management DFWSMUG.pdf -------------------------------------------------------------------------------- /2021-08 PowerShell Southampton/Connect-RunAsAccount.ps1: -------------------------------------------------------------------------------- 1 | # Get the current job Id 2 | $CurrentJobId = $PSPrivateMetadata.JobId.Guid 3 | 4 | # Get the Service Principal connection details for the Connection name 5 | $SPConnection = Get-AutomationConnection -Name 'AzureRunAsConnection' 6 | 7 | # Connect to Azure 8 | $Params = @{ 9 | TenantId = $SPConnection.TenantId 10 | CertificateThumbprint = $SPConnection.CertificateThumbprint 11 | ApplicationId = $SPConnection.ApplicationId 12 | } 13 | Add-AzAccount -ServicePrincipal @Params | Out-Null 14 | Set-AzContext -SubscriptionId $SPConnection.SubscriptionId | Out-Null 15 | 16 | #Get Automation account and resource group names 17 | $AutoAccts = Get-AzResource -ResourceType Microsoft.Automation/AutomationAccounts 18 | foreach ($Item in $AutoAccts) { 19 | # Loop through each Automation account to find this job 20 | $JobParams = @{ 21 | ResourceGroupName = $Item.ResourceGroupName 22 | AutomationAccountName = $Item.Name 23 | Id = $CurrentJobId 24 | ErrorAction = 'SilentlyContinue' 25 | } 26 | $Job = Get-AzAutomationJob @JobParams 27 | if ($Job) { 28 | $AutomationAccountName = $Item.Name 29 | $ResourceGroupName = $Item.ResourceGroupName 30 | break 31 | } 32 | } 33 | 34 | @{ 35 | ResourceGroupName = $ResourceGroupName 36 | AutomationAccountName = $AutomationAccountName 37 | } -------------------------------------------------------------------------------- /2021-08 PowerShell Southampton/Create-AzureDeployment.ps1: -------------------------------------------------------------------------------- 1 | # Create the schedule object 2 | $AzAutomationSchedule = @{ 3 | ResourceGroupName = $ResourceGroupName 4 | AutomationAccountName = $AutomationAccountName 5 | Name = "Domain Controller Group A ($((Get-Date).ToString('yyyy-MM-dd')))" 6 | StartTime = (Get-Date).AddMinutes(6) 7 | ForUpdateConfiguration = $true 8 | OneTime = $true 9 | } 10 | $schedule = New-AzAutomationSchedule @AzAutomationSchedule 11 | 12 | # Create the update deployment 13 | $duration = New-TimeSpan -Hours 2 14 | $UpdateConfiguration = @{ 15 | ResourceGroupName = $ResourceGroupName 16 | AutomationAccountName = $AutomationAccountName 17 | Schedule = $schedule 18 | AzureVMResourceId = $targetMachines 19 | Windows = $true 20 | IncludedUpdateClassification = 'Definition' 21 | Duration = $duration 22 | RebootSetting = 'IfRequired' 23 | PreTaskRunbookName = 'PreTask-CheckStarts' 24 | PostTaskRunbookName = 'PostTask-DomainControllerGroupB' 25 | } 26 | New-AzAutomationSoftwareUpdateConfiguration @UpdateConfiguration -------------------------------------------------------------------------------- /2021-08 PowerShell Southampton/DeploymentResults.kusto: -------------------------------------------------------------------------------- 1 | let timeago = ago(1d); 2 | let ScheduleName = "Domain Controller Group A"; 3 | // Get the Parent Jobs 4 | let Parent = AzureDiagnostics 5 | | where TimeGenerated >= timeago and ResourceProvider == "MICROSOFT.AUTOMATION" 6 | | where RunbookName_s == "Patch-MicrosoftOMSComputers" and StreamType_s == "Verbose" and Category == "JobStreams" 7 | | where ResultDescription contains "Created SUCR" 8 | | extend Name = trim("\\s",extract("-SoftwareUpdateConfigurationName(.*?)\\s-", 1, ResultDescription)) 9 | | extend operatingSystem = trim("\\s",extract("-OperatingSystem(.*?)-", 1, ResultDescription)) 10 | | where Name == ScheduleName 11 | | project TimeGenerated, ScheduleName = Name, OperatingSystem = operatingSystem, ParentJobId_g = JobId_g 12 | // Get the status for the parent jobs 13 | | join kind=leftouter ( 14 | AzureDiagnostics 15 | | where TimeGenerated > timeago and ResourceProvider == "MICROSOFT.AUTOMATION" 16 | | where RunbookName_s == "Patch-MicrosoftOMSComputers" and Category == "JobLogs" 17 | | summarize arg_max(TimeGenerated, *) by JobId_g 18 | | project ParentJobId_g = JobId_g, ParentStatus = ResultType 19 | ) on ParentJobId_g 20 | | project-away ParentJobId_g1 21 | // Get the child jobs for each parent 22 | | join kind=leftouter ( 23 | AzureDiagnostics 24 | | where TimeGenerated > timeago and ResourceProvider == "MICROSOFT.AUTOMATION" 25 | | where RunbookName_s == "Patch-MicrosoftOMSComputers" and Category == "JobStreams" and StreamType_s == "Output" 26 | | extend child = parse_json(ResultDescription).ChildJobs 27 | | mv-expand child 28 | | project JobId_g = tostring(child.JobId), Computer = substring(child.RunOn,0,strlen(child.RunOn)-37), ParentJobId_g = JobId_g 29 | ) on ParentJobId_g 30 | | project-away ParentJobId_g1; 31 | let childJobs = Parent | distinct JobId_g; 32 | Parent 33 | // Get the results and the computer for each child job 34 | | join kind=leftouter ( 35 | AzureDiagnostics 36 | | where TimeGenerated >= timeago and ResourceProvider == "MICROSOFT.AUTOMATION" and Category == "JobLogs" and JobId_g in (childJobs) 37 | | where RunbookName_s in ("Patch-MicrosoftOMSComputer","PatchMicrosoftOMSLinuxComputer") 38 | | summarize arg_max(TimeGenerated, *) by JobId_g 39 | | project JobId_g, JobResult = ResultType 40 | ) on JobId_g 41 | | project-away JobId_g1 42 | // Get the result description for each child job 43 | | join kind=leftouter ( 44 | AzureDiagnostics 45 | | where TimeGenerated > timeago and ResourceProvider == "MICROSOFT.AUTOMATION" and Category == "JobStreams" and JobId_g in (childJobs) 46 | | where (RunbookName_s == "PatchMicrosoftOMSLinuxComputer" and StreamType_s == "Output") or (RunbookName_s == "Patch-MicrosoftOMSComputer" and StreamType_s == "Verbose") 47 | | where ResultDescription != "" and ResultDescription != "" and ResultDescription !contains "Runbook runtime trace:" 48 | | summarize arg_max(TimeGenerated, *) by JobId_g 49 | | project JobId_g, Details = ResultDescription 50 | ) on JobId_g 51 | | project-away JobId_g1 52 | // Get the results from the patch installation tasks 53 | | join kind=leftouter ( 54 | // Windows results 55 | AzureDiagnostics 56 | | where TimeGenerated >= timeago and ResourceProvider == "MICROSOFT.AUTOMATION" and Category == "JobStreams" and JobId_g in (childJobs) 57 | | where RunbookName_s == "Patch-MicrosoftOMSComputer" and StreamType_s == "Output" 58 | | extend Summary = parse_json(tostring(parse_json(ResultDescription).Summary)) 59 | | where Summary.Status != "" 60 | | summarize arg_max(TimeGenerated, *) by JobId_g 61 | | project JobId_g, State = Summary.Status, TotalUpdatesFailed = toreal(Summary.TotalUpdatesFailed), 62 | TotalUpdatesInstalled = toreal(Summary.TotalUpdatesInstalled), StatusDescription = tostring(Summary.StatusDescription) 63 | | union ( 64 | // Linux results 65 | AzureDiagnostics 66 | | where TimeGenerated >= timeago and ResourceProvider == "MICROSOFT.AUTOMATION" and Category == "JobStreams" and JobId_g in (childJobs) 67 | | where RunbookName_s == "PatchMicrosoftOMSLinuxComputer" and StreamType_s == "Output" 68 | | where ResultDescription startswith "Installed update count:" 69 | | extend data = split(ResultDescription," ") 70 | | extend TotalUpdatesInstalled = data[array_index_of(data, "count:")+1] 71 | | project JobId_g, toreal(TotalUpdatesInstalled) 72 | | join kind=fullouter ( 73 | AzureDiagnostics 74 | | where TimeGenerated >= timeago and ResourceProvider == "MICROSOFT.AUTOMATION" and Category == "JobStreams" and JobId_g in (childJobs) 75 | | where RunbookName_s == "PatchMicrosoftOMSLinuxComputer" and StreamType_s == "Output" 76 | | extend runtrace = parse_json(substring(ResultDescription,indexof(ResultDescription, "Runbook runtime trace:")+23)) 77 | | where runtrace.category == "State" 78 | | summarize arg_max(TimeGenerated, *) by JobId_g 79 | | project JobId_g, State = runtrace.message 80 | ) on JobId_g 81 | | extend jobid = iif(isempty(JobId_g), JobId_g1, JobId_g) 82 | | project JobId_g = jobid, State, TotalUpdatesFailed = toreal(0), TotalUpdatesInstalled, StatusDescription = "" 83 | ) 84 | ) on JobId_g 85 | | project-away JobId_g1 86 | | extend finalStatus = iif(State =~ "Succeeded.", "Complete", iif(State =~ "InProgress" and ParentStatus == "Completed", "Suspended", 87 | iif(isempty(State), "Failed", State))) 88 | | extend statusDescription = iif(isnotempty(StatusDescription), StatusDescription, iif(isempty(Details), "Job failed to start", iif(Details startswith "Sleeping for", "Job suspended after reboot", Details))) 89 | | extend TotalUpdatesInstalled = iif(isnotempty(TotalUpdatesInstalled), TotalUpdatesInstalled, toreal(0)) 90 | | extend TotalUpdatesFailed = iif(isnotempty(TotalUpdatesFailed), TotalUpdatesFailed, toreal(0)) 91 | | project ScheduleName, Computer, OperatingSystem, Status = finalStatus, Details = statusDescription, UpdatesInstalled = TotalUpdatesInstalled, UpdatesFailed = TotalUpdatesFailed, JobId = JobId_g -------------------------------------------------------------------------------- /2021-08 PowerShell Southampton/Get-UpdateJobStates.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | $JobId 3 | ) 4 | 5 | $AutoAccount = .\Connect-RunAsAccount.ps1 6 | 7 | $SendTo = Get-AutomationVariable -Name 'NotificationEmail' 8 | $hbwGuidPattern = "(_)[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}" 9 | 10 | # Check for queued or suspended jobs 11 | [System.Collections.Generic.List[PSObject]] $PendingJobs = @() 12 | Get-AzAutomationJob @AutoAccount -RunbookName 'Patch-MicrosoftOMSComputer' -Status 'Queued' | ForEach-Object{ $PendingJobs.Add($_) } 13 | Get-AzAutomationJob @AutoAccount -RunbookName 'Patch-MicrosoftOMSComputer' -Status 'Suspended' | ForEach-Object{ $PendingJobs.Add($_) } 14 | Get-AzAutomationJob @AutoAccount -RunbookName 'PatchMicrosoftOMSLinuxComputer' -Status 'Queued' | ForEach-Object{ $PendingJobs.Add($_) } 15 | Get-AzAutomationJob @AutoAccount -RunbookName 'PatchMicrosoftOMSLinuxComputer' -Status 'Suspended' | ForEach-Object{ $PendingJobs.Add($_) } 16 | 17 | # Check each job and ensure it comes from the expected parent job 18 | foreach($job in $PendingJobs){ 19 | $RBjobStatus = Get-AzAutomationJob @AutoAccount -Id $job.JobId 20 | if($RBjobStatus.JobParameters['MasterJobId'] -eq $JobId){ 21 | $HybridWorker = [Regex]::Replace($RBjobStatus.HybridWorker, $hbwGuidPattern, '') 22 | #Send Notifications 23 | $EmailMessage = @{ 24 | EmailBody = "$($RBjobStatus | ConvertTo-Html)" 25 | Subject = "$($HybridWorker) Update Deployment Failed to Start" 26 | To = $SendTo 27 | } 28 | .\Send-Notification.ps1 @EmailMessage 29 | } 30 | } 31 | 32 | # Get the schedule for this job 33 | $AzAutomationScheduledRunbook = @{ 34 | RunbookName = 'Get-UpdateJobStates' 35 | ScheduleName = "$($JobId)-State" 36 | } 37 | $schedule = Get-AzAutomationScheduledRunbook @AutoAccount @AzAutomationScheduledRunbook -ErrorAction SilentlyContinue 38 | 39 | # If schedule is found unreigster and delete it 40 | if($schedule){ 41 | Unregister-AzAutomationScheduledRunbook @AutoAccount @AzAutomationScheduledRunbook -Force 42 | Remove-AzAutomationSchedule @AutoAccount -Name "$($JobId)-State" -Force 43 | } 44 | -------------------------------------------------------------------------------- /2021-08 PowerShell Southampton/Install Log Analytics Agent.ps1: -------------------------------------------------------------------------------- 1 | # Set the parameters for you workspace 2 | $WorkspaceID = '' 3 | $WorkSpaceKey = '' 4 | $agentURL = 'https://download.microsoft.com/download/3/c/d/3cd6f5b3-3fbe-43c0-88e0-8256d02db5b7/MMASetup-AMD64.exe' 5 | 6 | #Check if Log Analytics Agent is installed 7 | $Filter = 'name=''Microsoft Monitoring Agent''' 8 | $MMAObj = Get-WmiObject -Class Win32_Product -Filter $Filter 9 | 10 | #If the agent is not installed then download and install it 11 | if(-not $MMAObj){ 12 | Write-Verbose 'Agent not found. Downloading and installing' 13 | $FileName = 'MMASetup-AMD64.exe' 14 | $OMSFolder = $env:Temp 15 | $MMAFile = Join-Path -Path $OMSFolder -ChildPath $FileName 16 | 17 | 18 | # Check if folder exists, if not, create it 19 | if (-not (Test-Path $OMSFolder)){ 20 | New-Item $OMSFolder -type Directory | Out-Null 21 | } 22 | 23 | # Change the location to the specified folder 24 | Set-Location $OMSFolder 25 | Write-Verbose 'Downloading agent' 26 | # Check if file exists, if not, download it 27 | if(-not (Test-Path $FileName)){ 28 | Invoke-WebRequest -Uri $agentURL -OutFile $MMAFile | Out-Null 29 | } 30 | Write-Verbose 'Installing agent' 31 | # Install the agent 32 | $ArgumentList = '/C:"setup.exe /qn ADD_OPINSIGHTS_WORKSPACE=0 ' + 33 | 'AcceptEndUserLicenseAgreement=1"' 34 | $Install = @{ 35 | FilePath = $FileName 36 | ArgumentList = $ArgumentList 37 | ErrorAction = 'Stop' 38 | } 39 | Start-Process @Install -Wait | Out-Null 40 | } 41 | 42 | #Check if the CSE workspace is already configured 43 | $AgentCfg = New-Object -ComObject AgentConfigManager.MgmtSvcCfg 44 | $OMSWorkspaces = $AgentCfg.GetCloudWorkspaces() 45 | Write-Verbose 'Configuring agent' 46 | $CSEWorkspaceFound = $false 47 | foreach($OMSWorkspace in $OMSWorkspaces){ 48 | if($OMSWorkspace.workspaceId -eq $WorkspaceID){ 49 | $CSEWorkspaceFound = $true 50 | } 51 | } 52 | 53 | # If the workspace was not found in the agent, add it 54 | if(-not $CSEWorkspaceFound){ 55 | $AgentCfg.AddCloudWorkspace($WorkspaceID,$WorkspaceKey) 56 | # Restart the agent for the changes to take affect 57 | Write-Verbose 'Restarting agent' 58 | Restart-Service HealthService 59 | } 60 | 61 | # Display the configuration 62 | $AgentCfg.GetCloudWorkspaces() -------------------------------------------------------------------------------- /2021-08 PowerShell Southampton/PostTask-DomainControllerGroupB.ps1: -------------------------------------------------------------------------------- 1 | # Add SoftwareUpdateConfigurationRunContext to param so your script can read it 2 | param( 3 | $SoftwareUpdateConfigurationRunContext 4 | ) 5 | 6 | # Convert config information to PowerShell 7 | $Config = $SoftwareUpdateConfigurationRunContext | ConvertFrom-Json 8 | 9 | # Connect using Run As Account 10 | $AutoAccount = .\Connect-RunAsAccount.ps1 11 | 12 | # Get the deployment A settings 13 | $Deployment = Get-AzAutomationSoftwareUpdateConfiguration @AutoAccount -Name $Config.SoftwareUpdateConfigurationName 14 | 15 | # Get the group B machines 16 | $targetMachines = Get-AutomationVariable -Name 'DC_GroupB' 17 | 18 | # Create the schedule object 19 | $AzAutomationSchedule = @{ 20 | ResourceGroupName = $AutoAccount['ResourceGroupName'] 21 | AutomationAccountName = $AutoAccount['AutomationAccountName'] 22 | Name = "Domain Controller Group B ($((Get-Date).ToString('yyyy-MM-dd')))" 23 | StartTime = (Get-Date).AddMinutes(10) 24 | ForUpdateConfiguration = $true 25 | OneTime = $true 26 | } 27 | $schedule = New-AzAutomationSchedule @AzAutomationSchedule 28 | 29 | # Create the update deployment 30 | $duration = New-TimeSpan -Hours 2 31 | $UpdateConfiguration = @{ 32 | ResourceGroupName = $AutoAccount['ResourceGroupName'] 33 | AutomationAccountName = $AutoAccount['AutomationAccountName'] 34 | Schedule = $schedule 35 | AzureVMResourceId = $targetMachines 36 | Windows = $true 37 | IncludedUpdateClassification = $Deployment.UpdateConfiguration.Windows.IncludedUpdateClassifications 38 | Duration = $duration 39 | RebootSetting = $Deployment.UpdateConfiguration.Windows.rebootSetting 40 | } 41 | New-AzAutomationSoftwareUpdateConfiguration @UpdateConfiguration -------------------------------------------------------------------------------- /2021-08 PowerShell Southampton/PostTask-StopAzVM.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | $SoftwareUpdateConfigurationRunContext 3 | ) 4 | 5 | # Convert the SoftwareUpdateConfigurationRunContext JSON to a PowerShell object 6 | $Config = $SoftwareUpdateConfigurationRunContext | ConvertFrom-Json 7 | 8 | # Connect using Run As Account 9 | $AutoAccount = .\Connect-RunAsAccount.ps1 10 | 11 | # Get the Azure VMs associated with this deployment 12 | $AzureVMs = Get-AutomationVariable -Name 'UpdateStarted' 13 | 14 | if($AzureVMs -ne 'none'){ 15 | # Send stop command to the started Azure VMs 16 | foreach($VmId in $AzureVMs.Split(';')){ 17 | Stop-AzVM -Id $VmId -Force 18 | } 19 | Set-AutomationVariable -Name 'UpdateStarted' -Value 'none' 20 | } 21 | 22 | -------------------------------------------------------------------------------- /2021-08 PowerShell Southampton/PreTask-CheckStarts.ps1: -------------------------------------------------------------------------------- 1 | # Add SoftwareUpdateConfigurationRunContext to param so your script can read it 2 | param( 3 | $SoftwareUpdateConfigurationRunContext 4 | ) 5 | 6 | # Convert config information to PowerShell 7 | $Config = $SoftwareUpdateConfigurationRunContext | ConvertFrom-Json 8 | 9 | # Connect using Run As Account 10 | $AutoAccount = .\Connect-RunAsAccount.ps1 11 | 12 | # Create the schedule object 13 | $AzAutomationSchedule = @{ 14 | Name = "$($Config.SoftwareUpdateConfigurationRunId)-State" 15 | StartTime = (Get-Date).AddMinutes(15) 16 | OneTime = $true 17 | } 18 | $schedule = New-AzAutomationSchedule @AutoAccount @AzAutomationSchedule 19 | 20 | # Register the schedule with the runbook 21 | $AzAutomationScheduledRunbook = @{ 22 | Parameters = @{JobId = $Config.SoftwareUpdateConfigurationRunId} 23 | RunbookName = 'Get-UpdateJobStates' 24 | ScheduleName = $schedule.Name 25 | } 26 | Register-AzAutomationScheduledRunbook @AutoAccount @AzAutomationScheduledRunbook -------------------------------------------------------------------------------- /2021-08 PowerShell Southampton/PreTask-StartAzVM.ps1: -------------------------------------------------------------------------------- 1 | # Add SoftwareUpdateConfigurationRunContext to param so your script can read it 2 | param( 3 | $SoftwareUpdateConfigurationRunContext 4 | ) 5 | 6 | # Convert the SoftwareUpdateConfigurationRunContext JSON to a PowerShell object 7 | $Config = $SoftwareUpdateConfigurationRunContext | ConvertFrom-Json 8 | 9 | # Connect using Run As Account 10 | $AutoAccount = .\Connect-RunAsAccount.ps1 11 | 12 | # Get the Azure VMs associated with this deployment 13 | $AzureVMs = $Config.SoftwareUpdateConfigurationSettings.AzureVirtualMachines 14 | 15 | # Send start command to all Azure VMs 16 | [System.Collections.Generic.List[string]] $Started = @() 17 | foreach($VmId in $AzureVMs){ 18 | $status = Get-AzResource -Id $VmId | Get-AzVM -Status 19 | $check = $status.Statuses | Where-Object{ $_.Code -eq 'PowerState/running' } 20 | if(-not $check){ 21 | Start-AzVM -Id $VmId 22 | $Started.Add($VmId) 23 | } 24 | } 25 | 26 | if($Started.Count -gt 0){ 27 | Set-AutomationVariable -Name 'UpdateStarted' -Value ($Started -join(';')) 28 | } 29 | else{ 30 | Set-AutomationVariable -Name 'UpdateStarted' -Value 'none' 31 | } 32 | -------------------------------------------------------------------------------- /2021-08 PowerShell Southampton/PreTask-StopServices.ps1: -------------------------------------------------------------------------------- 1 | # Add SoftwareUpdateConfigurationRunContext to param so your script can read it 2 | param( 3 | $SoftwareUpdateConfigurationRunContext 4 | ) 5 | 6 | # Convert the SoftwareUpdateConfigurationRunContext JSON to a PowerShell object 7 | $Config = $SoftwareUpdateConfigurationRunContext | ConvertFrom-Json 8 | 9 | $HybridWorker = 'WIN-J6GVB2JARTD' 10 | $Runbook = 'Stop-WindowsServices' 11 | $Services = 'BITS;SNMPTRAP' 12 | 13 | # Connect using Run As Account 14 | $AutoAccount = .\Connect-RunAsAccount.ps1 15 | 16 | # Start the runbook on the hybrid worker 17 | $StartParams = @{ 18 | Name = $Runbook 19 | Parameters = @{ 'Services' = $Services } 20 | RunOn = $HybridWorker 21 | ErrorAction = 'Stop' 22 | } 23 | $JobId = Start-AutomationRunbook @StartParams 24 | 25 | # wait for the job to finish. 26 | # It is good to include a timeout to prevent exceeding maintenance windows 27 | Wait-AutomationJob -Id $JobId -TimeoutInMinutes 10 28 | 29 | # Get the results of the automation job 30 | $RBjobStatus = Get-AzAutomationJob @AutoAccount -Id $JobId 31 | #In this case, we want to terminate the patch job if any run fails 32 | if ($RBjobStatus.Status -ne 'Completed') { 33 | #We must throw in order to fail the patch deployment. 34 | throw "$($Runbook) returned a status of $($RBjobStatus.Status)" 35 | } 36 | 37 | # Get the output stream from the job 38 | $JobOutput = Get-AzAutomationJobOutput @AutoAccount -Id $jobId -stream Output 39 | [System.Collections.Generic.List[PSObject]] $ParsedResults = @() 40 | foreach($Output in $JobOutput | Where-Object{$_.Summary -like '*{*'}){ 41 | $JobOutputRecord = Get-AzAutomationJobOutputRecord @AutoAccount -JobId $Output.JobId -Id $Output.StreamRecordId 42 | ($JobOutputRecord.Value['value'] | ConvertFrom-Json) | Foreach-Object{ $ParsedResults.Add($_) } 43 | } 44 | 45 | # Confirm all services are stopped 46 | if($ParsedResults | Where-Object{ $_.Status -ne 1 }){ 47 | Write-Output "$(($Results | Where-Object{ $_.Status -ne 1 } | Format-List | Out-String).Trim())" 48 | throw "Something went wrong in the snapshot process" 49 | } 50 | 51 | # Output results 52 | Write-Output "$(($Results | Format-Table | Out-String).Trim())" 53 | -------------------------------------------------------------------------------- /2021-08 PowerShell Southampton/Send-Notification.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | $EmailBody, 3 | $Subject, 4 | $To, 5 | $CC, 6 | $bcc 7 | ) 8 | 9 | # Get variables 10 | $ApiKey = Get-AutomationVariable -Name 'SendGridKey' 11 | $From = Get-AutomationVariable -Name 'SmtpFrom' 12 | $Name = 'Update Management' 13 | 14 | # Create header 15 | $headers = @{} 16 | $headers.Add("Authorization", "Bearer $apiKey") 17 | $headers.Add("Content-Type", "application/json") 18 | 19 | 20 | Function Get-EmailArray { 21 | param($EmailString) 22 | $Emails = @() 23 | $EmailString.Split(';') | ForEach-Object { 24 | $Emails += @{email = $_ } 25 | } 26 | $Emails 27 | } 28 | 29 | $toEmail = Get-EmailArray $To 30 | 31 | $personalizations = @{ 32 | to = @($toEmail) 33 | subject = $Subject 34 | } 35 | 36 | if (-not [string]::IsNullOrEmpty($CC)) { 37 | $ccEmail = Get-EmailArray $CC 38 | $personalizations.Add('cc', @($ccEmail)) 39 | } 40 | 41 | if (-not [string]::IsNullOrEmpty($bcc)) { 42 | $bccEmail = Get-EmailArray $bcc 43 | $personalizations.Add('bCC', @($bccEmail)) 44 | } 45 | 46 | $jsonRequest = [ordered]@{ 47 | personalizations = @($personalizations) 48 | from = @{ 49 | email = $From 50 | name = $Name 51 | } 52 | content = @( 53 | @{ 54 | type = "text/html" 55 | value = $EmailBody 56 | } 57 | ) 58 | } | ConvertTo-Json -Depth 10 59 | 60 | Invoke-WebRequest -Uri 'https://api.sendgrid.com/v3/mail/send' -Method Post -Headers $headers -Body $jsonRequest -UseBasicParsing -ErrorAction Stop 61 | -------------------------------------------------------------------------------- /2021-08 PowerShell Southampton/Stop-LinuxServices.ps1: -------------------------------------------------------------------------------- 1 | import os 2 | import socket 3 | 4 | params = str(sys.argv[1]) 5 | # create array of services 6 | servicesList = params.split(";") 7 | 8 | for serviceName in servicesList: 9 | # Stop the service 10 | run = os.system('sudo service ' + serviceName + ' stop > /dev/null') 11 | time.sleep(1) 12 | # Get the status of the service 13 | output = commands.getstatusoutput('sudo service ' + serviceName + ' status') 14 | print(output) -------------------------------------------------------------------------------- /2021-08 PowerShell Southampton/Stop-WindowsServices.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | [string]$Services 3 | ) 4 | 5 | # split the string into an array 6 | $svcArray = $Services.Split(';') 7 | 8 | # stop all services at once and output results as JSON 9 | Get-Service $svcArray | Stop-Service -PassThru | 10 | Select-Object Name, Status | ConvertTo-Json -------------------------------------------------------------------------------- /2021-08 PowerShell Southampton/Update Management Southampton.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdowst/Presentation-Materials/7c5577ab08f8cae96555df27df12d103fea04d32/2021-08 PowerShell Southampton/Update Management Southampton.pdf -------------------------------------------------------------------------------- /2021-09 New York PowerShell User Group/Move-OutlookItems.ps1: -------------------------------------------------------------------------------- 1 | #region : Add outlook com objects 2 | Add-Type -Assembly "$($env:ProgramFiles)\Microsoft Office\root\Office16\ADDINS\Microsoft Power Query for Excel Integrated\bin\Microsoft.Office.Interop.Outlook.dll" 3 | $outlookApp = New-Object -comobject Outlook.Application 4 | $mapiNamespace = $outlookApp.GetNameSpace("MAPI") 5 | 6 | #endregion 7 | 8 | #region : Get the Inbox and all the emails in it 9 | $Inbox = $mapiNamespace.GetDefaultFolder([Microsoft.Office.Interop.Outlook.OlDefaultFolders]::olFolderInbox) 10 | [System.Collections.Generic.List[PSObject]] $InboxItems = @() 11 | $Inbox.Items | Foreach-Object{ $InboxItems.Add($_) } 12 | 13 | #endregion 14 | 15 | #region : Filter based on the email address 16 | $InboxItems | Where-Object{ $_.SenderEmailAddress -eq 'azure-noreply@microsoft.com' } | Format-Table Subject 17 | 18 | #endregion 19 | 20 | #region : Filter based on the email address and attempt to extract the name form the subject 21 | $InboxItems | Where-Object{ $_.SenderEmailAddress -eq 'azure-noreply@microsoft.com' } | ForEach-Object{ 22 | [Regex]::Matches($_.Subject, '(?<=\[)(.*?)(?=])').Value 23 | } 24 | 25 | #endregion 26 | 27 | #region : Filter based on the email address and attempt to extract the name form the subject, check for folder, and move if found 28 | Function Get-Subfolders{ 29 | [CmdletBinding()] 30 | param( 31 | [object]$folder 32 | ) 33 | Foreach($item in $folder.Folders){ 34 | $item 35 | Get-Subfolders $item 36 | } 37 | } 38 | 39 | $SubFolders = Get-Subfolders -folder $Inbox 40 | $SubFolders | Format-Table Name 41 | 42 | $InboxItems | Where-Object{ $_.SenderEmailAddress -eq 'azure-noreply@microsoft.com' } | ForEach-Object{ 43 | $extractedName = [Regex]::Matches($_.Subject, '(?<=\[)(.*?)(?=])').Value 44 | $folderCheck = $SubFolders | Where-Object{ $_.Name -eq $extractedName } 45 | if($folderCheck){ 46 | $_.Move($folderCheck) | Out-Null 47 | } 48 | } 49 | 50 | #endregion 51 | 52 | #region : Move-OutlookItem 53 | $Inbox = $mapiNamespace.GetDefaultFolder([Microsoft.Office.Interop.Outlook.OlDefaultFolders]::olFolderInbox) 54 | [System.Collections.Generic.List[PSObject]] $InboxItems = @() 55 | $Inbox.Items | Foreach-Object{ $InboxItems.Add($_) } 56 | Function Move-OutlookItem{ 57 | [CmdletBinding()] 58 | param( 59 | [Parameter(Mandatory=$true)] 60 | [Object]$item, 61 | [Parameter(Mandatory=$true)] 62 | [Object]$TargetFolder, 63 | [Parameter(Mandatory=$false)] 64 | [boolean]$MarkRead = $false, 65 | [Parameter(Mandatory=$false)] 66 | [boolean]$TestOnly = $false 67 | ) 68 | 69 | if($TestOnly){ 70 | $FolderPath = $TargetFolder.FolderPath.Substring($TargetFolder.FolderPath.IndexOf('\',3)+1,$TargetFolder.FolderPath.Length-$TargetFolder.FolderPath.IndexOf('\',3)-1) 71 | Write-Host " Moved: $($item.Subject) - $($FolderPath)" -ForegroundColor Cyan 72 | } else { 73 | if($MarkRead){ 74 | $item.UnRead = $false 75 | $item.Save() 76 | } 77 | try{ 78 | $item.Move($TargetFolder) | Out-Null 79 | } catch { 80 | Write-Host "Failed to Moved: $($item.Subject) - $($FolderPath)" -ForegroundColor Red 81 | } 82 | Write-Verbose "Moved: $($item.Subject) - $($FolderPath)" 83 | } 84 | 85 | } 86 | 87 | $azureFolder = $SubFolders | Where-Object{ $_.Name -eq 'Azure' } 88 | $InboxItems | Where-Object{ $_.SenderEmailAddress -eq 'azure-noreply@microsoft.com' } | ForEach-Object{ 89 | Move-OutlookItem -item $_ -TargetFolder $azureFolder -MarkRead $True -Verbose 90 | } 91 | 92 | #endregion -------------------------------------------------------------------------------- /2021-09 New York PowerShell User Group/New-DevServer.ps1: -------------------------------------------------------------------------------- 1 | #Requires -RunAsAdministrator 2 | 3 | Function Test-InstalByInfoUrl($URL){ 4 | Get-ChildItem "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall" | ForEach-Object{ 5 | if($_.GetValue('URLInfoAbout') -like $URL){ 6 | [pscustomobject]@{ 7 | Version = $_.GetValue('DisplayVersion') 8 | InstallLocation = $_.GetValue('InstallLocation') 9 | } 10 | } 11 | } 12 | if(-not ($Install)){ 13 | Get-ChildItem "HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall" | ForEach-Object{ 14 | if($_.GetValue('URLInfoAbout') -like $URL){ 15 | [pscustomobject]@{ 16 | Version = $_.GetValue('DisplayVersion') 17 | InstallLocation = $_.GetValue('InstallLocation') 18 | } 19 | } 20 | } 21 | } 22 | } 23 | 24 | Function Test-ChocoInstall{ 25 | try{ 26 | $Before = $ErrorActionPreference 27 | $ErrorActionPreference = 'Stop' 28 | $testchoco = choco -v 29 | } 30 | catch{ 31 | $testchoco = $null 32 | } 33 | $ErrorActionPreference = $Before 34 | $testchoco 35 | } 36 | 37 | $percent = 0 38 | $increment = 8 39 | $percent += $increment 40 | 41 | 42 | # Install PowerShell 7 43 | Write-Host 'Installing PowerShell 7...' -NoNewline 44 | if($PSVersionTable.PSVersion.Major -lt 7){ 45 | $testPoSh7 = Get-CimInstance -Class Win32_Product -Filter "Name='PowerShell 7-x64'" 46 | if(-not ($testPoSh7)){ 47 | Write-Progress -Activity 'Installing' -Status 'Installing PowerShell 7...' -PercentComplete $percent;$percent += $increment 48 | Invoke-Expression "& { $(Invoke-RestMethod https://aka.ms/install-powershell.ps1) } -UseMSI -Quiet -AddExplorerContextMenu" 49 | Write-Host ' done' -ForegroundColor Green 50 | } 51 | else{ 52 | Write-Progress -Activity 'Installing' -Status "PowerShell 7 is already installed" -PercentComplete $percent;$percent += $increment 53 | Write-Host " confirmed" -ForegroundColor Cyan 54 | } 55 | } 56 | else{ 57 | Write-Progress -Activity 'Installing' -Status "PowerShell 7 is already running" -PercentComplete $percent;$percent += $increment 58 | Write-Host " PowerShell 7 is already running" -ForegroundColor Cyan 59 | } 60 | 61 | # Install Chocolatey 62 | Write-Host 'Installing Chocolatey...' -NoNewline 63 | $testchoco = Test-ChocoInstall 64 | if(-not($testChoco)){ 65 | Write-Progress -Activity 'Installing' -Status 'Installing Chocolatey...' -PercentComplete $percent;$percent += $increment 66 | [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072 67 | Invoke-Expression "& { $(Invoke-RestMethod https://chocolatey.org/install.ps1) }" 68 | Write-Host ' done' -ForegroundColor Green 69 | } 70 | else{ 71 | Write-Progress -Activity 'Installing' -Status "Chocolatey Version $testchoco is already installed" -PercentComplete $percent;$percent += $increment 72 | Write-Host " $testchoco is already installed" -ForegroundColor Cyan 73 | } 74 | 75 | # Reload environment variables to ensure choco is avaiable 76 | $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User") 77 | 78 | # confirm choco is available 79 | $testChoco = Test-ChocoInstall 80 | if(-not($testChoco)){ 81 | Write-Host "Unable to locate choco package. If it was just installed try restarting this script." -ForegroundColor Red 82 | Start-Sleep -Seconds 30 83 | break 84 | } 85 | 86 | # Install Git for Windows 87 | Write-Host "Installing Git for Windows..." -NoNewline 88 | $testGit = Test-InstalByInfoUrl -Url '*gitforwindows.org*' 89 | if(-not ($testGit)){ 90 | Write-Progress -Activity 'Installing' -Status "Installing Git for Windows..." -PercentComplete $percent;$percent += $increment 91 | choco install git.install --params "/GitAndUnixToolsOnPath /NoGitLfs /SChannel /NoAutoCrlf" -y 92 | $testGit = Test-InstalByInfoUrl -Url '*gitforwindows.org*' 93 | Write-Host ' done' -ForegroundColor Green 94 | } 95 | else{ 96 | Write-Progress -Activity 'Installing' -Status "Git for Windows Version $($testGit.Version) is already installed" -PercentComplete $percent;$percent += $increment 97 | Write-Host "Git for Windows Version $($testGit.Version) is already installed" -ForegroundColor Cyan 98 | } 99 | 100 | # Install Visual Studio Code 101 | Write-Host "Installing Visual Studio Code..." -NoNewline 102 | $testVSCode = Test-InstalByInfoUrl -Url '*code.visualstudio.com*' 103 | if(-not ($testVSCode)){ 104 | Write-Progress -Activity 'Installing' -Status "Installing Visual Studio Code..." -PercentComplete $percent;$percent += $increment 105 | choco install vscode -y 106 | $testVSCode = Test-InstalByInfoUrl -Url '*code.visualstudio.com*' 107 | Write-Host ' done' -ForegroundColor Green 108 | } 109 | else{ 110 | Write-Progress -Activity 'Installing' -Status "Visual Studio Code Version $($testVSCode.Version) is already installed" -PercentComplete $percent;$percent += $increment 111 | Write-Host "$($testVSCode.Version) is already installed" -ForegroundColor Cyan 112 | } 113 | 114 | # Reload environment variables to get VS Code and Git 115 | $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User") 116 | 117 | # Get currently installed extensions 118 | Write-Progress -Activity 'Configuring' -Status 'Installing VS Code Extensions..' -PercentComplete $percent;$percent += $increment 119 | Write-Host 'Installing VS Code Extensions..' -NoNewline 120 | $InstalledExtensions = Invoke-Expression -Command "code --list-extensions" 121 | # Install the missing extensions 122 | $extensions = 'GitHub.vscode-pull-request-github','ms-vscode.powershell','Tyriar.shell-launcher' 123 | $extensions | Where-Object{ $_ -notin $InstalledExtensions } | ForEach-Object { 124 | Invoke-Expression -Command "code --install-extension $_ --force" 125 | } 126 | Write-Host ' done' -ForegroundColor Green 127 | 128 | # Install modules 129 | Write-Progress -Activity 'Configuring' -Status 'Installing modules..' -PercentComplete $percent;$percent += $increment 130 | $ModuleInstall = 'If(-not(Get-Module {0} -ListAvailable))' + 131 | '{{Write-Host "Installing {0}...";' + 132 | 'Set-PSRepository PSGallery -InstallationPolicy Trusted;' + 133 | 'Install-Module {0} -Confirm:$False -Force}}' + 134 | 'else{{Write-Host "{0} is already installed";' + 135 | 'Start-Sleep -Seconds 3}}' 136 | 137 | foreach($module in 'ImportExcel','dbatools'){ 138 | Write-Host "Install $module" -NoNewline 139 | $InstallCommand = $ModuleInstall -f $module 140 | $Arguments = '-Command "& {' + $InstallCommand +'}"' 141 | Start-Process -FilePath 'pwsh' -ArgumentList $Arguments -Wait 142 | Write-Host ' done' -ForegroundColor Green 143 | } -------------------------------------------------------------------------------- /2021-09 New York PowerShell User Group/Outlook-FreeTime.ps1: -------------------------------------------------------------------------------- 1 | #region : Add outlook com objects 2 | Add-Type -Assembly "$($env:ProgramFiles)\Microsoft Office\root\Office16\ADDINS\Microsoft Power Query for Excel Integrated\bin\Microsoft.Office.Interop.Outlook.dll" 3 | $outlookApp = New-Object -comobject Outlook.Application 4 | $mapiNamespace = $outlookApp.GetNameSpace("MAPI") 5 | 6 | #endregion 7 | 8 | #region : Get the calendar 9 | $CalendarFolder = $mapiNamespace.GetDefaultFolder([Microsoft.Office.Interop.Outlook.OlDefaultFolders]::olFolderCalendar) 10 | 11 | #endregion 12 | 13 | #region : Use the calendar export 14 | $calShare = $CalendarFolder.GetCalendarExporter() 15 | $calShare.StartDate = [datetime]::Today 16 | $calShare.EndDate = [datetime]::Today 17 | $calShare.CalendarDetail = [Microsoft.Office.Interop.Outlook.OlCalendarDetail]::olFreeBusyAndSubject 18 | 19 | 20 | $calShare | Get-Member -MemberType Method 21 | 22 | #endregion 23 | 24 | #region : Generate the export email 25 | $mail = $calShare.ForwardAsICal([Microsoft.Office.Interop.Outlook.OlCalendarMailFormat]::olCalendarMailFormatDailySchedule) 26 | $mail.body 27 | 28 | #endregion 29 | 30 | #region : Extract the free times from the email body 31 | $mail.Body.Split("`n").Trim() | Where-Object{ $_ -match '\tFree$' } 32 | 33 | 34 | #endregion 35 | 36 | #region : Extract the free times from the email body and only return the times 37 | $mail.Body.Split("`n").Trim() | Where-Object{ $_ -match '\tFree$' } | ForEach-Object{ 38 | Write-Host "`t• $($_.Split("`t")[0].Trim())" -ForegroundColor Yellow 39 | } 40 | 41 | #endregion -------------------------------------------------------------------------------- /2021-09 New York PowerShell User Group/SeleniumForm.ps1: -------------------------------------------------------------------------------- 1 | #region : Open browser and navigate to page 2 | Import-Module Selenium 3 | $Driver = Start-SeFirefox 4 | Enter-SeUrl 'https://docs.google.com/forms/d/e/1FAIpQLSdxFCNr-2Q31pARzPmApDIUM2c3I1biZCSWo7akFMMrNObFww/viewform?usp=sf_link' -Driver $Driver 5 | 6 | #endregion 7 | 8 | #region : Finding elements Text box 9 | $XPath = '/html/body/div/div[2]/form/div[2]/div/div[2]/div[1]/div/div/div[2]/div/div[1]/div/div[1]/input' 10 | $textBox = Get-SeElement -By XPath -Selection $XPath -Target $Driver 11 | Send-SeKeys -Element $textBox -Keys 'Arthur Dent' 12 | 13 | #endregion 14 | 15 | #region : Fill in text boxes 16 | Function Set-TextboxValue{ 17 | param( 18 | $Value, 19 | $XPath 20 | ) 21 | $textBox = Get-SeElement -By XPath -Selection $XPath -Target $Driver 22 | Send-SeKeys -Element $textBox -Keys $Value 23 | } 24 | 25 | Set-TextboxValue -Value 'Arthur Dent' -XPath '/html/body/div/div[2]/form/div[2]/div/div[2]/div[1]/div/div/div[2]/div/div[1]/div/div[1]/input' 26 | Set-TextboxValue -Value (Get-Date).AddDays(1).ToString('d') -XPath '/html/body/div/div[2]/form/div[2]/div/div[2]/div[2]/div/div/div[2]/div/div[1]/div/div[1]/input' 27 | Set-TextboxValue -Value 'end of the world' -XPath '/html/body/div/div[2]/form/div[2]/div/div[2]/div[6]/div/div/div[2]/div/div[1]/div[2]/textarea' 28 | 29 | 30 | #endregion 31 | 32 | #region : Finding elements radio buttons 33 | $radioButtons = Get-SeElement -By Class -Selection 'exportOuterCircle' -Target $Driver 34 | $radioButtons | Format-Table TagName, Text, Enabled, Location 35 | 36 | # send click to button 37 | Send-SeClick -Element $radioButtons[0] 38 | 39 | #endregion 40 | 41 | #region : Get button container 42 | $selector = Get-SeElement -By Class -Selection "docssharedWizToggleLabeledContainer" -Target $Driver 43 | $selector | Format-Table TagName, Text, Enabled, Location 44 | 45 | $selector.Count 46 | $radioButtons.Count 47 | 48 | $label = 'Full day' 49 | $index = $selector.Text.IndexOf($label) 50 | 51 | Send-SeClick -Element $radioButtons[$index] 52 | 53 | #endregion 54 | 55 | #region : Make it a function 56 | Function Set-RadioButton{ 57 | param( 58 | $label 59 | ) 60 | $selector = Get-SeElement -By Class -Selection "docssharedWizToggleLabeledContainer" -Target $Driver 61 | $index = $selector.Text.IndexOf($label) 62 | 63 | $buttons = Get-SeElement -By Class -Selection "exportOuterCircle" -Target $Driver 64 | Send-SeClick -Element $buttons[$index] 65 | } 66 | 67 | Set-RadioButton -label 'Full day' 68 | Set-RadioButton -label 'Personal leave' 69 | 70 | 71 | #endregion 72 | 73 | #region : Submit it 74 | $submitButton = Get-SeElement -By CssSelector -Selection ".appsMaterialWizButtonPaperbuttonLabel" -Target $Driver 75 | Send-SeClick -Element $submitButton 76 | 77 | #endregion 78 | 79 | #region : Stop it 80 | $Driver | Stop-SeDriver 81 | 82 | #endregion 83 | -------------------------------------------------------------------------------- /2021-09 New York PowerShell User Group/Spell Check Comments.ps1: -------------------------------------------------------------------------------- 1 | #region : Load Word Objects and dictionary 2 | $dictionary = New-Object -COM Scripting.Dictionary 3 | $wordApp = New-Object -COM Word.Application 4 | [void]$wordApp.Documents.Add() 5 | 6 | #endregion 7 | 8 | #region: Set dictionary language 9 | $wordApp.Languages | Format-Table Name, ID 10 | 11 | $ID = (Get-Culture).LCID 12 | $Language = $wordApp.Languages | Where-Object { $_.ID -eq $ID } 13 | $dictionary = $Language.ActiveSpellingDictionary 14 | 15 | #endregion 16 | 17 | #region : Spell check a word 18 | # $wordApp.checkSpelling(Word, CustomDictionary, IgnoreUppercase, MainDictionary) 19 | $Text = 'definitly' 20 | $wordApp.checkSpelling($Text, $null, $true, $dictionary) 21 | 22 | #endregion 23 | 24 | #region : Get spelling suggestions 25 | 26 | $Text = 'definitly' 27 | $wordApp.GetSpellingSuggestions($Text) 28 | 29 | #endregion 30 | 31 | #region : Get spelling suggestions 32 | 33 | $Content = Get-Content ".\PSNotes.ps1" 34 | $ScriptComments = [System.Management.Automation.PSParser]::Tokenize($Content, [ref]$null) | 35 | Where-Object { $_.type -eq "Comment" } 36 | 37 | Foreach ($comment in $ScriptComments) { 38 | if(-not $wordApp.checkSpelling($comment.Content, $null, $true, $dictionary)){ 39 | $comment 40 | break 41 | } 42 | } 43 | 44 | $wordApp.GetSpellingSuggestions($comment.Content) 45 | 46 | #endregion 47 | 48 | #region : Get spelling suggestions 49 | Function Test-Spelling($Comment) { 50 | foreach ($text in $Comment.Content.Split()) { 51 | if (-Not $wordApp.checkSpelling($Text, $null, $true, $dictionary)) { 52 | $Suggestions = $wordApp.GetSpellingSuggestions($Text) | Select-Object -ExpandProperty Name 53 | [pscustomobject]@{ 54 | Word = $Text 55 | Line = $Comment.StartLine 56 | Suggestions = $Suggestions -join ('; ') 57 | } 58 | } 59 | } 60 | } 61 | 62 | Foreach ($comment in $ScriptComments) { 63 | Test-Spelling $comment 64 | } 65 | 66 | #endregion -------------------------------------------------------------------------------- /2022-08 DFWSMUG/Azure Arc Functions.ps1: -------------------------------------------------------------------------------- 1 | Function Invoke-ArcCommand { 2 | [CmdletBinding()] 3 | param( 4 | [string]$ResourceGroupName, 5 | [string]$Name, 6 | [string]$ScriptContent 7 | ) 8 | 9 | $ArcSrv = Get-AzConnectedMachine -ResourceGroupName $ResourceGroupName -Name $Name -ErrorAction Stop 10 | 11 | $encodedcommand = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($ScriptContent)) 12 | $body = @{ 13 | "location" = $ArcSrv.Location 14 | "properties" = @{ 15 | "publisher" = "Microsoft.Compute" 16 | "typeHandlerVersion" = "1.10" 17 | "type" = "CustomScriptExtension" 18 | "forceUpdateTag" = (Get-Date).ToFileTime() 19 | "settings" = @{ 20 | "commandToExecute" = "powershell.exe -EncodedCommand $encodedcommand" 21 | } 22 | } 23 | } 24 | 25 | $URI = "https://management.azure.com$($ArcSrv.Id)/extensions/CustomScriptExtension?api-version=2021-05-20" 26 | $submit = Invoke-AzRestMethod -Uri $URI -Method 'Put' -Payload ($body | ConvertTo-Json) 27 | 28 | $timer = [system.diagnostics.stopwatch]::StartNew() 29 | do { 30 | $ext = Get-AzConnectedMachineExtension -ResourceGroupName $ResourceGroupName -MachineName $Name | 31 | Where-Object { $_.InstanceViewType -eq 'CustomScriptExtension' } 32 | } while ($ext.ProvisioningState -notin 'Updating', 'Creating', 'Waiting' -and $timer.Elapsed.TotalSeconds -le 30) 33 | $timer.Stop() 34 | 35 | if ($timer.Elapsed.TotalSeconds -gt 30) { 36 | Write-Error "Failed to start the provisioning - $($ext.ProvisioningState)" 37 | } 38 | elseif ($submit.StatusCode -ne 202) { 39 | Write-Error $submit.Content 40 | } 41 | else { 42 | $ext.Name 43 | } 44 | } 45 | 46 | Function Get-ArcScriptStatus { 47 | [CmdletBinding()] 48 | param( 49 | [string]$ResourceGroupName, 50 | [string]$MachineName, 51 | [string]$Name 52 | ) 53 | 54 | 55 | $AzConnectedMachineExtension = @{ 56 | Name = $Name 57 | ResourceGroupName = $ResourceGroupName 58 | MachineName = $MachineName 59 | } 60 | $ArcCmd = Get-AzConnectedMachineExtension @AzConnectedMachineExtension 61 | if($ArcCmd.ProvisioningState -in 'Succeeded','Failed'){ 62 | $ArcCmd 63 | } 64 | } 65 | 66 | Function Get-ArcScriptOutput { 67 | [CmdletBinding()] 68 | param( 69 | $ArcOutput 70 | ) 71 | 72 | $StdOut = [string]::Empty 73 | $StdErr = [string]::Empty 74 | 75 | if (-not [string]::IsNullOrEmpty($ArcOutput.InstanceViewStatusMessage)) { 76 | $StdOut = $ArcOutput.InstanceViewStatusMessage 77 | } 78 | elseif (-not [string]::IsNullOrEmpty($ArcOutput.StatusMessage)) { 79 | $StdOut = $ArcOutput.StatusMessage 80 | } 81 | else { 82 | $StdOut = $ArcOutput 83 | } 84 | if ($StdOut.IndexOf('StdOut:') -gt 0) { 85 | $StdOut = $StdOut.Substring($StdOut.IndexOf('StdOut:') + 7) 86 | } 87 | if ($StdOut.IndexOf(', StdErr:') -gt 0) { 88 | $StdErr = $StdOut.Substring($StdOut.IndexOf(', StdErr:') + 10) 89 | $StdOut = $StdOut.Substring(0, $StdOut.IndexOf(', StdErr:')) 90 | } 91 | else { 92 | $StdErr = '' 93 | } 94 | 95 | 96 | try { 97 | $StdOutReturn = $StdOut.Trim() | ConvertFrom-Json -ErrorAction Stop 98 | } 99 | catch { 100 | $StdOutReturn = $StdOut 101 | } 102 | 103 | try { 104 | $StdErrReturn = $StdErr.Trim() | ConvertFrom-Json -ErrorAction Stop 105 | } 106 | catch { 107 | $StdErrReturn = $StdErr 108 | } 109 | 110 | if ($vmJob.State -eq 'Failed') { 111 | $StdErrReturn = $StdOutReturn 112 | $StdOutReturn = '' 113 | } 114 | 115 | [pscustomobject]@{ 116 | StdOut = $StdOutReturn 117 | StdErr = $StdErrReturn 118 | } 119 | } 120 | 121 | $ScriptContent = Get-Content '.\DFWSMUG\VSCodeExt.ps1' -Raw 122 | $ArcCommand = Invoke-ArcCommand -ResourceGroupName 'ArcDev' -Name 'OP-Win01' -ScriptContent $ScriptContent 123 | $ArcScriptStatus = Get-ArcScriptStatus -ResourceGroupName 'ArcDev' -MachineName 'OP-Win01' -Name $ArcCommand 124 | Get-ArcScriptOutput -ArcOutput $ArcScriptStatus | Format-List -------------------------------------------------------------------------------- /2022-08 DFWSMUG/Azure Arc Script.ps1: -------------------------------------------------------------------------------- 1 | # Get Arc Server 2 | $ResourceGroupName = 'ArcDev' 3 | $ArcSrv = Get-AzConnectedMachine -ResourceGroupName $ResourceGroupName -Name 'OP-Win01' 4 | 5 | # Create Run Command on the Arc Server 6 | $encodedcommand = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($ScriptContent)) 7 | $body = @{ 8 | "location" = $ArcSrv.Location 9 | "properties" = @{ 10 | "publisher" = "Microsoft.Compute" 11 | "typeHandlerVersion" = "1.10" 12 | "type" = "CustomScriptExtension" 13 | "forceUpdateTag" = (Get-Date).ToFileTime() 14 | "settings" = @{ 15 | "commandToExecute" = "powershell.exe -EncodedCommand $encodedcommand" 16 | } 17 | } 18 | } 19 | 20 | $URI = "https://management.azure.com$($ArcSrv.Id)/extensions/CustomScriptExtension?api-version=2021-05-20" 21 | $submit = Invoke-AzRestMethod -Uri $URI -Method 'Put' -Payload ($body | ConvertTo-Json) 22 | 23 | 24 | # Get Results from the Command 25 | $AzConnectedMachineExtension = @{ 26 | Name = 'CustomScriptExtension' 27 | ResourceGroupName = $ResourceGroupName 28 | MachineName = $ArcSrv.Name 29 | } 30 | $ArcCmd = Get-AzConnectedMachineExtension @AzConnectedMachineExtension 31 | $ArcCmd | Format-List Name, ProvisioningState, InstanceViewStatusCode, InstanceViewStatusLevel, InstanceViewStatusMessage 32 | 33 | $ArcCmd.InstanceViewStatusMessage 34 | 35 | # Parse the output 36 | $StdOut = $ArcCmd.InstanceViewStatusMessage 37 | $StdOut = $StdOut.Substring($StdOut.IndexOf('StdOut:') + 7) 38 | $StdOut = $StdOut.Substring(0, $StdOut.IndexOf(', StdErr:')) 39 | $StdOut | ConvertFrom-Json -------------------------------------------------------------------------------- /2022-08 DFWSMUG/Azure VM Functions.ps1: -------------------------------------------------------------------------------- 1 | Function Invoke-VmCommand { 2 | [CmdletBinding()] 3 | param( 4 | [string]$ResourceGroupName, 5 | [string]$VMName, 6 | [string]$ScriptContent, 7 | [string]$RunCommandName 8 | ) 9 | 10 | $VM = Get-AzVM -ResourceGroupName $ResourceGroupName -Name $VMName 11 | 12 | # Create Run Command on VM 13 | $AzVMRunCommand = @{ 14 | ResourceGroupName = $VM.ResourceGroupName 15 | VMName = $VM.Name 16 | RunCommandName = $RunCommandName 17 | SourceScript = $ScriptContent 18 | Location = $VM.Location 19 | AsJob = $true 20 | } 21 | $SetCmd = Set-AzVMRunCommand @AzVMRunCommand 22 | $SetCmd 23 | } 24 | 25 | Function Get-VwScriptStatus { 26 | [CmdletBinding()] 27 | param( 28 | [string]$ResourceGroupName, 29 | [string]$VMName, 30 | [string]$Name 31 | ) 32 | 33 | $AzVMRunCommand = @{ 34 | ResourceGroupName = $ResourceGroupName 35 | VMName = $VMName 36 | RunCommandName = $Name 37 | Expand = 'instanceView' 38 | } 39 | $cmd = Get-AzVMRunCommand @AzVMRunCommand 40 | if ($cmd.ProvisioningState -in 'Succeeded', 'Failed') { 41 | $cmd 42 | } 43 | } 44 | 45 | Function Get-VmScriptOutput { 46 | param( 47 | $vmOutput 48 | ) 49 | 50 | $StdOut = $vmOutput.InstanceViewOutput 51 | $StdErr = $vmOutput.InstanceViewError 52 | if ([string]::IsNullOrEmpty($StdOut)) { 53 | $StdErr = $vmOutput 54 | } 55 | 56 | try { 57 | $StdOutReturn = $StdOut.Trim() | ConvertFrom-Json -ErrorAction Stop 58 | } 59 | catch { 60 | $StdOutReturn = $StdOut 61 | } 62 | 63 | try { 64 | $StdErrReturn = $StdErr.Trim() | ConvertFrom-Json -ErrorAction Stop 65 | } 66 | catch { 67 | $StdErrReturn = $StdErr 68 | } 69 | 70 | [pscustomobject]@{ 71 | StdOut = $StdOutReturn 72 | StdErr = $StdErrReturn 73 | } 74 | } 75 | 76 | $RunCommandName = 'VSCodeExt' 77 | $ScriptContent = Get-Content '.\DFWSMUG\VSCodeExt.ps1' -Raw 78 | $VmCommand = Invoke-VmCommand -ResourceGroupName 'ArcDev' -VMName 'az-win19' -RunCommandName $RunCommandName -ScriptContent $ScriptContent 79 | $VmScriptStatus = Get-VwScriptStatus -ResourceGroupName 'PoshVM' -VMName 'az-win19' -Name $RunCommandName 80 | Get-VmScriptOutput -vmOutput $VmScriptStatus | Format-List -------------------------------------------------------------------------------- /2022-08 DFWSMUG/Azure VM Script.ps1: -------------------------------------------------------------------------------- 1 | # Get Azure VM 2 | $VM = Get-AzVM -ResourceGroupName 'ArcDev' -Name 'az-win19' 3 | 4 | $ScriptContent = Get-Content '.\DFWSMUG\VSCodeExt.ps1' -Raw 5 | $RunCommandName = 'VSCodeExt' 6 | 7 | # Create Run Command on VM 8 | $AzVMRunCommand = @{ 9 | ResourceGroupName = $VM.ResourceGroupName 10 | VMName = $VM.Name 11 | RunCommandName = $RunCommandName 12 | SourceScript = $ScriptContent 13 | Location = $VM.Location 14 | AsJob = $true 15 | } 16 | $SetCmd = Set-AzVMRunCommand @AzVMRunCommand 17 | $SetCmd 18 | 19 | # Get Results from Run Command 20 | $AzVMRunCommand = @{ 21 | ResourceGroupName = $VM.ResourceGroupName 22 | VMName = $VM.Name 23 | RunCommandName = $RunCommandName 24 | Expand = 'instanceView' 25 | } 26 | $cmd = Get-AzVMRunCommand @AzVMRunCommand 27 | $cmd | Format-List Name, ProvisioningState, InstanceViewExecutionState, InstanceViewStartTime, InstanceViewEndTime, InstanceViewError, 28 | InstanceViewExitCode, InstanceViewOutput 29 | 30 | $cmd.InstanceViewOutput | ConvertFrom-Json 31 | 32 | 33 | -------------------------------------------------------------------------------- /2022-08 DFWSMUG/Combined Execution.ps1: -------------------------------------------------------------------------------- 1 | $ResourceGroupName = 'ArcDev' 2 | $RunCommandName = 'VSCodeExt' 3 | $ScriptContent = Get-Content '.\DFWSMUG\VSCodeExt.ps1' -Raw 4 | 5 | $resources = Get-AzResource -ResourceGroupName $ResourceGroupName | 6 | Where-Object{ $_.ResourceType -in 'Microsoft.HybridCompute/machines','Microsoft.Compute/virtualMachines'} | 7 | Select-Object -Property ResourceGroupName, Name, ResourceType, Location, 8 | @{l = 'Job'; e = { $null } }, @{l = 'Output'; e = { $null } } 9 | 10 | foreach($r in $resources){ 11 | if($r.ResourceType -eq 'Microsoft.Compute/virtualMachines'){ 12 | $r.Job = Invoke-VmCommand -ResourceGroupName $r.ResourceGroupName -VMName $r.Name -RunCommandName $RunCommandName -ScriptContent $ScriptContent 13 | } 14 | else{ 15 | $r.Job = Invoke-ArcCommand -ResourceGroupName $r.ResourceGroupName -Name $r.Name -ScriptContent $ScriptContent 16 | } 17 | } 18 | 19 | while($resources.Output -contains $null){ 20 | foreach($r in $resources | Where-Object{$_.Output -eq $null}){ 21 | if($r.ResourceType -eq 'Microsoft.Compute/virtualMachines'){ 22 | $r.Output = Get-VwScriptStatus -ResourceGroupName $r.ResourceGroupName -VMName $r.Name -Name $RunCommandName 23 | } 24 | else{ 25 | $r.Output = Get-ArcScriptStatus -ResourceGroupName $r.ResourceGroupName -MachineName $r.Name -Name $r.Job 26 | } 27 | } 28 | } 29 | 30 | foreach($r in $resources){ 31 | if($r.ResourceType -eq 'Microsoft.Compute/virtualMachines'){ 32 | Get-VmScriptOutput -vmOutput $r.Output | Format-List 33 | } 34 | else{ 35 | Get-ArcScriptOutput -ArcOutput $r.Output | Format-List 36 | } 37 | } -------------------------------------------------------------------------------- /2022-08 DFWSMUG/VSCodeExt.ps1: -------------------------------------------------------------------------------- 1 | [System.Collections.Generic.List[PSObject]] $extensions = @() 2 | # Set home folder path based on the operating system 3 | if ($IsLinux) { 4 | $homePath = '/home/' 5 | } 6 | else { 7 | $homePath = "$($env:HOMEDRIVE)\Users" 8 | } 9 | 10 | # Get the subfolders under the home patch 11 | $homeDirs = Get-ChildItem -Path $homePath -Directory 12 | 13 | # Parse through each folder and check for VS Code extensions 14 | foreach ($dir in $homeDirs) { 15 | $vscPath = Join-Path $dir.FullName '.vscode\extensions' 16 | # If the VS Code extension folder is present, search it for vsixmanifest files 17 | if (Test-Path -Path $vscPath) { 18 | $ChildItem = @{ 19 | Path = $vscPath 20 | Recurse = $true 21 | Filter = '.vsixmanifest' 22 | Force = $true 23 | } 24 | $manifests = Get-ChildItem @ChildItem 25 | foreach ($m in $manifests) { 26 | # Get the contents of the vsixmanifest file and convert it to PowerShell XML object 27 | [xml]$vsix = Get-Content -Path $m.FullName 28 | # Get the details from the manifest and add them to the extensions list 29 | $vsix.PackageManifest.Metadata.Identity | 30 | Select-Object -Property Id, Version, Publisher, 31 | # Add the folder path, computer name, and date to the output 32 | @{l = 'Folder'; e = { $m.FullName } }, 33 | @{l = 'ComputerName'; e = { [system.environment]::MachineName } }, 34 | @{l = 'Date'; e = { Get-Date } } | 35 | ForEach-Object { $extensions.Add($_) } 36 | } 37 | } 38 | } 39 | # If no extensions are found, return a PowerShell object with the same properties stating nothing found. 40 | if ($extensions.Count -eq 0) { 41 | $extensions.Add([pscustomobject]@{ 42 | Id = 'No extension found' 43 | Version = $null 44 | Publisher = $null 45 | Folder = $null 46 | ComputerName = [system.environment]::MachineName 47 | Date = Get-Date 48 | }) 49 | } 50 | # Just like an extension include the output at the end 51 | $extensions | ConvertTo-Json -Compress -------------------------------------------------------------------------------- /2022-12 RTPSUG/01 Azure VM Script.ps1: -------------------------------------------------------------------------------- 1 | # Get Azure VM 2 | $WinVM = Get-AzVM -ResourceGroupName $ResourceGroupName -Name $WinVMName 3 | 4 | # Source script is the command to execute on the remote machine. Natively supports PowerShell code. 5 | $ScriptContent = '[system.environment]::MachineName' 6 | 7 | # Run command name can be used to save the output of different scripts 8 | $RunCommandName = 'GetSystemName' 9 | 10 | # Create Run Command on VM 11 | $AzVMRunCommand = @{ 12 | ResourceGroupName = $WinVM.ResourceGroupName 13 | VMName = $WinVM.Name 14 | RunCommandName = $RunCommandName 15 | SourceScript = $ScriptContent 16 | Location = $WinVM.Location 17 | # Running as create background job for the execution, freeing your script to continue. 18 | AsJob = $true 19 | } 20 | $SetCmd = Set-AzVMRunCommand @AzVMRunCommand 21 | $SetCmd 22 | 23 | # Get Results from Run Command 24 | $AzVMRunCommand = @{ 25 | ResourceGroupName = $WinVM.ResourceGroupName 26 | VMName = $WinVM.Name 27 | RunCommandName = $RunCommandName 28 | Expand = 'instanceView' 29 | } 30 | $cmd = Get-AzVMRunCommand @AzVMRunCommand 31 | $cmd | Format-List Name, ProvisioningState, InstanceViewExecutionState, InstanceViewStartTime, InstanceViewEndTime, InstanceViewError, 32 | InstanceViewExitCode, InstanceViewOutput 33 | -------------------------------------------------------------------------------- /2022-12 RTPSUG/02 Azure VM Script Linux.ps1: -------------------------------------------------------------------------------- 1 | # Get Azure VM 2 | $LinuxVM = Get-AzVM -ResourceGroupName $ResourceGroupName -Name $LinuxVMName 3 | 4 | # Source script is the command to execute on the remote machine. Linux does not natively supports PowerShell. 5 | $ScriptContent = 'pwsh -command ''[system.environment]::MachineName''' 6 | 7 | # Run command name can be used to save the output of different scripts 8 | $RunCommandName = 'GetSystemName' 9 | 10 | # Create Run Command on VM 11 | $AzVMRunCommand = @{ 12 | ResourceGroupName = $LinuxVM.ResourceGroupName 13 | VMName = $LinuxVM.Name 14 | RunCommandName = $RunCommandName 15 | SourceScript = $ScriptContent 16 | Location = $LinuxVM.Location 17 | # Running as create background job for the execution, freeing your script to continue. 18 | AsJob = $true 19 | } 20 | $LinuxSetCmd = Set-AzVMRunCommand @AzVMRunCommand 21 | $LinuxSetCmd 22 | 23 | # Get Results from Run Command 24 | $AzVMRunCommand = @{ 25 | ResourceGroupName = $LinuxVM.ResourceGroupName 26 | VMName = $LinuxVM.Name 27 | RunCommandName = $RunCommandName 28 | Expand = 'instanceView' 29 | } 30 | $LinuxCmd = Get-AzVMRunCommand @AzVMRunCommand 31 | $LinuxCmd | Format-List Name, ProvisioningState, InstanceViewExecutionState, InstanceViewStartTime, InstanceViewEndTime, InstanceViewError, 32 | InstanceViewExitCode, InstanceViewOutput 33 | -------------------------------------------------------------------------------- /2022-12 RTPSUG/03 Azure Arc Script.ps1: -------------------------------------------------------------------------------- 1 | # Get Arc Server 2 | $ArcWinSrv = Get-AzConnectedMachine -ResourceGroupName $ResourceGroupName -Name $ArcWinSrvName 3 | 4 | # Create Run Command on the Arc Server 5 | $ScriptContent = '[system.environment]::MachineName' 6 | # The script is encoded to prevent issues with escape or illegal characters in the JSON 7 | $encodedcommand = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($ScriptContent)) 8 | $body = @{ 9 | "location" = $ArcWinSrv.Location 10 | "properties" = @{ 11 | "publisher" = "Microsoft.Compute" 12 | "typeHandlerVersion" = "1.10" 13 | "type" = "CustomScriptExtension" 14 | # The script will only reexecute if the forceUpdateTag is changed 15 | "forceUpdateTag" = (Get-Date).ToFileTime() 16 | "settings" = @{ 17 | # Command to execute is similar to the Run command, so you need to specify PowerShell. 18 | "commandToExecute" = "powershell.exe -EncodedCommand $encodedcommand" 19 | } 20 | } 21 | } 22 | $Payload = $body | ConvertTo-Json 23 | $URI = "https://management.azure.com$($ArcWinSrv.Id)/extensions/CustomScriptExtension?api-version=2021-05-20" 24 | $submit = Invoke-AzRestMethod -Uri $URI -Method 'Put' -Payload $Payload 25 | $submit 26 | 27 | # Get Results from the Command 28 | $AzConnectedMachineExtension = @{ 29 | Name = 'CustomScriptExtension' 30 | ResourceGroupName = $ResourceGroupName 31 | MachineName = $ArcWinSrv.Name 32 | } 33 | $ArcWinCmd = Get-AzConnectedMachineExtension @AzConnectedMachineExtension 34 | $ArcWinCmd | Format-List Name, ProvisioningState, InstanceViewStatusCode, InstanceViewStatusLevel, InstanceViewStatusMessage 35 | 36 | $ArcWinCmd.InstanceViewStatusMessage 37 | 38 | # Parse the output 39 | $StdOut = $ArcWinCmd.InstanceViewStatusMessage 40 | $StdOut = $StdOut.Substring($StdOut.IndexOf('StdOut:') + 8) 41 | $StdOut -------------------------------------------------------------------------------- /2022-12 RTPSUG/04 Azure Arc Script Linux.ps1: -------------------------------------------------------------------------------- 1 | # Get Arc Linux Server 2 | $ArcLinux = Get-AzConnectedMachine -ResourceGroupName $ResourceGroupName -Name $ArcLinuxName 3 | 4 | # Create Run Command on the Arc Server 5 | $ScriptContent = '[system.environment]::MachineName' 6 | $encodedcommand = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($ScriptContent)) 7 | $body = @{ 8 | "location" = $ArcLinux.Location 9 | "properties" = @{ 10 | "publisher" = "Microsoft.Azure.Extensions" 11 | "typeHandlerVersion" = "2.1.7" 12 | "type" = "CustomScript" 13 | # The script will only reexecute if the forceUpdateTag is changed 14 | "forceUpdateTag" = (Get-Date).ToFileTime() 15 | "settings" = @{ 16 | # Command to execute is similar to the terminal command, so you need to specify PowerShell. 17 | # The script is encoded to prevent issues with escape or illegal characters in the JSON 18 | "commandToExecute" = "pwsh -EncodedCommand $encodedcommand" 19 | } 20 | } 21 | } 22 | 23 | $URI = "https://management.azure.com$($ArcLinux.Id)/extensions/CustomScriptExtension?api-version=2021-05-20" 24 | $submit = Invoke-AzRestMethod -Uri $URI -Method 'Put' -Payload ($body | ConvertTo-Json) 25 | $submit 26 | 27 | # Get Results from the Command 28 | $AzConnectedMachineExtension = @{ 29 | Name = 'CustomScriptExtension' 30 | ResourceGroupName = $ResourceGroupName 31 | MachineName = $ArcLinux.Name 32 | } 33 | $ArcLinuxCmd = Get-AzConnectedMachineExtension @AzConnectedMachineExtension 34 | $ArcLinuxCmd | Format-List Name, ProvisioningState, InstanceViewStatusCode, InstanceViewStatusLevel, InstanceViewStatusMessage 35 | 36 | $ArcLinuxCmd.InstanceViewStatusMessage 37 | 38 | # Parse the output 39 | $StdOut = $ArcLinuxCmd.InstanceViewStatusMessage 40 | $StdOut = $StdOut.Substring($StdOut.IndexOf('[stdout]') + 8) 41 | $StdOut = $StdOut.Substring(0, $StdOut.IndexOf('[stderr]')) 42 | $StdOut -------------------------------------------------------------------------------- /2022-12 RTPSUG/05 Get-SystemInfo.ps1: -------------------------------------------------------------------------------- 1 | Function Get-SystemInfo{ 2 | [CmdletBinding()] 3 | param() 4 | # Check if the machine is running a Linux based OS 5 | if($IsLinux){ 6 | # Get the data from the os-release file and convert it to a PowerShell object 7 | $OS = Get-Content -Path /etc/os-release | 8 | ConvertFrom-StringData 9 | 10 | # Search the meminfo file for the MemTotal line and extract the number 11 | $search = @{ 12 | Path = '/proc/meminfo' 13 | Pattern = 'MemTotal' 14 | } 15 | $Mem = Select-String @search | 16 | ForEach-Object{ [regex]::Match($_.line, "(\d+)").value} 17 | 18 | # Run the stat command and parse the output for the Birth line, then extract the date 19 | $stat = Invoke-Expression -Command 'stat /' 20 | $InstallDate = $stat | Select-String -Pattern 'Birth:' | 21 | ForEach-Object{ 22 | Get-Date $_.Line.Replace('Birth:','').Trim() 23 | } 24 | 25 | # Run the df and uname commands and save the output as is 26 | $boot = Invoke-Expression -Command 'df /boot' 27 | $OSArchitecture = Invoke-Expression -Command 'uname -m' 28 | $CSName = Invoke-Expression -Command 'uname -n' 29 | 30 | # Build the results into a PowerShell object that matches the same properties as the existing Windows output 31 | [pscustomobject]@{ 32 | Caption = $OS.PRETTY_NAME.Replace('"',"") 33 | InstallDate = $InstallDate 34 | ServicePackMajorVersion = $OS.VERSION.Replace('"',"") 35 | OSArchitecture = $OSArchitecture 36 | BootDevice = $boot.Split("`n")[-1].Split()[0] 37 | BuildNumber = $OS.VERSION_ID.Replace('"',"") 38 | CSName = $CSName 39 | Total_Memory = [math]::Round($Mem/1MB) 40 | } 41 | } 42 | else{ 43 | # Orginal Windows system information commands 44 | Get-CimInstance -Class Win32_OperatingSystem | 45 | Select-Object Caption, InstallDate, ServicePackMajorVersion, 46 | OSArchitecture, BootDevice, BuildNumber, CSName, 47 | @{l='Total_Memory'; 48 | e={[math]::Round($_.TotalVisibleMemorySize/1MB)}} 49 | } 50 | } 51 | # Convert results to JSON to allow the output to be parsed 52 | Get-SystemInfo | ConvertTo-Json -------------------------------------------------------------------------------- /2022-12 RTPSUG/06 Azure VM Functions.ps1: -------------------------------------------------------------------------------- 1 | Function Invoke-VmCommand { 2 | [CmdletBinding()] 3 | param( 4 | [parameter(Mandatory = $true)] 5 | [string]$ResourceGroupName, 6 | [parameter(Mandatory = $true)] 7 | [string]$MachineName, 8 | [parameter(Mandatory = $true)] 9 | [string]$ScriptContent, 10 | [parameter(Mandatory = $false)] 11 | [string]$RunCommandName='CustomScriptExtension' 12 | ) 13 | 14 | $VM = Get-AzVM -ResourceGroupName $ResourceGroupName -Name $MachineName 15 | if($VM.OSProfile.LinuxConfiguration){ 16 | $encodedcommand = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($ScriptContent)) 17 | $ScriptContent = "pwsh -EncodedCommand $encodedcommand" 18 | } 19 | # Create Run Command on VM 20 | $AzVMRunCommand = @{ 21 | ResourceGroupName = $VM.ResourceGroupName 22 | VMName = $VM.Name 23 | RunCommandName = $RunCommandName 24 | SourceScript = $ScriptContent 25 | Location = $VM.Location 26 | AsJob = $true 27 | } 28 | Set-AzVMRunCommand @AzVMRunCommand | Out-Null 29 | $RunCommandName 30 | } 31 | 32 | Function Get-VwScriptStatus { 33 | [CmdletBinding()] 34 | param( 35 | [parameter(Mandatory = $true)] 36 | [string]$ResourceGroupName, 37 | [parameter(Mandatory = $true)] 38 | [string]$MachineName, 39 | [parameter(Mandatory = $true)] 40 | [string]$RunCommandName 41 | ) 42 | 43 | $AzVMRunCommand = @{ 44 | ResourceGroupName = $ResourceGroupName 45 | VMName = $MachineName 46 | RunCommandName = $RunCommandName 47 | Expand = 'instanceView' 48 | } 49 | $cmd = Get-AzVMRunCommand @AzVMRunCommand 50 | $cmd.ProvisioningState 51 | } 52 | 53 | Function Get-VmScriptOutput { 54 | [CmdletBinding()] 55 | param( 56 | [parameter(Mandatory = $true)] 57 | [string]$ResourceGroupName, 58 | [parameter(Mandatory = $true)] 59 | [string]$MachineName, 60 | [parameter(Mandatory = $true)] 61 | [string]$RunCommandName 62 | ) 63 | 64 | $AzVMRunCommand = @{ 65 | ResourceGroupName = $ResourceGroupName 66 | VMName = $MachineName 67 | RunCommandName = $RunCommandName 68 | Expand = 'instanceView' 69 | } 70 | $vmOutput = Get-AzVMRunCommand @AzVMRunCommand 71 | 72 | $StdOut = $vmOutput.InstanceViewOutput 73 | $StdErr = $vmOutput.InstanceViewError 74 | if ([string]::IsNullOrEmpty($StdOut)) { 75 | $StdErr = $vmOutput 76 | } 77 | 78 | try { 79 | $StdOutReturn = $StdOut.Trim() | ConvertFrom-Json -ErrorAction Stop 80 | } 81 | catch { 82 | $StdOutReturn = $StdOut 83 | } 84 | 85 | try { 86 | $StdErrReturn = $StdErr.Trim() | ConvertFrom-Json -ErrorAction Stop 87 | } 88 | catch { 89 | $StdErrReturn = $StdErr 90 | } 91 | 92 | [pscustomobject]@{ 93 | StdOut = $StdOutReturn 94 | StdErr = $StdErrReturn 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /2022-12 RTPSUG/07 VM Executions.ps1: -------------------------------------------------------------------------------- 1 | $ScriptContent = Get-Content '.\05 Get-SystemInfo.ps1' -Raw 2 | 3 | # Start the Azure Linux Job 4 | $LinuxVMJob = Invoke-VmCommand -ResourceGroupName $ResourceGroupName -MachineName $LinuxVMName -ScriptContent $ScriptContent 5 | # Start the Azure Windows Job 6 | $WinSrvVMJob = Invoke-VmCommand -ResourceGroupName $ResourceGroupName -MachineName $WinVMName -ScriptContent $ScriptContent 7 | 8 | # Check the status of each job 9 | Get-VwScriptStatus -ResourceGroupName $ResourceGroupName -MachineName $LinuxVMName -RunCommandName $LinuxVMJob 10 | Get-VwScriptStatus -ResourceGroupName $ResourceGroupName -MachineName $WinVMName -RunCommandName $WinSrvVMJob 11 | 12 | # Get the results from each job 13 | $LinuxVmOut = Get-VmScriptOutput -ResourceGroupName $ResourceGroupName -MachineName $LinuxVMName -RunCommandName $LinuxVMJob 14 | $LinuxVmOut.StdOut 15 | 16 | $WinVmOut = Get-VmScriptOutput -ResourceGroupName $ResourceGroupName -MachineName $WinVMName -RunCommandName $WinSrvVMJob 17 | $WinVmOut.StdOut 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /2022-12 RTPSUG/08 Azure Arc Functions.ps1: -------------------------------------------------------------------------------- 1 | Function Invoke-ArcCommand { 2 | [CmdletBinding()] 3 | param( 4 | [parameter(Mandatory = $true)] 5 | [string]$ResourceGroupName, 6 | [parameter(Mandatory = $true)] 7 | [string]$MachineName, 8 | [parameter(Mandatory = $true)] 9 | [string]$ScriptContent 10 | ) 11 | 12 | # Get the Arc Machine 13 | $ArcSrv = Get-AzConnectedMachine -ResourceGroupName $ResourceGroupName -Name $MachineName -ErrorAction Stop 14 | 15 | 16 | $encodedcommand = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($ScriptContent)) 17 | if ($ArcSrv.OSName -eq 'windows') { 18 | $body = @{ 19 | "location" = $ArcSrv.Location 20 | "properties" = @{ 21 | "publisher" = "Microsoft.Compute" 22 | "typeHandlerVersion" = "1.10" 23 | "type" = "CustomScriptExtension" 24 | "forceUpdateTag" = (Get-Date).ToFileTime() 25 | "settings" = @{ 26 | "commandToExecute" = "powershell.exe -EncodedCommand $encodedcommand" 27 | } 28 | } 29 | } 30 | } 31 | else { 32 | $body = @{ 33 | "location" = $ArcSrv.Location 34 | "properties" = @{ 35 | "publisher" = "Microsoft.Azure.Extensions" 36 | "typeHandlerVersion" = "2.1.7" 37 | "type" = "CustomScript" 38 | "forceUpdateTag" = (Get-Date).ToFileTime() 39 | "settings" = @{ 40 | "commandToExecute" = "pwsh -EncodedCommand $encodedcommand" 41 | } 42 | } 43 | } 44 | } 45 | # submit Rest request to start script 46 | $URI = "https://management.azure.com$($ArcSrv.Id)/extensions/CustomScriptExtension?api-version=2021-05-20" 47 | $submit = Invoke-AzRestMethod -Uri $URI -Method 'Put' -Payload ($body | ConvertTo-Json) 48 | 49 | # Monitor that the execution starts 50 | $timer = [system.diagnostics.stopwatch]::StartNew() 51 | do { 52 | $ext = Get-AzConnectedMachineExtension -ResourceGroupName $ResourceGroupName -MachineName $MachineName | 53 | Where-Object { $_.InstanceViewType -eq 'CustomScriptExtension' -or $_.Name -eq 'CustomScriptExtension' } 54 | } while ($ext.ProvisioningState -notin 'Updating', 'Creating', 'Waiting' -and $timer.Elapsed.TotalSeconds -le 30) 55 | $timer.Stop() 56 | 57 | if ($timer.Elapsed.TotalSeconds -gt 30) { 58 | Write-Error "Failed to start the provisioning - $($ext.ProvisioningState)" 59 | } 60 | elseif ($submit.StatusCode -ne 202) { 61 | Write-Error $submit.Content 62 | } 63 | else { 64 | $ext.Name 65 | } 66 | } 67 | 68 | Function Get-ArcScriptStatus { 69 | [CmdletBinding()] 70 | param( 71 | [parameter(Mandatory = $true)] 72 | [string]$ResourceGroupName, 73 | [parameter(Mandatory = $true)] 74 | [string]$MachineName, 75 | [parameter(Mandatory = $true)] 76 | [string]$RunCommandName 77 | ) 78 | 79 | 80 | $AzConnectedMachineExtension = @{ 81 | Name = $RunCommandName 82 | ResourceGroupName = $ResourceGroupName 83 | MachineName = $MachineName 84 | } 85 | $ArcCmd = Get-AzConnectedMachineExtension @AzConnectedMachineExtension 86 | 87 | $ArcCmd.ProvisioningState 88 | } 89 | 90 | Function Get-ArcScriptOutput { 91 | [CmdletBinding()] 92 | param( 93 | [parameter(Mandatory = $true)] 94 | [string]$ResourceGroupName, 95 | [parameter(Mandatory = $true)] 96 | [string]$MachineName, 97 | [parameter(Mandatory = $true)] 98 | [string]$RunCommandName 99 | ) 100 | 101 | 102 | $AzConnectedMachineExtension = @{ 103 | Name = $RunCommandName 104 | ResourceGroupName = $ResourceGroupName 105 | MachineName = $MachineName 106 | } 107 | $ArcOutput = Get-AzConnectedMachineExtension @AzConnectedMachineExtension 108 | 109 | $StdOut = [string]::Empty 110 | $StdErr = [string]::Empty 111 | 112 | if (-not [string]::IsNullOrEmpty($ArcOutput.InstanceViewStatusMessage)) { 113 | $StdOut = $ArcOutput.InstanceViewStatusMessage 114 | } 115 | elseif (-not [string]::IsNullOrEmpty($ArcOutput.StatusMessage)) { 116 | $StdOut = $ArcOutput.StatusMessage 117 | } 118 | else { 119 | $StdOut = $ArcOutput 120 | } 121 | 122 | if ($StdOut.IndexOf('StdOut:') -gt 0) { 123 | $StdOut = $StdOut.Substring($StdOut.IndexOf('StdOut:') + 7) 124 | } 125 | elseif ($StdOut.IndexOf('[stdout]') -gt 0) { 126 | $StdOut = $StdOut.Substring($StdOut.IndexOf('[stdout]') + 8) 127 | } 128 | 129 | if ($StdOut.IndexOf(', StdErr:') -gt 0) { 130 | $StdErr = $StdOut.Substring($StdOut.IndexOf(', StdErr:') + 10) 131 | $StdOut = $StdOut.Substring(0, $StdOut.IndexOf(', StdErr:')) 132 | } 133 | elseif ($StdOut.IndexOf('[stderr]') -gt 0) { 134 | $StdErr = $StdOut.Substring($StdOut.IndexOf('[stderr]') + 8) 135 | $StdOut = $StdOut.Substring(0, $StdOut.IndexOf('[stderr]')) 136 | } 137 | else { 138 | $StdErr = '' 139 | } 140 | 141 | try { 142 | $StdOutReturn = $StdOut.Trim() | ConvertFrom-Json -ErrorAction Stop 143 | } 144 | catch { 145 | $StdOutReturn = $StdOut 146 | } 147 | 148 | try { 149 | $StdErrReturn = $StdErr.Trim() | ConvertFrom-Json -ErrorAction Stop 150 | } 151 | catch { 152 | $StdErrReturn = $StdErr 153 | } 154 | 155 | if ($vmJob.State -eq 'Failed') { 156 | $StdErrReturn = $StdOutReturn 157 | $StdOutReturn = '' 158 | } 159 | 160 | [pscustomobject]@{ 161 | StdOut = $StdOutReturn 162 | StdErr = $StdErrReturn 163 | } 164 | } -------------------------------------------------------------------------------- /2022-12 RTPSUG/09 Arc Executions.ps1: -------------------------------------------------------------------------------- 1 | $ScriptContent = Get-Content '.\05 Get-SystemInfo.ps1' -Raw 2 | 3 | # Start the Arc Linux Job 4 | $ArcLinuxJob = Invoke-ArcCommand -ResourceGroupName $ResourceGroupName -MachineName $ArcLinuxName -ScriptContent $ScriptContent 5 | # Start the Arc Windows Job 6 | $ArcWinSrvJob = Invoke-ArcCommand -ResourceGroupName $ResourceGroupName -MachineName $ArcWinSrvName -ScriptContent $ScriptContent 7 | 8 | # Check the status of each job 9 | Get-ArcScriptStatus -ResourceGroupName $ResourceGroupName -MachineName $ArcLinuxName -RunCommandName $ArcLinuxJob 10 | Get-ArcScriptStatus -ResourceGroupName $ResourceGroupName -MachineName $ArcWinSrvName -RunCommandName $ArcWinSrvJob 11 | 12 | # Get the results from each job 13 | $LinOut = Get-ArcScriptOutput -ResourceGroupName $ResourceGroupName -MachineName $ArcLinuxName -RunCommandName $ArcLinuxJob 14 | $LinOut.StdOut 15 | 16 | $WinOut = Get-ArcScriptOutput -ResourceGroupName $ResourceGroupName -MachineName $ArcWinSrvName -RunCommandName $ArcWinSrvJob 17 | $WinOut.StdOut 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /2022-12 RTPSUG/Combined Execution.ps1: -------------------------------------------------------------------------------- 1 | $Concurrent = 4 2 | $ScriptContent = Get-Content '.\05 Get-SystemInfo.ps1' -Raw 3 | 4 | # Get all Virtual Machines and Arc Enabled Machines 5 | $Query = @' 6 | resources 7 | | where type in~ ('microsoft.hybridcompute/machines','microsoft.compute/virtualmachines') 8 | | extend statusRaw = iif(isempty(properties.status), 9 | coalesce(properties.powerState, properties.status.powerState, tostring(split(tolower(properties.extended.instanceView.powerState.code), "powerstate/")[1])), 10 | properties.status) 11 | | extend status = strcat(toupper(substring(statusRaw, 0, 1)), tolower(substring(statusRaw, 1, strlen(statusRaw)-1))) 12 | | extend os = case( 13 | properties.storageProfile.osDisk.osType =~ 'Windows' or properties.osProfile.osType =~ 'Windows', 'Windows', 14 | properties.storageProfile.osDisk.osType =~ 'Linux' or properties.osProfile.osType =~ 'Linux', 'Linux', 15 | properties.osName 16 | ) 17 | | extend operatingSystem = case( 18 | os =~ 'windows', 'Windows', 19 | os =~ 'linux', 'Linux', 20 | '') 21 | | project id, name, status, type, operatingSystem, resourceGroup, subscriptionId 22 | '@ 23 | $QueryResults = Search-AzGraph -Query $Query 24 | $resources = $QueryResults.Data | Select-Object -Property *, 25 | @{l = 'RunCommandName'; e = { $null } }, @{l = 'JobStatus'; e = { 'Pending' } }, @{l = 'Output'; e = { $null } } 26 | 27 | 28 | $resources | Format-Table Name, type, Status, RunCommandName, JobStatus, Output 29 | 30 | while ($resources | Where-Object { $_.JobStatus -notin 'Succeeded', 'Failed', 'Error' }) { 31 | 32 | foreach ($r in $resources | Where-Object { $_.JobStatus -eq 'Pending' }) { 33 | # If concurrent executions exceeded then stop processing new jobs 34 | if (@($resources | Where-Object { $_.JobStatus -notin 'Pending', 'Succeeded', 'Failed', 'Error' }).Count -ge $Concurrent) { 35 | Write-Host "Skipping $($r.Name)" 36 | continue 37 | } 38 | try { 39 | if ($r.type -eq 'Microsoft.Compute/virtualMachines') { 40 | $r.RunCommandName = Invoke-VmCommand -ResourceGroupName $r.resourceGroup -MachineName $r.Name -ScriptContent $ScriptContent -ErrorAction Stop 41 | } 42 | else { 43 | $r.RunCommandName = Invoke-ArcCommand -ResourceGroupName $r.resourceGroup -MachineName $r.Name -ScriptContent $ScriptContent -ErrorAction Stop 44 | } 45 | $r.JobStatus = 'Submitted' 46 | } 47 | catch { 48 | $r.JobStatus = 'Error' 49 | $r.Output = $_ 50 | } 51 | } 52 | 53 | foreach ($r in $resources | Where-Object { $_.JobStatus -notin 'Pending', 'Succeeded', 'Failed', 'Error' }) { 54 | if ($r.type -eq 'Microsoft.Compute/virtualMachines') { 55 | $r.JobStatus = Get-VwScriptStatus -ResourceGroupName $r.resourceGroup -MachineName $r.Name -RunCommandName $r.RunCommandName 56 | } 57 | else { 58 | $r.JobStatus = Get-ArcScriptStatus -ResourceGroupName $r.resourceGroup -MachineName $r.Name -RunCommandName $r.RunCommandName 59 | } 60 | } 61 | $resources | Format-Table Name, Status, RunCommandName, JobStatus, Output 62 | if ($resources | Where-Object { $_.JobStatus -notin 'Succeeded', 'Failed', 'Error' }) { 63 | Start-Sleep -Seconds 30 64 | } 65 | } 66 | 67 | 68 | foreach ($r in $resources | Where-Object { $_.JobStatus -in 'Succeeded', 'Failed' }) { 69 | if ($r.type -eq 'Microsoft.Compute/virtualMachines') { 70 | $r.Output = Get-VmScriptOutput -ResourceGroupName $r.resourceGroup -MachineName $r.Name -RunCommandName $r.RunCommandName 71 | } 72 | else { 73 | $r.Output = Get-ArcScriptOutput -ResourceGroupName $r.resourceGroup -MachineName $r.Name -RunCommandName $r.RunCommandName 74 | } 75 | } 76 | $resources | Format-Table Name, Status, RunCommandName, JobStatus, Output 77 | $resources | ForEach-Object{ 78 | $_.Name 79 | $_.Output.StdOut 80 | } 81 | -------------------------------------------------------------------------------- /2022-12 RTPSUG/Managing Hybrid Infrastructure.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdowst/Presentation-Materials/7c5577ab08f8cae96555df27df12d103fea04d32/2022-12 RTPSUG/Managing Hybrid Infrastructure.pdf -------------------------------------------------------------------------------- /2023-04 PSHSummit/LightingDemo/Move-OutlookItems.ps1: -------------------------------------------------------------------------------- 1 | # Add outlook com objects 2 | Add-Type -Assembly "$($env:ProgramFiles)\Microsoft Office\root\Office16\ADDINS\Microsoft Power Query for Excel Integrated\bin\Microsoft.Office.Interop.Outlook.dll" 3 | $outlookApp = New-Object -comobject Outlook.Application 4 | $mapiNamespace = $outlookApp.GetNameSpace("MAPI") 5 | 6 | 7 | 8 | # Get the Inbox and all the emails in it 9 | $Inbox = $mapiNamespace.GetDefaultFolder([Microsoft.Office.Interop.Outlook.OlDefaultFolders]::olFolderInbox) 10 | [Collections.Generic.List[PSObject]] $InboxItems = @() 11 | $Inbox.Items | Foreach-Object{ $InboxItems.Add($_) } 12 | 13 | 14 | 15 | # Filter based on the email address 16 | $InboxItems | Where-Object{ $_.SenderEmailAddress -eq 'azure-noreply@microsoft.com' } | Format-Table Subject 17 | 18 | # Get unique subjects 19 | $InboxItems | Where-Object{ $_.SenderEmailAddress -eq 'azure-noreply@microsoft.com' } | Select-Object Subject -Unique 20 | 21 | # Filter based on the email address and the subject 22 | $InboxItems | Where-Object{ $_.SenderEmailAddress -eq 'azure-noreply@microsoft.com' -and 23 | $_.Subject -match "Buggy Error" } | Format-Table Subject 24 | 25 | 26 | 27 | # Get Inbox Subfolders 28 | Function Get-Subfolders{ 29 | [CmdletBinding()] 30 | param( 31 | [object]$folder 32 | ) 33 | Foreach($item in $folder.Folders){ 34 | $item 35 | Get-Subfolders $item 36 | } 37 | } 38 | 39 | $SubFolders = Get-Subfolders -folder $Inbox 40 | $SubFolders | Format-Table Name 41 | 42 | # Move item to another folder 43 | $BugFolder = $SubFolders | Where-Object{ $_.Name -eq 'Buggy Error' } 44 | $InboxItems | Where-Object{ $_.SenderEmailAddress -eq 'azure-noreply@microsoft.com' -and 45 | $_.Subject -match "Buggy Error" } | Select-Object -First 1 | ForEach-Object{ 46 | $_.Move($BugFolder) | Out-Null 47 | } 48 | 49 | 50 | 51 | # Move-OutlookItem 52 | $Inbox = $mapiNamespace.GetDefaultFolder([Microsoft.Office.Interop.Outlook.OlDefaultFolders]::olFolderInbox) 53 | [Collections.Generic.List[PSObject]] $InboxItems = @() 54 | $Inbox.Items | Foreach-Object{ $InboxItems.Add($_) } 55 | Function Move-OutlookItem{ 56 | [CmdletBinding()] 57 | param( 58 | [Parameter(Mandatory=$true)] 59 | [Object]$item, 60 | [Parameter(Mandatory=$true)] 61 | [Object]$TargetFolder, 62 | [Parameter(Mandatory=$false)] 63 | [boolean]$MarkRead = $false, 64 | [Parameter(Mandatory=$false)] 65 | [boolean]$TestOnly = $false 66 | ) 67 | 68 | if($TestOnly){ 69 | $FolderPath = $TargetFolder.FolderPath.Substring($TargetFolder.FolderPath.IndexOf('\',3)+1,$TargetFolder.FolderPath.Length-$TargetFolder.FolderPath.IndexOf('\',3)-1) 70 | Write-Host " Moved: $($item.Subject) - $($FolderPath)" -ForegroundColor Cyan 71 | } else { 72 | if($MarkRead){ 73 | $item.UnRead = $false 74 | $item.Save() 75 | } 76 | try{ 77 | $item.Move($TargetFolder) | Out-Null 78 | } catch { 79 | Write-Host "Failed to Moved: $($item.Subject) - $($FolderPath)" -ForegroundColor Red 80 | } 81 | Write-Verbose "Moved: $($item.Subject) - $($FolderPath)" 82 | } 83 | 84 | } 85 | 86 | # Move and mark as read 87 | $InboxItems | Where-Object{ $_.SenderEmailAddress -eq 'azure-noreply@microsoft.com' -and 88 | $_.Subject -match "Buggy Error" } | ForEach-Object{ 89 | Move-OutlookItem -item $_ -TargetFolder $BugFolder -MarkRead $True -Verbose 90 | } 91 | 92 | -------------------------------------------------------------------------------- /2023-04 PSHSummit/LightingDemo/Outlook-FreeTime.ps1: -------------------------------------------------------------------------------- 1 | $Date = [datetime]::Today 2 | 3 | # Add outlook com objects 4 | Add-Type -Assembly "$($env:ProgramFiles)\Microsoft Office\root\Office16\ADDINS\Microsoft Power Query for Excel Integrated\bin\Microsoft.Office.Interop.Outlook.dll" 5 | $outlookApp = New-Object -comobject Outlook.Application 6 | $mapiNamespace = $outlookApp.GetNameSpace("MAPI") 7 | 8 | 9 | 10 | # Get the calendar 11 | $CalendarFolder = $mapiNamespace.GetDefaultFolder([Microsoft.Office.Interop.Outlook.OlDefaultFolders]::olFolderCalendar) 12 | 13 | 14 | 15 | # Use the calendar export 16 | $calShare = $CalendarFolder.GetCalendarExporter() 17 | $calShare.StartDate = $Date.Date 18 | $calShare.EndDate = $Date.Date 19 | $calShare.CalendarDetail = [Microsoft.Office.Interop.Outlook.OlCalendarDetail]::olFreeBusyAndSubject 20 | 21 | 22 | $calShare | Get-Member -MemberType Method 23 | 24 | 25 | 26 | # Generate the export email 27 | $mail = $calShare.ForwardAsICal([Microsoft.Office.Interop.Outlook.OlCalendarMailFormat]::olCalendarMailFormatDailySchedule) 28 | $mail.body 29 | 30 | 31 | 32 | # Extract the free times from the email body 33 | $mail.Body.Split("`n").Trim() | Where-Object{ $_ -match '\tFree$' } 34 | 35 | $freeTimeStrings = $mail.Body.Split("`n").Trim() | Where-Object{ $_ -match '\tFree$' -and 36 | $_ -notmatch '^Before' -and $_ -notmatch '^After' } 37 | 38 | 39 | 40 | # Parse free time and write to screen 41 | $freeTimeStrings | ForEach-Object{ 42 | Write-Host "`t• $($_.Split("`t")[0].Trim())" -ForegroundColor Yellow 43 | } 44 | 45 | 46 | 47 | # Parse free time, convert to datetime, and get duration 48 | $freeTimeStrings | ForEach-Object{ 49 | $times = $_.Split("`t")[0].Split('–').Trim() 50 | $startTime = Get-Date $times[0] 51 | $endTime = Get-Date $times[1] 52 | $timeSpan = New-TimeSpan -Start $startTime -End $endTime 53 | [PSCustomObject]@{ 54 | Time = $startTime.ToString('t') 55 | Minutes = $timeSpan.TotalMinutes 56 | } 57 | } 58 | 59 | 60 | 61 | # Account for Time Zones 62 | Import-Module PSDates 63 | $ToTimeZone = Find-TimeZone | Out-GridView -PassThru 64 | 65 | 66 | $freeTimeStrings | ForEach-Object{ 67 | $times = $_.Split("`t")[0].Split('–').Trim() 68 | $startTime = Get-Date $times[0] 69 | $endTime = Get-Date $times[1] 70 | $timeSpan = New-TimeSpan -Start $startTime -End $endTime 71 | $timeZoneConversion = Convert-TimeZone -ToTimeZone $ToTimeZone.Id -Date $startTime 72 | [PSCustomObject]@{ 73 | 'My Time Zone' = $timeZoneConversion.FromDateTime.ToString('t') 74 | 'Your Time Zone' = $timeZoneConversion.ToDateTime.ToString('t') 75 | Minutes = $timeSpan.TotalMinutes 76 | } 77 | } 78 | 79 | -------------------------------------------------------------------------------- /2023-04 PSHSummit/Managing Hybrid Infrastructure/AzRemoteCommand/ARM/Get-ArcScriptWrapper.ps1: -------------------------------------------------------------------------------- 1 | Function Get-ArcScriptWrapper { 2 | param( 3 | $ScriptContent, 4 | $OutputBlobUri, 5 | $ErrorBlobUri 6 | ) 7 | $ArcScriptWrapper = @' 8 | # Include the Write-StringToBlob so that data is written to the blob 9 | Function Write-StringToBlob {{ {0} }} 10 | 11 | # Set blob URIs 12 | $OutputBlobUri = '{1}' 13 | $ErrorBlobUri = '{2}' 14 | 15 | # Create script as script block 16 | $ScriptBlock = {{ 17 | {3} 18 | }} 19 | $termError = 'no errors' 20 | # Invoke the script block and write the return information and errors to the blob 21 | try {{ 22 | $cmdOutput = Invoke-Command -ScriptBlock $ScriptBlock 23 | }} 24 | catch {{ 25 | $termError = $_ 26 | }} 27 | finally {{ 28 | Write-StringtoBlob -BlobUri $OutputBlobUri -Content $cmdOutput 29 | Write-StringtoBlob -BlobUri $ErrorBlobUri -Content $termError 30 | Write-Output -InputObject "OutputBlobUri : $($OutputBlobUri.Split('?')[0])" 31 | Write-Output -InputObject "ErrorBlobUri : $($ErrorBlobUri.Split('?')[0])" 32 | Write-Output -InputObject $cmdOutput 33 | }} 34 | '@ 35 | 36 | $blobFunction = Get-Command -Name 'Write-StringToBlob' 37 | 38 | 39 | $ArcScriptWrapper -f $blobFunction.Definition, $OutputBlobUri, $ErrorBlobUri, $ScriptContent 40 | } -------------------------------------------------------------------------------- /2023-04 PSHSummit/Managing Hybrid Infrastructure/AzRemoteCommand/ARM/Get-ArcScriptWrapperArm.ps1: -------------------------------------------------------------------------------- 1 | Function Get-ArcScriptWrapperArm { 2 | <# 3 | .SYNOPSIS 4 | Arc based execution script wrapper 5 | 6 | .DESCRIPTION 7 | Arc based execution script wrapper to write the output and error streams 8 | 9 | .PARAMETER ScriptContent 10 | The content of the script 11 | 12 | .EXAMPLE 13 | An example 14 | 15 | .NOTES 16 | General notes 17 | #> 18 | [CmdletBinding()] 19 | param( 20 | [parameter(Mandatory = $true)] 21 | $ScriptContent 22 | ) 23 | $ArcScriptWrapper = @' 24 | param( 25 | [parameter(Mandatory = $false)] 26 | $OutputBlobUri, 27 | 28 | [parameter(Mandatory = $false)] 29 | $ErrorBlobUri 30 | ) 31 | # Include the Write-StringToBlob so that data is written to the blob 32 | Function Write-StringToBlob {{ {0} }} 33 | 34 | # Create script as script block 35 | $ScriptBlock = {{ 36 | {1} 37 | }} 38 | 39 | 40 | $termError = 'no errors' 41 | # Invoke the script block and write the return information and errors to the blob 42 | try {{ 43 | $cmdOutput = Invoke-Command -ScriptBlock $ScriptBlock 44 | }} 45 | catch {{ 46 | $termError = $_ 47 | }} 48 | finally {{ 49 | Write-StringtoBlob -BlobUri $OutputBlobUri -Content $cmdOutput 50 | Write-StringtoBlob -BlobUri $ErrorBlobUri -Content $termError 51 | Write-Output -InputObject "OutputBlobUri : $($OutputBlobUri.Split('?')[0])" 52 | Write-Output -InputObject "ErrorBlobUri : $($ErrorBlobUri.Split('?')[0])" 53 | Write-Output -InputObject $cmdOutput 54 | }} 55 | '@ 56 | 57 | $blobFunction = Get-Command -Name 'Write-StringToBlob' 58 | 59 | 60 | $ArcScriptWrapper -f $blobFunction.Definition, $ScriptContent 61 | } 62 | 63 | -------------------------------------------------------------------------------- /2023-04 PSHSummit/Managing Hybrid Infrastructure/AzRemoteCommand/ARM/Invoke-ArcCommandArm.ps1: -------------------------------------------------------------------------------- 1 | Function Invoke-ArcCommandArm { 2 | <# 3 | .SYNOPSIS 4 | Invoke a PowerShell script on any Arc based machines 5 | 6 | .DESCRIPTION 7 | Invoke a PowerShell script on any remote machine running the Arc Agent 8 | 9 | .PARAMETER ResourceGroupName 10 | The resource group name 11 | 12 | .PARAMETER Name 13 | The machine name 14 | 15 | .PARAMETER ScriptContent 16 | The content of the script 17 | 18 | .PARAMETER RunCommandName 19 | The name of the command 20 | 21 | .PARAMETER OutputBlobUri 22 | The URI to store the script's output stream 23 | 24 | .PARAMETER ErrorBlobUri 25 | The URI to store the script's error stream 26 | 27 | .EXAMPLE 28 | An example 29 | 30 | .NOTES 31 | General notes 32 | #> 33 | [cmdletbinding()] 34 | param( 35 | [parameter(Mandatory = $true)] 36 | [string]$ResourceGroupName, 37 | 38 | [parameter(Mandatory = $true)] 39 | [string]$Name, 40 | 41 | [parameter(Mandatory = $true)] 42 | [string]$RunCommandName, 43 | 44 | [parameter(Mandatory = $true)] 45 | [string]$ScriptContent, 46 | 47 | [parameter(Mandatory = $true)] 48 | [string]$OutputBlobUri, 49 | 50 | [parameter(Mandatory = $true)] 51 | [string]$ErrorBlobUri 52 | ) 53 | 54 | # Get Arc Server 55 | $ArcSrv = Get-AzConnectedMachine -ResourceGroupName $ResourceGroupName -Name $Name 56 | 57 | # Create the script wrapper 58 | $ArcScript = Get-ArcScriptWrapper -ScriptContent $ScriptContent 59 | 60 | $ScriptContentUri = $OutputBlobUri.Replace('output.txt', 'arcscript.ps1') 61 | 62 | Write-StringToBlob -BlobUri $ScriptContentUri -Content $ArcScript | Out-Null 63 | 64 | # Create Run Command on the Arc Server 65 | $TemplateObject = @{ 66 | "`$schema" = "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#" 67 | "contentVersion" = "1.0.0.0" 68 | "resources" = @( 69 | @{ 70 | "type" = "Microsoft.HybridCompute/machines/extensions" 71 | "apiVersion" = "2021-05-20" 72 | "name" = "$($Name)/CustomScriptExtension" 73 | "location" = "eastus" 74 | "properties" = @{ 75 | "publisher" = "Microsoft.Compute" 76 | "type" = "CustomScriptExtension" 77 | "autoUpgradeMinorVersion" = $true 78 | "protectedSettings" = @{ 79 | "commandToExecute" = "pwsh -ExecutionPolicy Unrestricted -File arcscript.ps1" 80 | "fileUris" = @( 81 | $ScriptContentUri 82 | ) 83 | } 84 | } 85 | } 86 | ) 87 | } 88 | 89 | $ArcCmd = New-AzResourceGroupDeployment -Name "$($RunCommandName)-$($Name)" -ResourceGroupName $ResourceGroupName -TemplateObject $TemplateObject -AsJob 90 | 91 | do { 92 | $ext = Get-AzConnectedMachineExtension -ResourceGroupName $ResourceGroupName -MachineName $Name | 93 | Where-Object { $_.InstanceViewType -eq 'CustomScriptExtension' } 94 | } while ($ext.ProvisioningState -notin 'Updating', 'Creating', 'Waiting' -and $ArcCmd -notin 'Completed', 'Failed') 95 | 96 | [pscustomobject]@{ 97 | ResourceId = $ArcSrv.Id 98 | ResourceGroupName = $ResourceGroupName 99 | Name = $ArcSrv.Name 100 | CommandName = $ext.Name 101 | State = $ext.ProvisioningState 102 | OutputBlobUri = $($OutputBlobUri.Split('?')[0]) 103 | ErrorBlobUri = $($ErrorBlobUri.Split('?')[0]) 104 | Type = 'Arc' 105 | } 106 | } -------------------------------------------------------------------------------- /2023-04 PSHSummit/Managing Hybrid Infrastructure/AzRemoteCommand/AzRemoteCommand.psd1: -------------------------------------------------------------------------------- 1 | # 2 | # Module manifest for module 'AzRemoteCommand' 3 | # 4 | # Generated by: Matthew Dowst 5 | # 6 | # Generated on: 4/23/2023 7 | # 8 | 9 | @{ 10 | 11 | # Script module or binary module file associated with this manifest. 12 | RootModule = '.\AzRemoteCommand.psm1' 13 | 14 | # Version number of this module. 15 | ModuleVersion = '1.0.0.0' 16 | 17 | # Supported PSEditions 18 | # CompatiblePSEditions = @() 19 | 20 | # ID used to uniquely identify this module 21 | GUID = 'b78e259f-c3aa-4eb8-abee-e8b0611afbd5' 22 | 23 | # Author of this module 24 | Author = 'Matthew Dowst' 25 | 26 | # Company or vendor of this module 27 | CompanyName = 'Unknown' 28 | 29 | # Copyright statement for this module 30 | Copyright = '(c) Matthew Dowst. All rights reserved.' 31 | 32 | # Description of the functionality provided by this module 33 | # Description = '' 34 | 35 | # Minimum version of the PowerShell engine required by this module 36 | PowerShellVersion = '7.0' 37 | 38 | # Name of the PowerShell host required by this module 39 | # PowerShellHostName = '' 40 | 41 | # Minimum version of the PowerShell host required by this module 42 | # PowerShellHostVersion = '' 43 | 44 | # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 45 | # DotNetFrameworkVersion = '' 46 | 47 | # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 48 | # ClrVersion = '' 49 | 50 | # Processor architecture (None, X86, Amd64) required by this module 51 | # ProcessorArchitecture = '' 52 | 53 | # Modules that must be imported into the global environment prior to importing this module 54 | # RequiredModules = @() 55 | 56 | # Assemblies that must be loaded prior to importing this module 57 | # RequiredAssemblies = @() 58 | 59 | # Script files (.ps1) that are run in the caller's environment prior to importing this module. 60 | # ScriptsToProcess = @() 61 | 62 | # Type files (.ps1xml) to be loaded when importing this module 63 | # TypesToProcess = @() 64 | 65 | # Format files (.ps1xml) to be loaded when importing this module 66 | # FormatsToProcess = @() 67 | 68 | # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess 69 | # NestedModules = @() 70 | 71 | # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. 72 | FunctionsToExport = '*' 73 | 74 | # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. 75 | CmdletsToExport = '*' 76 | 77 | # Variables to export from this module 78 | VariablesToExport = '*' 79 | 80 | # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. 81 | AliasesToExport = '*' 82 | 83 | # DSC resources to export from this module 84 | # DscResourcesToExport = @() 85 | 86 | # List of all modules packaged with this module 87 | # ModuleList = @() 88 | 89 | # List of all files packaged with this module 90 | # FileList = @() 91 | 92 | # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. 93 | PrivateData = @{ 94 | 95 | PSData = @{ 96 | 97 | # Tags applied to this module. These help with module discovery in online galleries. 98 | # Tags = @() 99 | 100 | # A URL to the license for this module. 101 | # LicenseUri = '' 102 | 103 | # A URL to the main website for this project. 104 | # ProjectUri = '' 105 | 106 | # A URL to an icon representing this module. 107 | # IconUri = '' 108 | 109 | # ReleaseNotes of this module 110 | # ReleaseNotes = '' 111 | 112 | # Prerelease string of this module 113 | # Prerelease = '' 114 | 115 | # Flag to indicate whether the module requires explicit user acceptance for install/update/save 116 | # RequireLicenseAcceptance = $false 117 | 118 | # External dependent modules of this module 119 | # ExternalModuleDependencies = @() 120 | 121 | } # End of PSData hashtable 122 | 123 | } # End of PrivateData hashtable 124 | 125 | # HelpInfo URI of this module 126 | # HelpInfoURI = '' 127 | 128 | # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. 129 | # DefaultCommandPrefix = '' 130 | 131 | } 132 | 133 | -------------------------------------------------------------------------------- /2023-04 PSHSummit/Managing Hybrid Infrastructure/AzRemoteCommand/AzRemoteCommand.psm1: -------------------------------------------------------------------------------- 1 | [System.Collections.Generic.List[PSObject]]$RequiredModules = @() 2 | # Create an object for each module to check 3 | $RequiredModules.Add([pscustomobject]@{ 4 | Name = 'Az.Compute' 5 | Version = '5.7.0' 6 | }) 7 | $RequiredModules.Add([pscustomobject]@{ 8 | Name = 'Az.ConnectedMachine' 9 | Version = '0.4.0' 10 | }) 11 | $RequiredModules.Add([pscustomobject]@{ 12 | Name = 'Az.Storage' 13 | Version = '5.5.0' 14 | }) 15 | 16 | 17 | # Loop through each module to check 18 | foreach($module in $RequiredModules){ 19 | # Check if whether the module is installed on the local machine 20 | $Check = Get-Module $module.Name -ListAvailable 21 | 22 | # If not found, throws a terminating error to stop this module from loading 23 | if(-not $check){ 24 | throw "Module $($module.Name) not found" 25 | } 26 | 27 | # If it is found, checks the version 28 | $VersionCheck = $Check | 29 | Where-Object{ $_.Version -ge $module.Version } 30 | 31 | # If an older version is found, writes an error but does not stop 32 | if(-not $VersionCheck){ 33 | Write-Error "Module $($module.Name) running older version" 34 | } 35 | 36 | # Imports the module into the current session 37 | Import-Module -Name $module.Name 38 | } 39 | 40 | 41 | $Path = Join-Path $PSScriptRoot 'Public' 42 | $Functions = Get-ChildItem -Path $Path -Filter '*.ps1' 43 | 44 | Foreach ($import in $Functions) { 45 | Try { 46 | Write-Verbose "dot-sourcing file '$($import.fullname)'" 47 | . $import.fullname 48 | } 49 | Catch { 50 | Write-Error -Message "Failed to import function $($import.name)" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /2023-04 PSHSummit/Managing Hybrid Infrastructure/AzRemoteCommand/Public/Get-ArcScriptStatus.ps1: -------------------------------------------------------------------------------- 1 | Function Get-ArcScriptStatus { 2 | <# 3 | .SYNOPSIS 4 | Get the status of a remote command execution from an Arc server 5 | 6 | .DESCRIPTION 7 | Get the status of a remote command execution from an Arc server 8 | 9 | .PARAMETER ResourceGroupName 10 | The resource group name 11 | 12 | .PARAMETER Name 13 | The machine name 14 | 15 | .PARAMETER RunCommandName 16 | The name of the command 17 | 18 | .EXAMPLE 19 | An example 20 | 21 | .NOTES 22 | General notes 23 | #> 24 | [CmdletBinding()] 25 | param( 26 | [parameter(Mandatory = $true)] 27 | [string]$ResourceGroupName, 28 | 29 | [parameter(Mandatory = $true)] 30 | [string]$Name, 31 | 32 | [parameter(Mandatory = $false)] 33 | [string]$RunCommandName = 'CustomScriptExtension' 34 | ) 35 | 36 | 37 | $AzConnectedMachineExtension = @{ 38 | Name = $RunCommandName 39 | ResourceGroupName = $ResourceGroupName 40 | MachineName = $Name 41 | } 42 | $ArcCmd = Get-AzConnectedMachineExtension @AzConnectedMachineExtension 43 | $ArcCmd.ProvisioningState 44 | } -------------------------------------------------------------------------------- /2023-04 PSHSummit/Managing Hybrid Infrastructure/AzRemoteCommand/Public/Get-ArcScriptWrapper.ps1: -------------------------------------------------------------------------------- 1 | Function Get-ArcScriptWrapper { 2 | param( 3 | $ScriptContentUri, 4 | $OutputBlobUri, 5 | $ErrorBlobUri 6 | ) 7 | $ArcScriptWrapper = @' 8 | # Include the Write-StringToBlob so that data is written to the blob 9 | Function Write-StringToBlob {{ {0} }} 10 | 11 | # Set blob URIs 12 | $ScriptContent = Invoke-RestMethod -Uri '{3}' 13 | $OutputBlobUri = '{1}' 14 | $ErrorBlobUri = '{2}' 15 | 16 | # Create script as script block 17 | $ScriptBlock = [Scriptblock]::Create($ScriptContent) 18 | 19 | $termError = 'no errors' 20 | # Invoke the script block and write the return information and errors to the blob 21 | try {{ 22 | $cmdOutput = Invoke-Command -ScriptBlock $ScriptBlock 23 | }} 24 | catch {{ 25 | $termError = $_ 26 | }} 27 | finally {{ 28 | Write-StringtoBlob -BlobUri $OutputBlobUri -Content $cmdOutput 29 | Write-StringtoBlob -BlobUri $ErrorBlobUri -Content $termError 30 | Write-Output -InputObject "OutputBlobUri : $($OutputBlobUri.Split('?')[0])" 31 | Write-Output -InputObject "ErrorBlobUri : $($ErrorBlobUri.Split('?')[0])" 32 | Write-Output -InputObject $cmdOutput 33 | }} 34 | '@ 35 | 36 | $blobFunction = Get-Command -Name 'Write-StringToBlob' 37 | 38 | 39 | $ArcScriptWrapper -f $blobFunction.Definition, $OutputBlobUri, $ErrorBlobUri, $ScriptContentUri 40 | } -------------------------------------------------------------------------------- /2023-04 PSHSummit/Managing Hybrid Infrastructure/AzRemoteCommand/Public/Get-AzRemoteCommandOutput.ps1: -------------------------------------------------------------------------------- 1 | Function Get-AzRemoteCommandOutput { 2 | <# 3 | .SYNOPSIS 4 | Get the output of a remote command execution from an Arc server or an Azure VM 5 | 6 | .DESCRIPTION 7 | Get the status of a remote command execution from an Arc server or an Azure VM 8 | 9 | .PARAMETER ResourceGroupName 10 | The resource group name 11 | 12 | .PARAMETER Name 13 | The machine name 14 | 15 | .PARAMETER RunCommandName 16 | The name of the command 17 | 18 | .PARAMETER Type 19 | Arc or AzVM 20 | 21 | .EXAMPLE 22 | An example 23 | 24 | .NOTES 25 | General notes 26 | #> 27 | [CmdletBinding()] 28 | param( 29 | [parameter(Mandatory = $false)] 30 | [string]$ResourceGroupName = '*', 31 | 32 | [parameter(Mandatory = $false)] 33 | [string]$Name = '*', 34 | 35 | [parameter(Mandatory = $true)] 36 | [string]$RunCommandName, 37 | 38 | [parameter(Mandatory = $true)] 39 | [string]$Container, 40 | 41 | [parameter(Mandatory = $true)] 42 | [Microsoft.WindowsAzure.Commands.Storage.AzureStorageContext]$StorageContext 43 | ) 44 | 45 | $Blobs = Get-AzStorageBlob -Container $container -Blob "$($RunCommandName)/*$($ResourceGroupName)/$($Name)/*.txt" -Context $StorageContext 46 | 47 | # Group them on the machine 48 | $BlobGroups = $Blobs | Group-Object -Property { Split-Path $_.Name } 49 | 50 | # Parse through them and get your data 51 | [Collections.Generic.List[PSObject]] $results = @() 52 | foreach ($run in $BlobGroups) { 53 | $data = $run.Name.Split('\') 54 | $results.Add([pscustomobject]@{ 55 | RunCommand = $data[0] 56 | Subscription = $data[1] 57 | ResourceGroup = $data[2] 58 | Computer = $data[3] 59 | Errors = $null 60 | Output = $null 61 | }) 62 | $run.Group | Where-Object { $_.Name -match 'output.txt' } | ForEach-Object { 63 | $results[-1].Output = $_.ICloudBlob.DownloadText() | ConvertFrom-Json 64 | } 65 | $run.Group | Where-Object { $_.Name -match 'error.txt' } | ForEach-Object { 66 | $results[-1].Errors = $_.ICloudBlob.DownloadText() 67 | } 68 | } 69 | 70 | $results 71 | } -------------------------------------------------------------------------------- /2023-04 PSHSummit/Managing Hybrid Infrastructure/AzRemoteCommand/Public/Get-AzRemoteCommandStatus.ps1: -------------------------------------------------------------------------------- 1 | Function Get-AzRemoteCommandStatus{ 2 | <# 3 | .SYNOPSIS 4 | Get the status of a remote command execution from an Arc server or an Azure VM 5 | 6 | .DESCRIPTION 7 | Get the status of a remote command execution from an Arc server or an Azure VM 8 | 9 | .PARAMETER ResourceGroupName 10 | The resource group name 11 | 12 | .PARAMETER Name 13 | The machine name 14 | 15 | .PARAMETER RunCommandName 16 | The name of the command 17 | 18 | .PARAMETER Type 19 | Arc or AzVM 20 | 21 | .EXAMPLE 22 | An example 23 | 24 | .NOTES 25 | General notes 26 | #> 27 | [CmdletBinding()] 28 | param( 29 | [parameter(Mandatory = $true)] 30 | [string]$ResourceGroupName, 31 | 32 | [parameter(Mandatory = $true)] 33 | [string]$Name, 34 | 35 | [parameter(Mandatory = $true)] 36 | [string]$RunCommandName, 37 | 38 | [parameter(Mandatory = $true)] 39 | [ValidateSet("Arc","VM")] 40 | [string]$Type 41 | ) 42 | 43 | $CommandParameters = @{ 44 | ResourceGroupName = $ResourceGroupName 45 | Name = $Name 46 | RunCommandName = $RunCommandName 47 | } 48 | 49 | # Invoke the command based on the device type 50 | if ($Type -eq 'VM') { 51 | Write-Verbose "$($Name) is an Azure VM" 52 | $Status = Get-VmScriptStatus @CommandParameters 53 | } 54 | else { 55 | Write-Verbose "$($Name) is an Arc Server" 56 | $Status = Get-ArcScriptStatus @CommandParameters 57 | } 58 | $Status 59 | } -------------------------------------------------------------------------------- /2023-04 PSHSummit/Managing Hybrid Infrastructure/AzRemoteCommand/Public/Get-VmScriptStatus.ps1: -------------------------------------------------------------------------------- 1 | Function Get-VmScriptStatus { 2 | <# 3 | .SYNOPSIS 4 | Get the status of a remote command execution from an Azure VM 5 | 6 | .DESCRIPTION 7 | Get the status of a remote command execution from an Azure VM 8 | 9 | .PARAMETER ResourceGroupName 10 | The resource group name 11 | 12 | .PARAMETER Name 13 | The machine name 14 | 15 | .PARAMETER RunCommandName 16 | The name of the command 17 | 18 | .EXAMPLE 19 | An example 20 | 21 | .NOTES 22 | General notes 23 | #> 24 | [CmdletBinding()] 25 | param( 26 | [parameter(Mandatory = $true)] 27 | [string]$ResourceGroupName, 28 | 29 | [parameter(Mandatory = $true)] 30 | [string]$Name, 31 | 32 | [parameter(Mandatory = $true)] 33 | [string]$RunCommandName 34 | ) 35 | 36 | $VM = Get-AzVM -ResourceGroupName $ResourceGroupName -Name $Name 37 | 38 | $rest = Invoke-AzRestMethod -Path "$($VM.Id)/runCommands/$($RunCommandName)?`$expand=instanceView&api-version=2022-11-01" -Method GET 39 | ($rest.Content | ConvertFrom-Json).Properties.provisioningState 40 | } -------------------------------------------------------------------------------- /2023-04 PSHSummit/Managing Hybrid Infrastructure/AzRemoteCommand/Public/Invoke-ArcCommand.ps1: -------------------------------------------------------------------------------- 1 | Function Invoke-ArcCommand { 2 | <# 3 | .SYNOPSIS 4 | Invoke a PowerShell script on any Arc based machines 5 | 6 | .DESCRIPTION 7 | Invoke a PowerShell script on any remote machine running the Arc Agent 8 | 9 | .PARAMETER ResourceGroupName 10 | The resource group name 11 | 12 | .PARAMETER Name 13 | The machine name 14 | 15 | .PARAMETER ScriptContent 16 | The content of the script 17 | 18 | .PARAMETER RunCommandName 19 | The name of the command 20 | 21 | .PARAMETER OutputBlobUri 22 | The URI to store the script's output stream 23 | 24 | .PARAMETER ErrorBlobUri 25 | The URI to store the script's error stream 26 | 27 | .EXAMPLE 28 | An example 29 | 30 | .NOTES 31 | General notes 32 | #> 33 | [cmdletbinding()] 34 | param( 35 | [parameter(Mandatory = $true)] 36 | [string]$ResourceGroupName, 37 | 38 | [parameter(Mandatory = $true)] 39 | [string]$Name, 40 | 41 | [parameter(Mandatory = $true)] 42 | [string]$RunCommandName, 43 | 44 | [parameter(Mandatory = $true)] 45 | [string]$ScriptContentUri, 46 | 47 | [parameter(Mandatory = $true)] 48 | [string]$OutputBlobUri, 49 | 50 | [parameter(Mandatory = $true)] 51 | [string]$ErrorBlobUri 52 | ) 53 | 54 | # Get Arc Server 55 | $ArcSrv = Get-AzConnectedMachine -ResourceGroupName $ResourceGroupName -Name $Name 56 | 57 | # Create the script wrapper 58 | $ArcScript = Get-ArcScriptWrapper -ScriptContentUri $ScriptContentUri -OutputBlobUri $OutputBlobUri -ErrorBlobUri $ErrorBlobUri 59 | 60 | # Encode the script in base64 61 | $encodedcommand = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($ArcScript)) 62 | 63 | # Create Run Command on the Arc Server 64 | $body = @{ 65 | "location" = $ArcSrv.Location 66 | "properties" = @{ 67 | "publisher" = "Microsoft.Compute" 68 | "typeHandlerVersion" = "1.10" 69 | "type" = "CustomScriptExtension" 70 | "forceUpdateTag" = $RunCommandName 71 | "settings" = @{ 72 | "commandToExecute" = "powershell.exe -EncodedCommand $EncodedCommand" 73 | } 74 | } 75 | } 76 | 77 | $URI = "https://management.azure.com$($ArcSrv.Id)/extensions/CustomScriptExtension?api-version=2021-05-20" 78 | $submit = Invoke-AzRestMethod -Uri $URI -Method 'Put' -Payload ($body | ConvertTo-Json) 79 | 80 | $timer = [system.diagnostics.stopwatch]::StartNew() 81 | do { 82 | $ext = Get-AzConnectedMachineExtension -ResourceGroupName $ResourceGroupName -MachineName $Name | 83 | Where-Object { $_.InstanceViewType -eq 'CustomScriptExtension' } 84 | } while ($ext.ProvisioningState -notin 'Updating', 'Creating', 'Waiting' -and $timer.Elapsed.TotalSeconds -le 60) 85 | $timer.Stop() 86 | 87 | if ($submit.StatusCode -ne 202) { 88 | Write-Error $submit.Content 89 | } 90 | elseif ($timer.Elapsed.TotalSeconds -gt 30) { 91 | Write-Error "Failed to start the provisioning - $($ext.ProvisioningState)" 92 | } 93 | else { 94 | [pscustomobject]@{ 95 | ResourceId = $ArcSrv.Id 96 | ResourceGroupName = $ResourceGroupName 97 | Name = $ArcSrv.Name 98 | CommandName = $ext.Name 99 | State = $ext.ProvisioningState 100 | OutputBlobUri = $($OutputBlobUri.Split('?')[0]) 101 | ErrorBlobUri = $($ErrorBlobUri.Split('?')[0]) 102 | Type = 'Arc' 103 | } 104 | } 105 | } -------------------------------------------------------------------------------- /2023-04 PSHSummit/Managing Hybrid Infrastructure/AzRemoteCommand/Public/Invoke-AzRemoteCommand.ps1: -------------------------------------------------------------------------------- 1 | Function Invoke-AzRemoteCommand { 2 | <# 3 | .SYNOPSIS 4 | Invoke a PowerShell script on any remote machine Azure VM or Arc Agent 5 | 6 | .DESCRIPTION 7 | Invoke a PowerShell script on any remote machine Azure VM or Arc Agent 8 | 9 | .PARAMETER ResourceId 10 | The Azure VM or Arc Server resource id 11 | 12 | .PARAMETER ScriptContent 13 | The script to execute on the remote machine 14 | 15 | .PARAMETER RunCommandName 16 | The name of the command to run 17 | 18 | .PARAMETER SasToken 19 | The SAS Token to store the output and error streams too 20 | 21 | .PARAMETER 22 | THe container for storing the output files 23 | 24 | .PARAMETER StorageContext 25 | The storage context for the SAS Token 26 | 27 | .EXAMPLE 28 | An example 29 | 30 | .NOTES 31 | General notes 32 | #> 33 | [CmdletBinding()] 34 | param( 35 | [parameter(Mandatory = $true)] 36 | [string]$ResourceId, 37 | 38 | [parameter(Mandatory = $true)] 39 | [string]$ScriptContent, 40 | 41 | [parameter(Mandatory = $true)] 42 | [string]$RunCommandName, 43 | 44 | [parameter(Mandatory = $true)] 45 | [string]$SasToken, 46 | 47 | [parameter(Mandatory = $true)] 48 | [string]$Container, 49 | 50 | [parameter(Mandatory = $true)] 51 | [Microsoft.WindowsAzure.Commands.Storage.AzureStorageContext]$StorageContext 52 | ) 53 | 54 | # Create Run Command on VM 55 | $SplitId = $ResourceId.Split('/', [System.StringSplitOptions]::RemoveEmptyEntries) 56 | $OutputBlobUri = "$($StorageContext.BlobEndPoint)$($container)/$RunCommandName/$($SplitId[1])/$($SplitId[3])/$($SplitId[-1])/output.txt$($SasToken)" 57 | $ErrorBlobUri = "$($StorageContext.BlobEndPoint)$($container)/$RunCommandName/$($SplitId[1])/$($SplitId[3])/$($SplitId[-1])/error.txt$($SasToken)" 58 | 59 | # Set the parameters 60 | $CommandParameters = @{ 61 | ResourceGroupName = $SplitId[3] 62 | Name = $SplitId[-1] 63 | RunCommandName = $RunCommandName 64 | OutputBlobUri = $OutputBlobUri 65 | ErrorBlobUri = $ErrorBlobUri 66 | } 67 | 68 | # Invoke the command based on the device type 69 | if ($ResourceId -match 'Microsoft\.Compute/virtualMachines') { 70 | Write-Verbose "$($SplitId[-1]) is an Azure VM" 71 | $RunCommand = Invoke-VmCommand @CommandParameters -ScriptContent $ScriptContent 72 | } 73 | else { 74 | Write-Verbose "$($SplitId[-1]) is an Arc Server" 75 | # Create the script URI 76 | $ScriptContentUri = "$($StorageContext.BlobEndPoint)$($container)/$RunCommandName/$($ArcSrvId[1])/$($ArcSrvId[3])/$($ArcSrvId[-1])/Get-SystemInfo.txt$($SasToken)" 77 | Write-StringToBlob -BlobUri $ScriptContentUri -Content $ScriptContent | Out-Null 78 | 79 | $RunCommand = Invoke-ArcCommand @CommandParameters -ScriptContentUri $ScriptContentUri 80 | } 81 | 82 | $RunCommand 83 | } -------------------------------------------------------------------------------- /2023-04 PSHSummit/Managing Hybrid Infrastructure/AzRemoteCommand/Public/Invoke-VmCommand.ps1: -------------------------------------------------------------------------------- 1 | Function Invoke-VmCommand { 2 | <# 3 | .SYNOPSIS 4 | Invoke a PowerShell script on any Azure VM 5 | 6 | .DESCRIPTION 7 | Invoke a PowerShell script on any remote machine Azure VM or Arc Agent 8 | 9 | .PARAMETER ResourceGroupName 10 | The resource group name 11 | 12 | .PARAMETER Name 13 | The machine name 14 | 15 | .PARAMETER ScriptContent 16 | The content of the script 17 | 18 | .PARAMETER RunCommandName 19 | The name of the command 20 | 21 | .PARAMETER OutputBlobUri 22 | The URI to store the script's output stream 23 | 24 | .PARAMETER ErrorBlobUri 25 | The URI to store the script's error stream 26 | 27 | .EXAMPLE 28 | An example 29 | 30 | .NOTES 31 | General notes 32 | #> 33 | [CmdletBinding()] 34 | param( 35 | [string]$ResourceGroupName, 36 | [string]$Name, 37 | [string]$ScriptContent, 38 | [string]$RunCommandName, 39 | [string]$OutputBlobUri, 40 | [string]$ErrorBlobUri 41 | ) 42 | 43 | $VM = Get-AzVM -ResourceGroupName $ResourceGroupName -Name $Name 44 | 45 | if($vm.StorageProfile.OsDisk.OsType -eq 'Linux'){ 46 | Write-Verbose "$($SplitId[-1]) is an Linux VM" 47 | $prefix = '' 48 | } 49 | else{ 50 | Write-Verbose "$($SplitId[-1]) is an Windows VM" 51 | $prefix = '. ' 52 | } 53 | 54 | $encodedcommand = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($ScriptContent)) 55 | $AzVMRunCommand = @{ 56 | ResourceGroupName = $VM.ResourceGroupName 57 | VMName = $VM.Name 58 | RunCommandName = $RunCommandName 59 | SourceScript = "$($prefix)pwsh -EncodedCommand $EncodedCommand" 60 | Location = $VM.Location 61 | OutputBlobUri = $OutputBlobUri 62 | ErrorBlobUri = $ErrorBlobUri 63 | AsJob = $true 64 | } 65 | $SetCmd = Set-AzVMRunCommand @AzVMRunCommand 66 | 67 | [pscustomobject]@{ 68 | ResourceId = $VM.Id 69 | ResourceGroupName = $VM.ResourceGroupName 70 | Name = $VM.Name 71 | CommandName = $RunCommandName 72 | State = $SetCmd.State 73 | OutputBlobUri = $($OutputBlobUri.Split('?')[0]) 74 | ErrorBlobUri = $($ErrorBlobUri.Split('?')[0]) 75 | Type = 'VM' 76 | } 77 | } -------------------------------------------------------------------------------- /2023-04 PSHSummit/Managing Hybrid Infrastructure/AzRemoteCommand/Public/Write-StringtoBlob.ps1: -------------------------------------------------------------------------------- 1 | Function Write-StringToBlob { 2 | [cmdletbinding()] 3 | param( 4 | [string]$BlobUri, 5 | [string]$Content 6 | ) 7 | 8 | $method = "PUT"; 9 | $contentLength = [System.Text.Encoding]::UTF8.GetByteCount($Content); 10 | 11 | [System.Net.HttpWebRequest]$request = [System.Net.WebRequest]::Create($BlobUri) 12 | 13 | $now = [DateTime]::UtcNow.ToString("R"); 14 | 15 | $request.Method = $method; 16 | $request.ContentType = "text/plain; charset=UTF-8"; 17 | $request.ContentLength = $contentLength; 18 | 19 | $request.Headers.Add("x-ms-version", "2022-11-02"); 20 | $request.Headers.Add("x-ms-date", $now); 21 | $request.Headers.Add("x-ms-blob-type", "BlockBlob"); 22 | 23 | 24 | $requestStream = $request.GetRequestStream(); 25 | $requestStream.Write([System.Text.Encoding]::UTF8.GetBytes($Content), 0, $contentLength); 26 | $resp = $request.GetResponse(); 27 | $resp.StatusCode 28 | 29 | } -------------------------------------------------------------------------------- /2023-04 PSHSummit/Managing Hybrid Infrastructure/AzRemoteCommand/Resources/pwshinstall.ps1: -------------------------------------------------------------------------------- 1 | Invoke-Expression ". { $(Invoke-RestMethod https://aka.ms/install-powershell.ps1) } -UseMSI -Quiet -AddExplorerContextMenu" -------------------------------------------------------------------------------- /2023-04 PSHSummit/Managing Hybrid Infrastructure/AzRemoteCommand/Resources/pwshinstall.sh: -------------------------------------------------------------------------------- 1 | # Update the list of packages 2 | sudo apt-get update 3 | # Install pre-requisite packages. 4 | sudo apt-get install -y wget apt-transport-https software-properties-common 5 | # Download the Microsoft repository GPG keys 6 | wget -q "https://packages.microsoft.com/config/ubuntu/18.04/packages-microsoft-prod.deb" 7 | # Register the Microsoft repository GPG keys 8 | sudo dpkg -i packages-microsoft-prod.deb 9 | # Delete the the Microsoft repository GPG keys file 10 | rm packages-microsoft-prod.deb 11 | # Update the list of packages after we added packages.microsoft.com 12 | sudo apt-get update 13 | # Install PowerShell 14 | sudo apt-get install -y powershell -------------------------------------------------------------------------------- /2023-04 PSHSummit/Managing Hybrid Infrastructure/Demo/Demo01 - Run Command on VM.ps1: -------------------------------------------------------------------------------- 1 | $ScriptContent = 'Get-CimInstance -Class Win32_OperatingSystem' 2 | $RunCommandName = 'Demo01' 3 | 4 | # Get Azure VM 5 | $VM = Get-AzVM -ResourceGroupName $VmResourceGroupName -Name $VmName 6 | 7 | # Create Run Command on VM 8 | $AzVMRunCommand = @{ 9 | ResourceGroupName = $VM.ResourceGroupName 10 | VMName = $VM.Name 11 | RunCommandName = $RunCommandName 12 | SourceScript = $ScriptContent 13 | Location = $VM.Location 14 | AsJob = $true 15 | } 16 | $SetCmd = Set-AzVMRunCommand @AzVMRunCommand 17 | $SetCmd 18 | 19 | # Wait for job to finish 20 | $i = 0 21 | while($SetCmd.State -eq 'Running' -and $i -le 3){ 22 | $SetCmd 23 | Start-Sleep -Seconds 3 24 | $i++ 25 | } 26 | 27 | # Wait for the command to complete 28 | $AzVMRunCommand = @{ 29 | ResourceGroupName = $VmResourceGroupName 30 | VMName = $VmName 31 | RunCommandName = $RunCommandName 32 | Expand = 'instanceView' 33 | } 34 | do{ 35 | $cmd = Get-AzVMRunCommand @AzVMRunCommand 36 | Write-Progress -Activity "Command : $($cmd.Name)" -Status "InstanceViewStatusCode : $($cmd.InstanceView.ExecutionState)" -PercentComplete 10 -id 1 37 | Start-Sleep -Seconds 3 38 | }while($cmd.InstanceView.ExecutionState -notin 'Succeeded','Failed') 39 | Write-Progress -Activity "Done" -Id 1 -Completed 40 | 41 | # Get Results from Run Command 42 | $cmd.InstanceView | Format-List -------------------------------------------------------------------------------- /2023-04 PSHSummit/Managing Hybrid Infrastructure/Demo/Demo02 - Run Command on Arc.ps1: -------------------------------------------------------------------------------- 1 | # Get Arc Server 2 | $RunCommandName = 'Demo01' + (Get-Date).ToString('yyyyMMddHHmm') 3 | $ArcSrv = Get-AzConnectedMachine -ResourceGroupName $ArcResourceGroupName -Name $ArcNameA 4 | 5 | # Create Run Command on the Arc Server 6 | # https://learn.microsoft.com/en-us/rest/api/hybridcompute/machine-extensions/create-or-update?tabs=HTTP 7 | $body = @{ 8 | "location" = $ArcSrv.Location 9 | "properties" = @{ 10 | "publisher" = "Microsoft.Compute" 11 | "typeHandlerVersion" = "1.10" 12 | "type" = "CustomScriptExtension" 13 | "forceUpdateTag" = $RunCommandName 14 | "settings" = @{ 15 | "commandToExecute" = "pwsh.exe -Command $ScriptContent" 16 | } 17 | } 18 | } 19 | $Payload = ($body | ConvertTo-Json) 20 | $Payload 21 | 22 | # Invoke to Azure Rest API 23 | $URI = "https://management.azure.com$($ArcSrv.Id)/extensions/CustomScriptExtension?api-version=2021-05-20" 24 | $submit = Invoke-AzRestMethod -Uri $URI -Method 'Put' -Payload ($body | ConvertTo-Json) 25 | $submit 26 | 27 | 28 | # Get Results from the Command 29 | $AzConnectedMachineExtension = @{ 30 | Name = 'CustomScriptExtension' 31 | ResourceGroupName = $ArcResourceGroupName 32 | MachineName = $ArcNameA 33 | } 34 | 35 | # Wait for success 36 | do{ 37 | $ArcCmd = Get-AzConnectedMachineExtension @AzConnectedMachineExtension 38 | Write-Progress -Activity "ProvisioningState : $($ArcCmd.ProvisioningState)" -Status "InstanceViewStatusCode : $($ArcCmd.InstanceViewStatusCode)" -PercentComplete 10 -id 1 39 | Start-Sleep -Seconds 3 40 | }while($ArcCmd.ProvisioningState -notin 'Succeeded','Failed') 41 | Write-Progress -Activity "Done" -Id 1 -Completed 42 | 43 | $ArcCmd | Format-List 44 | -------------------------------------------------------------------------------- /2023-04 PSHSummit/Managing Hybrid Infrastructure/Demo/Demo03 - Output VM results to Blob.ps1: -------------------------------------------------------------------------------- 1 | # Get script, but output to JSON 2 | $ScriptContent = Get-Content -Path '.\Scripts\Get-WinSystemInfo.ps1' -Raw 3 | $RunCommandName = 'Demo02' 4 | 5 | 6 | # Create SAS Token to save Script Output 7 | $key = Get-AzStorageAccountKey -ResourceGroupName $StorageResourceGroup -Name $StorageAccount | Select-Object -First 1 8 | $StorageContext = New-AzStorageContext -StorageAccountName $StorageAccount -StorageAccountKey $key.Value 9 | $StartTime = Get-Date 10 | $EndTime = $startTime.AddDays(1) 11 | # SAS Token must have (R)ead, (A)dd, (C)reate, and (W)rite permissions 12 | $SasToken = New-AzStorageContainerSASToken -Name $container -Permission racw -StartTime $StartTime -ExpiryTime $EndTime -context $StorageContext 13 | 14 | 15 | # Get Azure VM 16 | $VM = Get-AzVM -ResourceGroupName $VMResourceGroupName -Name $VMName 17 | 18 | # Create Run Command on VM 19 | $VmId = $VM.Id.Split('/',[System.StringSplitOptions]::RemoveEmptyEntries) 20 | "`n`n$($StorageContext.BlobEndPoint)$($container)/$RunCommandName/$($VmId[1])/$($VmId[3])/$($VmId[-1])/output.txt$($SasTokenPlaceHolder)`n`n" 21 | 22 | $AzVMRunCommand = @{ 23 | ResourceGroupName = $VM.ResourceGroupName 24 | VMName = $VM.Name 25 | RunCommandName = $RunCommandName 26 | SourceScript = $ScriptContent 27 | Location = $VM.Location 28 | OutputBlobUri = "$($StorageContext.BlobEndPoint)$($container)/$RunCommandName/$($VmId[1])/$($VmId[3])/$($VmId[-1])/output.txt$($SasToken)" 29 | ErrorBlobUri = "$($StorageContext.BlobEndPoint)$($container)/$RunCommandName/$($VmId[1])/$($VmId[3])/$($VmId[-1])/error.txt$($SasToken)" 30 | AsJob = $true 31 | } 32 | $SetCmd = Set-AzVMRunCommand @AzVMRunCommand 33 | $SetCmd 34 | 35 | # Wait for command to finish 36 | $AzVMRunCommand = @{ 37 | ResourceGroupName = $VmResourceGroupName 38 | VMName = $VmName 39 | RunCommandName = $RunCommandName 40 | Expand = 'instanceView' 41 | } 42 | do{ 43 | try{ 44 | $cmd = Get-AzVMRunCommand @AzVMRunCommand -ErrorAction Stop 45 | } 46 | catch{ 47 | if($_.Exception.Message -notmatch 'ResourceNotFound'){ 48 | throw $_ 49 | } 50 | $cmd = $null 51 | } 52 | Write-Progress -Activity "Command : $($cmd.Name)" -Status "InstanceViewStatusCode : $($cmd.InstanceView.ExecutionState)" -PercentComplete 10 -id 1 53 | Start-Sleep -Seconds 3 54 | }while($cmd.InstanceView.ExecutionState -notin 'Succeeded','Failed' -or -not $cmd ) 55 | Write-Progress -Activity "Done" -Id 1 -Completed 56 | 57 | # Get return data from Blob 58 | $VMData = Invoke-RestMethod -Uri "$($StorageContext.BlobEndPoint)$($container)/$RunCommandName/$($VmId[1])/$($VmId[3])/$($VmId[-1])/output.txt$($SasToken)" 59 | $VMData -------------------------------------------------------------------------------- /2023-04 PSHSummit/Managing Hybrid Infrastructure/Demo/Demo04 - Write to Blob.ps1: -------------------------------------------------------------------------------- 1 | # Get Arc Server 2 | $ScriptFile = Get-Item -Path '.\Scripts\Get-SystemInfo.ps1' 3 | $ScriptText = $ScriptFile | Get-Content -Raw 4 | 5 | # Create URI similar to the VM URI 6 | "$($StorageContext.BlobEndPoint)$($container)/$RunCommandName/test/$($ScriptFile.Name)$($SasTokenPlaceHolder)" 7 | 8 | # Test writing to blob 9 | $ScriptContentUri = "$($StorageContext.BlobEndPoint)$($container)/$RunCommandName/test/$($ScriptFile.Name)$($SasToken)" 10 | Write-StringToBlob -BlobUri $ScriptContentUri -Content $ScriptText 11 | -------------------------------------------------------------------------------- /2023-04 PSHSummit/Managing Hybrid Infrastructure/Demo/Demo05 - Arc Script Wrapper.ps1: -------------------------------------------------------------------------------- 1 | 2 | $OutputBlobUri = "$($StorageContext.BlobEndPoint)$($container)/$RunCommandName/test/output.txt$($SasToken)" 3 | $ErrorBlobUri = "$($StorageContext.BlobEndPoint)$($container)/$RunCommandName/test/error.txt$($SasToken)" 4 | 5 | $ArcScript = Get-ArcScriptWrapper -ScriptContentUri $ScriptContentUri -OutputBlobUri $OutputBlobUri -ErrorBlobUri $ErrorBlobUri 6 | 7 | $ArcScript | Out-File '.\Demo\Demo06 - Write Command Output to Blob.ps1' 8 | 9 | 10 | -------------------------------------------------------------------------------- /2023-04 PSHSummit/Managing Hybrid Infrastructure/Demo/Demo06 - Write Command Output to Blob.ps1: -------------------------------------------------------------------------------- 1 | # Include the Write-StringToBlob so that data is written to the blob 2 | Function Write-StringToBlob { 3 | [cmdletbinding()] 4 | param( 5 | [string]$BlobUri, 6 | [string]$Content 7 | ) 8 | 9 | $method = "PUT"; 10 | $contentLength = [System.Text.Encoding]::UTF8.GetByteCount($Content); 11 | 12 | [System.Net.HttpWebRequest]$request = [System.Net.WebRequest]::Create($BlobUri) 13 | 14 | $now = [DateTime]::UtcNow.ToString("R"); 15 | 16 | $request.Method = $method; 17 | $request.ContentType = "text/plain; charset=UTF-8"; 18 | $request.ContentLength = $contentLength; 19 | 20 | $request.Headers.Add("x-ms-version", "2022-11-02"); 21 | $request.Headers.Add("x-ms-date", $now); 22 | $request.Headers.Add("x-ms-blob-type", "BlockBlob"); 23 | 24 | 25 | $requestStream = $request.GetRequestStream(); 26 | $requestStream.Write([System.Text.Encoding]::UTF8.GetBytes($Content), 0, $contentLength); 27 | $resp = $request.GetResponse(); 28 | $resp.StatusCode 29 | 30 | } 31 | 32 | # Set blob URIs 33 | $ScriptContent = Invoke-RestMethod -Uri 'https://poshvmscripts.blob.core.windows.net/vmscripts/Demo02/test/Get-SystemInfo.ps1?sv=2021-10-04&st=2023-04-25T20%3A11%3A57Z&se=2023-04-26T20%3A11%3A57Z&sr=c&sp=racw&sig=aXuETeZnHL6%2F%2B0BZCWX88eC%2BIJ93U%2BdTmTwzvOpL%2Blk%3D' 34 | $OutputBlobUri = 'https://poshvmscripts.blob.core.windows.net/vmscripts/Demo02/test/output.txt?sv=2021-10-04&st=2023-04-25T20%3A11%3A57Z&se=2023-04-26T20%3A11%3A57Z&sr=c&sp=racw&sig=aXuETeZnHL6%2F%2B0BZCWX88eC%2BIJ93U%2BdTmTwzvOpL%2Blk%3D' 35 | $ErrorBlobUri = 'https://poshvmscripts.blob.core.windows.net/vmscripts/Demo02/test/error.txt?sv=2021-10-04&st=2023-04-25T20%3A11%3A57Z&se=2023-04-26T20%3A11%3A57Z&sr=c&sp=racw&sig=aXuETeZnHL6%2F%2B0BZCWX88eC%2BIJ93U%2BdTmTwzvOpL%2Blk%3D' 36 | 37 | # Create script as script block 38 | $ScriptBlock = [Scriptblock]::Create($ScriptContent) 39 | 40 | $termError = 'no errors' 41 | # Invoke the script block and write the return information and errors to the blob 42 | try { 43 | $cmdOutput = Invoke-Command -ScriptBlock $ScriptBlock 44 | } 45 | catch { 46 | $termError = $_ 47 | } 48 | finally { 49 | Write-StringtoBlob -BlobUri $OutputBlobUri -Content $cmdOutput 50 | Write-StringtoBlob -BlobUri $ErrorBlobUri -Content $termError 51 | Write-Output -InputObject "OutputBlobUri : $($OutputBlobUri.Split('?')[0])" 52 | Write-Output -InputObject "ErrorBlobUri : $($ErrorBlobUri.Split('?')[0])" 53 | Write-Output -InputObject $cmdOutput 54 | } 55 | -------------------------------------------------------------------------------- /2023-04 PSHSummit/Managing Hybrid Infrastructure/Demo/Demo07 - Output Arc results to Blob.ps1: -------------------------------------------------------------------------------- 1 | # Get Arc Server 2 | $ArcSrv = Get-AzConnectedMachine -ResourceGroupName $ArcResourceGroupName -Name $ArcNameB 3 | 4 | # Create URI similar to the VM URI 5 | $ArcSrvId = $ArcSrv.Id.Split('/',[System.StringSplitOptions]::RemoveEmptyEntries) 6 | $OutputBlobUri = "$($StorageContext.BlobEndPoint)$($container)/$RunCommandName/$($ArcSrvId[1])/$($ArcSrvId[3])/$($ArcSrvId[-1])/output.txt$($SasToken)" 7 | $ErrorBlobUri = "$($StorageContext.BlobEndPoint)$($container)/$RunCommandName/$($ArcSrvId[1])/$($ArcSrvId[3])/$($ArcSrvId[-1])/error.txt$($SasToken)" 8 | 9 | # Create the script wrapper 10 | $ScriptContent = Get-Content -Path '.\Scripts\Get-SystemInfo.ps1' -Raw 11 | $ScriptContentUri = "$($StorageContext.BlobEndPoint)$($container)/$RunCommandName/$($ArcSrvId[1])/$($ArcSrvId[3])/$($ArcSrvId[-1])/Get-SystemInfo.txt$($SasToken)" 12 | Write-StringToBlob -BlobUri $ScriptContentUri -Content $ScriptContent 13 | $ArcScript = Get-ArcScriptWrapper -ScriptContentUri $ScriptContentUri -OutputBlobUri $OutputBlobUri -ErrorBlobUri $ErrorBlobUri 14 | 15 | # Encode the script in base64 16 | $encodedcommand = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($ArcScript)) 17 | 18 | # Create Run Command on the Arc Server 19 | $body = @{ 20 | "location" = $ArcSrv.Location 21 | "properties" = @{ 22 | "publisher" = "Microsoft.Compute" 23 | "typeHandlerVersion" = "1.10" 24 | "type" = "CustomScriptExtension" 25 | "forceUpdateTag" = $RunCommandName 26 | "settings" = @{ 27 | "commandToExecute" = "pwsh -EncodedCommand $EncodedCommand" 28 | } 29 | } 30 | } 31 | $URI = "https://management.azure.com$($ArcSrv.Id)/extensions/CustomScriptExtension?api-version=2021-05-20" 32 | $submit = Invoke-AzRestMethod -Uri $URI -Method 'Put' -Payload ($body | ConvertTo-Json) 33 | $submit 34 | 35 | 36 | # Get Results from the Command 37 | $AzConnectedMachineExtension = @{ 38 | Name = 'CustomScriptExtension' 39 | ResourceGroupName = $ArcResourceGroupName 40 | MachineName = $ArcNameB 41 | } 42 | Write-Host "Wait for update to start" 43 | do{ 44 | try{ 45 | $ArcCmd = Get-AzConnectedMachineExtension @AzConnectedMachineExtension -ErrorAction Stop 46 | } 47 | catch{ 48 | if($_.Exception.Message -notmatch 'The requested resource was not found.'){ 49 | throw $_ 50 | } 51 | } 52 | Write-Progress -Activity "ProvisioningState : $($ArcCmd.ProvisioningState)" -Status "InstanceViewStatusCode : $($ArcCmd.InstanceViewStatusCode)" -PercentComplete 10 -id 1 53 | Start-Sleep -Seconds 3 54 | }while($ArcCmd.ProvisioningState -notin 'Updating', 'Creating', 'Waiting') 55 | 56 | Write-Host "Wait for success state" 57 | while($ArcCmd.ProvisioningState -notin 'Succeeded','Failed'){ 58 | $ArcCmd = Get-AzConnectedMachineExtension @AzConnectedMachineExtension 59 | Write-Progress -Activity "ProvisioningState : $($ArcCmd.ProvisioningState)" -Status "InstanceViewStatusCode : $($ArcCmd.InstanceViewStatusCode)" -PercentComplete 10 -id 1 60 | Start-Sleep -Seconds 3 61 | } 62 | Write-Progress -Activity "Done" -Id 1 -Completed 63 | 64 | # Get return data from Blob 65 | $ArcData = Invoke-RestMethod -Uri $OutputBlobUri 66 | $ArcData -------------------------------------------------------------------------------- /2023-04 PSHSummit/Managing Hybrid Infrastructure/Demo/Demo08 - Get return data from Everything.ps1: -------------------------------------------------------------------------------- 1 | # Get all the results from a particular job run 2 | $Blobs = Get-AzStorageBlob -Container $container -Prefix $RunCommandName -Context $StorageContext 3 | $Blobs | Format-Table Name 4 | 5 | # Group them on the machine 6 | $BlobGroups = $Blobs | Group-Object -Property {Split-Path $_.Name} 7 | $BlobGroups 8 | 9 | # Parse through them and get your data 10 | [Collections.Generic.List[PSObject]] $results = @() 11 | foreach($run in $BlobGroups){ 12 | $data = $run.Name.Split('\') 13 | $results.Add([pscustomobject]@{ 14 | RunCommand = $data[0] 15 | Subscription = $data[1] 16 | ResourceGroup = $data[2] 17 | Computer = $data[3] 18 | Errors = $null 19 | Output = $null 20 | }) 21 | $run.Group | Where-Object{ $_.Name -match 'output.txt' } | ForEach-Object{ 22 | $results[-1].Output = $_.ICloudBlob.DownloadText() | ConvertFrom-Json 23 | } 24 | } 25 | 26 | $results.Output | Format-List -------------------------------------------------------------------------------- /2023-04 PSHSummit/Managing Hybrid Infrastructure/Demo/Demo09 - Functions.ps1: -------------------------------------------------------------------------------- 1 | # Get Arc Server 2 | $ResourceGroupName = 'AzureArcDev' 3 | $Name = 'Arcbox-Ubuntu-01' 4 | 5 | $RunCommandName = 'Demo09_' + (Get-Date).ToString('yyyyMMddHHmm') 6 | $ArcSrv = Get-AzConnectedMachine -ResourceGroupName $ResourceGroupName -Name $Name 7 | $ArcSrvId = $ArcSrv.Id.Split('/',[System.StringSplitOptions]::RemoveEmptyEntries) 8 | $OutputBlobUri = "$($StorageContext.BlobEndPoint)$($container)/$RunCommandName/$($ArcSrvId[1])/$($ArcSrvId[3])/$($ArcSrvId[-1])/output.txt$($SasToken)" 9 | $ErrorBlobUri = "$($StorageContext.BlobEndPoint)$($container)/$RunCommandName/$($ArcSrvId[1])/$($ArcSrvId[3])/$($ArcSrvId[-1])/error.txt$($SasToken)" 10 | $ScriptContent = Get-Content -Path '.\Scripts\Get-WinSystemInfo.ps1' -Raw 11 | $ScriptContentUri = "$($StorageContext.BlobEndPoint)$($container)/$RunCommandName/$($ArcSrvId[1])/$($ArcSrvId[3])/$($ArcSrvId[-1])/Get-SystemInfo.txt$($SasToken)" 12 | Write-StringToBlob -BlobUri $ScriptContentUri -Content $ScriptContent | Out-Null 13 | 14 | Invoke-ArcCommand -ResourceGroupName $ResourceGroupName -Name $Name -RunCommandName $RunCommandName -ScriptContentUri $ScriptContent -OutputBlobUri $OutputBlobUri -ErrorBlobUri $ErrorBlobUri 15 | 16 | 17 | Get-ArcScriptStatus -ResourceGroupName $ResourceGroupName -Name $Name 18 | 19 | Get-AzRemoteCommandOutput -ResourceGroupName $ResourceGroupName -Name $Name -RunCommandName $RunCommandName -Container $container -StorageContext $StorageContext -------------------------------------------------------------------------------- /2023-04 PSHSummit/Managing Hybrid Infrastructure/Demo/Demo10 - All Together.ps1: -------------------------------------------------------------------------------- 1 | $ArcResourceGroupName = 'ArcServers' 2 | $VM = Get-AzResource -ResourceGroupName $VMResourceGroupName 3 | $ArcSrv = Get-AzResource -ResourceGroupName $ArcResourceGroupName 4 | $resources = @($VM) + @($ArcSrv) | 5 | Where-Object{ $_.ResourceType -in 'Microsoft.HybridCompute/machines','Microsoft.Compute/virtualMachines'} 6 | 7 | # Get script, but output to JSON 8 | $ScriptContent = Get-Content -Path '.\Scripts\Get-SystemInfo.ps1' -Raw 9 | $RunCommandName = 'Demo03_' + (Get-Date).ToFileTime() 10 | 11 | 12 | $Executions = foreach ($r in $resources) { 13 | $CommandParameters = @{ 14 | ResourceId = $r.Id 15 | RunCommandName = $RunCommandName 16 | ScriptContent = $ScriptContent 17 | SasToken = $SasToken 18 | StorageContext = $StorageContext 19 | Container = $Container 20 | } 21 | Invoke-AzRemoteCommand @CommandParameters -Verbose 22 | } 23 | 24 | while ($Executions | Where-Object { $_.State -notin 'Succeeded', 'Failed' }) { 25 | $running = @($Executions | Where-Object { $_.State -notin 'Succeeded', 'Failed' }) 26 | Write-Progress -Activity "Waiting for execution" -Status "$($running.Count) of $($Executions.count)" -PercentComplete $(($running.Count / $($Executions.count)) * 100) -id 1 27 | foreach ($e in $Executions | Where-Object { $_.State -notin 'Succeeded', 'Failed' }) { 28 | $CommandParameters = @{ 29 | ResourceGroupName = $e.ResourceGroupName 30 | Name = $e.Name 31 | RunCommandName = $e.CommandName 32 | Type = $e.Type 33 | } 34 | $e.State = Get-AzRemoteCommandStatus @CommandParameters 35 | if ($e.State -in 'Succeeded', 'Failed') { 36 | Invoke-RestMethod -Uri "$($e.OutputBlobUri)$($SasToken)" 37 | } 38 | } 39 | Start-Sleep -Seconds 3 40 | } 41 | Write-Progress -Activity "Done" -Id 1 -Completed 42 | -------------------------------------------------------------------------------- /2023-04 PSHSummit/Managing Hybrid Infrastructure/Managing Hybrid Infrastructure - PSHSummit.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdowst/Presentation-Materials/7c5577ab08f8cae96555df27df12d103fea04d32/2023-04 PSHSummit/Managing Hybrid Infrastructure/Managing Hybrid Infrastructure - PSHSummit.pdf -------------------------------------------------------------------------------- /2023-04 PSHSummit/Managing Hybrid Infrastructure/Scripts/Get-SystemInfo.ps1: -------------------------------------------------------------------------------- 1 | # Check if the machine is running a Linux-based OS 2 | if (Get-Variable -Name IsLinux -ValueOnly -ErrorAction SilentlyContinue) { 3 | # Get the data from the os-release file, and convert it to a PowerShell object 4 | $OS = Get-Content -Path /etc/os-release | 5 | ConvertFrom-StringData 6 | 7 | # Search the meminfo file for the MemTotal line and extract the number 8 | $search = @{ 9 | Path = '/proc/meminfo' 10 | Pattern = 'MemTotal' 11 | } 12 | $Mem = Select-String @search | 13 | ForEach-Object { [regex]::Match($_.line, "(\d+)").value } 14 | 15 | # Run the stat command, parse the output for the Birth line, and then extract the date 16 | $stat = Invoke-Expression -Command 'stat /' 17 | $InstallDate = $stat | Select-String -Pattern 'Birth:' | 18 | ForEach-Object { 19 | Get-Date $_.Line.Replace('Birth:', '').Trim() 20 | } 21 | 22 | # Run the df and uname commands, and save the output as is 23 | $boot = Invoke-Expression -Command 'df /boot' 24 | $OSArchitecture = Invoke-Expression -Command 'uname -m' 25 | $CSName = Invoke-Expression -Command 'uname -n' 26 | 27 | # Build the results into a PowerShell object that matches the same properties as the existing Windows output 28 | $info = [pscustomobject]@{ 29 | Caption = $OS.PRETTY_NAME.Replace('"', "") 30 | InstallDate = $InstallDate 31 | ServicePackMajorVersion = $OS.VERSION.Replace('"', "") 32 | OSArchitecture = $OSArchitecture 33 | BootDevice = $boot.Split("`n")[-1].Split()[0] 34 | BuildNumber = $OS.VERSION_ID.Replace('"', "") 35 | CSName = $CSName 36 | Total_Memory = [math]::Round($Mem / 1MB) 37 | } 38 | } 39 | else { 40 | # Original Windows system information commands 41 | $info = Get-CimInstance -Class Win32_OperatingSystem | 42 | Select-Object Caption, InstallDate, ServicePackMajorVersion, 43 | OSArchitecture, BootDevice, BuildNumber, CSName, 44 | @{l = 'Total_Memory'; 45 | e = { [math]::Round($_.TotalVisibleMemorySize / 1MB) } 46 | } 47 | } 48 | $info | ConvertTo-Json -------------------------------------------------------------------------------- /2023-04 PSHSummit/Managing Hybrid Infrastructure/Scripts/Get-WinSystemInfo.ps1: -------------------------------------------------------------------------------- 1 | # Windows system information commands 2 | Get-CimInstance -Class Win32_OperatingSystem | 3 | Select-Object Caption, InstallDate, ServicePackMajorVersion, 4 | OSArchitecture, BootDevice, BuildNumber, CSName, 5 | @{l = 'Total_Memory';e = { [math]::Round($_.TotalVisibleMemorySize / 1MB) }} | 6 | ConvertTo-Json 7 | -------------------------------------------------------------------------------- /2024-04 DFWSMUG/AzurePester/ManualDeployment.ps1: -------------------------------------------------------------------------------- 1 | $ResourceGroupName = 'dfwsmugstorage' 2 | $TemplateFile = Join-Path $PSScriptRoot 'azuredeploy.json' 3 | 4 | $params = @{ 5 | ResourceGroupName = $ResourceGroupName 6 | TemplateFile = $TemplateFile 7 | storageAccountType = 'Standard_LRS' 8 | } 9 | 10 | 11 | $deployment = New-AzResourceGroupDeployment @params 12 | $deployment 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | $env:Sku = $deployment.Outputs['storageAccountSku'].Value 29 | $env:allowBlobPublicAccess = $deployment.Outputs['allowBlobPublicAccess'].Value 30 | $env:supportsHttpsTrafficOnly = $deployment.Outputs['supportsHttpsTrafficOnly'].Value 31 | -------------------------------------------------------------------------------- /2024-04 DFWSMUG/AzurePester/PesterTester.ps1: -------------------------------------------------------------------------------- 1 | Describe 'Check Storage Account' { 2 | It "Test Sku" { 3 | $Env:Sku | Should -Be 'Standard_LRS' 4 | } 5 | 6 | It "Test Public Access" { 7 | $Env:allowBlobPublicAccess | Should -Be 'False' 8 | } 9 | 10 | It "Test HTTPS Traffic" { 11 | $Env:allowBlobPublicAccess | Should -BeTrue 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /2024-04 DFWSMUG/AzurePester/azstorage.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_dispatch: 3 | name: Storage Acct 4 | 5 | permissions: 6 | contents: read 7 | actions: read 8 | checks: write 9 | 10 | jobs: 11 | build-and-deploy: 12 | runs-on: ubuntu-latest 13 | steps: 14 | 15 | # Checkout code 16 | - uses: actions/checkout@main 17 | 18 | # Log into Azure 19 | - uses: azure/login@v1 20 | with: 21 | creds: ${{ secrets.AZURE_CREDENTIALS }} 22 | 23 | # Deploy ARM template 24 | - name: Run ARM deploy 25 | id: deploy 26 | uses: azure/arm-deploy@v1 27 | with: 28 | subscriptionId: ${{ secrets.AZURE_SUBSCRIPTION }} 29 | resourceGroupName: ${{ secrets.AZURE_RG }} 30 | template: ./azuredeploy.json 31 | parameters: storageAccountType=Standard_LRS 32 | 33 | # output containerName variable from template 34 | - run: echo ${{ steps.deploy.outputs.storageAccountSku }} 35 | 36 | - name: Check Storage Account 37 | shell: pwsh 38 | run: | 39 | Set-PSRepository PSGallery -InstallationPolicy Trusted 40 | Install-Module Pester -MinimumVersion 5.5.0 41 | Import-Module Pester -MinimumVersion 5.5.0 42 | $config = New-PesterConfiguration 43 | $config.TestResult.Enabled = $true 44 | $config.TestResult.OutputFormat = 'JUnitXml' 45 | $config.Output.Verbosity = 'Detailed' 46 | $config.Run.Path = '.\PesterTester.ps1' 47 | Invoke-Pester -Configuration $config 48 | env: 49 | Sku: ${{ steps.deploy.outputs.storageAccountSku }} 50 | allowBlobPublicAccess: ${{ steps.deploy.outputs.allowBlobPublicAccess }} 51 | supportsHttpsTrafficOnly: ${{ steps.deploy.outputs.supportsHttpsTrafficOnly }} 52 | 53 | - name: Deployment Report 54 | uses: dorny/test-reporter@v1 55 | if: success() || failure() # run this step even if previous step failed 56 | with: 57 | name: ARM Tests 58 | path: '*.xml' 59 | reporter: java-junit -------------------------------------------------------------------------------- /2024-04 DFWSMUG/AzurePester/azuredeploy.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "metadata": { 5 | "_generator": { 6 | "name": "bicep", 7 | "version": "0.13.1.58284", 8 | "templateHash": "13120038605368246703" 9 | } 10 | }, 11 | "parameters": { 12 | "storageAccountType": { 13 | "type": "string", 14 | "defaultValue": "Standard_LRS", 15 | "allowedValues": [ 16 | "Premium_LRS", 17 | "Premium_ZRS", 18 | "Standard_GRS", 19 | "Standard_GZRS", 20 | "Standard_LRS", 21 | "Standard_RAGRS", 22 | "Standard_RAGZRS", 23 | "Standard_ZRS" 24 | ], 25 | "metadata": { 26 | "description": "Storage Account type" 27 | } 28 | }, 29 | "location": { 30 | "type": "string", 31 | "defaultValue": "[resourceGroup().location]", 32 | "metadata": { 33 | "description": "The storage account location." 34 | } 35 | }, 36 | "storageAccountName": { 37 | "type": "string", 38 | "defaultValue": "[format('store{0}', uniqueString(resourceGroup().id))]", 39 | "metadata": { 40 | "description": "The name of the storage account" 41 | } 42 | } 43 | }, 44 | "resources": [ 45 | { 46 | "type": "Microsoft.Storage/storageAccounts", 47 | "apiVersion": "2022-09-01", 48 | "name": "[parameters('storageAccountName')]", 49 | "location": "[parameters('location')]", 50 | "sku": { 51 | "name": "[parameters('storageAccountType')]" 52 | }, 53 | "kind": "StorageV2", 54 | "properties": {} 55 | } 56 | ], 57 | "outputs": { 58 | "storageAccountName": { 59 | "type": "string", 60 | "value": "[parameters('storageAccountName')]" 61 | }, 62 | "storageAccountSku": { 63 | "type": "string", 64 | "value": "[reference(parameters('storageAccountName'), '2022-09-01', 'Full').sku.name]" 65 | }, 66 | "allowBlobPublicAccess": { 67 | "type": "bool", 68 | "value": "[reference(parameters('storageAccountName'), '2022-09-01', 'Full').properties.allowBlobPublicAccess]" 69 | }, 70 | "supportsHttpsTrafficOnly": { 71 | "type": "bool", 72 | "value": "[reference(parameters('storageAccountName'), '2022-09-01', 'Full').properties.supportsHttpsTrafficOnly]" 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /2024-04 DFWSMUG/DataDriven.Tests.ps1: -------------------------------------------------------------------------------- 1 | Describe "Spooler Service" { 2 | It "Should be stopped and disabled" { 3 | $service = Get-Service -Name "Spooler" 4 | $service.Status | Should -Be "Stopped" 5 | $service.StartupType | Should -Be "Disabled" 6 | } 7 | } 8 | 9 | Describe "Spooler Service Separate Tests" { 10 | BeforeAll { 11 | $service = Get-Service -Name "Spooler" 12 | } 13 | It "Should be stopped" { 14 | $service.Status | Should -Be "Stopped" 15 | } 16 | It "Should be disabled" { 17 | $service.StartupType | Should -Be "Disabled" 18 | } 19 | } 20 | 21 | Describe "Service Status with Foreach" { 22 | $servicesToCheck = @( 23 | @{ Name = "Spooler" } 24 | ) 25 | Context " Service" -Foreach $servicesToCheck { 26 | BeforeAll { 27 | $service = Get-Service -Name $Name 28 | } 29 | It "Should be stopped" { 30 | $service.Status | Should -Be "Stopped" 31 | } 32 | It "Should be disabled" { 33 | $service.StartupType | Should -Be "Disabled" 34 | } 35 | } 36 | } 37 | 38 | Describe "Service Status" { 39 | $servicesToCheck = @( 40 | @{Name = "mpssvc"; Status = 'Running'; Startup = 'Automatic' } 41 | @{Name = "Spooler"; Status = 'Stopped'; Startup = 'Disabled' } 42 | ) 43 | Context " Service" -Foreach $servicesToCheck { 44 | BeforeAll { 45 | $service = Get-Service -Name $Name 46 | } 47 | It "Should be " { 48 | $service.Status | Should -Be $Status 49 | } 50 | It "Should be " { 51 | $service.StartupType | Should -Be $Startup 52 | } 53 | } 54 | } 55 | 56 | Describe "Service Status" { 57 | $servicesToCheck = Get-Content .\ServiceChecks.json -Raw | ConvertFrom-Json -AsHashtable 58 | 59 | Context " Service" -Foreach $servicesToCheck { 60 | BeforeAll { 61 | $service = Get-Service -Name $name 62 | } 63 | It "Should be " { 64 | $service.Status | Should -Be $Status 65 | } 66 | It "Should be " { 67 | $service.StartupType | Should -Be $Startup 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /2024-04 DFWSMUG/RunTests.ps1: -------------------------------------------------------------------------------- 1 | $config = New-PesterConfiguration 2 | $config.TestResult.Enabled = $true 3 | Invoke-Pester -Configuration $config 4 | 5 | $config.TestResult.OutputFormat = 'JUnitXml' 6 | Invoke-Pester -Configuration $config 7 | 8 | $FileName = "$($env:COMPUTERNAME)_$((Get-Date).ToString('yyyyMMdd'))" 9 | $config.TestResult.OutputPath = "C:\allure\reports\$($FileName).xml" 10 | Invoke-Pester -Configuration $config 11 | 12 | $content = Get-Content $config.TestResult.OutputPath.Value | ForEach-Object{ 13 | $_.Replace("$($PSScriptRoot)\", "$($FileName) ") 14 | } 15 | $content | Out-File $config.TestResult.OutputPath.Value -------------------------------------------------------------------------------- /2024-04 DFWSMUG/ServerConfig.Tests.ps1: -------------------------------------------------------------------------------- 1 | # Testing Local Server Configuration 2 | 3 | # Testing if a Service is Running 4 | Describe "WinRM Service" { 5 | It "Should be running" { 6 | $service = Get-Service -Name "WinRM" 7 | $service.Status | Should -Be "Running" 8 | } 9 | } 10 | 11 | # Verifying a File Exists 12 | Describe "Configuration File" { 13 | It "Should exist" { 14 | Test-Path "C:\configs\myconfig.cfg" | Should -Be $true 15 | } 16 | } 17 | 18 | # Checking an Application Setting (Registry) 19 | Describe "Registry Setting for MyApp" { 20 | It "Should have the correct value" { 21 | $regValue = Get-ItemPropertyValue -Path "HKLM:\Software\MyApp\Settings" -Name "SettingName" 22 | $regValue | Should -Be "ExpectedValue" 23 | } 24 | } 25 | 26 | # Ensuring a Network Port is Listening 27 | Describe "Port 3389" { 28 | It "Should be listening" { 29 | $port = Test-NetConnection -ComputerName localhost -Port 3389 30 | $port.TcpTestSucceeded | Should -Be $true 31 | } 32 | } -------------------------------------------------------------------------------- /2024-04 DFWSMUG/Taking Pester Beyond the Code DFWSMUG.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdowst/Presentation-Materials/7c5577ab08f8cae96555df27df12d103fea04d32/2024-04 DFWSMUG/Taking Pester Beyond the Code DFWSMUG.pdf -------------------------------------------------------------------------------- /2024-04 DFWSMUG/TheHardWay.ps1: -------------------------------------------------------------------------------- 1 | $Service = Get-Service -Name 'mpssvc' 2 | if($service.Status -eq 'Running'){ 3 | Write-Host "Good" 4 | } 5 | 6 | $Name = 'Spooler' 7 | Write-Host $Name -NoNewline 8 | $service = Get-Service -Name $Name 9 | if($service.Status -eq 'Stopped'){ 10 | Write-Host " Good!" -ForegroundColor Green 11 | } 12 | else{ 13 | Write-Host " Bad" -ForegroundColor Red 14 | } 15 | 16 | $Name = 'Spooler' 17 | Write-Host $Name -NoNewline 18 | $service = Get-Service -Name $Name 19 | if($service.Status -eq 'Stopped' -and $Service.StartupType -eq 'Disabled'){ 20 | Write-Host " Good!" -ForegroundColor Green 21 | } 22 | elseif($Service.StartupType -ne 'Disabled'){ 23 | Write-Host " kind of good" -ForegroundColor Yellow 24 | } 25 | else{ 26 | Write-Host " Bad" -ForegroundColor Red 27 | } 28 | -------------------------------------------------------------------------------- /2025-04 PSHSummit/Building Resilient Automations/Building Resilient Automations.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdowst/Presentation-Materials/7c5577ab08f8cae96555df27df12d103fea04d32/2025-04 PSHSummit/Building Resilient Automations/Building Resilient Automations.pdf -------------------------------------------------------------------------------- /2025-04 PSHSummit/Building Resilient Automations/ErrorHandling-No.ps1: -------------------------------------------------------------------------------- 1 | Function Invoke-MyCustomAPI { 2 | param( 3 | $Uri 4 | ) 5 | $InvokeRestMethodParam = @{ 6 | Method = 'GET' 7 | Uri = $Uri 8 | Headers = @{ Authorization = "Bearer $($tokenRequest.token)" } 9 | ErrorAction = 'Stop' 10 | } 11 | try { 12 | $Request = Invoke-RestMethod @InvokeRestMethodParam 13 | $Uri = $Request.NextLink 14 | } 15 | catch {} 16 | 17 | if ($Uri) { 18 | Invoke-MyCustomAPI -Uri $Uri 19 | } 20 | } 21 | 22 | 23 | 24 | 25 | 26 | 27 | Function Invoke-MyCustomAPI { 28 | param( 29 | $Uri, 30 | $retry 31 | ) 32 | 33 | if ($retry -gt 5) { 34 | throw "Failed after 5 attempts" 35 | } 36 | $InvokeRestMethodParam = @{ 37 | Method = 'GET' 38 | Uri = $Uri 39 | Headers = @{ Authorization = "Bearer $($tokenRequest.token)" } 40 | ErrorAction = 'Stop' 41 | } 42 | try { 43 | $Request = Invoke-RestMethod @InvokeRestMethodParam 44 | $Uri = $Request.NextLink 45 | } 46 | catch {} 47 | 48 | if ($Uri) { 49 | $retry ++ 50 | Invoke-MyCustomAPI -Uri $Uri -retry $retry 51 | } 52 | } -------------------------------------------------------------------------------- /2025-04 PSHSummit/Building Resilient Automations/ErrorHandling-Yes.ps1: -------------------------------------------------------------------------------- 1 | 2 | Function Invoke-MyCustomAPI { 3 | param( 4 | $Uri 5 | ) 6 | 7 | [System.Collections.Generic.List[PSObject]] $Users = @() 8 | $retry = 0 9 | while ($Uri) { 10 | try { 11 | $InvokeRestMethodParam = @{ 12 | Method = 'GET' 13 | Uri = $Uri 14 | Headers = @{ Authorization = "Bearer $($tokenRequest.token)" } 15 | ErrorAction = 'Stop' 16 | } 17 | $Request = Invoke-RestMethod @InvokeRestMethodParam 18 | $Request.Users | ForEach-Object { $Users.Add($_) } 19 | $Uri = $Request.NextLink 20 | $retry = 0 21 | } 22 | catch { 23 | $foo = $_ 24 | if ($_.Exception.Response.StatusCode -eq 'TooManyRequests') { 25 | Write-Host "Too many requests, sleeping for 5 seconds" -ForegroundColor Yellow 26 | Start-Sleep -Seconds 5 27 | } 28 | elseif ($_.Exception.Response.StatusCode -eq 'Unauthorized') { 29 | Write-Host "Unauthorized, getting new token" -ForegroundColor Yellow 30 | $tokenRequest = Invoke-RestMethod -Method Post -Uri 'http://localhost:8081/api/authorize' -Body "{'name':'matt'}" -ContentType "application/json" 31 | } 32 | elseif ($retry -ge 5) { 33 | Write-Host "Error: $($_.Exception.Response.StatusCode) - $($_.Exception.Response.ReasonPhrase)" -ForegroundColor Red 34 | $Uri = $null 35 | } 36 | else { 37 | Write-Host "Error: $($_.Exception.Response.StatusCode) - $($_.Exception.Response.ReasonPhrase)" -ForegroundColor Red 38 | } 39 | $retry++ 40 | } 41 | finally { 42 | Write-Host "Users: $($Users.Count) | Retry: $($retry)" 43 | } 44 | } 45 | $Users 46 | } 47 | 48 | $tokenRequest = Invoke-RestMethod -Method Post -Uri 'http://localhost:8081/api/authorize' -Body "{'name':'matt'}" -ContentType "application/json" 49 | $Uri = 'http://localhost:8081/api/test' 50 | 51 | $myUsers = Invoke-MyCustomAPI -Uri $uri 52 | -------------------------------------------------------------------------------- /2025-04 PSHSummit/Building Resilient Automations/FileDownload.ps1: -------------------------------------------------------------------------------- 1 | $DownloadPath = Join-Path $PSScriptRoot 'Downloads' 2 | 3 | 4 | Function Set-DownloadFilePath{ 5 | [CmdletBinding()] 6 | [OutputType([string])] 7 | param( 8 | [Parameter(Mandatory = $true)] 9 | [string]$Directory, 10 | 11 | [Parameter(Mandatory = $true)] 12 | [string]$FileName 13 | ) 14 | 15 | # check if the folder path exists and create it if it doesn't 16 | if(-not (Test-Path -Path $Directory)){ 17 | New-Item -Path $Directory -ItemType Directory | Out-Null 18 | Write-Verbose "Created folder '$Directory'" 19 | } 20 | 21 | # Set the full path of the file 22 | $FilePath = Join-Path $Directory $FileName 23 | 24 | # confirm the file doesn't already exist. Throw a terminating error if it does 25 | if(Test-Path -Path $FilePath){ 26 | $FilePath = Join-Path $Directory "$($FileName.Substring(0,$FileName.LastIndexOf('.')))_$($(Get-Date).ToString('yyyyMMdd')).$($FileName.Substring($FileName.LastIndexOf('.')+1))" 27 | } 28 | 29 | # Return the file path 30 | $FilePath 31 | } 32 | 33 | $TimePath = Join-Path $DownloadPath (Get-Date).ToFileTimeUtc() 34 | $fileDownloads = Invoke-RestMethod -Uri 'http://localhost:8081/files' 35 | $fileDownloads.Files | ForEach-Object { 36 | $outFile = Set-DownloadFilePath -Directory $TimePath -FileName $_.Name 37 | Invoke-WebRequest -Uri $_.Url -OutFile $outFile 38 | } -------------------------------------------------------------------------------- /2025-04 PSHSummit/Building Resilient Automations/FileUpload.ps1: -------------------------------------------------------------------------------- 1 | $SourceFolder = $DownloadPath 2 | $StorageAccountName = '' 3 | $ContainerName = '' 4 | $ResourceGroupName = '' 5 | 6 | # Get the Storage account context 7 | $ctx = (Get-AzStorageAccount -ResourceGroupName $ResourceGroupName -Name $StorageAccountName).Context 8 | 9 | # Upload files 10 | Get-ChildItem -Path $SourceFolder -File -Recurse | ForEach-Object { 11 | $localFile = $_.FullName 12 | $blobName = $_.Name 13 | $matchingBlob = Get-AzStorageBlob -Container $ContainerName -Context $ctx -Blob $blobName -ErrorAction SilentlyContinue 14 | 15 | if ($matchingBlob) { 16 | Write-Output "DUPLICATE FOUND: '$blobName' exists in blob storage. Comparing hashes..." 17 | 18 | $localHash = (Get-FileHash -Path $localFile -Algorithm MD5).Hash 19 | $azblob = Get-AzStorageBlob -Blob $blobName -Container $ContainerName -Context $ctx 20 | $blobHash = -join ($azblob.BlobProperties.ContentHash | ForEach-Object { $_.ToString("x2") }) 21 | 22 | if ($localHash -eq $blobHash) { 23 | Write-Output "MATCH: Hashes match. '$blobName'." 24 | } else { 25 | $timestamp = (Get-Date).ToFileTimeUtc() 26 | $newBlobName = "{0}_{1}{2}" -f $_.BaseName, $timestamp, $_.Extension 27 | Write-Output "MISMATCH: Renaming to '$newBlobName' and uploading." 28 | Set-AzStorageBlobContent -File $localFile -Container $ContainerName -Blob $newBlobName -Context $ctx | Out-Null 29 | } 30 | } else { 31 | Write-Output "NEW: Uploading '$blobName'..." 32 | Set-AzStorageBlobContent -File $localFile -Container $ContainerName -Blob $blobName -Context $ctx | Out-Null 33 | } 34 | Remove-Item -Path $localFile -Force 35 | } 36 | -------------------------------------------------------------------------------- /2025-04 PSHSummit/Building Resilient Automations/NewUserDemo.ps1: -------------------------------------------------------------------------------- 1 | $First = 'John' 2 | $Last = 'Smith' 3 | $Department = 'Information Technology' 4 | $OfficeLocation = 'Building 1, Room 123' 5 | $JobTitle = 'IT Admin' 6 | $EmployeeId = 'abc123456' 7 | 8 | # Set attributes parameter 9 | $attributesParams = [ordered]@{ 10 | Department = $Department 11 | JobTitle = $JobTitle 12 | OfficeLocation = $OfficeLocation 13 | } 14 | 15 | # Set dyamic variables 16 | $UPNSuffix = 'mdowstlive.onmicrosoft.com' 17 | $DisplayName = "$First $Last" 18 | $UserPrincipalName = "$First.$Last@$UPNSuffix" 19 | $MailNickname = "$First.$Last" 20 | $Password = (New-Guid).ToString() 21 | $AccountEnabled = $true 22 | 23 | # Check if user already exists and compare to employee ID 24 | $newUser = $null 25 | $number = 0 26 | do { 27 | $User = Get-MgUser -Filter "(UserPrincipalName eq '$UserPrincipalName')" -Property Id, EmployeeId 28 | if ($User.EmployeeId -eq $EmployeeId) { 29 | Write-Host "User with EmployeeId $EmployeeId already exists" -ForegroundColor Yellow 30 | $newUser = $user | Select-Object -Property * 31 | $user = $null 32 | } 33 | else { 34 | $number++ 35 | $UserPrincipalName = "$First.$Last$($number.ToString('00'))@$UPNSuffix" 36 | } 37 | } while ($User) 38 | $UserPrincipalName 39 | 40 | $userParams = @{ 41 | DisplayName = $DisplayName 42 | UserPrincipalName = $UserPrincipalName 43 | MailNickname = $MailNickname 44 | AccountEnabled = $AccountEnabled 45 | PasswordProfile = @{ 46 | Password = $Password 47 | ForceChangePasswordNextSignIn = $true 48 | } 49 | EmployeeId = $EmployeeId 50 | } 51 | # if user was found, do not run again 52 | if (-not $newUser) { 53 | $newUser = New-MgUser @userParams 54 | } 55 | 56 | # Set client attributes 57 | $attributesParams.GetEnumerator() | ForEach-Object { 58 | $attribute = @{$_.Key = $_.Value } 59 | try { 60 | Update-MgUser -UserId $newUser.Id @attribute -ErrorAction Stop 61 | Write-Host "Updated $($attribute.Keys) with value $($attribute.Values)" 62 | } 63 | catch { 64 | Write-Host "Failed to update $($attribute.Keys) with value $($attribute.Values): $($_.Exception.Message)" -ForegroundColor Red 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /2025-04 PSHSummit/IaC Automated Assurance/.github/workflows/azure-analyze.yaml: -------------------------------------------------------------------------------- 1 | # Analyze repository with PSRule for Azure 2 | 3 | name: Analyze Azure resources 4 | 5 | on: 6 | pull_request: 7 | branches: 8 | - main 9 | workflow_dispatch: 10 | 11 | permissions: {} 12 | 13 | jobs: 14 | analyze: 15 | name: Analyze repository 16 | runs-on: ubuntu-latest 17 | permissions: 18 | contents: read 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v4.2.2 22 | 23 | - name: Set input path 24 | id: setpath 25 | shell: pwsh 26 | run: | 27 | if ($env:GITHUB_EVENT_NAME -eq 'pull_request') { 28 | "inputPath=WhatIf/" | Out-File -FilePath $env:GITHUB_OUTPUT -Append 29 | } 30 | elseif ($env:GITHUB_EVENT_NAME -eq 'workflow_dispatch') { 31 | "inputPath=PSRule/" | Out-File -FilePath $env:GITHUB_OUTPUT -Append 32 | } 33 | else { 34 | "inputPath=." | Out-File -FilePath $env:GITHUB_OUTPUT -Append 35 | } 36 | 37 | - name: Run PSRule analysis 38 | uses: microsoft/ps-rule@v2.9.0 39 | with: 40 | inputPath: ${{ steps.setpath.outputs.inputPath }} 41 | modules: PSRule.Rules.Azure 42 | outputFormat: Sarif 43 | outputPath: reports/ps-rule-results.sarif 44 | summary: true 45 | 46 | - name: Upload results 47 | uses: actions/upload-artifact@v4.6.2 48 | if: always() 49 | with: 50 | name: PSRule-Sarif 51 | path: reports/ps-rule-results.sarif 52 | retention-days: 1 53 | if-no-files-found: error 54 | -------------------------------------------------------------------------------- /2025-04 PSHSummit/IaC Automated Assurance/.github/workflows/bicep-diff.yml: -------------------------------------------------------------------------------- 1 | name: Bicep What-If Comparison 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | bicep-diff: 10 | runs-on: ubuntu-latest 11 | 12 | permissions: 13 | pull-requests: write 14 | contents: read 15 | 16 | steps: 17 | - name: Checkout PR branch 18 | uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | 22 | - name: Fetch base (main) branch 23 | run: git fetch origin main 24 | 25 | - name: Azure login 26 | uses: azure/login@v2 27 | with: 28 | creds: ${{ secrets.AZURE_CREDENTIALS }} 29 | enable-AzPSSession: true 30 | 31 | - name: Save base branch main.bicep 32 | run: | 33 | git show origin/main:main.bicep > base-main.bicep 34 | 35 | - name: Run Azure PowerShell Script File 36 | uses: azure/powershell@v2 37 | with: 38 | inlineScript: ./WhatIf/Compare-BicepWhatIf.ps1 -BicepFile1 ./base-main.bicep -BicepFile2 ./main.bicep -ResourceGroupName 'summit2025' -ParameterFile ./WhatIf/dev.bicepparam -OutputFile ./diff.md 39 | azPSVersion: "latest" 40 | 41 | - name: Post comment to PR 42 | uses: marocchino/sticky-pull-request-comment@v2 43 | with: 44 | recreate: true 45 | path: diff.md 46 | -------------------------------------------------------------------------------- /2025-04 PSHSummit/IaC Automated Assurance/.github/workflows/bicep-linter.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_dispatch: 3 | name: Bicep Linter 4 | 5 | permissions: 6 | contents: read 7 | actions: read 8 | checks: write 9 | 10 | jobs: 11 | build-and-deploy: 12 | runs-on: ubuntu-latest 13 | steps: 14 | 15 | # Checkout code 16 | - uses: actions/checkout@main 17 | 18 | - name: Check main.bicep 19 | shell: pwsh 20 | run: | 21 | Set-PSRepository PSGallery -InstallationPolicy Trusted 22 | Install-Module Pester -MinimumVersion 5.5.0 23 | Import-Module Pester -MinimumVersion 5.5.0 24 | Install-Module Bicep -MinimumVersion 2.8.0 25 | Install-Module Bicep -MinimumVersion 2.8.0 26 | $config = New-PesterConfiguration 27 | $config.TestResult.Enabled = $true 28 | $config.TestResult.OutputFormat = 'JUnitXml' 29 | $config.TestResult.OutputPath = 'main.bicep.xml' 30 | $config.Output.Verbosity = 'Detailed' 31 | $config.Run.Path = '.\PesterTests\Bicep.Test.ps1' 32 | Invoke-Pester -Configuration $config 33 | 34 | - name: Deployment Report 35 | uses: dorny/test-reporter@v1 36 | if: success() || failure() # run this step even if previous step failed 37 | with: 38 | name: Bicep Tests 39 | path: '*.xml' 40 | reporter: java-junit 41 | 42 | - name: Write JUnit results to summary 43 | if: success() || failure() # run this step even if previous step failed 44 | shell: pwsh 45 | run: | 46 | [xml]$junit = Get-Content 'main.bicep.xml' 47 | $tests = $junit.testsuites 48 | $summary = @() 49 | $summary += "## 🧪 Test Summary" 50 | $summary += "" 51 | $summary += "**Total:** $($tests.tests) " 52 | $summary += "**Passed:** $($tests.tests - $tests.failures - $tests.errors - $tests.skipped) " 53 | $summary += "**Failed:** $($tests.failures) " 54 | $summary += "**Errors:** $($tests.errors) " 55 | $summary += "**Skipped:** $($tests.skipped)" 56 | $summary += "" 57 | 58 | foreach ($case in $tests.testsuite.testcase) { 59 | $status = if ($case.failure) { "❌ Failed" } elseif ($case.skipped) { "⚠️ Skipped" } else { "✅ Passed" } 60 | $summary += "$status - $($case.name)" 61 | } 62 | 63 | $summaryText = $summary -join "`n" 64 | $summaryText | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append 65 | -------------------------------------------------------------------------------- /2025-04 PSHSummit/IaC Automated Assurance/.ps-rule/Bicep.Naming.Rule.ps1: -------------------------------------------------------------------------------- 1 | # Synopsis: Parameters should be in in camel case 2 | Rule 'Azure.Bicep.Parameter.CamelCase' -Type 'Microsoft.Resources/deployments' { 3 | foreach ($parameter in $TargetObject.properties.template.parameters.PSObject.properties) { 4 | $Assert.Match($parameter, 'Name', '^[a-z]+[A-Za-z0-9]*$', $true) 5 | } 6 | } 7 | 8 | # Synopsis: Variables should be in in camel case 9 | Rule 'Azure.Bicep.Variable.CamelCase' -Type 'Microsoft.Resources/deployments' { 10 | foreach ($parameter in $TargetObject.properties.template.variables.PSObject.properties) { 11 | $Assert.Match($parameter, 'Name', '^[a-z]+[A-Za-z0-9]*$', $true) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /2025-04 PSHSummit/IaC Automated Assurance/.ps-rule/Org.Tag.Rule.ps1: -------------------------------------------------------------------------------- 1 | # Synopsis: Must have env tag defined. 2 | Rule 'Org.Azure.RG.Tags' -Type 'Microsoft.Storage/storageAccounts' { 3 | $hasTags = $Assert.HasField($TargetObject, 'Tags') 4 | 5 | $Assert.In($TargetObject, 'tags.env', @( 6 | 'dev', 7 | 'prod', 8 | 'uat' 9 | ), $True) 10 | } -------------------------------------------------------------------------------- /2025-04 PSHSummit/IaC Automated Assurance/.ps-rule/Suppression.Rule.yaml: -------------------------------------------------------------------------------- 1 | # Synopsis: Ignore soft delete for development storage accounts 2 | apiVersion: github.com/microsoft/PSRule/v1 3 | kind: SuppressionGroup 4 | metadata: 5 | name: Local.IgnoreNonProdStorage 6 | spec: 7 | rule: 8 | - Azure.Storage.SoftDelete 9 | - Azure.Storage.UseReplication 10 | - Azure.Storage.ContainerSoftDelete 11 | if: 12 | field: tags.env 13 | equals: dev -------------------------------------------------------------------------------- /2025-04 PSHSummit/IaC Automated Assurance/IaC Automated Assurance.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdowst/Presentation-Materials/7c5577ab08f8cae96555df27df12d103fea04d32/2025-04 PSHSummit/IaC Automated Assurance/IaC Automated Assurance.pdf -------------------------------------------------------------------------------- /2025-04 PSHSummit/IaC Automated Assurance/Linter/main.bicep: -------------------------------------------------------------------------------- 1 | @description('Storage Account type') 2 | @allowed([ 3 | 'Premium_LRS' 4 | 'Premium_ZRS' 5 | 'Standard_GRS' 6 | 'Standard_GZRS' 7 | 'Standard_LRS' 8 | 'Standard_RAGRS' 9 | 'Standard_RAGZRS' 10 | 'Standard_ZRS' 11 | ]) 12 | param StorageAccountType string = 'Standard_LRS' 13 | 14 | param storageAccountBlahB string = 'Standard_LRS' 15 | 16 | @description('The name of the storage account') 17 | param storageAccountName string = 'store${uniqueString(resourceGroup().id)}' 18 | 19 | var StorageAccountTypeA = 'Standard_LRS' 20 | 21 | output storageAccountName string = storageAccountName 22 | resource storageAccount 'Microsoft.Storage/storageAccounts@2022-09-01' = { 23 | name: storageAccountName 24 | location: location 25 | sku: { 26 | name: StorageAccountType 27 | } 28 | kind: storageKind 29 | properties: {} 30 | } 31 | 32 | output storageAccountSku string = reference(storageAccountName, '2022-09-01', 'Full').sku.name 33 | output allowBlobPublicAccess bool = reference(storageAccountName, '2022-09-01', 'Full').properties.allowBlobPublicAccess 34 | output supportsHttpsTrafficOnly bool = reference(storageAccountName, '2022-09-01', 'Full').properties.supportsHttpsTrafficOnly 35 | 36 | var storageKind = 'StorageV2' 37 | @description('The storage account location.') 38 | param location string = resourceGroup().location 39 | -------------------------------------------------------------------------------- /2025-04 PSHSummit/IaC Automated Assurance/PSRule/dev.bicepparam: -------------------------------------------------------------------------------- 1 | using 'main.bicep' 2 | 3 | param storageAccountType = 'Standard_LRS' 4 | param environment = 'dev' 5 | -------------------------------------------------------------------------------- /2025-04 PSHSummit/IaC Automated Assurance/PSRule/main.bicep: -------------------------------------------------------------------------------- 1 | @description('Storage Account type') 2 | @allowed([ 3 | 'dev' 4 | 'prod' 5 | 'uat' 6 | ]) 7 | param environment string = 'uat' 8 | 9 | @description('Storage Account type') 10 | @allowed([ 11 | 'Premium_LRS' 12 | 'Premium_ZRS' 13 | 'Standard_GRS' 14 | 'Standard_GZRS' 15 | 'Standard_LRS' 16 | 'Standard_RAGRS' 17 | 'Standard_RAGZRS' 18 | 'Standard_ZRS' 19 | ]) 20 | param storageAccountType string = 'Standard_LRS' 21 | 22 | @description('The storage account location.') 23 | param location string = resourceGroup().location 24 | 25 | var storageAccountName = 'store${uniqueString(resourceGroup().id)}' 26 | 27 | resource storageAccount 'Microsoft.Storage/storageAccounts@2022-09-01' = { 28 | name: storageAccountName 29 | location: location 30 | sku: { 31 | name: storageAccountType 32 | } 33 | kind: 'StorageV2' 34 | properties: { 35 | publicNetworkAccess: 'Disabled' 36 | allowCrossTenantReplication: false 37 | minimumTlsVersion: 'TLS1_2' 38 | allowBlobPublicAccess: false 39 | networkAcls: { 40 | resourceAccessRules: [] 41 | bypass: 'AzureServices' 42 | virtualNetworkRules: [] 43 | ipRules: [] 44 | defaultAction: 'Deny' 45 | } 46 | } 47 | tags: { 48 | costCentre: 'a10000' 49 | env: environment 50 | } 51 | } 52 | 53 | output storageAccountSku string = reference(storageAccountName, '2022-09-01', 'Full').sku.name 54 | output allowBlobPublicAccess bool = reference(storageAccountName, '2022-09-01', 'Full').properties.allowBlobPublicAccess 55 | output supportsHttpsTrafficOnly bool = reference(storageAccountName, '2022-09-01', 'Full').properties.supportsHttpsTrafficOnly 56 | output storageAccountName string = storageAccountName 57 | -------------------------------------------------------------------------------- /2025-04 PSHSummit/IaC Automated Assurance/PSRule/prod.bicepparam: -------------------------------------------------------------------------------- 1 | using 'main.bicep' 2 | 3 | param storageAccountType = 'Standard_GZRS' 4 | param environment = 'prod' 5 | -------------------------------------------------------------------------------- /2025-04 PSHSummit/IaC Automated Assurance/PesterTests/Bicep.Test.ps1: -------------------------------------------------------------------------------- 1 | Get-ChildItem -Path './PesterTests/Functions' -Filter *.ps1 | %{ 2 | . $_.FullName 3 | } 4 | 5 | Describe "Bicep Linter Tests" { 6 | $ErrorActionPreference = 'Continue' 7 | $script:TestCases = Invoke-BicepTests -Path .\Linter\main.bicep 8 | It ' - ' -TestCases $script:TestCases { 9 | param ($Name, $Group, $Passed, $Failures) 10 | $Failures | Should -Be $null 11 | $Passed | Should -Be $True 12 | } 13 | } -------------------------------------------------------------------------------- /2025-04 PSHSummit/IaC Automated Assurance/PesterTests/Functions/ConvertFrom-BicepFile.ps1: -------------------------------------------------------------------------------- 1 | Function ConvertFrom-BicepFile { 2 | [CmdletBinding()] 3 | param( 4 | $Path 5 | ) 6 | # Get the contents of the bicep file 7 | $bicepData = Get-Content $Path 8 | 9 | $lineNumber = 1 10 | $bracket = 0 11 | $area = 'main' 12 | [System.Collections.Generic.List[PSObject]]$resources = @() 13 | # Parse through each line of the bicep file to build the resources list 14 | foreach ($line in $bicepData) { 15 | [string]$declarations = '' 16 | [string]$property = '' 17 | if (($line.TrimStart() -match '^resource ' -or $line.TrimStart() -match '^param ' -or $line.TrimStart() -match '^var ' -or 18 | $line.TrimStart() -match '^targetScope ' -or $line.TrimStart() -match '^output ' -or $line.TrimStart() -match '^module ') -and $area -eq 'main') { 19 | 20 | [System.Collections.Generic.List[PSObject]] $properties = @() 21 | $resources.Add([pscustomobject]@{ 22 | Element = $line.Split()[0] 23 | Name = $line.Split()[1] 24 | Type = $line.Split()[2] 25 | description = $description 26 | DefaultValue = '' 27 | LineNumber = $lineNumber 28 | ElementOrder = -1 29 | properties = $properties 30 | }) 31 | 32 | if ($line -notmatch '^module ' -and $line -notmatch '^module ' -and $line -match '=') { 33 | $resources[-1].DefaultValue = [Regex]::Matches($line.Substring($line.IndexOf('=')), "(?<=\')(.*?)(?=\')").Value 34 | } 35 | 36 | $area = $line.Split()[0] 37 | } 38 | 39 | if ($line -match '^@description' -and $area -eq 'main') { 40 | $description = [Regex]::Matches($line, "(?<=\')(.*?)(?=\')").Value 41 | } 42 | else { 43 | $description = '' 44 | } 45 | 46 | if ($line.trim() -match '^@allowed') { 47 | $area = 'allowed' 48 | } 49 | $line.ToCharArray() | ForEach-Object { 50 | if ($_ -eq '{' -or $_ -eq '[') { 51 | $bracket++ 52 | } 53 | elseif ($_ -eq '}' -or $_ -eq ']') { 54 | $bracket-- 55 | } 56 | elseif ($bracket -eq 0) { 57 | $declarations += $_ 58 | } 59 | elseif ($bracket -eq 1) { 60 | $property += $_ 61 | } 62 | } 63 | 64 | if (-not [string]::IsNullOrEmpty($property) -and $area -ne 'allowed') { 65 | $resources[-1].properties.Add([pscustomobject]@{ 66 | Property = $property.Split(':')[0].Trim() 67 | LineNumber = $lineNumber 68 | ElementOrder = -1 69 | }) 70 | } 71 | 72 | if ($bracket -eq 0 -and $area -ne 'main') { 73 | $area = 'main' 74 | } 75 | 76 | Write-Verbose "$($lineNumber) : $($bracket): $($line)" 77 | $lineNumber++ 78 | } 79 | $resources 80 | 81 | } 82 | 83 | -------------------------------------------------------------------------------- /2025-04 PSHSummit/IaC Automated Assurance/PesterTests/Functions/Invoke-BicepTests.ps1: -------------------------------------------------------------------------------- 1 | Function Invoke-BicepTests { 2 | param( 3 | $Path 4 | ) 5 | 6 | # Get all functions that start with Test- 7 | Get-ChildItem -Path $PSScriptRoot -Filter 'Test-*.ps1' | ForEach-Object { 8 | . $($_.BaseName) -Path $Path | ForEach-Object { 9 | @{ 10 | Name = $_.Name 11 | Group = $_.Group 12 | Passed = $_.Passed 13 | Failures = ($_.Failures | ForEach-Object { "$($_.Message) : $($_.LineNumber)" }) 14 | } 15 | } 16 | } 17 | 18 | } -------------------------------------------------------------------------------- /2025-04 PSHSummit/IaC Automated Assurance/PesterTests/Functions/New-TestResults.ps1: -------------------------------------------------------------------------------- 1 | Function New-TestResults { 2 | 3 | param( 4 | $Name, 5 | $Group 6 | ) 7 | [System.Collections.Generic.List[PSObject]] $Failures = @() 8 | [System.Collections.Generic.List[PSObject]] $Warnings = @() 9 | [pscustomobject]@{ 10 | Name = $name 11 | Group = $group 12 | Passed = $true 13 | Failures = $Failures 14 | Warnings = $Warnings 15 | } 16 | 17 | } -------------------------------------------------------------------------------- /2025-04 PSHSummit/IaC Automated Assurance/PesterTests/Functions/Test-BicepLinter.ps1: -------------------------------------------------------------------------------- 1 | Function Test-BicepLinter { 2 | param( 3 | $Path 4 | ) 5 | 6 | $Results = New-TestResults -Name 'Bicep passes linter tests' -Group 'Bicep: deploymentTemplate' 7 | 8 | 9 | $test = Test-BicepFile -Path $Path -WarningVariable linterwarn -ErrorVariable lintererr 10 | 11 | $linterwarn | ForEach-Object{ 12 | $msg = $_.Message.Substring($Path.Length) 13 | $LineNumber = [Regex]::Match($msg, "(?<=\()(.*?)(?=\,)").Value 14 | $msg = $msg.Substring($msg.IndexOf(':') + 1).Trim() 15 | $Results.Failures.Add((Write-Failure -LineNumber $LineNumber -Message $msg)) 16 | $Results.Passed = $false 17 | } 18 | 19 | $lintererr | ForEach-Object{ 20 | $msg = $_.Message.Expection.Substring($Path.Length) 21 | $LineNumber = [Regex]::Match($msg, "(?<=\()(.*?)(?=\,)").Value 22 | $msg = $msg.Substring($msg.IndexOf(':') + 1).Trim() 23 | $Results.Failures.Add((Write-Failure -LineNumber $LineNumber -Message $msg)) 24 | $Results.Passed = $false 25 | } 26 | $Results 27 | } -------------------------------------------------------------------------------- /2025-04 PSHSummit/IaC Automated Assurance/PesterTests/Functions/Test-BicepResourceOrder.ps1: -------------------------------------------------------------------------------- 1 | Function Test-BicepResourceOrder { 2 | 3 | param( 4 | $Path 5 | ) 6 | $resources = ConvertFrom-BicepFile -Path $Path 7 | $elements = @( 8 | 'targetScope' 9 | 'param' 10 | 'var' 11 | 'resource' 12 | 'module' 13 | 'output' 14 | ) 15 | [System.Collections.ArrayList]$elementCheck = $elements | ForEach-Object { $_ } 16 | 17 | $Results = New-TestResults -Name 'Elements are in proper order' -Group 'Bicep: deploymentTemplate' 18 | 19 | 20 | foreach ($e in $elements) { 21 | $order = $resources | Where-Object { $_.Element -eq $e } | Sort-Object LineNumber | Select-Object -ExpandProperty LineNumber -First 1 22 | $elementCheck.RemoveAt(0) 23 | if ($order -gt 0) { 24 | $resources | Where-Object { $_.LineNumber -gt $order -and $_.Element -notin $elementCheck } | ForEach-Object { 25 | if ($e -eq $_.Element) { 26 | $Results.Failures.Add((Write-Failure -LineNumber $_.LineNumber -Message "Warning element-order: Element ""$($_.Element)"" elements should all be grouped together")) 27 | $Results.Passed = $false 28 | } 29 | else { 30 | $Results.Failures.Add((Write-Failure -LineNumber $_.LineNumber -Message "Warning element-order: Element ""$($_.Element)"" should come before ""$e""")) 31 | $Results.Passed= $false 32 | } 33 | } 34 | } 35 | } 36 | 37 | $Results 38 | } 39 | -------------------------------------------------------------------------------- /2025-04 PSHSummit/IaC Automated Assurance/PesterTests/Functions/Write-Failure.ps1: -------------------------------------------------------------------------------- 1 | Function Write-Failure { 2 | 3 | param( 4 | $LineNumber, 5 | $Message 6 | ) 7 | 8 | #Write-Host "Line $($LineNumber) : $($Message)" -ForegroundColor Red 9 | 10 | [pscustomobject]@{ 11 | LineNumber = $LineNumber 12 | Message = $Message 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /2025-04 PSHSummit/IaC Automated Assurance/WhatIf/Compare-BicepWhatIf.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | [Parameter(Mandatory)] 3 | [string]$BicepFile1, 4 | 5 | [Parameter(Mandatory)] 6 | [string]$BicepFile2, 7 | 8 | [Parameter(Mandatory)] 9 | [string]$ResourceGroupName, 10 | 11 | [Parameter(Mandatory)] 12 | [string]$ParameterFile, 13 | 14 | [Parameter(Mandatory)] 15 | [string]$OutputFile 16 | ) 17 | 18 | function Get-WhatIfResult { 19 | param ( 20 | [string]$BicepFile, 21 | [string]$DeploymentName, 22 | [string]$ParameterFile 23 | ) 24 | 25 | $whatif = @{ 26 | ResourceGroupName = $ResourceGroupName 27 | Name = $DeploymentName 28 | TemplateFile = $BicepFile 29 | TemplateParameterFile = $ParameterFile 30 | } 31 | $result = Get-AzResourceGroupDeploymentWhatIfResult @whatif 32 | 33 | $result.Changes | ForEach-Object { 34 | [PSCustomObject]@{ 35 | ResourceId = $_.FullyQualifiedResourceId 36 | ChangeType = $_.ChangeType 37 | Delta = $_.BeforeAfterJson 38 | } 39 | } 40 | } 41 | 42 | $whatIf1 = Get-WhatIfResult -BicepFile $BicepFile1 -DeploymentName "deployment1" -ParameterFile $ParameterFile 43 | $whatIf2 = Get-WhatIfResult -BicepFile $BicepFile2 -DeploymentName "deployment2" -ParameterFile $ParameterFile 44 | 45 | $allResourceIds = ($whatIf1.ResourceId + $whatIf2.ResourceId) | Sort-Object -Unique 46 | 47 | $diffResults = foreach ($id in $allResourceIds) { 48 | $res1 = $whatIf1 | Where-Object { $_.ResourceId -eq $id } 49 | $res2 = $whatIf2 | Where-Object { $_.ResourceId -eq $id } 50 | 51 | if ($res1 -and -not $res2) { 52 | "**REMOVED in PR**: $id`nChangeType: $($res1.ChangeType)`n" 53 | } 54 | elseif (-not $res1 -and $res2) { 55 | "**ADDED in PR**: $id`nChangeType: $($res2.ChangeType)`n" 56 | } 57 | elseif ($res1.ChangeType -ne $res2.ChangeType -or $res1.Delta -ne $res2.Delta) { 58 | "**MODIFIED**: $id`nChangeType: Bicep1: $($res1.ChangeType), Bicep2: $($res2.ChangeType)`n" 59 | } 60 | } 61 | 62 | if (-not $diffResults) { 63 | $diffResults = "✅ No differences detected between main and PR Bicep deployments." 64 | } 65 | 66 | $diffResults -join "`n---`n" | Out-File -FilePath $OutputFile -Encoding UTF8 67 | -------------------------------------------------------------------------------- /2025-04 PSHSummit/IaC Automated Assurance/WhatIf/demo01.bicep: -------------------------------------------------------------------------------- 1 | @description('Storage Account type') 2 | @allowed([ 3 | 'dev' 4 | 'prod' 5 | 'uat' 6 | ]) 7 | param environment string = 'dev' 8 | 9 | @description('Storage Account type') 10 | @allowed([ 11 | 'Premium_LRS' 12 | 'Premium_ZRS' 13 | 'Standard_GRS' 14 | 'Standard_GZRS' 15 | 'Standard_LRS' 16 | 'Standard_RAGRS' 17 | 'Standard_RAGZRS' 18 | 'Standard_ZRS' 19 | ]) 20 | param storageAccountType string = 'Standard_LRS' 21 | 22 | @description('The storage account location.') 23 | param location string = resourceGroup().location 24 | 25 | @description('The storage account name.') 26 | param storageAccountName string = 'store${uniqueString(resourceGroup().id)}' 27 | 28 | var metricAlertsUsedCapacityName = 'Used capacity' 29 | var actionGroup = '{storageAccountName}-ag' 30 | var metricAlertsStorageAvailabilityName = 'Storage Availability' 31 | var metricAlertsTransactionsThresholdName = 'Transactions thresholds' 32 | 33 | resource actionGroup_resource 'microsoft.insights/actionGroups@2024-10-01-preview' = { 34 | name: actionGroup 35 | location: 'Global' 36 | properties: { 37 | groupShortName: 'storage' 38 | enabled: true 39 | emailReceivers: [ 40 | { 41 | name: 'admin' 42 | emailAddress: 'admin@contoso.com' 43 | useCommonAlertSchema: true 44 | } 45 | ] 46 | smsReceivers: [] 47 | webhookReceivers: [] 48 | eventHubReceivers: [] 49 | itsmReceivers: [] 50 | azureAppPushReceivers: [] 51 | automationRunbookReceivers: [] 52 | voiceReceivers: [] 53 | logicAppReceivers: [] 54 | azureFunctionReceivers: [] 55 | armRoleReceivers: [] 56 | } 57 | tags: { 58 | costCentre: 'a10000' 59 | env: environment 60 | } 61 | } 62 | 63 | resource storageAccount 'Microsoft.Storage/storageAccounts@2022-09-01' = { 64 | name: storageAccountName 65 | location: location 66 | sku: { 67 | name: storageAccountType 68 | } 69 | kind: 'StorageV2' 70 | properties: { 71 | publicNetworkAccess: 'Disabled' 72 | allowCrossTenantReplication: false 73 | minimumTlsVersion: 'TLS1_2' 74 | allowBlobPublicAccess: false 75 | networkAcls: { 76 | resourceAccessRules: [] 77 | bypass: 'AzureServices' 78 | virtualNetworkRules: [] 79 | ipRules: [] 80 | defaultAction: 'Deny' 81 | } 82 | } 83 | tags: { 84 | costCentre: 'a10000' 85 | env: environment 86 | } 87 | } 88 | 89 | resource metricAlertsStorageAvailabilityName_resource 'microsoft.insights/metricAlerts@2018-03-01' = { 90 | name: metricAlertsStorageAvailabilityName 91 | location: 'global' 92 | properties: { 93 | severity: 3 94 | enabled: true 95 | scopes: [ 96 | storageAccount.id 97 | ] 98 | evaluationFrequency: 'PT1M' 99 | windowSize: 'PT5M' 100 | criteria: { 101 | allOf: [ 102 | { 103 | threshold: json('90') 104 | name: 'Metric1' 105 | metricNamespace: 'Microsoft.Storage/storageAccounts' 106 | metricName: 'Availability' 107 | operator: 'LessThan' 108 | timeAggregation: 'Average' 109 | skipMetricValidation: false 110 | criterionType: 'StaticThresholdCriterion' 111 | } 112 | ] 113 | 'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria' 114 | } 115 | autoMitigate: true 116 | targetResourceType: 'Microsoft.Storage/storageAccounts' 117 | targetResourceRegion: 'westus2' 118 | actions: [ 119 | { 120 | actionGroupId: actionGroup_resource.id 121 | webHookProperties: {} 122 | } 123 | ] 124 | } 125 | tags: { 126 | costCentre: 'a10000' 127 | env: environment 128 | } 129 | } 130 | 131 | resource metricAlertsTransactionsThresholdName_resource 'microsoft.insights/metricAlerts@2018-03-01' = { 132 | name: metricAlertsTransactionsThresholdName 133 | location: 'global' 134 | properties: { 135 | severity: 3 136 | enabled: true 137 | scopes: [ 138 | storageAccount.id 139 | ] 140 | evaluationFrequency: 'PT1M' 141 | windowSize: 'PT5M' 142 | criteria: { 143 | allOf: [ 144 | { 145 | alertSensitivity: 'Medium' 146 | failingPeriods: { 147 | numberOfEvaluationPeriods: 4 148 | minFailingPeriodsToAlert: 4 149 | } 150 | name: 'Metric1' 151 | metricNamespace: 'Microsoft.Storage/storageAccounts' 152 | metricName: 'Transactions' 153 | operator: 'GreaterOrLessThan' 154 | timeAggregation: 'Total' 155 | skipMetricValidation: false 156 | criterionType: 'DynamicThresholdCriterion' 157 | } 158 | ] 159 | 'odata.type': 'Microsoft.Azure.Monitor.MultipleResourceMultipleMetricCriteria' 160 | } 161 | autoMitigate: true 162 | targetResourceType: 'Microsoft.Storage/storageAccounts' 163 | targetResourceRegion: 'westus2' 164 | actions: [ 165 | { 166 | actionGroupId: actionGroup_resource.id 167 | webHookProperties: {} 168 | } 169 | ] 170 | } 171 | tags: { 172 | costCentre: 'a10000' 173 | env: environment 174 | } 175 | } 176 | 177 | resource metricAlertsUsedCapacityName_resource 'microsoft.insights/metricAlerts@2018-03-01' = { 178 | name: metricAlertsUsedCapacityName 179 | location: 'global' 180 | properties: { 181 | severity: 3 182 | enabled: true 183 | scopes: [ 184 | storageAccount.id 185 | ] 186 | evaluationFrequency: 'PT1M' 187 | windowSize: 'PT1H' 188 | criteria: { 189 | allOf: [ 190 | { 191 | threshold: json('10') 192 | name: 'Metric1' 193 | metricNamespace: 'Microsoft.Storage/storageAccounts' 194 | metricName: 'UsedCapacity' 195 | operator: 'GreaterThan' 196 | timeAggregation: 'Average' 197 | skipMetricValidation: false 198 | criterionType: 'StaticThresholdCriterion' 199 | } 200 | ] 201 | 'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria' 202 | } 203 | autoMitigate: true 204 | targetResourceType: 'Microsoft.Storage/storageAccounts' 205 | targetResourceRegion: 'westus2' 206 | actions: [ 207 | { 208 | actionGroupId: actionGroup_resource.id 209 | webHookProperties: {} 210 | } 211 | ] 212 | } 213 | tags: { 214 | costCentre: 'a10000' 215 | env: environment 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /2025-04 PSHSummit/IaC Automated Assurance/WhatIf/demo02.bicep: -------------------------------------------------------------------------------- 1 | @description('Storage Account type') 2 | @allowed([ 3 | 'dev' 4 | 'prod' 5 | 'uat' 6 | ]) 7 | param environment string = 'dev' 8 | 9 | @description('Storage Account type') 10 | @allowed([ 11 | 'Premium_LRS' 12 | 'Premium_ZRS' 13 | 'Standard_GRS' 14 | 'Standard_GZRS' 15 | 'Standard_LRS' 16 | 'Standard_RAGRS' 17 | 'Standard_RAGZRS' 18 | 'Standard_ZRS' 19 | ]) 20 | param storageAccountType string = 'Standard_LRS' 21 | 22 | @description('The storage account location.') 23 | param location string = resourceGroup().location 24 | 25 | @description('The storage account name.') 26 | param storageAccountName string = 'store${uniqueString(resourceGroup().id)}' 27 | 28 | var metricAlertsUsedCapacityName = 'Used capacity' 29 | var actionGroup = '{storageAccountName}-ag' 30 | var metricAlertsStorageAvailabilityName = 'Storage Availability' 31 | var metricAlertsTransactionsThresholdName = 'Transactions threshold' 32 | 33 | resource actionGroup_resource 'microsoft.insights/actionGroups@2024-10-01-preview' = { 34 | name: actionGroup 35 | location: 'Global' 36 | properties: { 37 | groupShortName: 'storage' 38 | enabled: true 39 | emailReceivers: [ 40 | { 41 | name: 'admin' 42 | emailAddress: 'admin@contoso.com' 43 | useCommonAlertSchema: true 44 | } 45 | ] 46 | smsReceivers: [] 47 | webhookReceivers: [] 48 | eventHubReceivers: [] 49 | itsmReceivers: [] 50 | azureAppPushReceivers: [] 51 | automationRunbookReceivers: [] 52 | voiceReceivers: [] 53 | logicAppReceivers: [] 54 | azureFunctionReceivers: [] 55 | armRoleReceivers: [] 56 | } 57 | tags: { 58 | costCentre: 'a10000' 59 | env: environment 60 | } 61 | } 62 | 63 | resource storageAccount 'Microsoft.Storage/storageAccounts@2022-09-01' = { 64 | name: storageAccountName 65 | location: location 66 | sku: { 67 | name: storageAccountType 68 | } 69 | kind: 'StorageV2' 70 | properties: { 71 | publicNetworkAccess: 'Disabled' 72 | allowCrossTenantReplication: false 73 | minimumTlsVersion: 'TLS1_2' 74 | allowBlobPublicAccess: false 75 | networkAcls: { 76 | resourceAccessRules: [] 77 | bypass: 'AzureServices' 78 | virtualNetworkRules: [] 79 | ipRules: [] 80 | defaultAction: 'Deny' 81 | } 82 | } 83 | tags: { 84 | costCentre: 'a10000' 85 | env: environment 86 | } 87 | } 88 | 89 | resource metricAlertsStorageAvailabilityName_resource 'microsoft.insights/metricAlerts@2018-03-01' = { 90 | name: metricAlertsStorageAvailabilityName 91 | location: 'global' 92 | properties: { 93 | severity: 3 94 | enabled: true 95 | scopes: [ 96 | storageAccount.id 97 | ] 98 | evaluationFrequency: 'PT1M' 99 | windowSize: 'PT5M' 100 | criteria: { 101 | allOf: [ 102 | { 103 | threshold: json('90') 104 | name: 'Metric1' 105 | metricNamespace: 'Microsoft.Storage/storageAccounts' 106 | metricName: 'Availability' 107 | operator: 'LessThan' 108 | timeAggregation: 'Average' 109 | skipMetricValidation: false 110 | criterionType: 'StaticThresholdCriterion' 111 | } 112 | ] 113 | 'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria' 114 | } 115 | autoMitigate: true 116 | targetResourceType: 'Microsoft.Storage/storageAccounts' 117 | targetResourceRegion: 'westus2' 118 | actions: [ 119 | { 120 | actionGroupId: actionGroup_resource.id 121 | webHookProperties: {} 122 | } 123 | ] 124 | } 125 | tags: { 126 | costCentre: 'a10000' 127 | env: environment 128 | } 129 | } 130 | 131 | resource metricAlertsTransactionsThresholdName_resource 'microsoft.insights/metricAlerts@2018-03-01' = { 132 | name: metricAlertsTransactionsThresholdName 133 | location: 'global' 134 | properties: { 135 | severity: 3 136 | enabled: true 137 | scopes: [ 138 | storageAccount.id 139 | ] 140 | evaluationFrequency: 'PT1M' 141 | windowSize: 'PT5M' 142 | criteria: { 143 | allOf: [ 144 | { 145 | alertSensitivity: 'Medium' 146 | failingPeriods: { 147 | numberOfEvaluationPeriods: 4 148 | minFailingPeriodsToAlert: 4 149 | } 150 | name: 'Metric1' 151 | metricNamespace: 'Microsoft.Storage/storageAccounts' 152 | metricName: 'Transactions' 153 | operator: 'GreaterOrLessThan' 154 | timeAggregation: 'Total' 155 | skipMetricValidation: false 156 | criterionType: 'DynamicThresholdCriterion' 157 | } 158 | ] 159 | 'odata.type': 'Microsoft.Azure.Monitor.MultipleResourceMultipleMetricCriteria' 160 | } 161 | autoMitigate: true 162 | targetResourceType: 'Microsoft.Storage/storageAccounts' 163 | targetResourceRegion: 'westus2' 164 | actions: [ 165 | { 166 | actionGroupId: actionGroup_resource.id 167 | webHookProperties: {} 168 | } 169 | ] 170 | } 171 | tags: { 172 | costCentre: 'a10000' 173 | env: environment 174 | } 175 | } 176 | 177 | resource metricAlertsUsedCapacityName_resource 'microsoft.insights/metricAlerts@2018-03-01' = { 178 | name: metricAlertsUsedCapacityName 179 | location: 'global' 180 | properties: { 181 | severity: 3 182 | enabled: true 183 | scopes: [ 184 | storageAccount.id 185 | ] 186 | evaluationFrequency: 'PT1M' 187 | windowSize: 'PT1H' 188 | criteria: { 189 | allOf: [ 190 | { 191 | threshold: json('10') 192 | name: 'Metric1' 193 | metricNamespace: 'Microsoft.Storage/storageAccounts' 194 | metricName: 'UsedCapacity' 195 | operator: 'GreaterThan' 196 | timeAggregation: 'Average' 197 | skipMetricValidation: false 198 | criterionType: 'StaticThresholdCriterion' 199 | } 200 | ] 201 | 'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria' 202 | } 203 | autoMitigate: true 204 | targetResourceType: 'Microsoft.Storage/storageAccounts' 205 | targetResourceRegion: 'westus2' 206 | actions: [ 207 | { 208 | actionGroupId: actionGroup_resource.id 209 | webHookProperties: {} 210 | } 211 | ] 212 | } 213 | tags: { 214 | costCentre: 'a10000' 215 | env: environment 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /2025-04 PSHSummit/IaC Automated Assurance/WhatIf/dev.bicepparam: -------------------------------------------------------------------------------- 1 | using 'demo01.bicep' 2 | 3 | param storageAccountName = 'summit2025dev' 4 | param storageAccountType = 'Standard_LRS' 5 | param environment = 'dev' 6 | -------------------------------------------------------------------------------- /2025-04 PSHSummit/IaC Automated Assurance/main.bicep: -------------------------------------------------------------------------------- 1 | @description('Storage Account type') 2 | @allowed([ 3 | 'dev' 4 | 'prod' 5 | 'uat' 6 | ]) 7 | param environment string = 'uat' 8 | 9 | @description('Storage Account type') 10 | @allowed([ 11 | 'Premium_LRS' 12 | 'Premium_ZRS' 13 | 'Standard_GRS' 14 | 'Standard_GZRS' 15 | 'Standard_LRS' 16 | 'Standard_RAGRS' 17 | 'Standard_RAGZRS' 18 | 'Standard_ZRS' 19 | ]) 20 | param storageAccountType string = 'Standard_LRS' 21 | 22 | @description('The storage account location.') 23 | param location string = resourceGroup().location 24 | 25 | param storageAccountName string 26 | 27 | var metricAlerts_Used_capacity_name = 'Used capacity' 28 | var actionGroup = '{storageAccountName}-ag' 29 | var metricAlerts_Storage_Availability_name = 'Storage Availability' 30 | var metricAlerts_Transactions_threshold_name = 'Transaction threshold' 31 | 32 | resource actionGroup_resource 'microsoft.insights/actionGroups@2024-10-01-preview' = { 33 | name: actionGroup 34 | location: 'Global' 35 | properties: { 36 | groupShortName: 'storage' 37 | enabled: true 38 | emailReceivers: [ 39 | { 40 | name: 'admin' 41 | emailAddress: 'admin@contoso.com' 42 | useCommonAlertSchema: true 43 | } 44 | ] 45 | smsReceivers: [] 46 | webhookReceivers: [] 47 | eventHubReceivers: [] 48 | itsmReceivers: [] 49 | azureAppPushReceivers: [] 50 | automationRunbookReceivers: [] 51 | voiceReceivers: [] 52 | logicAppReceivers: [] 53 | azureFunctionReceivers: [] 54 | armRoleReceivers: [] 55 | } 56 | } 57 | 58 | resource storageAccount 'Microsoft.Storage/storageAccounts@2022-09-01' = { 59 | name: storageAccountName 60 | location: location 61 | sku: { 62 | name: storageAccountType 63 | } 64 | kind: 'StorageV2' 65 | properties: { 66 | publicNetworkAccess: 'Disabled' 67 | allowCrossTenantReplication: false 68 | minimumTlsVersion: 'TLS1_2' 69 | allowBlobPublicAccess: false 70 | networkAcls: { 71 | resourceAccessRules: [] 72 | bypass: 'AzureServices' 73 | virtualNetworkRules: [] 74 | ipRules: [] 75 | defaultAction: 'Deny' 76 | } 77 | } 78 | tags: { 79 | costCentre: 'a10000' 80 | env: environment 81 | } 82 | } 83 | 84 | resource metricAlerts_Storage_Availability_name_resource 'microsoft.insights/metricAlerts@2018-03-01' = { 85 | name: metricAlerts_Storage_Availability_name 86 | location: 'global' 87 | properties: { 88 | severity: 3 89 | enabled: true 90 | scopes: [ 91 | storageAccount.id 92 | ] 93 | evaluationFrequency: 'PT1M' 94 | windowSize: 'PT5M' 95 | criteria: { 96 | allOf: [ 97 | { 98 | threshold: json('90') 99 | name: 'Metric1' 100 | metricNamespace: 'Microsoft.Storage/storageAccounts' 101 | metricName: 'Availability' 102 | operator: 'LessThan' 103 | timeAggregation: 'Average' 104 | skipMetricValidation: false 105 | criterionType: 'StaticThresholdCriterion' 106 | } 107 | ] 108 | 'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria' 109 | } 110 | autoMitigate: true 111 | targetResourceType: 'Microsoft.Storage/storageAccounts' 112 | targetResourceRegion: 'westus2' 113 | actions: [ 114 | { 115 | actionGroupId: actionGroup_resource.id 116 | webHookProperties: {} 117 | } 118 | ] 119 | } 120 | } 121 | 122 | resource metricAlerts_Transactions_threshold_name_resource 'microsoft.insights/metricAlerts@2018-03-01' = { 123 | name: metricAlerts_Transactions_threshold_name 124 | location: 'global' 125 | properties: { 126 | severity: 3 127 | enabled: true 128 | scopes: [ 129 | storageAccount.id 130 | ] 131 | evaluationFrequency: 'PT1M' 132 | windowSize: 'PT5M' 133 | criteria: { 134 | allOf: [ 135 | { 136 | alertSensitivity: 'Medium' 137 | failingPeriods: { 138 | numberOfEvaluationPeriods: 4 139 | minFailingPeriodsToAlert: 4 140 | } 141 | name: 'Metric1' 142 | metricNamespace: 'Microsoft.Storage/storageAccounts' 143 | metricName: 'Transactions' 144 | operator: 'GreaterOrLessThan' 145 | timeAggregation: 'Total' 146 | skipMetricValidation: false 147 | criterionType: 'DynamicThresholdCriterion' 148 | } 149 | ] 150 | 'odata.type': 'Microsoft.Azure.Monitor.MultipleResourceMultipleMetricCriteria' 151 | } 152 | autoMitigate: true 153 | targetResourceType: 'Microsoft.Storage/storageAccounts' 154 | targetResourceRegion: 'westus2' 155 | actions: [ 156 | { 157 | actionGroupId: actionGroup_resource.id 158 | webHookProperties: {} 159 | } 160 | ] 161 | } 162 | } 163 | 164 | resource metricAlerts_Used_capacity_name_resource 'microsoft.insights/metricAlerts@2018-03-01' = { 165 | name: metricAlerts_Used_capacity_name 166 | location: 'global' 167 | properties: { 168 | severity: 3 169 | enabled: true 170 | scopes: [ 171 | storageAccount.id 172 | ] 173 | evaluationFrequency: 'PT1M' 174 | windowSize: 'PT1H' 175 | criteria: { 176 | allOf: [ 177 | { 178 | threshold: json('10') 179 | name: 'Metric1' 180 | metricNamespace: 'Microsoft.Storage/storageAccounts' 181 | metricName: 'UsedCapacity' 182 | operator: 'GreaterThan' 183 | timeAggregation: 'Average' 184 | skipMetricValidation: false 185 | criterionType: 'StaticThresholdCriterion' 186 | } 187 | ] 188 | 'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria' 189 | } 190 | autoMitigate: true 191 | targetResourceType: 'Microsoft.Storage/storageAccounts' 192 | targetResourceRegion: 'westus2' 193 | actions: [ 194 | { 195 | actionGroupId: actionGroup_resource.id 196 | webHookProperties: {} 197 | } 198 | ] 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /2025-04 PSHSummit/IaC Automated Assurance/ps-rule.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # Specifies that the rules from the PSRule should use. 3 | # 4 | 5 | # Specifies a minimum version of PSRule for Azure. 6 | requires: 7 | PSRule.Rules.Azure: '>=1.40.0' 8 | 9 | # Automatically use rules for Azure. 10 | include: 11 | module: 12 | - PSRule.Rules.Azure 13 | 14 | # Limits the files that PSRule analyzes to only those matching the *.bicepparam pattern. 15 | # Helps avoid processing unrelated files (like .json, .md, etc.). 16 | input: 17 | pathIgnore: 18 | - '**' # (3) 19 | - '!**/*.bicepparam' # (4) 20 | 21 | configuration: 22 | # Tells PSRule to automatically expand (compile) Bicep files into ARM templates before analysis. 23 | # Equivalent to running 'bicep build' behind the scenes. 24 | # Required for directly analyzing .bicep files without needing to manually convert them to ARM. 25 | AZURE_BICEP_FILE_EXPANSION: true 26 | 27 | # Detailed provides extended information in the result, like the rule ID, target object, recommendation, and result status (Pass, Fail, etc.). 28 | execution: 29 | output: Detailed 30 | 31 | 32 | # This tells PSRule how to bind input objects (like those from Bicep or ARM templates) to rules. 33 | # resourceType and type help PSRule identify what kind of resource is being evaluated (e.g., Microsoft.Storage/storageAccounts) and apply the correct rules accordingly. 34 | binding: 35 | targetType: 36 | - 'resourceType' 37 | - 'type' -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Presentation-Materials 2 | A collection of materials from my past presentations 3 | --------------------------------------------------------------------------------