Azure Storage Queues desde Perl

¿Que tal se llevan Perl y las Azure Storage Queues? ¡Mucho mejor de lo que parece a primera vista! Y en este artículo tienes un módulo Perl desarrollado por servidor para que puedas modificarlo y utilizarlo en tus propios desarrollos.

7 min de lectura
Azure Storage Queues desde Perl

Hace unas semanas os contaba sobre la sesión de Alberto y servidor en la Global Azure Bootcamp de Madrid, pero se me quedaban bastantes detalles técnicos en el tintero. Entre los temas que habíamos tratado era que el bot consultaba una cola que tenía asignada en Azure Storage para ver si tenía alguna orden que ejecutar. La siguiente imagen nos ilustra la parte a la que nos referimos.

azure_perl1

Como se puede ver, desde Alexa conectada con una Azure Functions, insertábamos órdenes en la cola, mientras que el bot -OpenKore- se encargaba de leerlas.

El primer problema con el que me encontré es que OpenKore está desarrollado en Perl, así que a la hora de programar un plugin para el bot, este también debía estar hecho en Perl... y no existe ningún SDK nativo de Perl para Azure Storage Queues, donde Microsoft da soporte oficial a .NET, Java, Node.js, C++. PHP, Python y Ruby. ¿Estaba sin opciones? ¡Ni mucho menos!

Aunque hay una excepción honrosa con el Azure SDK for Perl de CAPSiDE, advierten que el SDK es alpha-quality, y como lo que servidor quería llevar a cabo era muy específico, pensé que era mejor idea programarme mi propio módulo.

Llevando Azure Storage Queues a Perl

Si en algo brillan los distintos servicios de Azure, es que todos poseen una API REST que hace bastante sencillo operar con ellos desde cualquier lenguaje, siempre que este se maneje bien llamando a web services de este tipo.

Por su parte, Perl es un lenguaje interpretado de alto nivel, muy influenciado por C, C++, AWK, Lisp, sed y distintas shells de UNIX. Aunque su peculiar sintaxis le ha llevado en varias ocasiones a considerarsele un write-only language, he de decir que la experiencia de desarrollo que he tenido con él ha sido bastante grata.

Básicamente hay dos elementos fundamentales que me han hecho la vida muy fácil a la hora de aprender y trabajar con Perl para el tema que en este artículo comento:

  • Es un lenguaje de muy alto nivel, por lo que con pocas líneas de código podemos realizar tareas bastante complejas.
  • Dispone de un repositorio masivo de módulos llamado CPAN, con más de 180.000 bibliotecas de Perl que nos permiten hacer casi cualquier tarea imaginable.
  • Maneja servicios web, JSON y XML con muchísima soltura. He podido comprobar que Perl hace gala de su fama bien merecida sobre cómo de bueno es manejando cadenas de texto.

Talk is cheap, show me the code

Dicho y hecho, el aspecto del módulo a la hora de escribir estas líneas es el siguiente:

#!/usr/bin/perl
###############################################################################
# PROGRAM       : Azure Storage Queue API plugin for Perl
# DESCRIPTION   : Provides easy abstraction routines to work with commom
#                 Azure Storage Queues with Shared Key auth method.
# DATE          : 10/03/2019
# AUTHOR        : Carlos Milán Figueredo
# LICENSE       : MIT License
###############################################################################
# Copyright 2019 Carlos Milán Figueredo
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# 
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
###############################################################################
# ======== XML formal of Azure Storage Queue messages =======
# <?xml version="1.0" encoding="utf-8"?>
# <QueueMessagesList>
#     <QueueMessage>
#         <MessageId>91044a4d-5355-404a-9fac-e3d1991c0305</MessageId>
#         <InsertionTime>Sun, 10 Mar 2019 10:33:52 GMT</InsertionTime>
#         <ExpirationTime>Sun, 17 Mar 2019 10:33:52 GMT</ExpirationTime>
#         <PopReceipt>AgAAAAMAAAAAAAAAJXtUwSzX1AE=</PopReceipt>
#         <TimeNextVisible>Sun, 10 Mar 2019 10:33:52 GMT</TimeNextVisible>
#     </QueueMessage>
# </QueueMessagesList>
###############################################################################
# Subroutines expect the following hash reference with Storage Account data:
# my %azureStorageQueue = (
#    'AccountName' => $ENV{'STORAGE_ACCOUNT_NAME'},
#    'QueueName'   => $ENV{'STORAGE_QUEUE_NAME'},
#    'ApiVersion'  => '2018-03-28',
#    'AccountKey'  => $ENV{'STORAGE_ACCOUNT_KEY'},
#);
###############################################################################
# Subroutes expect a externally created REST::Client
###############################################################################
package Azure::StorageQueue;

use strict;
use REST::Client;
use Digest::SHA qw(hmac_sha256);
use Time::Piece;
use MIME::Base64;
use XML::LibXML;
use Data::Dumper;

#Test_PutMessagesInQueue($client, "c Prueba", 10);
#Test_GetMessagesInQueue($client, 10, 10);

