Query Azure Storage with PowerShell but without the SDK or Cmdlets

I had a need to query Azure Storage via PowerShell, but I was not guaranteed that the machines I'd be running from would have either the Azure SDK or the Azure PowerShell Cmdlets installed.

The script below (and in the attached .ZIP file) does not require the Azure SDK or the Azure PowerShell Cmdlets

The script below is a compilation of bits of information I found in many different sources.  I'll share links to many of the relevant places I gleaned information from.

What the script does

  1. Authenticates to the Azure management service using a certificate in a .PFX file
  2. It uses that to get the storage keys, then constructs the Authentication headers
    1. Please note that SharedKey Authentication header for Table Storage is different from the SharedKey header used by Queue and Blob Storage
  3. Loops through Blob, Queue, and Table storage to check if Metrics are enabled

Helpful Links

How to construct and hash a SharedKey header

https://blogs.msdn.com/b/rxg/archive/2009/04/02/accessing-azure-tables-via-rest.aspx

https://blog.einbu.no/2009/08/authenticating-against-azure-table-storage/

Documentation on SharedKey headers

https://msdn.microsoft.com/en-us/library/azure/dd179428.aspx

How to get Storage Account Keys

https://msdn.microsoft.com/en-us/library/azure/ee460785.aspx

Blob Service Properties (Table and Queue are basically the same)

https://msdn.microsoft.com/en-us/library/azure/hh452239.aspx

Storage Analytics

https://msdn.microsoft.com/en-us/library/azure/hh343270.aspx


##########################################################################
# Hard-coded Parameters
# set $proxyAddr = '' if you don't need a proxy server
##########################################################################

$mgmtUri = 'https://management.core.windows.net'
$msApiVer = '2012-08-01'
$ssApiVer = '2009-09-19'
$subId = '12345678-1234-1234-1234-123456789ABC'
$servName = 'qwerty'
$blobEP = 'https://qwerty.blob.core.windows.net/'
$queueEP = 'https://qwerty.queue.core.windows.net/'
$tableEP = 'https://qwerty.table.core.windows.net/'
$certPW = '< PFX File Password>'
$certPath = 'C:\mycert.pfx'
$proxyAddr = 'https://proxy:80'

##########################################################################
# Create a X509 Certificate from the byte array
##########################################################################
$flags = [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]"MachineKeySet"
$clientCert = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2 -ArgumentList $certPath, $certPW, $flags

##########################################################################
# Call the management service to get the storage keys
##########################################################################
$keysUri = $mgmtUri + '/' + $subId + '/services/storageservices/' + $servName + '/keys'
$mgmtWebRequest = [System.Net.HttpWebRequest]::Create($keysUri)
$mgmtWebRequest.Timeout = 15000
$mgmtWebRequest.ClientCertificates.Add($clientCert)
$mgmtWebRequest.Method = "GET"
$mgmtWebRequest.Headers.Add("x-ms-version", $msApiVer)
If ($proxyAddr -eq '')
{
  $mgmtWebRequest.Proxy = $null
}
Else
{
  $myWebProxy = New-Object System.Net.WebProxy($proxyAddr)
  $mgmtWebRequest.Proxy = $myWebProxy
}

##########################################################################
# Get the storage service keys from the response
##########################################################################
$response = $mgmtWebRequest.GetResponse()
$responseReader = New-Object System.IO.StreamReader($response.GetResponseStream())
[xml]$xmlResponseBody = $responseReader.ReadToEnd()
$responseReader.Close()
$primaryKey = $xmlResponseBody.StorageService.StorageServiceKeys.Primary

##########################################################################
# Create a hasher and seed it with the storage key
##########################################################################
$sharedKey = [System.Convert]::FromBase64String($primaryKey)
$myHasher = New-Object System.Security.Cryptography.HMACSHA256
$myHasher.Key = $sharedKey

