Localize Web Applications with Microsoft MVC


Localize Web Application with Microsoft MVC

Demo Solution Structure

Solution Structure

Modifications required to localize the application

ModificationsRequired

Presentation

As you can see, this is a standard MVC4 web application. To make it localizable, we have to take these actions.

Right click on the project name, click on Add, click on Add ASP.NET Folder, click on App_GlobalResources, this is required to make the MVC engine think it has global resources.

This action is just for create the folder. Leave it empty.

Modify the file RouteConfig.cs to add support for languages in the QueryString parameters. Leave like this:

public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Default",
url: "{lang}/{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional, lang = "en" }
);
}

We are telling route engine that, if there is not a language in the route, it has to add “en” by default. Otherwise, it has to add the supplied QueryString Param to the route.

We will come back to this project below. Leave it at this moment and let’s go to the Infrastructure layer.

Infrastructure.CrossCutting.Localization

In that layer, there is a project called Infrastructure.CrossCutting.Localization

LocalizationProject

As you can see, in there, there are several classes and 3 resx files. In these resx files we have to write all required strings.

ResourceFiles

The important thing here is to put the Resource Access Modifier to Public as shown.

After that, if you click on the Global (can be of the resx files in the project, and press F4 to see its properties, you can see these properties:

CodeGenerator

The important thing here is that the Build Action is selected as Embedded Resource and the Custom Tool is PublicResXFileCodeGenerator (which is selected when you put Access Modifier to Public at the last point). Another thing to see here is the Custom Tool Namespace used to make it directly readable by the MVC4 application without adding anything else. This is the namespace that will contain the auto generated code with the strings.

The next thing that we have to look at is the CultureHelpers class. This simple static class, only have one static method. This method is the responsible of changing the current culture on the current thread (as it is a web application the thread is regenerated between requests). This is the method we have to invoke to change language. We can see this invocation below.

The next class to read is the LocalizedDisplayName class. This class inherits from the DisplayNameAttribute to override the DisplayName attribute used in the models of the MVC4 application.

public class LocalizedDisplayNameAttribute : DisplayNameAttribute
{
private readonly string _resourceName;
public LocalizedDisplayNameAttribute(string resourceName)
{
_resourceName = resourceName;
}
public override string DisplayName
{
get { return Global.ResourceManager.GetString(_resourceName); }
}
}

Thanks to this override, where we were using

[Display(Name = "User name")]

Now we can use

[LocalizedDisplayName("USERNAME")]

And the application will retrieve the translation from the Global resx file based on the current culture.

Putting all together

You can see an example of this behavior on the view Account/Login. The definition of the Login Model, has changed to this (you can see it in Models/AccountModels):

public class LoginModel
{

[Required(ErrorMessageResourceType = typeof(Global), ErrorMessageResourceName = "REQUIRED")]
[LocalizedDisplayName("USERNAME")]
public string UserName { get; set; }

[Required(ErrorMessageResourceType = typeof(Global), ErrorMessageResourceName = "REQUIRED")]
[DataType(DataType.Password)]
[LocalizedDisplayName("PASSWORD")]
public string Password { get; set; }

[LocalizedDisplayName("REMEMBERME")]
public bool RememberMe { get; set; }

}

Note that in the new model class, we are using the Global Resources to translate both DisplayName and the Required Error Message.

We do not have to do anything else in the view, because with the razor helpers, the application will get the correct translation based on the current culture:

<ol>
<li>
@Html.LabelFor(m => m.UserName)
@Html.TextBoxFor(m => m.UserName)
@Html.ValidationMessageFor(m => m.UserName)
</li>
<li>
@Html.LabelFor(m => m.Password)
@Html.PasswordFor(m => m.Password)
@Html.ValidationMessageFor(m => m.Password)
</li>
<li>
@Html.CheckBoxFor(m => m.RememberMe)
@Html.LabelFor(m => m.RememberMe, new { @class = "checkbox" })
</li>
</ol>
<input type="submit" value="@Global.LOGIN" />

Note that we are introducing translations also in the cshtml file directly from the Global resources, right in the value of the input button. To achieve this, look at the first line of the view, it is using the Resources Namespace:

@using Resources

Finally, we have a class called SetCultureFilter. It is a class that inherits from ActionFilterAttribute. We will override the method OnActionExecuting to set the culture from the QueryString Params:

public class SetCultureFilter : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var culture = (string) filterContext.RouteData.Values["lang"];
CultureHelpers.SetCulture(culture);
base.OnActionExecuting(filterContext);
}
}

