Una function en Fluent C# para on/off mi VM

Sitio dedicado a Microsoft Azure y otras tecnologías Cloud

Código más Eventos más Datos = Azure Functiones

Bueno, en estas vacaciones me la he pasado muy bien montando un servicio cognitivo para este blog y, sobre todo, para publicar una función de Azure desde la cual puedo hacer operaciones básicas sobre mi máquina virtual de desarrollo en Azure.

Además, para hacerlo más interesante la he construido desde Visual Studio 2019 con el paquete Nuget de Azure Functions SDK para codificar y publicar el código, y con xUnit para construir el testing.

No voy a entrar en detalle en la configuración del contexto de desarrollo o en Azure, y lo que quiero es centrarme en el código y explicarlo un poquito.

Proyecto base

Recuerdo a todos mis lectores que «los test no son opcionales«, y que uso una aproximación de desarrollo en TDD. Por ello una vez dado de alta el proyecto de Visual Studio 2019 del tipo Azure Functions, le añado el proyecto de testing con xUnit al cual le añado la referencia al proyecto principal.

Otra opción que he elegido es utilizar una librería que me permite hacer codificar las operaciones en Azure en C# con programación fluida, porque me parece mucho más agradable de picar y, sobre todo, de mantener.

Para tener más información sobre lo que hay que configurar lee este artículo, y lo necesario para utilizar Azure Fluent C# pásate por este GitHub.

Para finalizar, he añadido el proyecto de testing con xUnit. La configuración y los paquetes necesarios se describen en este tutorial.

Show me the Code

Utilizando test como guía, empece por programar la generación de las credenciales necesarias para poder realizar acciones en la cuenta de Azure adecuada. Esto no es difícil con el siguiente código que utiliza los datos de un Service Principal que ya he dado de alta anteriormente.

using Microsoft.Azure.Management.Fluent; using Microsoft.Azure.Management.ResourceManager.Fluent; using Microsoft.Azure.Management.ResourceManagr.Fluent.Core; namespace trainerfunctions { public class Credenciales { public IAzure Get() { var applicationId = $"3a5aa828-077f-4123-bde4-7a571f29dde3"; var applicationSecret = $"1eLpaNqgvcF_Wiasdf8asdgET7c:f/n"; var tenantId = $"sdp3mdo-9b62-333d-bcca-a3dk4lsl3"; var subscriptionId = $"83e4fb3b-4aa-345-dcg-xxxxxxxxxx"; var credentials = SdkContext.AzureCredentialsFactory .FromServicePrincipal(applicationId, applicationSecret, tenantId, AzureEnvironment.AzureGlobalCloud); var azure = Azure .Configure() .WithLogLevel(HttpLoggingDelegatingHandler.Level.Basic) .Authenticate(credentials) .WithSubscription(subscriptionId); return azure; } } }

Como puedes ver, ya utilizo la librería Fluent para facilitarme el mantenimiento en el futuro. Un mejora imprescindible que tengo que realizar es meter los datos del Service Principal dentro de un Key Vault para evitar tener metido a fuego estos datos críticos en el código.

A continuación me fuí al método inicial de la función, para modificar el nombre del parámetro que estoy esperando, y compruebo que la respuesta sea correcta si no lo recibo.

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System.IO;
using System.Threading.Tasks;

namespace trainerfunctions
{
    public static class OperacionesVM
    {
        [FunctionName("OperacionesVM")]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
            ILogger log)
        {
            log.LogInformation("C# HTTP trigger function processed a request.");

            string name = req.Query["op"];

            string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
            dynamic data = JsonConvert.DeserializeObject(requestBody);
            name = name ?? data?.name;

            if (name != null)
            {
                var result = Accion(name);
                return new OkObjectResult(result);
            }
            else
            {
                return new BadRequestObjectResult($"Please pass a op param on the query string or in the request body");
            }
        }

        private static string Accion(string operacion)
        {
            var respuesta = operacion + " operation isn't implemented";
            switch (operacion)
            {
                case "start":
                    new VirtualMachine().Start();
                    respuesta = "Trainer vm starting";
                    break;
                case "stop":
                    new VirtualMachine().Stop();
                    respuesta = "Trainer vm stoping";
                    break;
                case "status":
                    respuesta = new VirtualMachine().Status();
                    break;
                default:
                    break;
            }
            return respuesta;
        }
    }
}

El segundo método de la clase, Accion(), es el que va a recoger el valor de los parámetros y va a lanzar la operación adecuada que he codificado en la clase VirtualMachines.

