Error utilizando SMO y SQL Azure: “Index was outside the bounds of the array.”

Todas aquellas aplicaciones que estaban haciendo uso de SMO para trabajar con SQL Azure, habrán experimentado el siguiente error al intentar recuperar las bases de datos del servidor en la nube: “Index was outside the bounds of the array”. En el mes de Julio se anunció una nueva release de este servicio donde se añaden nuevas funcionalidades, así como actualizaciones en todos los data centers que prestan el servicio.

Debido a esto, el número de versión de las bases de datos se verá incrementado y tanto SQL Server Management Studio como SQL Server Management Studio Express pueden presentar problemas al intentar conectar con SQL Azure. En este caso, basta con instalar la última versión de los mismos:

Otra opción para continuar utilizando la herramientas de SQL Server sería bien instalar el Cumulative Update Package 7 for SQL Server 2008 R2 o bien acceder a Windows Update para instalar Microsoft SQL Server 2008 R2 Service Pack 1 (KB2528583).

Espero que haya sido de utilidad :D

¡Saludos!

SQL Azure Import and Export CTP

Hace pocos días se anunció una nueva CTP en el área de SQL Azure, dándonos la posibilidad de poder exportar e importar nuestras bases de datos. La forma de trabajar de estas herramientas es bastante sencilla: Recupera el esquema y los datos de la base de datos seleccionada y almacena el backup dentro de una cuenta de Windows Azure Storage.

Para ver los pasos vamos a realizar una prueba con una base de datos cualquiera, como Adventure Works :P Entramos en el portal de Windows Azure y accedemos al apartado de SQL Azure. A partir de ahora en el menú superior veremos una nueva sección donde aparecerán las acciones Import y Export.

Seleccionamos la suscripción y la base de datos que queremos exportar, en este caso AdventureWorksDWAZ2008R2, y hacemos clic en la opción Export que ahora aparece habilitada.

A partir de este momento se nos pedirán una serie de valores:

En el primer apartado debemos introducir el nombre de usuario y la password del administrador del servidor de SQL Azure para poder acceder a la base de datos seleccionada y recuperar el esquema. En el segundo apartado debemos introducir los valores de destino. New Blob URL se trata de la dirección donde se va a guardar el archivo final. Por ejemplo, una URL válida podría ser: http://gis.blob.core.windows.net/exports/adventureworks.bacpac. Tanto el import como el export trabajan con BACPAC, un nuevo formato de archivo que contiene tanto el esquema como los datos. Para que la exportación se realice con éxito, el container debe existir y el nombre el archivo no debe coincidir con ningún otro del contenedor, o lo que es lo mismo, este proceso no creará contenedores ni sobrescribirá archivos ya existentes. En cuanto a la key podemos utilizar la primary access key de nuestra cuenta de Windows Azure Storage o una clave compartida que hayamos generado previamente. Pulsamos en el botón Finish para que comience el proceso.

En este cuadro de diálogo nos informan que nuestra petición tiene asignado el ID que aparece en él y que es importante no modificar o acceder al nuevo archivo que se está generando en la cuenta de Windows Azure Storage. Por otro lado podemos ver el estado de las peticiones desde el portal. En ese caso, vamos a verlo :) Cerramos la ventana anterior y nos posicionamos sobre el servidor lógico que contiene la base de datos que acabamos de exportar. Al hacerlo, el icono Status se habilitará.

Al pulsar sobre él se nos pedirá de nuevo las credenciales de administrador del servidor y se mostrarán todas las peticiones de exportación e importación que se han realizado para el mismo.

Cuando la operación haya finalizado podremos recuperar el archivo de la cuenta de storage o hacer uso de él para la tarea contraria.

En el caso del import el proceso sería exactamente el mismo, posicionándonos sobre el servidor donde queremos importar el archivo BACPAC, hacemos clic sobre el botón Import para indicar tanto el nombre de la nueva base de datos como la URL donde se encuentra el backup.

Espero que haya sido de utilidad :D

¡Saludos!

Windows Azure Storage Analytics