The job is almost done. We only have to add the attribute to the Controllers that we want to be auto translated and it will works. For example, if you open HomeController, you can see below the class, the attributed we created in the Localization dll:

[SetCultureFilter]
public class HomeController : Controller

This is telling the MVC4 engine to execute the specified culture filter for every action on the Controller (although we can restrict the filter and apply only to selected actions if required).

If you see the HomeController, you can see an action called ChangeLanguage. Well this action is a quick way to change the current language:

public ActionResult ChangeLanguage(int id)
{
switch (id)
{
case 1:
#if DEBUG
CultureHelpers.SetCulture("es"); // Duplicated code due to be used in unit tests as filter not executing in test, to be investigated
#endif
_currentCulture = "es";
break;
default:
#if DEBUG
CultureHelpers.SetCulture("en");
#endif
_currentCulture = "en";
break;
}
return RedirectToAction("Index", new {lang = _currentCulture});
}

Evidently, the duplicated code is not best way to do this, it is written this way just for the prototype.

To execute this action, you can open the shared view _LoginPartial. We had added to this list, two more items:

<li>@Html.ActionLink(@Global.LANGENGLISH, "ChangeLanguage", "Home", new { id = 0 }, null)</li>
<li>@Html.ActionLink(@Global.LANGSPANISH, "ChangeLanguage", "Home", new { id = 1 }, null)</li>

These two items execute the language change, and the method on the controller redirects to the home page.

Tests Driven

Additionally, you can see on the Tests layer, a couple of projects that show how to test the controllers (integration tests) and how to test the method (unit tests).

Conclusion:

After done all this actions, you can simply use

on the views the syntax @Global.KEY,

on the C# classes the syntax Global.KEY and