sub Test_PutMessagesInQueue {
    my $storageAccountRef = shift;
    my $client = shift;
    my $message = shift;
    my $num_messages = shift;

    for (my $i=0; $i < $num_messages; $i++) {
        if(Put_AzureStorageQueueMessage($storageAccountRef, $client, "$message $i")) {
            print "$i: FAILED POST message '$message $i'\n";
        } else {
            print "$i: POST of message '$message'\n";
        }
    }
}

sub Test_GetMessagesInQueue {
    my $storageAccountRef = shift;
    my $client = shift;
    my $numofrequests = shift // 1;
    my $numofmessages = shift // 1;
    my @messages;

    for (my $i=0; $i <= $numofrequests; $i++) {
        @messages = Get_AzureStorageQueueMessages($storageAccountRef, $client, $numofmessages, 1);
        print "$i: Got a message\n";
        foreach(@messages) {
            print Dumper(\$_);
        }
    }
}

sub Get_AzureSignatureTime {
    my $t = gmtime(time);
    my $strftime = $t->strftime();
    $strftime =~ s/UTC/GMT/g;

    return $strftime;
}

sub Get_AzureAuthorizationSignature {
    my $storageAccountRef = shift;
    my $verb = shift // 'GET';
    my $strftime = shift;
    my $resource = shift // 'messages';

    my $canonicalizedHeaders = "x-ms-date:$strftime\nx-ms-version:$storageAccountRef->{'ApiVersion'}";
    my $canonicalizedResources = "/$storageAccountRef->{'AccountName'}/$storageAccountRef->{'QueueName'}/$resource";
    my $signatureString = "$verb\n\n\n\n$canonicalizedHeaders\n$canonicalizedResources";
    my $signature = encode_base64(hmac_sha256($signatureString, decode_base64($storageAccountRef->{'AccountKey'})));

    return $signature;
}

sub Set_AzureStorageRestHeaders {
    my $storageAccountRef = shift;
    my $client = shift;
    my $verb = shift;
    my $resource = shift;
    my $strftime = Get_AzureSignatureTime();
    my $signature = Get_AzureAuthorizationSignature($storageAccountRef, $verb, $strftime, $resource);

    $client->setHost("https://$storageAccountRef->{'AccountName'}.queue.core.windows.net");
    $client->addHeader('x-ms-date', $strftime);
    $client->addHeader('x-ms-version', $storageAccountRef->{'ApiVersion'});
    $client->addHeader('Authorization', "SharedKeyLite $storageAccountRef->{'AccountName'}:$signature");
    $client->setTimeout(10);

    return $client;
}

sub Get_AzureStorageQueueMessages {
    my $storageAccountRef = shift;
    my $client = shift;
    my $numofmessages = shift // 1;
    my $peekonly = shift // 0;
    my $dom;
    my $queueMessage;
    my %message = (
        'ExpirationTime' => '',
        'TimeNextVisible' => '',
        'DequeueCount' => '',
        'MessageText' => '',
        'MessageId' => '',
        'InsertionTime' => '',
        'PopReceipt' => '',
    );
    my @messagesArray;

    if ($numofmessages > 32) { $numofmessages = 32; }
    Set_AzureStorageRestHeaders($storageAccountRef, $client, 'GET');
    if($peekonly) {
        $client->request('GET', "/$storageAccountRef->{'QueueName'}/messages?numofmessages=$numofmessages&peekonly=true");
    } else {
        $client->request('GET', "/$storageAccountRef->{'QueueName'}/messages?numofmessages=$numofmessages");
    }
    if($client->responseCode() eq '200'){
        $dom = XML::LibXML->load_xml(string => $client->responseContent());
        foreach $queueMessage ($dom->findnodes('//QueueMessage')) {
            $message{'ExpirationTime'}=$queueMessage->findvalue('./ExpirationTime');
            $message{'TimeNextVisible'}=$queueMessage->findvalue('./TimeNextVisible');
            $message{'DequeueCount'}=$queueMessage->findvalue('./DequeueCount');
            $message{'MessageText'}=decode_base64($queueMessage->findvalue('./MessageText'));
            $message{'MessageId'}=$queueMessage->findvalue('./MessageId');
            $message{'InsertionTime'}=$queueMessage->findvalue('./InsertionTime');
            $message{'PopReceipt'}=$queueMessage->findvalue('./PopReceipt');
            push (@messagesArray, \%message);
        }
    }
    return @messagesArray;
}

sub Clear_AzureStorageQueueMessages {
    my $storageAccountRef = shift;
    my $client = shift;
    my $popReceipt = shift;

    Set_AzureStorageRestHeaders($storageAccountRef, $client, 'DELETE');
    $client->request('DELETE', "/$storageAccountRef->{'QueueName'}/messages");
    if ($client->responseCode() eq '204') {
        return 0;
    } else {
        return $client->responseContent();
    }
}