##########################################################################
# Loop through the tables we want to query
##########################################################################
$myInvariantCulture = New-Object System.Globalization.CultureInfo("")
$storageEndpoints = $blobEP, $queueEP, $tableEP
$requestTail = '?restype=service&comp=properties'
ForEach ($endpoint in $storageEndpoints)
{
  $storageUri = $endpoint + $requestTail

##########################################################################
# Get the current date/time and format it properly
# The culture "" is the invariant culture
##########################################################################
$myDateObj = Get-Date
$strDate = $myDateObj.ToUniversalTime().ToString("R", $myInvariantCulture)

##########################################################################
# Preare the table HttpWebRequest
##########################################################################
$tableWebRequest = [System.Net.HttpWebRequest]::Create($storageUri)
$tableWebRequest.Timeout = 15000
$tableWebRequest.ContentType = "application/xml"
$tableWebRequest.Method = "GET"
$tableWebRequest.Headers.Add("x-ms-date", $strDate)
$tableWebRequest.Headers.Add("x-ms-version", $ssApiVer)
If ($proxyAddr -eq '')
{
  $tableWebRequest.Proxy = $null
}
Else
{
  $tableWebProxy = New-Object System.Net.WebProxy($proxyAddr)
  $tableWebRequest.Proxy = $tableWebProxy
}

##########################################################################
# Create the Authorization header
# Note that Table storage needs a different Authorization header
# than Blob and Queue need.
##########################################################################
If ($endpoint.Contains('.table.'))
{
  $strToSign = $tableWebRequest.Method + "`n" `
             + $tableWebRequest.Headers.Get("Content-MD5") + "`n" `
             + $tableWebRequest.Headers.Get("Content-Type") + "`n" `
             + $tableWebRequest.Headers.Get("x-ms-date") + "`n" `
             + '/' + $servName + '/' + '?comp=properties'
  $bytesToSign = [System.Text.Encoding]::UTF8.GetBytes($strToSign)
  $strSignedStr = [System.Convert]::ToBase64String($myHasher.ComputeHash($bytesToSign))
  $strAuthHeader = "SharedKey " + $servName + ":" + $strSignedStr
}
Else
{
  $canonicalizedHeader = 'x-ms-date:' + $tableWebRequest.Headers.Get("x-ms-date") + "`n" `
                       + 'x-ms-version:' + $tableWebRequest.Headers.Get("x-ms-version") + "`n"
  $canonicalizedResource = '/' + $servName + '/' + "`n" + 'comp:properties' + "`n" + 'restype:service'
  $strToSign = $tableWebRequest.Method + "`n" `
             + $tableWebRequest.Headers.Get("Content-Encoding") + "`n" `
             + $tableWebRequest.Headers.Get("Content-Language") + "`n" `
             + $tableWebRequest.Headers.Get("Content-Length") + "`n" `
             + $tableWebRequest.Headers.Get("Content-MD5") + "`n" `
             + $tableWebRequest.Headers.Get("Content-Type") + "`n" `
             + $tableWebRequest.Headers.Get("Date") + "`n" `
             + $tableWebRequest.Headers.Get("If-Modified-Since") + "`n" `
             + $tableWebRequest.Headers.Get("If-Match") + "`n" `
             + $tableWebRequest.Headers.Get("If-None-Match") + "`n" `
             + $tableWebRequest.Headers.Get("If-Unmodified-Since") + "`n" `
             + $tableWebRequest.Headers.Get("Range") + "`n" `
             + $canonicalizedHeader + $canonicalizedResource
  $bytesToSign = [System.Text.Encoding]::UTF8.GetBytes($strToSign)
  $strSignedStr = [System.Convert]::ToBase64String($myHasher.ComputeHash($bytesToSign))
  $strAuthHeader = "SharedKey " + $servName + ":" + $strSignedStr
}
$tableWebRequest.Headers.Add("Authorization", $strAuthHeader)

##########################################################################
# Read the results
# The response body changed between version 2012-02-12 and 2013-08-15
# which is why we check for Metrics and HourMetrics
# https://msdn.microsoft.com/en-us/library/azure/hh343258.aspx
##########################################################################
$tableResponse = $tableWebRequest.GetResponse()
$tableResponseReader = New-Object System.IO.StreamReader($tableResponse.GetResponseStream())
[xml]$tableResponseBody = $tableResponseReader.ReadToEnd()
$tableResponseReader.Close()
$metricsEnabled = $tableResponseBody.StorageServiceProperties.Metrics.Enabled
$hourMetricsEnabled = $tableResponseBody.StorageServiceProperties.HourMetrics.Enabled

If ($endpoint.Contains('.blob.'))
{
  If ($metricsEnabled -ne $null)
  {
    $blobEnabled = $metricsEnabled
  }
  If ($hourMetricsEnabled -ne $null)
  {
    $blobEnabled = $hourMetricsEnabled
  }
}

If ($endpoint.Contains('.queue.'))
{
  If ($metricsEnabled -ne $null)
  {
    $queueEnabled = $metricsEnabled
  }
  If ($hourMetricsEnabled -ne $null)
  {
    $queueEnabled = $hourMetricsEnabled
  }
}

If ($endpoint.Contains('.table.'))
{
  If ($metricsEnabled -ne $null)
  {
    $tableEnabled = $metricsEnabled
  }
  If ($hourMetricsEnabled -ne $null)
  {
    $tableEnabled = $hourMetricsEnabled
  }
}

##########################################################################
# Close the ForEach loop
##########################################################################
}

##########################################################################
# Write the results
##########################################################################
Write-Host 'Blob Metrics Enabled:' $blobEnabled
Write-Host 'Queue Metrics Enabled:' $queueEnabled
Write-Host 'Table Metrics Enabled: '$tableEnabled

CheckMetricsEnabled.zip