azureazure-cachingazure-managementazure-management-api

Create and Update Named Caches in Azure Managed Cache using Management API


I am attempting to create an Azure Managed Cache using PowerShell and the Azure Management API, this two pronged approach is required because the Offical Azure PowerShell Cmdlets only have very limited support for Creation and Update of Azure Managed Cache. There is however an established pattern for calling the Azure Management API from PowerShell.

My attempts at finding the correct API to call have been somewhat hampered by limited documentation on the Azure Managed Cache API. However after working my way through the cmdlets using both the source code and the -Debug option in PowerShell I have been able to find what appear to be the correct API endpoints, as such I have developed some code to access these endpoints.

However, I have become stuck after the PUT request has been accepted to the Azure API as subsequent calls to the Management API /operations endpoint show that the result of this Operation was Internal Server Error.

I have been using Joseph Alabarhari's LinqPad to explore the API as it allows me to rapidly itterate on a solution using the minimum possible code, so to execute the following code snippets you will need both LinqPad and the following extension in your My Extensions script:

public static X509Certificate2 GetCertificate(this StoreLocation storeLocation, string thumbprint) {
    var certificateStore = new X509Store(StoreName.My, storeLocation);
    certificateStore.Open(OpenFlags.ReadOnly);
    var certificates = certificateStore.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, false);
    return certificates[0];
}

The complete source code including the includes are available below:

The following settings are used throughout the script, the following variables will need to it for anyone who is following along using their own Azure Subscription ID and Management Certificate:

var cacheName = "amc551aee";
var subscriptionId = "{{YOUR_SUBSCRIPTION_ID}}";
var certThumbprint = "{{YOUR_MANAGEMENT_CERTIFICATE_THUMBPRINT}}";
var endpoint = "management.core.windows.net";
var putPayloadXml = @"{{PATH_TO_PUT_PAYLOAD}}\cloudService.xml"

First I have done some setup on the HttpClient:

var handler = new WebRequestHandler();
handler.ClientCertificateOptions = ClientCertificateOption.Manual;
handler.ClientCertificates.Add(StoreLocation.CurrentUser.GetCertificate(certThumbprint));
var client = new HttpClient(handler);
client.DefaultRequestHeaders.Add("x-ms-version", "2012-08-01");

This configures HttpClient to both use a Client Certificate and the x-ms-version header, the first call to the API fetches the existing CloudService that contains the Azure Managed Cache. Please note this is using an otherwise empty Azure Subscription.

var getResult = client.GetAsync("https://" + endpoint + "/" + subscriptionId + "/CloudServices");
getResult.Result.Dump("GET " + getResult.Result.RequestMessage.RequestUri);

This request is successful as it returns StatusCode: 200, ReasonPhrase: 'OK', I then parse some key information out of the request: the CloudService Name, the Cache Name and the Cache ETag:

var cacheDataReader = new XmlTextReader(getResult.Result.Content.ReadAsStreamAsync().Result);
var cacheData = XDocument.Load(cacheDataReader);
var ns = cacheData.Root.GetDefaultNamespace();
var nsManager = new XmlNamespaceManager(cacheDataReader.NameTable);
nsManager.AddNamespace("wa", "http://schemas.microsoft.com/windowsazure");

var cloudServices = cacheData.Root.Elements(ns + "CloudService");
var serviceName = String.Empty;
var ETag = String.Empty;
foreach (var cloudService in cloudServices) {
    if (cloudService.XPathSelectElements("//wa:CloudService/wa:Resources/wa:Resource/wa:Name", nsManager).Select(x => x.Value).Contains(cacheName)) {
        serviceName = cloudService.XPathSelectElement("//wa:CloudService/wa:Name", nsManager).Value;
        ETag = cloudService.XPathSelectElement("//wa:CloudService/wa:Resources/wa:Resource/wa:ETag", nsManager).Value;
    }
} 

I have pre-created a XML file that contains the payload of the following PUT request:

<Resource xmlns="http://schemas.microsoft.com/windowsazure">
  <IntrinsicSettings>
    <CacheServiceInput xmlns="">
      <SkuType>Standard</SkuType>
      <Location>North Europe</Location>
      <SkuCount>1</SkuCount>
      <ServiceVersion>1.3.0</ServiceVersion>
      <ObjectSizeInBytes>1024</ObjectSizeInBytes>
      <NamedCaches>
        <NamedCache>
          <CacheName>default</CacheName>
          <NotificationsEnabled>false</NotificationsEnabled>
          <HighAvailabilityEnabled>false</HighAvailabilityEnabled>
          <EvictionPolicy>LeastRecentlyUsed</EvictionPolicy>
        </NamedCache>
        <NamedCache>
          <CacheName>richard</CacheName>
          <NotificationsEnabled>true</NotificationsEnabled>
          <HighAvailabilityEnabled>true</HighAvailabilityEnabled>
          <EvictionPolicy>LeastRecentlyUsed</EvictionPolicy>
        </NamedCache>
      </NamedCaches>
    </CacheServiceInput>
  </IntrinsicSettings>