sub Delete_AzureStorageQueueMessage {
    my $storageAccountRef = shift;
    my $client = shift;
    my $messageId = shift;
    my $popReceipt = shift;

    Set_AzureStorageRestHeaders($storageAccountRef, $client, 'DELETE', "messages/$messageId");
    $client->request('DELETE', "/$storageAccountRef->{'QueueName'}/messages/$messageId?popreceipt=$popReceipt");
    if ($client->responseCode() eq '204') {
        return 0;
    } else {
        return $client->responseContent();
    }
}

sub Put_AzureStorageQueueMessage {
    my $storageAccountRef = shift;
    my $client = shift;
    my $messageContent = shift;
    my $base64messageContent = encode_base64($messageContent);
    my $queueMessage = "<QueueMessage>\n\t<MessageText>$base64messageContent</MessageText>\n</QueueMessage>";

    Set_AzureStorageRestHeaders($storageAccountRef, $client, 'POST');
    $client->request('POST', "/$storageAccountRef->{'QueueName'}/messages", $queueMessage);
    if($client->responseCode() eq '201') {
        return 0;
    } else {
        return $client->responseContent();
    }
}

1;

Siempre se puede consultar la última versión aquí. Como podéis ver, lo he liberado bajo licencia MIT por lo que podéis disponer de él como gustéis, con la única obligación de dar atribución.

Lo primero que podemos ver del código es que espera dos elementos previamente existentes:

  • Las funciones esperan como parámetros un objeto $client que es una instancia de REST::Client.
  • Una tabla hash con los datos de acceso a la Storage Account. Debemos tener en cuenta que estamos accediendo mediante el método de SharedKeys.

La tabla hash tiene el siguiente aspecto:

my %azureStorageQueue = (
    'AccountName' => $ENV{'STORAGE_ACCOUNT_NAME'},
    'QueueName'   => $ENV{'STORAGE_QUEUE_NAME'},
    'ApiVersion'  => '2018-03-28',
    'AccountKey'  => $ENV{'STORAGE_ACCOUNT_KEY'},
);

En este caso estamos tomando la mayoría de valores de acceso de variables de entorno, algo que es particularmente conveniente si vamos a trabajar con Docker.

Una vez hecho esto llamar a las operaciones del módulo es tan trivial como esta pieza de código que obtiene un único mensaje de la cola:

use strict;
use Azure::StorageQueue;
use REST::Client;

my $client = REST::Client->new();
my @messagesArray = Get_AzureStorageQueueMessages(\%azureStorageQueue, $client, 1, 0);

El módulo Perl se ocupará por nosotros de hacer la validación con la key para obtener el token, formar las cabeceras HTTP y llamar a las API de Azure Storage Queue en función de la operación que estemos realizando.

Los métodos del módulo

Haciendo un repaso rápido por los métodos del módulo tenemos:

  • Get_AzureSignatureTime.
    • Descripción: Obtiene la hora actual del sistema y la compone al formato que espera Azure en la cabecera HTTP.
    • Entradas: Ninguna.
    • Devuelve: Cadena de texto con la hora formateada.
  • Get_AuthorizationSignature.
    • Descripción: Obtiene la cadena de autorización a usar en la cabecera HTTP a partir de los datos facilitados por entrada.
    • Entradas: Hash de datos de la Storage Account, verbo HTTP, hora del sistema, recurso (por defecto "messages").
    • Devuelve: La cadena de autorización en base64.
  • Set_AzureStorageRestHeaders.
    • Descripción: Establece las cabeceras HTTP a utilizar en las llamadas a la API.
    • Entradas: Hash de datos de la Storage Account, el REST:Client, verbo HTTP, recurso (por defecto "messages").
    • Devuelve: El REST::Client.
  • Get_AzureStorageQueueMessages.
    • Descripción: Obtiene elementos de una cola de Azure Storage.
    • Entradas: Hash de datos de la Storage Account, el REST:Client, número de mensajes a returar (máximo 32), si sólo queremos mirarlos y no procesarlos.
    • Devuelve: Un array de tablas hash con los mensajes retirados.
  • Clear_AzureStorageQueueMessages.
    • Descripción: Elimina todos los elementos de la cola.
    • Entradas: Hash de datos de la Storage Account, el REST:Client.
    • Devuelve: 0 si la operación tuvo éxito.
  • Delete_AzureStorageQueueMessages.
    • Descripción: Elimina un mensaje de la cola.
    • Entradas: Hash de datos de la Storage Account, el REST:Client, el id del mensaje y el código de extración (Pop Receipt).
    • Devuelve: 0 si la operación tuvo éxito.
  • Put_AzureStorageQueueMessages.
    • Descripción: Inserta un mensaje en la cola.
    • Entradas: Hash de datos de la Storage Account, el REST:Client, el contenido del mensaje que queremos insertar.
    • Devuelve: 0 si la operación tuvo éxito.

Conclusiones

En muchas ocasiones no importa si Azure no dispone de un SDK oficial para trabajar con un determinado servicio, la disponibilidad de API REST para muchos de ellos hace que sea realmente fácil de integralos con nuestras soluciones.

En este caso, hacer que un programa Perl trabajase con Azure Storage Queues fue verdaderamente sencillo.

Happy Perling!