El 3 de Agosto se anunció una nueva funcionalidad llamada Windows Azure Storage Analytics, la cual nos permitirá obtener trazas y métricas de una cuenta de Windows Azure Storage. Con esta nueva característica podremos obtener trazas de las peticiones, analizar el uso de nuestro almacenamiento así como diagnosticar posibles issues. Este servicio tiene un límite de 20TB, independientemente de los 100TB de nuestra cuenta, por lo que requerirá de algún tipo de mantenimiento que veremos después :D

Debido a que a día de hoy la librería Microsoft.WindowsAzure.StorageClient.dll no contiene aún esta mejora, voy a dedicar este post a mostraros cuál sería la forma de configurar tanto las métricas como las trazas en una cuenta de Windows Azure Storage. Para ello, he creado la siguiente aplicación, la cual iremos viendo paso a paso :)

Tanto el sistema de métricas como de logging debe ser configurado a nivel de servicio, es decir a nivel de blobs, tablas y queues. Los logs serán almacenados en blobs dentro de una nueva carpeta llamada $logs y las métricas hará uso del servicio tables, utilizando dos tablas por servicio, por ejemplo $MetricsTransactionsBlob y $MetricsCapacityBlob. La primera de las tablas guardará información sobre las transacciones relacionadas contra los blobs de la cuenta, la segunda nos informará del uso de los mismos.

Recuperación de la configuración de un servicio

La configuración de cada uno de los servicios del storage está representada a través de un XML.

<?xml version="1.0" encoding="utf-8"?>
<StorageServiceProperties>
  <Logging>
    <Version>1.0</Version>
    <Read>false</Read>
    <Write>false</Write>
    <Delete>false</Delete>
    <RetentionPolicy>
      <Enabled>false</Enabled>
    </RetentionPolicy>
  </Logging>
  <Metrics>
    <Enabled>true</Enabled>
    <Version>1.0</Version>
    <IncludeAPIs>true</IncludeAPIs>
    <RetentionPolicy>
      <Enabled>false</Enabled>
    </RetentionPolicy>
  </Metrics>
</StorageServiceProperties>

Este XML es indivisible, ya que no podemos enviar la configuración de logging sin especificar el apartado de las métricas y viceversa. Para recuperar este formato, he creado una serie de objectos para poder mapear cada uno de los elementos a su correspondiente propiedad. Si desglosamos dicho XML, obtendríamos 4 clases:

StorageServiceProperties

using System.Xml.Serialization;

namespace MvcWebRole.Models.Objects
{
    [XmlType(TypeName = "StorageServiceProperties")]
    public class StorageServiceProperties
    {
        public StorageServiceProperties()
        {
            Logging = new Logging();
            Metrics = new Metrics();
        }

        [XmlIgnore]
        public string ServiceName { get; set; }

        [XmlElement("Logging")]
        public Logging Logging { get; set; }

        [XmlElement("Metrics")]
        public Metrics Metrics { get; set; }
    }
}

Logging

using System.Xml.Serialization;

namespace MvcWebRole.Models.Objects
{
    [XmlType(TypeName = "Logging")]
    public class Logging
    {
        public Logging()
        {
            RetentionPolicy = new RetentionPolicy();
            Version = "1.0";
        }

        [XmlElement("Version")]
        public string Version { get; set; }

        [XmlElement("Read")]
        public bool IsReadEnabled { get; set; }

        [XmlElement("Write")]
        public bool IsWriteEnabled { get; set; }

        [XmlElement("Delete")]
        public bool IsDeleteEnabled { get; set; }

        [XmlElement("RetentionPolicy")]
        public RetentionPolicy RetentionPolicy { get; set; }
    }
}

Metrics

using System.Xml.Serialization;

namespace MvcWebRole.Models.Objects
{
    [XmlType(TypeName = "Metrics")]
    public class Metrics
    {
        public Metrics()
        {
            RetentionPolicy = new RetentionPolicy();
            Version = "1.0";
        }

        [XmlElement("Enabled")]
        public bool Enabled { get; set; }

        [XmlElement("Version")]
        public string Version { get; set; }

        [XmlIgnore]
        [XmlElement("IncludeAPIs")]
        public bool IncludeAPIs { get; set; }

        [XmlElement("RetentionPolicy")]
        public RetentionPolicy RetentionPolicy { get; set; }
    }
}

RetentionPolicy

using System.Xml.Serialization;