using System;

namespace trainerfunctions
{
    public class VirtualMachine
    {
        public Boolean Start()
        {
            var Azure = new Credenciales().Get();
            Azure.VirtualMachines.Start("grupoderecursos", "vmname");

            return true;
        }

        public Boolean Stop()
        {
            var Azure = new Credenciales().Get();
            Azure.VirtualMachines.Deallocate("grupoderecursos", "vname");

            return true;
        }

        public string Status()
        {
            var Azure = new Credenciales().Get();
            var vm = Azure.VirtualMachines.GetById($"/subscriptions/xxxxxx-4aa5-ee0e-9ad3-6dededssa/resourceGroups/MiGrupoDeRecursos/providers/Microsoft.Compute/virtualMachines/mimaquinavirtual");

            return vm.PowerState.Value;
        }

    }
}

Testeando

Como comentaba anteriormente, siempre me baso en pruebas para ir descubriendo la implantación, configuración, codificación e implementación de tecnologías que no controlo.

En este caso he ido haciendo y borrando pruebas hasta quedarme solamente con unas pocas que me revisan, sobre todo, que el código soporte cuando no reciba los parámetros esperados.

Pero lo primero fue probar que me conectaba corréctamente con los datos de las credenciales.

using trainerfunctions;
using Xunit;

namespace trainerfunctionTest
{
    public class CredencialesTest
    {
        [Fact]
        public void Me_conecto_con_las_credenciales_de_robotijo()
        {
            var credenciales = new Credenciales();

            var resultado = credenciales.Get();

            Assert.NotNull(resultado);
        }
    }
}

Sin embargo este test tiene poca utilidad como red de seguridad porque no estoy comprobando la excepción que devuelve en caso de que algo no vaya bien. Sin embargo me permitió entender y configurar de forma correcta la conexión.

Y aquí tengo las tres únicas pruebas que he dejado finalmente.

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using trainerfunctions;
using Xunit;

namespace trainerfunctionTest
{
    public class OperacionesVMdTest
    {
        private readonly ILogger logger = TestFactory.CreateLogger();

        [Fact]
        public async void OperacionesVM_avisa_que_necesita_el_parametro_op()
        {
            var request = TestFactory.CreateHttpRequest("", "");
            var response = (BadRequestObjectResult)await OperacionesVM.Run(request, logger);
            Assert.Contains("Please pass a op", response.Value.ToString());
        }

        [Fact]
        public async void OperacionesVM_avisa_que_la_operacion_no_esta_implementada()
        {
            var request = TestFactory.CreateHttpRequest("op", "desconocida");
            var response = (OkObjectResult)await OperacionesVM.Run(request, logger);
            Assert.Contains("desconocida operation isn't implemented", response.Value.ToString());
        }

        [Fact]
        public async void OperacionesVM_devuelve_el_estado_de_la_vm()
        {
            var request = TestFactory.CreateHttpRequest("op", "status");
            var response = (OkObjectResult)await OperacionesVM.Run(request, logger);
            Assert.Contains("PowerState", response.Value.ToString());
        }
    }
}

Como se puede intuir, aquí faltan unas cuantas clases que son invocadas por estos test y que son los que permiten poder hacer pruebas por medio de http request, y si quieres ver de donde sale lo puedes encontrar aquí.

Conclusiones

Lo primero decirte que es muy fácil. Mucho más fácil que hacerlo a través del portal y tengo la ventaja de poder codificar mi código con su batería de test unitarios imprescindible.

La extensión para trabajar en Azure Fluent es muy cómoda, aunque peca de «verbose». Me recuerda a la versión antigua de los módulos Powershell en la versión 5.x.

La publicación es tan sencilla como solo el tandem VS2019 + Azure me permite. Y lo podría meter sin problemas en un pipeline de Build y Deploy automátizado de mi AzureDevOps.

La seguridad es la que tiene por defecto la Function, y me exige que añada el token de seguridad a cualquiera de las tres métodos que he implementado (por ahora).

https://trainerfunctions.azurewebsites.net/api/OperacionesVM?code=/xxxxxxxxxxxxxxxqpSlhUCo0nQrlTxxxxxxxxxxxxxxx==&op=status

Espero que sea de utilidad.

 

 

Una respuesta

  1. […] En el artículo anterior he construido una Function para la gestión básica de operaciones sobre mi máquina virtual en Azure de desarrollo y formación. […]

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.