</Resource>

I construcuct a HttpRequestMessage with the above Payload and a URL comprised of the CloudService and Cache Names:

var resourceUrl = "https://" + endpoint + "/" + subscriptionId + "/cloudservices/" + serviceName + "/resources/cacheservice/Caching/" + cacheName;
var data = File.ReadAllText(putPayloadXml);
XDocument.Parse(data).Dump("Payload");
var message = new HttpRequestMessage(HttpMethod.Put, resourceUrl);
message.Headers.TryAddWithoutValidation("If-Match", ETag);
message.Content = new StringContent(data, Encoding.UTF8, "application/xml");
var putResult = client.SendAsync(message);
putResult.Result.Dump("PUT " + putResult.Result.RequestMessage.RequestUri);
putResult.Result.Content.ReadAsStringAsync().Result.Dump("Content " + putResult.Result.RequestMessage.RequestUri);

This request is nominally accepted by the Azure Service Management API as it returns a StatusCode: 202, ReasonPhrase: 'Accepted' response; this essentially means that the payload has been accepted and will be processed offline, the Operation ID can be parsed out of the HTTP Header to retreve further information:

var requestId = putResult.Result.Headers.GetValues("x-ms-request-id").FirstOrDefault();

This requestId can be used to request an update upon the status of the operation:

var operation = client.GetAsync("https://" + endpoint + "/" + subscriptionId + "/operations/" + requestId);
operation.Result.Dump(requestId);
XDocument.Load(operation.Result.Content.ReadAsStreamAsync().Result).Dump("Operation " + requestId);

The request to the /operations endpoint results in the following payload:

<Operation xmlns="http://schemas.microsoft.com/windowsazure" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
  <ID>5364614d-4d82-0f14-be41-175b3b85b480</ID>
  <Status>Failed</Status>
  <HttpStatusCode>500</HttpStatusCode>
  <Error>
    <Code>InternalError</Code>
    <Message>The server encountered an internal error. Please retry the request.</Message>
  </Error>
</Operation>

And this is where I am stuck, the chances are I am subtly malforming the request in such a way that the underlying request is throwing a 500 Internal Server Error, however without a more detailed error message or API documentation I don't think there is anywhere I can go with this.


Solution

  • We worked with Richard offline and the following XML payload got him un-blocked. Note - When adding/removing named cache to an existing cache, the object size is fixed.

    Note 2- The Azure Managed Cache API is sensitive to whitespace between the element and the element.

    Also please note, we are working on adding Named cache capability to our PowerShell itself, so folks don't have to use APIs to do so.

    <Resource xmlns="http://schemas.microsoft.com/windowsazure" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
      <IntrinsicSettings><CacheServiceInput xmlns="" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
          <SkuType>Standard</SkuType>
          <Location>North Europe</Location>
          <SkuCount>1</SkuCount>
          <ServiceVersion>1.3.0</ServiceVersion>
          <ObjectSizeInBytes>1024</ObjectSizeInBytes>
        <NamedCaches>
          <NamedCache>
            <CacheName>default</CacheName>
            <NotificationsEnabled>false</NotificationsEnabled>
            <HighAvailabilityEnabled>false</HighAvailabilityEnabled>
            <EvictionPolicy>LeastRecentlyUsed</EvictionPolicy>
            <ExpirationSettings>
              <TimeToLiveInMinutes>10</TimeToLiveInMinutes>
              <Type>Absolute</Type>
            </ExpirationSettings>
          </NamedCache>
          <NamedCache>
            <CacheName>richard</CacheName>
            <NotificationsEnabled>false</NotificationsEnabled>
            <HighAvailabilityEnabled>false</HighAvailabilityEnabled>
            <EvictionPolicy>LeastRecentlyUsed</EvictionPolicy>
            <ExpirationSettings>
              <TimeToLiveInMinutes>10</TimeToLiveInMinutes>
              <Type>Absolute</Type>
            </ExpirationSettings>
          </NamedCache>
        </NamedCaches>    
      </CacheServiceInput>
      </IntrinsicSettings>
    </Resource>