namespace MvcWebRole.Models.Objects
{
    [XmlType(TypeName = "RetentionPolicy")]
    public class RetentionPolicy
    {
        [XmlElement("Enabled")]
        public bool IsEnabled { get; set; }

        [XmlElement("Days")]
        public long Days { get; set; }
    }
}

Entraremos más en detalle en cada una de ellas cuando llegue el momento de elegir la configuración. Por el momento, lo más importante es ser capaces de recuperar lo que tenemos establecido antes de comenzar a utilizar el servicio (por defecto todo estará deshabilitado).

Gracias a estas clases seremos capaces de trabajar de una forma orientada a objetos, facilitando la compresión del código y minimizando las iteraciones a través del XML. ¿Cómo conseguimos recuperar la infomación dentro de nuestras clases?  Para este escenario he creado una aplicación ASP.NET MVC 3, la cual tendrá un controlador llamado SettingsController. En él vamos a tener las siguientes acciones para interactuar con el servicio:

using System.Web.Mvc;
using MvcWebRole.Models;
using MvcWebRole.Models.Implementations;
using MvcWebRole.Models.Interfaces;
using MvcWebRole.Models.Objects;

namespace MvcWebRole.Controllers
{
    public class SettingsController : Controller
    {
        private readonly IWindowsAzureStorageService _windowsAzureStorageService;
        public SettingsController()
        {
            _windowsAzureStorageService = new WindowsAzureStorageService("YourAccountName", "YourPrimaryKeyAccessKey");
        }

        [HttpGet]
        public ActionResult Index()
        {
            return View();
        }

        [HttpPost]
        public ActionResult Settings(string service)
        {
            return PartialView("_Settings", _windowsAzureStorageService.GetSettings(service));
        }

        [HttpPost]
        public ActionResult SetSettings(StorageServiceProperties storageServiceProperties)
        {
            _windowsAzureStorageService.SetSettings(storageServiceProperties);

            return RedirectToAction("Settings", new { service = storageServiceProperties.ServiceName });
        }
    }
}

Como cualquier buena aplicación con ASP.NET MVC que se precie, el controlador solamente se va a encargar de realizar las peticiones oportunas a una nueva clase llamada WindowsAzureStorageService la cual va a ser la encargada de realizar las peticiones a la plataforma. En este punto del post vamos a centrarnos en la acción Settings la cual recuperará los valores para el servicio que pasemos como parámetro al método GetSettings.

using System;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Xml;
using System.Xml.Linq;
using System.Xml.Serialization;
using Microsoft.WindowsAzure;
using MvcWebRole.Models.Interfaces;
using MvcWebRole.Models.Objects;

namespace MvcWebRole.Models.Implementations
{
    public class WindowsAzureStorageService : IWindowsAzureStorageService
    {
        private readonly StorageCredentialsAccountAndKey _credentials;
        public WindowsAzureStorageService(string accountName, string accountKey)
        {
            _credentials = new StorageCredentialsAccountAndKey(accountName, accountKey);
        }