on the data models the syntax [LocalizedDisplayName(“KEY”)

or, i.e. [Required(ErrorMessageResourceType = typeof(Global), ErrorMessageResourceName = “KEY”)]

to retrieve a localized string.

Take a look also at _Layout.cshtml, it has this line:

<p>&copy; @DateTime.Now.ToShortDateString() – Translation Example</p>

At the footer. Well, if you run the application in Spanish, to demonstrate date format changes the format shown is dd/mm/yyyy but if you run the application in English, the format shown is mm/dd/yyyy. Validation of dates on the client side would require some more investigation.

You can download the source project here: https://github.com/victorxata/TranslatedWeb/.

Send emails from SQL Server


To use this feature from SQL Server, you can use the stored procedure sp_send_dbmail following the next steps:

Configure the Database Mail

Open, within the SQL Server Management Studio, the Database Mail item.

Do it with double click on it or with right click and selecting Configure Database Mail.

rightClick

It will start the wizard to configure the Database Mail.

Skip the first screen, and click on Next on the second screen to set up the service.

screen1

Click Yes to enable the feature:

confirmDialog

Write a name and a description for the new profile and click on Add to add a new SMTP account:

AddProfile

Fill all the needed fields and click Ok:

accountData

Click on Next to go to the next screen.

Check the checkbox on the column Public to make the profile public:

publicProfile

Click on Next to go to the next screen.

If you need it, change values like Retry Attempts, etc, on this screen, if not, simply click on Next to go to the next screen.

Parameters

Click on Finish and wait to end the feature activation and click on Close.

Now open a New Query window. We are going to set the Profile as default with this script:


EXECUTE msdb.dbo.sysmail_update_principalprofile_sp
 @profile_name = 'Test Profile',
 @principal_name = 'public',
 @is_default = 1 ;

Now we can write a, for instance, a trigger which sends an email in certain circumstancies. Let’s say we have a table called Names and we want to know when a record is updated. We can write a trigger like this one:

CREATE TRIGGER TESTEMAIL
 ON NAMES
 AFTER UPDATE
AS
BEGIN
 EXEC msdb.dbo.sp_send_dbmail
 @profile_name=N'Test_Email',
 @importance=N'High',
 @recipients=N'receiver_email@server.com',
 @subject=N'Testing email from db',
 @body=N'Names record updated.' ;

END
GO

That’s it. I hope it will help you.

More documentation about this Stored Procedure on:
http://msdn.microsoft.com/en-us/library/ms190307.aspx
http://technet.microsoft.com/en-us/library/ms175036(v=sql.105).aspx
http://msdn.microsoft.com/en-us/library/ms187891.aspx

Visit my profile on Google Plus

2012 in review


The WordPress.com stats helper monkeys prepared a 2012 annual report for this blog.

Here’s an excerpt:

This blog was viewed by people of more than 20 countries.

Click here to see the complete report.

Seguridad en aplicaciones desplegadas en Windows Azure


Política de seguridad en aplicaciones en Microsoft.Windows.Azure

Recientemente un compañero de IT me ha preguntado por los sistemas de seguridad que implementamos en las plataformas que hemos desarrollado sobre Azure. Bien, había algunos que yo ya tenía claro, pero para acabar de clarificarlo todo, he decidido investigar un poco, recopilar información de diferentes fuentes y plasmarlo en este artículo. Espero que este resumen le sirva a alguien más.

Para establecer que una solución es segura, se deben tener en cuenta diferentes niveles de seguridad. En los que son responsabilidad del proveedor no me voy a extender mucho, pues ya hay mucha literatura al respecto en internet:

Seguridad física

Responsabilidad de Microsoft Windows Azure.

Centros de datos Microsoft Global Foundation Services (GFS) con ISO 27001.

Implementación de Safe Harbor Framework para garantizar la transmisión y custodia de los datos entre U.S.A. y la U.E. (con apartado especial para U.S.A. y Suiza).

Seguridad de la red

Responsabilidad de Microsoft Windows Azure.

Sistema diseñado sobre la base del Hypervisor de Hyper-V.

Arquitectura triple de aislamiento para cada VM de otras VM’s y de elementos que procedan de Internet. Una VLAN con las VM. Otra VLAN con los nodos de FC (Fabric Controller) que se encargan de crear nuevos roles y supervisar los existentes. La tercera se encarga de los dispositivos de red y otros elementos de infraestructura.

Esta arquitectura asegura que si el código que se está ejecutando en una VM compromete la seguridad, este compromiso no se va a transmitir a los nodos de FC ni a los dispositivos de red.

Además de esta protección interna, se añaden tres elementos de filtrado:

                Filtrado de Paquetes

El hypervisor implementa un filtro de paquetes para evitar Spoffing y tráfico no autorizado hacia las VM’s.

                Canales SSL

Todas las comunicaciones en Azure se producen mediante un canal seguro.

Entre el desarrollador y el entorno para despliegue de aplicaciones.

Entre la aplicación y SQL Azure para ejecución de consultas.

Entre la aplicación y el BlobStorage para guardar y recuperar archivos.

Entre la aplicación y el TableStorage para guardar y recuperar registros.

Entre el cliente y la aplicación (siempre y cuando no especifiquemos un EndPoint no seguro por ejemplo por el puerto 80).

                Firewalls de IP

Superficie de ataque minimizada en SQL Azure al seleccionar específicamente las IP desde las que se permite acceder a los datos.

Al definir EndPoints en los Roles de Azure, automáticamente se generan las reglas que se configuran en los Firewalls de dichos roles.

Seguridad del host

Responsabilidad de Microsoft Windows Azure.

Entorno completamente virtualizado. Dicha virtualización es, en parte, responsable de la capacidad de escalado horizontal rápido tanto hacia arriba como hacia abajo. No existe el almacenamiento persistente en nodos de este tipo.

El hipervisor de Windows.Azure hereda directamente del Hypervisor de Hyper-V usando únicamente los elementos imprescindibles de Windows Server para ser capaz de hospedar VM’s, como el Fabric Controller. Es una manera también de reducir la superficie posible de ataque.

Otra barrera crítica es el aislamiento de la VM raíz de las VM’s invitadas y las VM’s invitadas unas de otras. Esta barrera la maneja el hipervisor y el SO raíz.

Azure revisa automáticamente el VHD(disco virtual) que contiene el sistema operativo y lo mantiene actualizado.

El diseño de las máquinas virtuales es otro mecanismo mediante el cual poder controlar la integridad de la aplicación. Cada VM está conectada a tres VHD:

D: Versiones de SO invitado. Mantenidas actualizadas por el sistema.

E: Imagen construida por el FC basado en el paquete de la aplicación subido por el cliente.

C: Configuración, archivos de paginación y otros tipos de almacenamiento.

Seguridad de las aplicaciones

Responsabilidad del desarrollador.

Para asegurar nuestras aplicaciones, podemos utilizar diversas técnicas, algunas o todas a la vez. Un ejemplo serían las siguientes:

Ejecución de aplicaciones en Modo PARTIAL TRUST

En modo FullTrust, la aplicación tendrá acceso a:

Ejecución en modo no-administrador

Ejecución de Código Nativo

Variables de Entorno

P/ Invoke

Registro

Sistema de archivos

Sin embargo, si ejecutamos la aplicación en modo Partial Trust:

Ejecución en modo no-administrador

Acceso parcial al sistema de archivo (TEMP)

Espacio de nombres

Usar un nombre personalizado para guardar este nombre en las cookies (p.ej. midominio.com) . No usar nunca un nombre tipo <nombreapp>.cloudapp.net.

GateKeeper

El GateKeeper se implementa en un WebRole que recibe las peticiones desde internet. Este WebRole se comunica con el KeyMaster que es un WorkerRole que sólo permite comunicaciones internas dentro de Azure, por lo que está aislado del exterior. El KeyMaster recibe peticiones del WebRole, en quien confía, y traslada las peticiones del WebRole al sistema de almacenamiento a través de SSL.

XSS (Cross Side Scripting)

Aparentemente, si usamos el framework MVC3, la protección contra XSS parece bastante robusta.

Llegados a este punto, será perfectamente comprensible que separemos absolutamente el código <script> de nuestro renderizado Html en ficheros diferentes.

Session Hijacking

Aun así, si un atacante pudiera orquestar un ataque XSS, lo siguiente que haría sería intentar tomar control de la cuenta de usuario.

HttpOnly Flag

Si en la creación de la cookie de autenticación, añadimos este flag, ningún código en Javascript será capaz de usar la cookie y, por tanto, inyectarnos código.

Además, si añadimos al formulario el helper Html.AntiForgeryToken(), haremos que la cookie contenga el mismo valor aleatorio que el campo renderizado. Una vez hecho esto, podemos decorar nuestros actions de [HttpPost] además con [ValidateAntiForgeryToken].

Ataques DDOS

En Web.Config, estos valores evitarán algún tipo de estos ataques. Se puede consultar en esta documentación para lo que sirve cada uno.

<httpRuntime

enableHeaderChecking = “True”

executionTimeout = “60”

maxQueryStringLength = “2048”

maxRequestLength = “4096”

maxUrlLength = “260”

/>

Seguridad de los datos

Responsabilidad de Microsoft Windows Azure y del desarrollador.

Autenticación SQL

Se provee el usuario en cada conexión y se obliga a autentificar cada 60 minutos.

Bloqueo de IP Nativo en el Firewall de SQL Azure

SQL Azure nos proporciona de manera nativa un Firewall en el que, si queremos acceder desde fuera de Azure tendremos que añadir nuestras IP. Bien, esto no es excesivamente seguro y, para reducir la superficie posible de ataque, podemos eliminar incluso el acceso desde dentro de Azure y habilitar únicamente la ip que corresponda con nuestros WebRoles.

Inyección SQL

Partimos de la base de que estamos usando un ORM. En mi caso EF. Si queremos evitar que nos inyecten código SQL es sencillo. En el caso de EntityFramework, sólo existe la posibilidad de que juntemos el resultado de un editor con el contenido de un Command Text de Entity. Si evitamos esto, será difícil que nos cojan desprevenidos.

Evitar devolver IQueryable

Bueno, además de no devolver IQUERYABLE, intentar devolver un IENUMERABLE (con .ToList()) de EXCLUSIVAMENTE lo que se va a renderizar.  Linq permite el método Take y el operador Limit para limitar el tamaño del resultado. Esto, además de hacer que la página se renderice más deprisa, evita que se pueda iterar sobre el objeto devuelto.

(…)

Bueno, aquí me paro por hoy, ampliaré más información en  breve.

(…)

Google
Visita mi perfil en Google Plus

Error en ubicación de seguridad al editar un archivo con el editor de Excel desde Sharepoint


Si en alguna ocasión nos aparece este error al intentar editar o visualizar un archivo Excel con el editor que incorpora Sharepoint 2010 (Office Web Apps):

Es porque en nuestra configuración de Excel Services a nivel de Granja no tenemos configurada la ubicación de la lista de documentos en que tenemos el archivo.

Para configurarla, seguir estos pasos:

Abrir la Administración Central de Sharepoint y seleccionar “Administrar aplicaciones de servicio”:

En la lista de aplicaciones, seleccionar Excel Services Application:

Seleccionar Ubicaciones de Confianza:

Seleccionar Agregar Ubicaciones de Confianza:

 

Incluir aquí la ruta de nuestra librería de archivos:

Y pulsar en Aceptar. A partir de aquí, ya podemos leer archivos de esta ubicación. Explora el resto de opciones de esta vista, verás que hay alguna realmente interesante…

Google
Visita mi perfil en Google Plus

Error “Validación de Datos” al abrir una hoja Excel con el editor o visor de Excel del navegador


El editor de archivos Excel en el navegador, tiene algunas restricciones. Por ejemplo, no es capaz de editar, ni siquiera visualizar, archivos que contengan “Validación de Campos”.

¿Qué es la validación de campos?
Pues algo tan sencillo como que configuremos una casilla para que contenga un valor definido de una lista de otras casillas. Por ejemplo, si queremos que una casilla sólo contenga Sí o No, introducimos en otras coordenadas estos valores:
Lista de Valores
Y después configuramos la celda en la que queremos estos valores:
Paso 1:
Validación de datos

Paso 2:
Validación de datos2
Paso 3:
Validación de datos3
Y a partir de aquí, cuando introduzcamos datos en este campo, nos aparece un desplegable con el contenido de la lista que hemos seleccionado:
Validación de datos4
Esto, que es muy práctico de usar, hace que el editor en el navegador que incorpora Sharepoint no sea compatible con nuestro archivo y que, para poder editarlo, tengamos que hacerlo con la aplicación de escritorio de Excel.
Para realizar esta edición, seleccionar la opción desde el menú desplegable:
Editar En Excel
De este modo evitamos el error siguiente:
Error

Espero que os sea de utilidad esta información.

Google
Visita mi perfil en Google Plus

Despliegue automatizado en Azure


En este artículo voy a describir los pasos necesarios para conseguir realizar un despliegue automatizado en Azure.

Requisitos previos

PowerShell v.2.0

AzureCmdlets

Preparación del entorno para el despliegue automatizado en Azure

Lo primero que hay que hacer es instalar los requisitos previos. Para ello, descargamos de los siguientes links los instalables:

PowerShell v.2.0 (no es necesario en Windows 7 ni en Windows 2008R2, pues ya va incluido con el sistema)

Azure PowerShell Cmdlets

Instalar los paquetes

  • Ejecutar WindowsAzurePowerShell.3f.3f.3fnew.exe
  • Ejecutar WAPPSCmdletsBin.Setup.exe

Para comenzar a usar Windows Azure PowerShell Cmdlets, hay que seguir los siguientes pasos:

Ejecutar el archivo StartHere.cmd que está en el directorio de instalación.

El script lanzará automáticamente la herramienta Dependency Checker. Esta herramienta verifica que el sistema operativo está configurado adecuadamente para todas las dependencias necesarias al usar los Cmdlets de Azure.

El primer paso que sigue la herramienta es chequear las dependencias necesarias. Si no están instaladas en la máquina, la herramienta sugerirá instalarlas en un link usando el Web Platform installer.


Cuando las dependencias terminen de intalarse, comienza el programa de instalación de los Cmdlets de Azure para PowerShell. Escoger la opción de instalación como Snap-in.

Cuando termine la instalación, para probar que se ha instalado bien, seguir los siguientes pasos

Ejecutar PowerShell v 2.0

Añadir los Cmdlets ejecutando:

Add-PSSnapin WAPPSCmdlets

Listar los comandos incluidos en el SnapIn:

Get-Command –Module WAPPSCmdlets

Preparación de la carpeta con los archivos de despliegue

Copiar en una carpeta los archivos cscfg, csdef y cspkg junto con el archivo adjunto deploy.ps1

Ejecución del script

Ejecutar el script con todos los parámetros bien rellenos. El script tiene los siguientes parámetros y POR ESTE ORDEN:

  1. Carpeta de compilación. En este parámetro indicamos cuál es la carpeta que contiene los archivos implicados en el despliegue. P.Ej.: c:\Projects\AzureDeploy.
  2. Nombre del paquete. Nombre del archive cspkg que contiene la aplicación a desplegar. Este es el paquete que se genera con la herramienta cspack del SDK de Azure. P.Ej.: CloudServiceSmall.cspkg.
  3. Archivo de configuración. Nombre del archivo de configuración. El que modificamos desde VS. P.Ej.: ServiceConfigurationSmall.cscfg.
  4. Nombre del servicio. Típicamente será el prefijo de DNS que hemos escogido para nuestra subscripción. P.Ej.: misite.
  5. Nombre del servicio de almacenamiento que queremos usar para subir el paquete para desplegar. Incluir aquí el nombre del servicio. P.Ej.: misitestorage.
  6. Firma del certificado del servicio. Aquí tenemos que incorporar la ruta donde lo tenemos instalado. P.Ej.: cert:\CurrentUser\My\DD6656D6D6666DD66D66D666D66D66D6D6D6D66D.
  7. Id de la subscripción a Azure que vamos a usar. P.Ej.: “66666666-d6d6-6666-d6d6-d666666d6dd6”

Con todo esto, la línea de comando que deberemos ejecutar es la siguiente:

PS C:\Projectes\AzureDeploy> ./deploy.ps1 c:\Projects\AzureDeploy CloudServiceSmall.cspkg ServiceConfigurationSmall.cscfg misite misitestorage cert:\CurrentUser\My\ DD6656D6D6666DD66D66D666D66D66D6D6D6D66D "66666666-d6d6-6666-d6d6-d666666d6dd6"

Y con esto, veremos que automáticamente se nos despliega el paquete en nuestra subscripción. Este sistema nos serviría para, por ejemplo, ejecutar esta última línea de comando desde un entorno de Integración Continua como Jenkins (Hudson).

En próximos artículos, escribiré sobre cómo generar el paquete que subimos con este sistema, de modo que podamos automatizar completamente la compilación, generación y despliegue en entornos gratuitos como SVN+Jenkins en lugar de usar TFS.

deploy.ps1

#Variables que se recogen desde los parámetros de la línea de comandos
#---------------------------------------------------------------------

$buildPath = $args[0]
$packagename = $args[1]
$serviceconfig = $args[2]
$servicename = $args[3]
$storage = $args[4]
$cert = Get-Item $args[5]
$sub = $args[6]

#Crear variables nuevas con los valores de los argumentos
#---------------------------------------------------------------------
$package = join-path $buildPath $packageName
$config = join-path $buildPath $serviceconfig
$a = Get-Date
$buildLabel = $servicename + "-" + $a.ToShortDateString() + "-" + $a.ToShortTimeString()

#Comprobar si no tenemos instalados los Cmdlets
#---------------------------------------------------------------------
if ((Get-PSSnapin | ?{$_.Name -eq "WAPPSCmdlets"}) -eq $null)
{
 Add-PSSnapin WAPPSCmdlets
}

#Obtener el servicio hosteado
#---------------------------------------------------------------------
$hostedService = Get-HostedService $servicename -Certificate $cert -SubscriptionId $sub | Get-Deployment -Slot Staging

#Si existe el servicio, eliminar
#---------------------------------------------------------------------
if ($hostedService.Status -ne $null)
{
 $hostedService |
 Set-DeploymentStatus 'Suspended' |
 Get-OperationStatus -WaitToComplete
 $hostedService |
 Remove-Deployment |
 Get-OperationStatus -WaitToComplete
}

#Desplegar en Staging
#---------------------------------------------------------------------
Get-HostedService $servicename -Certificate $cert -SubscriptionId $sub |
 New-Deployment -Slot Staging -package $package -configuration $config -label $buildLabel -serviceName $servicename -StorageServiceName $storage|
 Get-OperationStatus -WaitToComplete

#Activar el servicio en Staging
#---------------------------------------------------------------------
Get-HostedService $servicename -Certificate $cert -SubscriptionId $sub |
 Get-Deployment -Slot Staging |
 Set-DeploymentStatus 'Running' |
 Get-OperationStatus -WaitToComplete

Google
Visita mi perfil en Google Plus