<?xml version="1.0" encoding="utf-8"?>
<rss xmlns:atom="https://www.w3.org/2005/Atom" xmlns:sy="https://purl.org/rss/1.0/modules/syndication/" xmlns:dc="https://purl.org/dc/elements/1.1/" xmlns:content="https://purl.org/rss/1.0/modules/content/" xmlns:wfw="https://wellformedweb.org/CommentAPI/" xmlns:slash="https://purl.org/rss/1.0/modules/slash/"
  version="2.0">
  <channel>
    <title>James Tabor’s blog</title>
    <atom:link
      href="https://docs.microsoft.com/archive/blogs/jrt/feed.xml"
      rel="self"
      type="application/rss+xml" />
    <link>https://docs.microsoft.com/archive/blogs/jrt/feed.xml</link>
    <description>Azure for Everything and Everyone</description>
    <lastBuildDate>Tue, 16 Apr 2019 05:16:07 GMT</lastBuildDate>
    <language>en-US</language>
    <sy:updatePeriod>hourly</sy:updatePeriod>
    <sy:updateFrequency>1</sy:updateFrequency>
    <item>
      <title>Pause Idle Azure SQL Data Warehouse Instances</title>
      <link>https://docs.microsoft.com/archive/blogs/jrt/pause-idle-azure-sql-data-warehouse-instances</link>
      <pubDate>Mon, 20 Nov 2017 12:11:07 GMT</pubDate>
      <dc:creator><![CDATA[James R Tabor]]></dc:creator>
      <guid
        isPermaLink="false">https://blogs.msdn.microsoft.com/jrt/?p=717</guid>
      <description><![CDATA[Azure SQL Data Warehouse (SQL DW) instances are quite expensive to keep running, so it makes sense...]]></description>
      <content:encoded><![CDATA[Azure SQL Data Warehouse (SQL DW) instances are quite expensive to keep running, so it makes sense to Pause them if they are not being used.

This script will NOT resume them if connection attempts are made to a paused SQL DW instance.

The easiest way to check if it has been awhile since the SQL DW has 'done anything' is to query a system view called 'dm_pdw_exec_requests'.
The script (found here <a target="_blank" href="https://gallery.technet.microsoft.com/scriptcenter/Pause-Idle-SQL-DW-Instances-33f35c9a" rel="noopener">Pause-Idle-SQL-DW-Instances</a> ) will
<ul>
 	<li>Get all the SQL Servers defined in a subscription</li>
 	<li>Loop through these SQL Servers and get all the online SQL DW instances</li>
 	<li>Connect to the SQL DW and query the dm_pdw_exec_requests system view to get the last time a query ended</li>
 	<li>If a specified threshold of minutes has elapsed, then pause the SQL DW instance.</li>
</ul>
You will need to add SQL usernames and passwords into a KeyVault as secrets in order to connect to the SQL DW instances.
The KeyVault Secret Name for UserName and Password is in the format
<ul>
 	<li>UserName: '{SQL Server Name}{SQL DW Instance Name}UserName'</li>
 	<li>Password: '{SQL Server Name}{SQL DW Instance Name}Password'</li>
</ul>
&nbsp;]]></content:encoded>
    </item>
    <item>
      <title>Resize Azure VMs in bulk</title>
      <link>https://docs.microsoft.com/archive/blogs/jrt/resize-azure-vms-in-bulk</link>
      <pubDate>Mon, 30 Oct 2017 19:33:31 GMT</pubDate>
      <dc:creator><![CDATA[James R Tabor]]></dc:creator>
      <guid
        isPermaLink="false">https://blogs.msdn.microsoft.com/jrt/?p=705</guid>
      <description><![CDATA[If you need to do a bulk update of VM sizes from one type to another to take advantage of new...]]></description>
      <content:encoded><![CDATA[If you need to do a bulk update of VM sizes from one type to another to take advantage of new technology being released, this script should help you out.

This script will work for Resource Manager VMs, and NOT Classic VMs.
It targets VMs that are deallocated or have failed deployments only.
VMs in a Failed deployment state will automatically start when re-sized.

I've added a select grid at the end to let you toggle the state of the VMs that have been changed.
i.e Selected VMs will be Started if Deallocated and Stopped if in any other state

I've preloaded the SizeMapping table with the mapping to move from Dv2 Promo sizes to their equivilent Dv3 and Ev3 sizes.
You can change this as to your requirements, just set the Old and New elements for each record.
Make sure only select ONE subscription from the Select Grid.

[sourcecode gutter="false" title="Resize VMs" language="powershell"]
#If you want to stop running VMs, Resize and restart again, set this to $true
$ForceStop = $false
#Prompt to confirm shutdown for each VM
$StopConfirm = $true
#Filter on VM name to restrict the list of VMs returned
$VMFilter = &quot;*&quot;

$AvailSubscriptions = Get-AzureRmSubscription

$ErrorActionPreference = &quot;Stop&quot;
$SelectedSubscription = $AvailSubscriptions | select Name, Id | Out-GridView -Title &quot;Select ONE (only) Subscription&quot; -PassThru 

$SubscriptionGUID = $SelectedSubscription.Id

Select-AzureRmSubscription -Subscription $SubscriptionGUID

#Update this table if you have different mapping size requirements from old to new
$SizeMapping = @()
$SizeMapping += @{Old='Standard_D2_v2_Promo';New='Standard_D2_v3'}
$SizeMapping += @{Old='Standard_D3_v2_Promo';New='Standard_D4_v3'}
$SizeMapping += @{Old='Standard_D4_v2_Promo';New='Standard_D8_v3'}
$SizeMapping += @{Old='Standard_D5_v2_Promo';New='Standard_D16_v3'}
$SizeMapping += @{Old='Standard_D11_v2_Promo';New='Standard_E2_v3'}
$SizeMapping += @{Old='Standard_D12_v2_Promo';New='Standard_E4_v3'}
$SizeMapping += @{Old='Standard_D13_v2_Promo';New='Standard_E8_v3'}
$SizeMapping += @{Old='Standard_D14_v2_Promo';New='Standard_E16_v3'}
$SizeMapping += @{Old='Standard_DS2_v2_Promo';New='Standard_D2s_v3'}
$SizeMapping += @{Old='Standard_DS3_v2_Promo';New='Standard_D4s_v3'}
$SizeMapping += @{Old='Standard_DS4_v2_Promo';New='Standard_D8s_v3'}
$SizeMapping += @{Old='Standard_DS5_v2_Promo';New='Standard_D16s_v3'}
$SizeMapping += @{Old='Standard_DS11_v2_Promo';New='Standard_E2s_v3'}
$SizeMapping += @{Old='Standard_DS12_v2_Promo';New='Standard_E4s_v3'}
$SizeMapping += @{Old='Standard_DS13_v2_Promo';New='Standard_E8s_v3'}
$SizeMapping += @{Old='Standard_DS14_v2_Promo';New='Standard_E16s_v3'}

#You can use a $VMFilter, set above, to limit the script to only add filtered VM names to the array
$VMs = Get-AzureRmVM | Where-Object {$_.Name -like $VMFilter}

$AlteredVMs = @()
foreach ($VM in $VMs) {
    if ($VM.HardwareProfile.VmSize -in $SizeMapping.Old) {
        $Status = Get-AzureRmVM -ResourceGroupName $VM.ResourceGroupName -Name $VM.Name -Status
        $NewSize = $SizeMapping.New[$SizeMapping.Old.IndexOf($VM.HardwareProfile.VmSize)]

        #If the VM is deallocated, just resize it.        
        if ($Status.Statuses[1].DisplayStatus -eq &quot;VM deallocated&quot;) {
            
            write-output &quot;Updating VM: $($VM.Name) from $($VM.HardwareProfile.VmSize) to $($NewSize)&quot;

            $VM.HardwareProfile.VmSize = $NewSize
        
            Update-AzureRmVM -VM $VM -ResourceGroupName $VM.ResourceGroupName -Verbose
        
            $AlteredVM = new-object psobject -Property @{
                Name = $VM.Name
                ResourceGroup = $VM.ResourceGroupName
                Location = $VM.Location
                ResourceID = $VM.Id
                SubscriptionID = $SubscriptionGUID
                OldSize = 
                OldProvisioningStatus = $Status.Statuses[0].DisplayStatus
                OldRunStatus = $Status.Statuses[1].DisplayStatus
                NewProvisioningStatus = $null
                NewRunStatus = $null
            }
            $AlteredVMs += $AlteredVM
        }

        #If the VM has failed provisioning, try and stop the VM firstso it clears the allocation error and doesn't start automatically after changing size
        if ($Status.Statuses[0].DisplayStatus -eq &quot;Provisioning failed&quot;) {
            
            $StopAction = $false
            $TryCount = 0
            $ErrorActionPreference = &quot;Stop&quot;
            do {
                try {
                    $TryCount += 1
                    Write-Output &quot;Stopping VM: $($VM.Name)&quot;
                    Stop-AzureRmVM -Name $VM.Name -ResourceGroupName $VM.ResourceGroupName -Force
                    $StopAction=$true
                } catch {
                    sleep -10
                    $StopAction = $false
                }
            } until ($StopAction -eq $true -or $TryCount -ge 5)
            $ErrorActionPreference = &quot;Continue&quot;

            write-output &quot;Updating VM: $($VM.Name) from $($VM.HardwareProfile.VmSize) to $($NewSize)&quot;
            $VM.HardwareProfile.VmSize = $NewSize
        
            Update-AzureRmVM -VM $VM -ResourceGroupName $VM.ResourceGroupName -Verbose
        
            $AlteredVM = new-object psobject -Property @{
                Name = $VM.Name
                ResourceGroup = $VM.ResourceGroupName
                Location = $VM.Location
                ResourceID = $VM.Id
                SubscriptionID = $SubscriptionGUID
                OldProvisioningStatus = $Status.Statuses[0].DisplayStatus
                OldRunStatus = $Status.Statuses[1].DisplayStatus
                NewProvisioningStatus = $null
                NewRunStatus = $null
            }
            $AlteredVMs += $AlteredVM

        }

        #If you have ForceStop set to $true and the VM is running
        if ($Status.Statuses[1].DisplayStatus -eq &quot;VM running&quot; -and $ForceStop) {
            
            if ($StopConfirm) {
                $ConfirmStop = Read-Host -Prompt &quot;Please type 'YES' to confirm you want to stop VM $($VM.Name)&quot;
                if ($ConfirmStop -eq &quot;YES&quot;) {
                    $StopAction = $false
                    $TryCount = 0
                    $ErrorActionPreference = &quot;Stop&quot;
                    do {
                        try {
                            $TryCount += 1
                            Write-Output &quot;Stopping VM: $($VM.Name)&quot;
                            Stop-AzureRmVM -Name $VM.Name -ResourceGroupName $VM.ResourceGroupName -Force
                            $StopAction=$true
                        } catch {
                            sleep -10
                            $StopAction = $false
                        }
                    } until ($StopAction -eq $true -or $TryCount -ge 5)
                    $ErrorActionPreference = &quot;Continue&quot;

                    write-output &quot;Updating VM: $($VM.Name) from $($VM.HardwareProfile.VmSize) to $($NewSize)&quot;
                    $VM.HardwareProfile.VmSize = $NewSize
        
                    Update-AzureRmVM -VM $VM -ResourceGroupName $VM.ResourceGroupName -Verbose
                    
                    Write-Output &quot;Starting VM: $($VM.Name) after resize&quot;
                    Start-AzureRmVM -Name $VM.Name -ResourceGroupName $VM.ResourceGroupName

                    $AlteredVM = new-object psobject -Property @{
                        Name = $VM.Name
                        ResourceGroup = $VM.ResourceGroupName
                        Location = $VM.Location
                        ResourceID = $VM.Id
                        SubscriptionID = $SubscriptionGUID
                        OldProvisioningStatus = $Status.Statuses[0].DisplayStatus
                        OldRunStatus = $Status.Statuses[1].DisplayStatus
                        NewProvisioningStatus = $null
                        NewRunStatus = $null
                    }
                    $AlteredVMs += $AlteredVM
                }
            }
        }
    }
}

$CurrentVMState = @()

foreach ($AlteredVM in $AlteredVMs) {
    $Status = Get-AzureRmVM -ResourceGroupName $AlteredVM.ResourceGroup -Name $AlteredVM.Name -Status
    $AlteredVM.NewProvisioningStatus = $Status.Statuses[0].DisplayStatus
    $AlteredVM.NewRunStatus = $Status.Statuses[1].DisplayStatus
    $CurrentVMState += $AlteredVM
}
$ToggleStates = $null
if ($CurrentVMState -ne $null) {
    $ToggleStates = $CurrentVMState | select Name, ResourceGroup, NewRunStatus | Out-GridView -Title &quot;Select VMs to toggle state (Running to Deallocated; Deallocated to Running); Click Cancel to change none&quot; -PassThru
    foreach ($ToggleState in $ToggleStates) {
        if ($ToggleState.NewRunStatus -eq &quot;VM deallocated&quot;) {
            Start-AzureRmVM -ResourceGroupName $ToggleState.ResourceGroup -Name $ToggleState.Name
        } else {
            Stop-AzureRmVM -ResourceGroupName $ToggleState.ResourceGroup -Name $ToggleState.Name -Force
        }
    }
}


[/sourcecode]
]]></content:encoded>
    </item>
    <item>
      <title>Azure VMs - Active Directory members and getting time sync (ntp) right</title>
      <link>https://docs.microsoft.com/archive/blogs/jrt/azure-vms-active-directory-members-and-getting-time-sync-ntp-right</link>
      <pubDate>Sat, 28 Oct 2017 13:52:14 GMT</pubDate>
      <dc:creator><![CDATA[James R Tabor]]></dc:creator>
      <guid
        isPermaLink="false">https://blogs.msdn.microsoft.com/jrt/?p=686</guid>
      <description><![CDATA[I've recently had this question asked of me multiple times recently.
"If a VM in Azure is a member...]]></description>
      <content:encoded><![CDATA[I've recently had this question asked of me multiple times recently.
"If a VM in Azure is a member of a Domain, where does it get its time from, Azure (time.microsoft.com) or the NTP source set by AD?"
The answer is both, unless you make some changes.

Here are the specific time setting recomendations, buried inside this post <a target="_blank" href="https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/get-started/windows-time-service/accurate-time" rel="noopener noreferrer">https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/get-started/windows-time-service/accurate-time</a>

<strong>Azure and Windows IaaS considerations</strong>
<ul>
 	<li><strong>Azure Virtual Machine: Active Directory Domain Services</strong>
If the Azure VM running Active Directory Domain Services is part of an existing on-premises Active Directory Forest, then TimeSync(VMIC), should be disabled. This is to allow all DCs in the Forest, both physical and virtual, to use a single time sync hierarchy. Refer to the best practice whitepaper “<a target="_blank" href="https://technet.microsoft.com/en-us/library/virtual_active_directory_domain_controller_virtualization_hyperv.aspx" rel="noopener noreferrer">Running Domain Controllers in Hyper-V</a>”</li>
 	<li><strong>Azure Virtual Machine: Domain-joined machine</strong>
If you are hosting a machine which is domain joined to an existing Active Directory Forest, virtual or physical, the best practice is to disable TimeSync for the guest and ensure W32Time is configured to synchronize with its Domain Controller via configuring time for Type=NTP5</li>
 	<li><strong>Azure Virtual Machine: Standalone workgroup machine
</strong>If the Azure VM is not joined to a domain, nor is it a Domain Controller, the recommendation is to keep the default time configuration and have the VM synchronize with the host.</li>
</ul>
So in short, for Active Directory VMs in Azure, update the registry path: HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\W32Time\TimeProviders\VMICTimeProvider and set the value for 'Enabled' to 0

To do this via Group Policy the setting looks something like this -
<a href="https://msdnshared.blob.core.windows.net/media/2017/10/time.png"><img src="https://msdnshared.blob.core.windows.net/media/2017/10/time.png" alt="" width="414" height="460" class="alignnone size-full wp-image-695" /></a>]]></content:encoded>
    </item>
    <item>
      <title>Using OMS and Runbooks to update VMs when they're down</title>
      <link>https://docs.microsoft.com/archive/blogs/jrt/using-oms-and-runbooks-to-update-vms-when-theyre-down</link>
      <pubDate>Tue, 25 Jul 2017 16:47:45 GMT</pubDate>
      <dc:creator><![CDATA[James R Tabor]]></dc:creator>
      <guid
        isPermaLink="false">https://blogs.msdn.microsoft.com/jrt/?p=676</guid>
      <description><![CDATA[The aim of this post is to enable a powershell Azure Automation runbook to be fired when a VM is...]]></description>
      <content:encoded><![CDATA[The aim of this post is to enable a powershell Azure Automation runbook to be fired when a VM is deallocated (Stopped from the Azure management fabric), that will perform given tasks.
This is useful if you have non-urgent infrastructural changes that need to be done, but you want it done with the least amount of user impact.  When the user, or a schedule stops the VM, the runbook is trigger.

You will need an OMS workspace set-up and linked to an Azure Automation account.
In the linked Automation Account, create a new Runbook called "DeallocationAction".
This will accept the OMS log alert results and allow the required actions to be performed

Here is one I've written to give you the idea and get you started.
This runbook checks for VMs in East US, created by System and resizes them based on a mapping array.
[sourcecode gutter="false" title="Runbook - DeallocationAction" language="powershell"] 
 Param
    (
        [parameter(Mandatory=$false)]
        [object] $WebhookData
    ) 
    # Collect properties of WebhookData
    $WebhookName    = $WebhookData.WebhookName
    $WebhookHeaders = $WebhookData.RequestHeader
    $WebhookBody    = $WebhookData.RequestBody

    # Collect results. Information converted from JSON.
    $SearchResults = (ConvertFrom-Json $WebhookBody).SearchResults.value

    #Retrive the RunAs Connection details
    $Conn = Get-AutomationConnection -Name 'AzureRunAsConnection'

    #Login with the RunAs Account
    Add-AzureRMAccount -ServicePrincipal -Tenant $Conn.TenantID `
        -ApplicationId $Conn.ApplicationID -CertificateThumbprint $Conn.CertificateThumbprint 
        
    $SizeMap = @()
    $SizeMap += @{Old='Standard_F1';New='Standard_D1'}
    $SizeMap += @{Old='Standard_F2';New='Standard_D2'}
    $SizeMap += @{Old='Standard_F4';New='Standard_D3'}
    $SizeMap += @{Old='Standard_F8';New='Standard_D4'}
 
    $CurrSub = $null
    $CurrSub = (Get-AzureRmContext).Subscription.Id

    foreach ($Result in $SearchResults) {
        Write-Output &quot;Result&quot;
        Write-Output $Result
        
        #Make sure it's the final succeeded action, and not Started or Accepted
        if ($Result.ActivityStatus -eq &quot;Succeeded&quot;) {

            #Change the Subscription if required
            if ($CurrSub -ne $Result.SubscriptionId) {
                Select-AzureRmSubscription -SubscriptionId $Result.SubscriptionId
                $CurrSub = $Result.SubscriptionId
            }
            #Get the VM details
            $ResourceGroup = $Result.ResourceGroup 
            $VMName = $Result.Resource 
            $VM = Get-AzureRmVM -ResourceGroupName $ResourceGroup -Name $VMName
            $CurrSize = $VM.HardwareProfile.VmSize
            Write-Output &quot;$($VMName) - Current VM Size: $($CurrSize)&quot;
            #Check the machine is still deallocated
            $VMStatus = Get-AzureRmVM -ResourceGroupName $ResourceGroup -Name $VMName -Status
            if ($VMStatus.Statuses[1].Code -eq &quot;PowerState/deallocated&quot;) {
                
                #Put any additional filters in here for continuing to change VM size
                #These are negatives, start with the VM being included then remove based on filters
                $Enabled = $true
                if ($VM.Location.ToLower() -ne &quot;eastus&quot;) {
                    $Enabled = $false
                }
                if ($VM.Tags.CreatedBy -ne &quot;System&quot;) {
                    $Enabled = $false
                }
                #Continue if passed through previous filters
                if ($Enabled) {
                    #Make the required changes
                    foreach ($Size in $SizeMap) {
                        if ($CurrSize -eq $Size.Old) {
                            $NewSize = $Size.New
                        } 
                    }
                    
                    if ($CurrSize -ne $NewSize) {
                        Write-Output &quot;$($VMName) - New VM Size: $($NewSize)&quot;

                        #Update the VM Size
                        $VM.HardwareProfile.VmSize = $NewSize
                        #Apply the change
                        $UpdateVM = Update-AzureRmVM -VM $VM -ResourceGroupName $ResourceGroup 
                    
                        #If the update succeeds, update the SizeChange Tag
                        if ($UpdateVM.IsSuccessStatusCode) {

                            Write-Output &quot;$($VMName) - Change Succeeded&quot;
                            $SizeChange = @{Old=$($CurrSize);UTC=(Get-Date (Get-Date).ToUniversalTime() -Format &quot;dd/MM/yy HH:mm&quot;)}
                            $SizeJSON = $SizeChange | ConvertTo-Json -Compress
                            $Tags = $null
                            $Tags = (Get-AzureRmResource -ResourceId $VM.Id).Tags
                            if ($Tags -eq $null) {
                                $Tags = @{
                                    SizeChanged = $SizeJSON
                                }
                            } else {
                                if ($Tags.ContainsKey(&quot;SizeChanged&quot;)) {
                                
                                    #Make sure the current JSON is configured as an array
                                    $a = [regex]&quot;\[*\]&quot;
                                    if ($a.Match($Tags.SizeChanged).Success) {
                                        $CurrSizeChange = (ConvertFrom-Json $Tags.SizeChanged)
                                    } else {
                                        $CurrSizeChange = (ConvertFrom-Json &quot;[`n$($Tags.SizeChanged)`n]&quot;)
                                    }
                                    $CurrSizeChange += $($SizeChange)
                                    $SizeJSON = $CurrSizeChange | ConvertTo-Json -Compress
                                    
                                    #Make sure Tag value is less than 256, keep dropping first record off until under 256 in length
                                    $RecSkip = 0
                                    do {
                                        $RecSkip
                                        $SizeJSON = $CurrSizeChange | Select-Object -Skip $RecSkip | ConvertTo-Json -Compress
                                        $RecSkip += 1
                                    } until ($SizeJSON.Length -lt 256)

                                    #Set Tag value
                                    $Tags.SizeChanged = $SizeJSON
                                } else {
                                    $Tags += @{SizeChanged=$SizeJSON}
                                }
                            }
                            Set-AzureRmResource -Tag $Tags -ResourceId $VM.Id -Force
                        } else {
                            Write-Output &quot;$($VMName) - Change Failed&quot;
                        }
                    }
                }
            }
        }
    }
[/sourcecode]

Next, create an alert on the following OMS log query 

[sourcecode gutter="false" title="OMS Log Query" language="text"] 
Type=AzureActivity AND OperationName = &quot;Microsoft.Compute/virtualMachines/deallocate/action&quot; AND ActivityStatus=&quot;Succeeded&quot;
[/sourcecode]

Set the other attributes for the alert as follows
  Time window: 5 minutes
  Frequency: 5 minutes
  Number of results greater than 0
  Runbook: YES
    Select the runbook created above

Now whenever a VM Deallocation event occurs and is logged through from Azure Activity to OMS (this can take up to 15 minutes), the Runbook you created is triggered and sent the VM information (within 5 minutes) so the required actions can be performed.
]]></content:encoded>
    </item>
    <item>
      <title>Make practical use of NSG Flow Logs for troubleshooting</title>
      <link>https://docs.microsoft.com/archive/blogs/jrt/make-practical-use-of-nsg-flow-logs-for-troubleshooting</link>
      <pubDate>Fri, 07 Jul 2017 13:05:27 GMT</pubDate>
      <dc:creator><![CDATA[James R Tabor]]></dc:creator>
      <guid
        isPermaLink="false">https://blogs.msdn.microsoft.com/jrt/?p=655</guid>
      <description><![CDATA[So, you've found the new Azure Network Watcher features.
Cool, now I can start getting some real...]]></description>
      <content:encoded><![CDATA[So, you've found the new Azure Network Watcher features.
Cool, now I can start getting some real information about what's going on in my Azure Network.

The NSG flow logs section of the Network Watcher blade, in the Azure Portal, lets you specify a Storage Account for each NSG (Network Security Group) to output detailed information on all the traffic hitting the NSG.
Note: The Storage Account must be in the same region as the NSG

Awesome right!
Then you try and use that information and find there are a heap of sub-folders created for each Resource Group, NSG all with separate files for each hour of logging.
Also, they are all in JSON format and really hard to find anything specific.

I don't know about you, but I found it really hard to troubleshoot a particular network communication issue with a specific VM and/or Subnet.

So, I've written the following Powershell script that pulls all these files from the Storage Account down to a local folder and then builds an array of these 'Events'.
The properties built out into the array are -
ResourceGroup, NSG, Ref, SrcIP, DstIP, SrcPort, DstPort, Protocol '<em>T(TCP)/U(UDP)'</em>, Direction '<em>O(Outbound)/I(Inbound)'</em>, Type '<em>A(Accept)/D(Denied)'</em>, Time, RuleType, and RuleName.

Version 1 :
<a href="https://msdnshared.blob.core.windows.net/media/2017/07/Screenshot-7_07_2017-11_38_53-AM.png"><img width="1024" height="139" class="alignnone size-large wp-image-665" alt="" src="https://msdnshared.blob.core.windows.net/media/2017/07/Screenshot-7_07_2017-11_38_53-AM-1024x139.png" /></a>

Version 2:
<a href="https://msdnshared.blob.core.windows.net/media/2017/07/FlowLogsV2.png"><img src="https://msdnshared.blob.core.windows.net/media/2017/07/FlowLogsV2-1024x178.png" alt="" width="1024" height="178" class="wp-image-735 size-large alignnone" /></a>

This is then presented in a GridView that can be sorted and filtered to find what you are looking for.
I've also added an option at the end to dump it out to a CSV if you need to send the output to someone else.

Note: Due to the folder nesting for the NSG logging you will need to create a 'short' local root folder to cache locally.
Generally the $env:TEMP location will be too many folders deep to allow for the required folder nesting for NSG logs.
I've used C:\Temp\AzureNSGFlowLogs here, you will need to either create this folder or update that variable set line in the script.

<strong>Update [July 22nd 2017]:</strong> Allow for user selection of Storage Account and cater for multiple NSGs, in multiple locations, logging to different Storage Accounts.
<strong>Update [Nov 7th 2018]:</strong> Update Find-AzureRmResource to Get-AzureRmResource due to deprecated command (thanks to Victor Dantas Mehmeri for finding)
<strong>Update [Nov 7th 2018]:</strong> Added Version 2 new fields - State (B-Begin, C- Continue, &amp; E-End), Send and Recv Packets and Bytes.

[sourcecode gutter="false" title="NSGFlowLogAnalysis" language="powershell"] 

#Prompt user to select storage account(s) 
$StorageAccountNames = (Get-AzureRmStorageAccount | Select-Object Location, StorageAccountName | Out-GridView -Title &quot;Select Storage Account(s)&quot; -PassThru).StorageAccountName

#$LocalFolder = &quot;$($env:TEMP)\AzureNSGFlowLogs&quot;
$LocalFolder = &quot;C:\Temp\AzureNSGFlowLogs&quot;
#If you want to override where the local copy of the NSG Flow Log files go to, un-comment the following line and update

$TimeZoneStr = (Get-TimeZone).Id 
#If you want to override the Timezone to something other than the locally detected one, un-comment the following line and update
#$TimeZoneStr = &quot;AUS Eastern Standard Time&quot;
$StorageDetails = @()
foreach ($StorageAccountName in $StorageAccountNames) {
    $StorageResource = $null
    $ResourceGroupName = $null
    $StoKeys = $null
    $StorageAccountKey = $null

    $StorageAccountName
    $StorageResource = Get-AzureRmResource -ResourceType Microsoft.Storage/storageAccounts -Name $StorageAccountName
    $ResourceGroupName = $StorageResource.ResourceGroupName
    $ResourceGroupName
    $StoKeys = Get-AzureRmStorageAccountKey -ResourceGroupName $ResourceGroupName -Name $StorageAccountName
    $StorageAccountKey = $StoKeys[0].Value
    $StorageContext = New-AzureStorageContext -StorageAccountName $StorageAccountName -StorageAccountKey $StorageAccountKey

    $StorageDetail = New-Object psobject -Property @{
        StorageAccountName = $StorageAccountName
        StorageResource = $StorageResource
        ResourceGroupName = $ResourceGroupName
        StorageAccountKey = $StorageAccountKey
        StorageContext = $StorageContext
    }
    $StorageDetails += $StorageDetail
}

$SubscriptionID = (Get-AzureRmContext).Subscription.Id

#Function to create the sub folders as required
function MakeFolders (
        [object]$Path,
        [string]$root
    )
{
    if (!(Test-Path -Path $root)) {
        $rootsplit = $root.Split(&quot;\&quot;)
        $tPath = ''
        foreach ($tPath in $rootsplit) {
            $BuildPath = &quot;$($BuildPath)$($tPath)\&quot;
            $BuildPath
            if (!(Test-Path -Path $BuildPath)) {
                mkdir $BuildPath
            }
        }
    }
    $Build = &quot;$($root)&quot;
    foreach ($fld in $path) {
        $Build = &quot;$($Build)\$($fld)&quot;
        #$Build
        mkdir &quot;$($build)&quot; -ErrorAction SilentlyContinue
    }
}
#End function MakeFolders

#Function to get the Timezone information
function GetTZ (
        [string]$TZ_string
    ) 
{
            
    $r = [regex] &quot;\[([^\[]*)\]&quot;
    $match = $r.match($($TZ_string))
    
    # If there is a successful match for a Timezone ID
    if ($match.Success) {
        $TZId = $match.Groups[1].Value
        # Try and get a valid TimeZone entry for the matched TimeZone Id
        try {
                    
            $TZ = [System.TimeZoneInfo]::FindSystemTimeZoneById($TZId)
        } 
        # Otherwise assume UTC
        catch {
                    
            $TZ = [System.TimeZoneInfo]::FindSystemTimeZoneById(&quot;UTC&quot;)
        }
    } else {
        try {
            $TZ = [System.TimeZoneInfo]::FindSystemTimeZoneById($TZ_string)
        } catch {
            $TZ = [System.TimeZoneInfo]::FindSystemTimeZoneById(&quot;UTC&quot;)
        }
    }
    return $TZ
}
#end function GetTZ

#Set the $TZ variable up with the required TimeZone information
$TZ = GetTZ -TZ_string $TimeZoneStr

#Get all the blobs in the insights-logs-networksecuritygroupflowevent container from the specified Storage Account
$Blobs = @()
foreach ($StorageDetail in $StorageDetails) {
    
    #$StorageContext
    $Blob = Get-AzureStorageBlob -Container &quot;insights-logs-networksecuritygroupflowevent&quot; -Context $StorageDetail.StorageContext
    $Blobs += $Blob 
}


#Build an array of the available selection criteria
$AvailSelection = @()
foreach ($blobpath in $blobs.name) {
    $PathSplit = $blobpath.Split(&quot;/&quot;)
    $datestr = Get-Date -Year $PathSplit[9].Substring(2) -Month $PathSplit[10].Substring(2) -Day $PathSplit[11].Substring(2) -Hour $PathSplit[12].Substring(2) -Minute $PathSplit[13].Substring(2) -Second 0 -Millisecond 0 -Format &quot;yyyy-MM-ddTHH:mm:ssZ&quot;#2017-07-06T02:53:02.3410000Z
    $blobdate = (get-date $datestr).ToUniversalTime()
    #Write-Output &quot;RG: $($PathSplit[4])`tNSG:$($PathSplit[8])`tDate:$($blobdate)&quot;
    $SelectData = New-Object psobject -Property @{
        ResourceGroup = $PathSplit[4]
        NSG = $PathSplit[8]
        Date = $blobdate
    }
    $AvailSelection += $SelectData
}

#Prompt user to select the required ResourceGroup(s), NSG(s), and hourly sectioned files
$SelectedResourceGroups = ($AvailSelection | Select-Object -Property &quot;ResourceGroup&quot; -Unique | Out-GridView -Title &quot;Select required Resource Group(s)&quot; -PassThru).ResourceGroup
$selectedNSGs = ($AvailSelection | Where-Object {$_.ResourceGroup -in $SelectedResourceGroups} | Select-Object -Property &quot;NSG&quot; -Unique | Out-GridView -Title &quot;Select required NSG(s)&quot; -PassThru).NSG
$SelectedDates = ($AvailSelection | Where-Object {$_.ResourceGroup -in $SelectedResourceGroups -and $_.NSG -in $selectedNSGs} | Select-Object -Property @{n=&quot;System DateTime&quot;;e={$_.Date}},@{n=&quot;Local DateTime&quot;;e={[System.TimeZoneInfo]::ConvertTimeFromUtc((Get-Date $_.Date),$TZ)}} -Unique | Sort-Object 'System DateTime' -Descending| Out-GridView -Title &quot;Select required times (1 hour blocks)&quot; -PassThru).'System DateTime'

#Loop though blobs and download any that meet the specified selection and are newer than those already downloaded
foreach ($blob in $blobs) {
    $PathSplit = $blob.Name.Split(&quot;/&quot;)
    $blobdate = get-date -Year $PathSplit[9].Substring(2) -Month $PathSplit[10].Substring(2) -Day $PathSplit[11].Substring(2) -Hour $PathSplit[12].Substring(2) -Minute $PathSplit[13].Substring(2) -Second 0 -Millisecond 0
    if ($PathSplit[4] -in $SelectedResourceGroups -and $PathSplit[8] -in $selectedNSGs -and $blobdate -in $SelectedDates) {
        $fld = $blob.name.Replace(&quot;/&quot;,&quot;\&quot;)
        $flds = $fld.Split(&quot;\&quot;) | Where-Object {$_ -ne &quot;PT1H.json&quot;}
        $lcl = &quot;$($LocalFolder)\$($fld)&quot;
        MakeFolders -Path $flds -root $LocalFolder
        if (Test-Path -Path $lcl) {
            $lclfile = Get-ItemProperty -Path $lcl
            $lcldate = (get-date $lclfile.LastWriteTimeUtc)
        } else {
            $lcldate = get-date &quot;1 Jan 1970&quot;
        }
        $blobdate = $blob.LastModified

        if ($blobdate -gt $lcldate) {
            Write-Output &quot;Copied`t$($blob.Name)&quot;
            Get-AzureStorageBlobContent -Container &quot;insights-logs-networksecuritygroupflowevent&quot; -Context $blob.Context -Blob $blob.Name -Destination $lcl -Force
        } else {
            Write-Output &quot;Leave`t$($blob.Name)&quot;
        }
    }
}

#Get a list of all the files in the specified local directory
$Files = dir -Path &quot;$($LocalFolder)\resourceId=\SUBSCRIPTIONS\$($SubscriptionID)\RESOURCEGROUPS\&quot; -Filter &quot;PT1H.json&quot; -Recurse

#Loop through local files and build $Events array up with files that meet the selected criteria.
$Events=@()

foreach ($file in $Files) {
    $PathSplit = ($file.DirectoryName.Replace(&quot;$($LocalFolder)\&quot;,'')).split(&quot;\&quot;)
    $blobdate = get-date -Year $PathSplit[9].Substring(2) -Month $PathSplit[10].Substring(2) -Day $PathSplit[11].Substring(2) -Hour $PathSplit[12].Substring(2) -Minute $PathSplit[13].Substring(2) -Second 0 -Millisecond 0
    $blobResourceGroup = $PathSplit[4]
    $blobNSG = $PathSplit[8]
    if ($blobResourceGroup -in $SelectedResourceGroups -and $blobNSG -in $selectedNSGs -and $blobdate -in $SelectedDates) {
        $TestFile = $file.FullName
        $json = Get-Content -Raw -Path $TestFile | ConvertFrom-Json
        foreach ($entry in $json.records) {
            $time = (get-date $entry.time).ToUniversalTime()
            $time = [System.TimeZoneInfo]::ConvertTimeFromUtc($time, $TZ)
            $FlowVer = $entry.properties.version
            foreach ($flow in $entry.properties.flows) {
                $rules = $flow.rule.split(&quot;_&quot;)
                $RuleType = $rules[0]
                $RuleName = $rules[1]
                foreach ($f in $flow.flows) {
                    if ($FlowVer -eq 2) {
                        $Header = &quot;Ref&quot;,&quot;SrcIP&quot;,&quot;DstIP&quot;,&quot;SrcPort&quot;,&quot;DstPort&quot;,&quot;Protocol&quot;,&quot;Direction&quot;,&quot;Type&quot;,&quot;State&quot;,&quot;SentPackets&quot;,&quot;SentBytes&quot;,&quot;RecvPackets&quot;,&quot;RecvBytes&quot;
                        $o = $f.flowTuples | ConvertFrom-Csv -Delimiter &quot;,&quot; -Header $Header
                        $o = $o | select @{n='ResourceGroup';e={$blobResourceGroup}},@{n='NSG';e={$blobNSG}},Ref,SrcIP,DstIP,SrcPort,DstPort,Protocol,Direction,Type,State,SentPackets,SentBytes,RecvPackets,RecvBytes,@{n='Time';e={$time}},@{n=&quot;RuleType&quot;;e={$RuleType}},@{n=&quot;RuleName&quot;;e={$RuleName}}
                        $Events += $o
                    } else {
                        $Header = &quot;Ref&quot;,&quot;SrcIP&quot;,&quot;DstIP&quot;,&quot;SrcPort&quot;,&quot;DstPort&quot;,&quot;Protocol&quot;,&quot;Direction&quot;,&quot;Type&quot;
                        $o = $f.flowTuples | ConvertFrom-Csv -Delimiter &quot;,&quot; -Header $Header
                        $o = $o | select @{n='ResourceGroup';e={$blobResourceGroup}},@{n='NSG';e={$blobNSG}},Ref,SrcIP,DstIP,SrcPort,DstPort,Protocol,Direction,Type,@{n='Time';e={$time}},@{n=&quot;RuleType&quot;;e={$RuleType}},@{n=&quot;RuleName&quot;;e={$RuleName}}
                        $Events += $o
                    }
                }
            }
        }
    }
}
#Open $Events in GridView for user to see and filter as required
$Events | Sort-Object Time -Descending | Out-GridView

#Prompt to export to CSV.
$CSVExport = read-host &quot;Do you want to export to excel? (Y/N)&quot;
#If CSV required, export and open CSV file
if ($CSVExport.ToUpper() -eq &quot;Y&quot;) {
    $FileName = &quot;$($LocalFolder)\NSGFlowLogs-$(Get-Date -Format &quot;yyyyMMddHHmm&quot;).csv&quot;
    $Events | Sort-Object Time -Descending | export-csv -Path $FileName -NoTypeInformation
    Start-Process &quot;Explorer&quot; -ArgumentList $FileName 
}
[/sourcecode]

<strong>Important Note:</strong>
The Azure Network Watcher team has provided a release note (<a href="https://azure.microsoft.com/en-us/updates/azure-network-watcher-network-security-group-flow-logs-blob-storage-path-update/">https://azure.microsoft.com/en-us/updates/azure-network-watcher-network-security-group-flow-logs-blob-storage-path-update/</a>) detailing that the path of the Flow Logs in the storage account will change on July 31st 2017.
Once this is released, I will update my script, if required, to cater for the change.]]></content:encoded>
    </item>
    <item>
      <title>Get Azure PaaS Endpoint IPs</title>
      <link>https://docs.microsoft.com/archive/blogs/jrt/get-azure-paas-endpoint-ips</link>
      <pubDate>Mon, 03 Jul 2017 14:30:16 GMT</pubDate>
      <dc:creator><![CDATA[James R Tabor]]></dc:creator>
      <guid
        isPermaLink="false">https://blogs.msdn.microsoft.com/jrt/?p=645</guid>
      <description><![CDATA[I've had a few situations where people are trying to find out the public IP Addresses for their own...]]></description>
      <content:encoded><![CDATA[I've had a few situations where people are trying to find out the public IP Addresses for their own resources in Azure to use for Network Security Groups or User Defined Routes.

Unfortunately there is nothing generic out there that can return this information for every Resource Type, so specific code needs to be developed for each type.
I've made a start on this for Storage Accounts and Azure SQL Servers, being two of the most popular ones.

You can use the function(s) below to return an array containing the IP, Location and Resource Type for all Storage Accounts and SQL Servers in a given subscription.
If you have another Resource Type you need to resolve public IPs for, add the required code under the '<em>switch ($Res.ResourceTypes)</em>' section.
Please feel free to drop me a line if you need help working out how to get the Public IP for different resource types.

[Updated: 13 Nov 2017]: Updated to include KeyVault and added function parameter switches to filter which Resource Types will be returned.

[sourcecode language="powershell"]
function Get-FQDNIP (
    [string]$FQDN,
    [object]$Resource
) {
    $IPDetails = [System.Net.Dns]::GetHostAddresses($FQDN)
    $IP = New-Object PSObject -Property @{
        IP = $IPDetails.IPAddressToString
        Locn = $Resource.Location
        Type = $Resource.ResourceType
    }
    Write-Verbose &quot;`t$($IP)&quot;
    return $IP
}
 
 
function Get-PaaSIPs (
    [string]$SubscriptionID,
    [switch]$StorageAccounts,
    [switch]$SQLServers,
    [switch]$KeyVaults
) {    
    Select-AzureRmSubscription -SubscriptionId $SubscriptionID | Out-Null
    $AllRes = Get-AzureRmResource
    $IPs = @()
    $CurrType = &quot;&quot;
    $AllRes = $AllRes | Sort-Object ResourceType
    if (!$StorageAccounts -and !$SQLServers -and !$KeyVaults) {
        $FilteredRes = $AllRes
    }
    if ($StorageAccounts) {
        $FilteredRes = $AllRes | Where-Object {$_.ResourceType -eq &quot;Microsoft.Storage/storageAccounts&quot;}
    }
    if ($SQLServers) {
        $FilteredRes += $AllRes | Where-Object {$_.ResourceType -eq &quot;Microsoft.Sql/servers&quot;}
    }
    if ($KeyVaults) {
        $FilteredRes += $AllRes | Where-Object {$_.ResourceType -eq &quot;Microsoft.KeyVault/vaults&quot;}
    }
    ForEach ($Res in $FilteredRes) {
        if ($CurrType -ne $Res.ResourceType) {
            $CurrType = $Res.ResourceType
            Write-Verbose &quot;$($Res.ResourceType)&quot;
        }
        switch ($Res.ResourceType) {
            &quot;Microsoft.Storage/storageAccounts&quot; {
                $ResDet = Get-AzureRmStorageAccount -Name $Res.ResourceName -ResourceGroupName $Res.ResourceGroupName
                $Blob = $null; $Blob = $ResDet.PrimaryEndpoints.Blob
                $File = $null; $File = $ResDet.PrimaryEndpoints.File
                $Table = $null; $Table = $ResDet.PrimaryEndpoints.Table
                $Queue = $null; $Queue = $ResDet.PrimaryEndpoints.Queue
                 
                if ($Blob -ne $null) {
                    $Blob = $Blob.Substring(8,$Blob.Length-9)
                    $IP = Get-FQDNIP -FQDN $Blob -Resource $Res
                    if ($IP.IP -notin $IPs.IP) {
                        $IPs += $IP
                    }
                }
                if ($File -ne $null) {
                    $File = $File.Substring(8,$File.Length-9)
                    $IP = Get-FQDNIP -FQDN $File -Resource $Res
                    if ($IP.IP -notin $IPs.IP) {
                        $IPs += $IP
                    }
                }
                if ($Table -ne $null) {
                    $Table = $Table.Substring(8,$Table.Length-9)
                    $IP = Get-FQDNIP -FQDN $Table -Resource $Res
                    if ($IP.IP -notin $IPs.IP) {
                        $IPs += $IP
                    }
                }
                    if ($Queue -ne $null) {
                    $Queue = $Queue.Substring(8,$Queue.Length-9)
                    $IP = Get-FQDNIP -FQDN $Queue -Resource $Res
                    if ($IP.IP -notin $IPs.IP) {
                        $IPs += $IP
                    }
                }
            }
            &quot;Microsoft.Sql/servers&quot; {
                $IP = Get-FQDNIP -FQDN &quot;$($Res.ResourceName).database.windows.net&quot; -Resource $Res
                if ($IP.IP -notin $IPs.IP) {
                    $IPs += $IP
                }
            }
            &quot;Microsoft.KeyVault/vaults&quot; {
                #Write-Output &quot;`t$($Res.Name)&quot;
                $Vault = Get-AzureRmKeyVault -VaultName $Res.Name -ResourceGroupName $Res.ResourceGroupName
                $VaultFQDN = $Vault.VaultUri.Substring(8,$Vault.VaultUri.Length-9)
                $IP = Get-FQDNIP -FQDN $VaultFQDN -Resource $Res
                if ($IP.IP -notin $IPs.IP) {
                    $IPs += $IP
                }
            }
            default {
                #Write-Verbose &quot;`t$($Res.Name)&quot;
                #Write-Verbose &quot;`t`t$($Res.ResourceType)&quot;
            }
        }
    }
    return $IPs | Sort-Object Locn, IP, Type
}
[/sourcecode]
]]></content:encoded>
    </item>
    <item>
      <title>Log results from Azure Automation Runbooks directly to OMS</title>
      <link>https://docs.microsoft.com/archive/blogs/jrt/log-results-from-an-azure-automation-runbook-to-oms</link>
      <pubDate>Wed, 21 Jun 2017 00:18:23 GMT</pubDate>
      <dc:creator><![CDATA[James R Tabor]]></dc:creator>
      <guid
        isPermaLink="false">https://blogs.msdn.microsoft.com/jrt/?p=625</guid>
      <description><![CDATA[An easy way to keep track of Azure Automation runbook operations is to create log entries to OMS...]]></description>
      <content:encoded><![CDATA[An easy way to keep track of Azure Automation runbook operations is to create log entries to OMS during execution.
<a href="https://docs.microsoft.com/en-us/azure/log-analytics/log-analytics-overview">https://docs.microsoft.com/en-us/azure/log-analytics/log-analytics-overview</a>

There is a <a href="https://docs.microsoft.com/en-au/azure/log-analytics/log-analytics-data-collector-api">Rest API</a> service as part of OMS that let's you post log entries directly into your OMS Workspace, allowing for easy searching and alerting of anything you're runbooks do.  There are some code samples provided, in the link above, that allow you to build your own functions to use this API, or you can use the hard work someone else has done and import the <a href="https://www.powershellgallery.com/packages/OMSIngestionAPI">OMSIngestionAPI</a> module, which is what I have done.

Before you start, you will need to have your OMS Workspace Name, ID, and key available.  You can get these from the OMS Workspace settings page.
I recommend using (and will use in this post) Azure KeyVault to hold your OMS Workspace Key securely and allow for any key rotation or changes in a programmatic way.  Have a look at <a href="https://docs.microsoft.com/en-us/azure/key-vault/key-vault-get-started">Getting Started - Azure KeyVault</a> to get one set up if you haven't got one already.

Here's the code I used to get things setup and ready for use.
This creates Automation variables for OMSWorkspaceName, OMSWorkspaceID and KeyVaultName.
Also created is a KeyVault secret for the OMS Workspace Key, granting the Automation RunAs account rights to get secrets from your KeyVault.

[sourcecode language="powershell"]
$AutomationAccountName = &quot;{My Automation Account Name}&quot;
$ResourceGroupName = &quot;{My Automation Account Resource Group Name}&quot;
$KeyVaultName = &quot;{My KeyVault Name}&quot;
$KeyVaultResourceGroupName = &quot;{My KeyVault Resource Group Name}&quot;
$OMSWorkspaceName = &quot;{My OMS Workspace Name}&quot;
$OMSWorkspaceID = &quot;{My OMS Workspace ID}&quot;
$OMSWorkspaceKey_Raw = &quot;{My OMS Workspace Key}&quot;
$OMSWorkspaceKey = convertto-securestring ($OMSWorkspaceKey_Raw)  -asplaintext -force 

New-AzureRmAutomationVariable -Name &quot;OMSWorkspaceName&quot; -Value $OMSWorkspaceName -AutomationAccountName $AutomationAccountName -ResourceGroupName $ResourceGroupName -Encrypted $false
New-AzureRmAutomationVariable -Name &quot;OMSWorkspaceID&quot; -Value $OMSWorkspaceID -AutomationAccountName $AutomationAccountName -ResourceGroupName $ResourceGroupName -Encrypted $false
New-AzureRmAutomationVariable -Name &quot;KeyVaultName&quot; -Value $KeyVaultName -AutomationAccountName $AutomationAccountName -ResourceGroupName $ResourceGroupName -Encrypted $false
Set-AzureKeyVaultSecret -VaultName $KeyVaultName -Name &quot;OMSWorkspaceKey-$($OMSWorkspaceName)&quot; -SecretValue $OMSWorkspaceKey

$RunAsConn = Get-AzureRmAutomationConnection -Name 'AzureRunAsConnection' -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName
Set-AzureRmKeyVaultAccessPolicy -VaultName $KeyVaultName -ServicePrincipalName $RunAsConn.FieldDefinitionValues.ApplicationId -ResourceGroupName $KeyVaultResourceGroupName -PermissionsToSecrets get
[/sourcecode]

Now, to log information to OMS you need to pass a json payload and a LogType.  I have some script that I add to the top of all my scripts to do this.
You will notice I've include various data types so as to ensure the log data is formatted correctly, I've borrowed this from the <a href="https://docs.microsoft.com/en-au/azure/log-analytics/log-analytics-data-collector-api">Rest API reference</a>, so please look the 'Record type and properties' section for all the details.
For example, if it's a number the use _d (double) at the end of the parameter so no quotes are placed around the value.

[code language="powershell"]
#Retrive the RunAs Connection details
$Conn = Get-AutomationConnection -Name 'AzureRunAsConnection'

#Login with the RunAs Account
Add-AzureRMAccount -ServicePrincipal -Tenant $Conn.TenantID `
    -ApplicationId $Conn.ApplicationID `
    -CertificateThumbprint $Conn.CertificateThumbprint `
    -SubscriptionId $Conn.SubscriptionId

#Retrieve KeyVault Name to get secure variables (Namely, the OMS Workspace Key)
$KeyVaultName = Get-AutomationVariable -Name &quot;KeyVaultName&quot;

#Get OMS Workspace details
$OMSWorkspaceID = $null
$OMSWorkspaceName = Get-AutomationVariable -Name &quot;OMSWorkspaceName&quot;
$OMSWorkspaceID = Get-AutomationVariable -Name &quot;OMSWorkspaceID&quot;
if ($OMSWorkspaceID -ne $null) {
    $OMSKey = (Get-AzureKeyVaultSecret -VaultName $KeyVaultName -Name &quot;OMSWorkspaceKey-$($OMSWorkspaceName)&quot;).SecretValueText
    $LogType = &quot;Runbook_AutoshutdownFinal&quot;
} else {
    $OMSKey = $null
    $LogType = $null
}

#Set the LogType to use when posting to OMS
$LogType = &quot;OMSLogTest&quot;

function AddToLog ([Object]$OMSLogData) {

    if ($OMSWorkspaceID -ne $null) {
        $LogData = ''
        foreach ($Key in $OMSLogData.Keys) {
            switch ($Key.Substring($Key.Length-2)) {
                '_s' {$sep = '&quot;';$trim=$Key.Length-2}
                '_t' {$sep = '&quot;';$trim=$Key.Length-2}
                '_b' {$sep = '';$trim=$Key.Length-2}
                '_d' {$sep = '';$trim=$Key.Length-2}
                '_g' {$sep = '&quot;';$trim=$Key.Length-2}
                default {$sep = '&quot;';$trim=$Key.Length}
            }
            $LogData = $LogData + '&quot;' + $Key.Substring(0,$trim) + '&quot;:' + $sep + $OMSLogData.Item($Key) + $sep + ','
        }
        $TimeStamp = Get-Date (Get-Date).ToUniversalTime()
        $LogData = $LogData + '&quot;TimeStamp&quot;:&quot;' + $timestamp + '&quot;'

        Write-Verbose &quot;LogData: $($LogData)&quot;
        $json = &quot;{$($LogData)}&quot;

        $PostResult = Send-OMSAPIIngestionFile -customerId $OMSWorkspaceID -sharedKey $OMSKey -body $json -logType $LogType -TimeStampField &quot;TimeStamp&quot;
        Write-Verbose &quot;PostResult: $($PostResult)&quot;
        if ($PostResult -ne &quot;Accepted&quot;) {
            Write-Error &quot;Error posting to OMS - $PostResult&quot;
        }
    }
}
[/code]

So whenever you want to log anything you would build a hashtable of the data you want to log and pass that to the AddToLog function.
For example, if you were to log information from a runbook that starts and stops VMs you would use something like this -

[code language="powershell"]
$VMName = &quot;MyVM1&quot;
$ResourceGroupName = &quot;MyResourceGroup&quot;
$SubscriptionID = &quot;7d94a8a1-12c4-1234-aaaa-xxxxxxxxxxxx&quot;
$VMState = Get-AzureRmVM -Name $VMName -ResourceGroupName $ResourceGroupName -Status
if ($VMState.Statuses[1].DisplayStatus -eq &quot;VM running&quot;) {
    Stop-AzureRmVM -Name $VMName -ResourceGroupName $ResourceGroupName -Force
    $Action = &quot;Stop VM&quot;
} else {
    $Action = &quot;VM Already Stopped&quot;
}

$LogData = @{
    Trigger_s = &quot;Scheduled&quot;
    VMName_s = $VMName
    SubscriptionGUID_g = $SubscriptionId
    ResourceGroup_s = $ResourceGroupName
    TargetState_s = &quot;VM dealloacted&quot;
    CurrentSatet_s = $($VMState.Statuses[1].DisplayStatus)
    Action_s = $Action
}
AddToLog -OMSLogData $LogData
[/code]

Which will look something like this in the OMS logs

<a href="https://msdnshared.blob.core.windows.net/media/2017/06/Screenshot-20_06_2017-10_54_59-PM.png"><img src="https://msdnshared.blob.core.windows.net/media/2017/06/Screenshot-20_06_2017-10_54_59-PM-400x269.png" alt="" width="400" height="269" class="alignnone size-medium wp-image-635" /></a>]]></content:encoded>
    </item>
    <item>
      <title>Transition Azure Automation schedules from one Runbook to another</title>
      <link>https://docs.microsoft.com/archive/blogs/jrt/transition-azure-automation-schedules-from-one-runbook-to-another</link>
      <pubDate>Wed, 14 Jun 2017 23:40:10 GMT</pubDate>
      <dc:creator><![CDATA[James R Tabor]]></dc:creator>
      <guid
        isPermaLink="false">https://blogs.msdn.microsoft.com/jrt/?p=615</guid>
      <description><![CDATA[So you've spent a great deal of time setting up all the schedules for an Azure Automation Runbook...]]></description>
      <content:encoded><![CDATA[So you've spent a great deal of time setting up all the schedules for an Azure Automation Runbook but now you have a new version that you want to test against all the parameters for the schedules of the existing released version.

You've already created and published the new Runbook but what a pain if you've got more than a few different schedules with different parameters for each one.

Here is a script that can be used to run tests for each of the existing schedules of the existing Runbook with their parameters and then link the schedules and their parameters to the new one.
The test results get displayed in a notepad file for you to review.

Tests don't work?  Leave the old Runbook schedules as they are  and keep tweaking!

<a href="https://gallery.technet.microsoft.com/scriptcenter/Transition-Azure-f3a38c9a">https://gallery.technet.microsoft.com/scriptcenter/Transition-Azure-f3a38c9a</a>]]></content:encoded>
    </item>
    <item>
      <title>Export Available Azure VM Sizes for all locations</title>
      <link>https://docs.microsoft.com/archive/blogs/jrt/export</link>
      <pubDate>Tue, 16 May 2017 15:14:15 GMT</pubDate>
      <dc:creator><![CDATA[James R Tabor]]></dc:creator>
      <guid
        isPermaLink="false">https://blogs.msdn.microsoft.com/jrt/?p=215</guid>
      <description><![CDATA[I wanted to share a quick post with a script I seem to use all the time to get a spreadsheet with...]]></description>
      <content:encoded><![CDATA[I wanted to share a quick post with a script I seem to use all the time to get a spreadsheet with all the available VM sizes available for use in each region

Grab it from here --&gt;
<a href="https://gallery.technet.microsoft.com/scriptcenter/Export-Available-Azure-VM-35876745">Download script</a>]]></content:encoded>
    </item>
    <item>
      <title>Using Azure Automation and SharePoint Online lists to schedule VM start-up and shutdown</title>
      <link>https://docs.microsoft.com/archive/blogs/jrt/using-azure-automation-and-sharepoint-online-lists-to-schedule-vm-start-up-and-shutdown</link>
      <pubDate>Mon, 15 May 2017 17:37:14 GMT</pubDate>
      <dc:creator><![CDATA[James R Tabor]]></dc:creator>
      <guid
        isPermaLink="false">https://blogs.msdn.microsoft.com/jrt/?p=115</guid>
      <description><![CDATA[To make the most efficient use of Public cloud you need to ensure your servers are only running when...]]></description>
      <content:encoded><![CDATA[To make the most efficient use of Public cloud you need to ensure your servers are only running when they need to be.
Using SharePoint online as the source of virtual machine start-up and shutdown schedules is an option that allows for an easy, consolidated view of all your VMs and their schedules plus allows for the granular allocation of permissions for your staff, without having to grant them any Azure fabric access.

I'm going to assume some familiarity with SharePoint Online custom lists and Azure Automation principles for this blog post.
If you need some help getting started with these, try the following links.
<a target="_blank" href="https://blogs.technet.microsoft.com/catastrophic_failure_joannav/2013/10/23/how-to-create-a-custom-list-in-sharepoint-online-w15/">How to create a Custom List in SharePoint Online</a>
<a target="_blank" href="https://docs.microsoft.com/en-us/azure/automation/automation-offering-get-started">Getting Started with Azure Automation</a>

To integrate SharePoint Online with Azure Automation (and Powershell scripts), I've used a fantastic module written by <a target="_blank" href="https://www.powershellgallery.com/profiles/tao.yang/">Tao Yang</a>, SharepointSDK.
The details can be found <a target="_blank" href="https://www.powershellgallery.com/packages/SharePointSDK/2.1.5">here</a>.

To get set up for using SharePoint Online and Azure Automation, please refer to my Blog post here --&gt; <a href="https://blogs.msdn.microsoft.com/jrt/2017/04/07/setting-it-up-sharepoint-online-lists-with-azure-automation/">https://blogs.msdn.microsoft.com/jrt/2017/04/07/setting-it-up-sharepoint-online-lists-with-azure-automation/</a>
<ul>
 	<li>Create the SharePoint Custom list for your VMS
The columns you'll need are
<ul>
 	<li>VMName {Renamed Title column} - 'Single line of text'
This will be the Name of your VM in Azure</li>
 	<li>SubscriptionGUID - 'Single line of text'
The Azure Subscription GUID where the VM is situated</li>
 	<li>ResourceGroup - 'Single line of text'
The Resource Group Name of the VM</li>
 	<li>TimeZone - 'Choice'
A drop down list of the available Timezones for the shutdown times to be run against {<em>See note below</em>}</li>
 	<li>ShutdownTimeRange - 'Single line of text' or 'Choice'
This is a comma separated list of standard date formatted values using a ~ between times
i.e. '6AM~6PM,Saturday,Sunday' or '25 April, 25 December, 1 January'</li>
 	<li>OverRide-Start - 'Yes/No'
A checkbox indicating to force VM to be Started</li>
 	<li>OverRide-Stop - 'Yes/No'
A checkbox indicating to force VM to be Stopped {takes precedence over 'OverRide-Start'}</li>
</ul>
</li>
 	<li>Note: To create the list of available Timezones I used the following snippet of Powershell script -
<blockquote><code>ForEach ($TZ in ([System.TimeZoneInfo]::GetSystemTimeZones())) { Write-Output "$($TZ.DisplayName) [$($TZ.Id)]"}</code></blockquote>
</li>
 	<li>(Optional) Create the SharePoint Custom list for logging Automation Actions.
The columns you'll need are
<ul>
 	<li>VMName {Renamed Title column} - 'Single line of text'</li>
 	<li>SubscriptionGUID - 'Single line of text'</li>
 	<li>ResourceGroup - 'Single line of text'</li>
 	<li>AutoActionLog - 'Multiple lines of text'</li>
</ul>
</li>
 	<li>Create a script or Azure Automation runbook to populate the SharePoint List with your VMs</li>
</ul>
<p style="padding-left: 60px">The best way to populate your SharePoint list is to either run a Powershell script or create an Azure Automation Runbook to do that.
I've posted a script to do this here --&gt; <a target="_blank" href="https://gallery.technet.microsoft.com/scriptcenter/Load-Azure-VMs-into-a-7699d195">Load Azure VMs into a SharePoint Online List</a>
and an Automation Runbook that can be imported here --&gt; <a target="_blank" href="https://gallery.technet.microsoft.com/scriptcenter/Load-Azure-VMs-into-a-b95765bd">Load Azure VMs into a SharePoint Online List - Automation Runbook
</a></p>
<p style="padding-left: 60px">Alternatively you can just enter the required Virtual Machine details directly into the SharePoint List.</p>

<ul>
 	<li>Lastly, the actual automation script that does all the work.</li>
</ul>
<p style="padding-left: 60px"><a href="https://gallery.technet.microsoft.com/scriptcenter/Azure-Automation-Runbook-e506a1ec">Download Link</a> from TechNet.</p>
<p style="padding-left: 60px">The most complex piece to this is the TimeZone adjustments.
Azure Automation runs on UTC time so you need to make sure when you specify to shutdown a VM at 6pm on a Friday, you don't end up shutting down your VM at 8am on Friday, which is what happens using AST (Sydney, Australia) time!</p>
<p style="padding-left: 60px">I got myself lost when trying to make a small change that I had to stop all code development and actually write a Flow Chart of how script works.
I strongly suggest if you are trying to work out how this script does what it does, that you work with this Flowchart open alongside.
<a target="_blank" href="https://msdnshared.blob.core.windows.net/media/2017/05/AutoShutdown-Flowchart.pdf">AutoShutdown-Flowchart</a>  (Note: Please ping me if you want the Visio version of this to update any changes of your own)</p>

<ul>
 	<li style="padding-left: 30px">To have this run regularly to check all VMs for their required state simply add a schedule for the Runbook.
I would suggest running it at 5 minutes past the hour to ensure it picks up any machines correctly that need to be stopped or started.
The frequency of the schedule will depend on how many different timeframes you need to use across all your VMs.</li>
</ul>]]></content:encoded>
    </item>
  </channel>
</rss>