        public StorageServiceProperties GetSettings(string serviceName)
        {
            var request = MakeRequest("http://{0}.{1}.core.windows.net/?restype=service&comp=properties", serviceName, "GET");

            var xmlResponse = XDocument.Parse(new StreamReader(request.GetResponse().GetResponseStream()).ReadToEnd());

            var xmlSerializer = new XmlSerializer(typeof(StorageServiceProperties));

            var reader = new StringReader(xmlResponse.ToString());

            var storageServiceProperties = (StorageServiceProperties)xmlSerializer.Deserialize(reader);

            storageServiceProperties.ServiceName = serviceName;

            return storageServiceProperties;
        }
[...]

Utilizando las credenciales pasadas a través del constructor de WindowsAzureStorageService, procedemos a realizar una petición a la siguiente URL: http://accountName.ServiceName.core.windows.net/?restype=service&comp=properties. En esta URL especificamos para qué servicio estamos solicitando la configuración. Para facilitar la compresión del código, se ha refactorizado el proceso en un método llamado MakeRequest que será útil tanto para recibir como enviar la configuración de nuestra cuenta.

private HttpWebRequest MakeRequest(string uri, string serviceName, string action = "PUT")
{
    var request = (HttpWebRequest)HttpWebRequest.Create(string.Format(uri, _credentials.AccountName, serviceName));
    request.Method = action;
    request.Headers.Add("x-ms-version", "2009-09-19");

    if (action == "GET")
    {
        if (serviceName == "table") _credentials.SignRequestLite(request);
        else _credentials.SignRequest(request);
    }

    return request;
}

En este método crearemos un objeto del tipo HttpWebRequest donde además añadiremos la cabecera x-ms-version con valor 2009-09-19. Cuando solicitamos una configuración, el Http Method a utilizar será GET, ya que queremos obtener un recurso alojando en algún sitio. Para que la petición sea aceptada de manera satisfactoria, firmaremos la misma utilizando el objeto StorageCredentialsAccountAndKey. El siguiente paso será realizar la petición. Como somos conscientes del resultado de la misma, se ha utilizado XDocumnet para parsear la respuesta como xml. Para finalizar, debemos deserializar el resultado haciendo uso de las clases creadas anteriormnete a través de un objeto XmlSerializer como se muestra en el código anterior.

El resultado será parecido a lo siguiente:

¡Perfecto! Ya tenemos la configuración actual lista para mostrarla por pantalla.

Asignar la configuración a un servicio

La recuperación de los datos no es nada preocupante, ya que sólamente debemos realizar una petición GET y esperar a que llegue el resultado ;) Cuando queremos especificar ciertos valores a nuestro storage la cosa se complica, aunque al final sólo bastará con tener en cuenta una serie de factores para crear una buena petición :D

Loggging

En el caso de los logs tenemos varias opciones disponibles:

  • delete
  • read
  • write

Estas opciones son acumulables y tomarán el valor de true o false para la activación o desactivación respectivamente.

Retention Policy

Storage Analytics no elimina los logs ni las métricas de manera automática. Tanto es así que si sobrepasamos los 20TB disponibles para este servicio, el mismo dejará de escribir hasta que exista espacio libre disponible. Existen dos formas de eliminar estos valores: Haciendo peticiones de forma manual para eliminar el contenido que deseemos, lo cual es un gasto facturable, o bien a través de la configuración de Retention policy. Con esta política podremos liberar espacio indicando el número de días de antigüedad de los logs y registros. Es decir, todos aquellos logs y registros que tengan más de X días de antigüedad serán eliminados de manera automática sin coste alguno para nosotros. El número máximo de días de retención es de 365 días.

Un punto a tener en cuenta respecto a esta política es que no podemos habilitarla si el número de días es inferior a uno. De ser así nuestra petición será denegada. Del mismo modo, si el elemento Enabled tiene valor false y especificamos un número de días la petición tampoco sería satisfactoria.

Metrics

En el caso de las métricas las opciones se reducen a dos:

  • Enabled
  • Retention Policy

El primer error que se comete cuando intentamos habilitar el servicio de métricas es darle únicamente valor al elemento Enabled. Una petición con este único valor positivo será denegada. El motivo es que necesitamos agregar un nuevo elemento llamado IncludeAPIs y asignarle el valor a true, mostrado anteriormente tanto en el XML de ejemplo como en la clase Metrics que utilizamos para el mapeo. Inicialmente este valor es ignorado puesto que sólo es necesario cuando el elemento Enabled es igual a true. Dicho esto, el apartado de las métricas de la petición debería ser igual al siguiente:

<?xml version="1.0" encoding="utf-8"?>
<StorageServiceProperties>
  <Logging>
    <Version>1.0</Version>
    <Read>false</Read>
    <Write>false</Write>
    <Delete>false</Delete>
    <RetentionPolicy>
      <Enabled>false</Enabled>
    </RetentionPolicy>
  </Logging>
  <Metrics>
    <Enabled>true</Enabled>
    <Version>1.0</Version>
    <IncludeAPIs>true</IncludeAPIs>
    <RetentionPolicy>
      <Enabled>false</Enabled>
    </RetentionPolicy>
  </Metrics>
</StorageServiceProperties>

Al igual que para la configuración de Logging, también podemos especificar una política de retención para las métricas. El sistema es exactamente el mismo. De hecho, la clase de mapeo utilizada para ambos campos, Loggging y Metrics, es RetentionPolicy.

Una vez que hemos aclarado todos los conceptos, veamos cuál sería el método para tramitar la petición:

[...]
        public void SetSettings(StorageServiceProperties storageServiceProperties)
        {
            var request = MakeRequest("http://{0}.{1}.core.windows.net/?restype=service&comp=properties", storageServiceProperties.ServiceName);

            using (var memoryStream = new MemoryStream())
            {
                var xmlSettings = new XmlWriterSettings { Indent = true };
                var xmlWriter = XmlWriter.Create(memoryStream, xmlSettings);

                //XmlIgnores
                var overrides = new XmlAttributeOverrides();

                if (storageServiceProperties.Metrics.Enabled)
                {
                    overrides.Add(typeof(Metrics), "IncludeAPIs", new XmlAttributes { XmlIgnore = false });
                    storageServiceProperties.Metrics.IncludeAPIs = true;
                }

                var xmlSerializer = new XmlSerializer(typeof(StorageServiceProperties), overrides);

                var xmlSerializerNamespaces = new XmlSerializerNamespaces();
                xmlSerializerNamespaces.Add("", ""); //remove namespaces

                xmlSerializer.Serialize(xmlWriter, storageServiceProperties, xmlSerializerNamespaces);

                memoryStream.Seek(0, SeekOrigin.Begin); //Start at the beggining

                var xml = new XmlDocument();
                xml.Load(memoryStream);

                //Delete nodes which have zero
                var days = xml.GetElementsByTagName("Days");
                for (var i = 0; i < days.Count; i++)
                {
                    if (int.Parse(days[i].InnerText) == 0)
                    {
                        var retentionPolicy = (days[i]).ParentNode.FirstChild;
                        retentionPolicy.InnerText = "false";
                        days[i].ParentNode.RemoveChild(days[i]);
                        i--;
                    }
                }

                memoryStream.SetLength(0); //reusing memory stream instance

                xml.Save(memoryStream); //Save changes

                request.ContentLength = memoryStream.Length;

                //Sign Request
                if (storageServiceProperties.ServiceName == "table")
                    _credentials.SignRequestLite(request);
                else
                    _credentials.SignRequest(request);

                using (var stream = request.GetRequestStream())
                {
                    memoryStream.Seek(0, SeekOrigin.Begin);
                    memoryStream.CopyTo(stream);
                }

                try
                {
                    using (var httpWebResponse = (HttpWebResponse)request.GetResponse())
                    {
                        var requestId = httpWebResponse.Headers["RequestIdHeaderName"];
                        Trace.TraceInformation("Request id:{0} Status: {1}", requestId, httpWebResponse.StatusCode);

                        if (HttpStatusCode.Accepted != httpWebResponse.StatusCode)
                            throw new Exception(string.Format("Request {0} failed! Status code: {1}", requestId, httpWebResponse.StatusCode));
                    }
                }
                catch (WebException ex)
                {
                    Trace.TraceError(new StreamReader(ex.Response.GetResponseStream()).ReadToEnd());
                }
            }
        }

El método SetSettings va a ser el encargado de gestionar el contenido recibido en la clase StorageServiceProperties. El primer paso será recuperar una petición genérica a través del método auxiliar MakeRequest pero en este caso aceptando el valor por defecto para el parámetro action, el método PUT, ya que queremos actualizar contenido en el destino. Para poder declarar los valores que queremos configurar, será necesario generar un contenido XML en función a los valores recibidos como parámetros. Si aprovechamos al máximo el objeto StorageServiceProperties, utilizaremos XmlSerializer pero en esta ocasión para serializar los valores que contiene. El mismo podrá ser modificado si:

  1. Las métricas van a ser habilitadas: Por lo que se sobreescribirá el atributo [XmlIgnore] de IncludeAPIs y se le añadirá el valor true a la propiedad para que figure en el XML resultante.
  2. Si existe algún valor a cero en los elementos Days de Retention Policy, tanto de Loggging como de Metrics, serán eliminados del XML y se modificará el valor del elemeto Enabled a false.

Con el XML generado ya estamos listos para firmar la petición y copiar contenido del memoryStream en la petición creada al principio del código. Dentro del bloque try se procederá al envío de nuestra nueva configuración. Si la misma no finalizara con el código 202 podríamos capturar la respuesta y visualizar cuál es el motivo del rechazo.

Adjunto el proyecto por si fuera de utilidad :D

¡Saludos!