Optimizaciones Raspberry Pi

Recopilación de algunas mejoras de rendimiento para diferentes escenarios. Incluye todas las versiones de Debian hasta Bookworm y para cualquier Raspberry que use alguna de estas versiones.

Hasta la versión de Debian Bookworm el archivo de configuración config.txt estaba en /boot/config.txt ahora lo podéis encontrar en /boot/firmware/config.txt. Lo mismo sucede con el fichero de configuración del kernel cmdline.txt pasa de estar en /boot/cmdline.txt a estar en /boot/firmware/cmdline.txt.

También se incluyen algunas recomendaciones, sobre todo para procesos de larga duración.

Rendimiento

En esta sección incluyo algunas mejoras que se pueden usar para mejorar el rendimiento. Hay que tener cuidado con algunas de ellas así que es bueno evaluar cada una para ver si proporciona el rendimiento esperado.

Reducir memoria dedicada a la gráfica

Para aumentar la memoria disponible del sistema se puede reducir la cantidad de memoria dedicada a la parte gráfica, esto no suele ser un problema ya que muchas veces Raspberry no se usa para aplicaciones gráficas.

Esto lo hacemos a través de la utilidad raspi-config, vamos a la opción Performance options -> GPU Memory e indicamos como valor 16 que es el mínimo permitido.

En Bookworm esto ha cambiado y ahora tenemos que editar manualmente el fichero config.txt. Añadimos esta línea en el fichero, por ejemplo antes de las opciones de audio (buscar dtparam), aquí podemos indicar el valor que queramos, no se recomiendan valores muy bajos (menores a 64MB), por ejemplo el mínimo para una Raspberry Pi 1 sería 16MB. Después de aplicar este cambio reiniciamos:

# Set memory split (add this line to set GPU memory allocation)
gpu_mem=16

Overclock

Existe también la opción de hacer overclocking a la Raspberry Pi a través de la herramienta raspi-config, vamos a la opción Performance options -> Overclock y elegimos una de las opciones para aumentar la velocidad del procesador, estas opciones son delicadas y no es recomendable usarla sobre todo porque Raspberry no tiene ningún sistema de refrigeración adicional.

Aumentar archivo swap

En general el tamaño del archivo swap es adecuado pero para ciertos procesos muy intensivos o Raspberry limitadas se puede aumentar para evitar que se quede colgada, la contrapartida es que esto producirá un mayor aumento de E/S por lo que el tiempo de vida de la tarjeta SD se puede ver reducida. Se puede aumentar de la siguiente forma:

Primero paramos el uso del archivo swap.

sudo dphys-swapfile swapoff

Ahora modificamos el tamaño del fichero dphys-swapfile, por ejemplo:

sudo nano /etc/dphys-swapfile

Vamos a elegir un tamaño de 256MB así que modificamos la siguiente línea.

CONF_SWAPSIZE=256

Y ahora podemos volver a habilitar el uso del fichero swap, primero lo inicializamos y luego lo arrancamos.

sudo dphys-swapfile setup
sudo dphys-swapfile swapon

Si queremos ver la memoria swap asignada así como la que está en uso podemos usar el comando top o htop o free -h.

Usando top la línea que tiene MiB Swap se refiere a la memoria swap, total es toda la que disponemos, free es la que actualmente no está en uso y used es la que está en uso, en avail Mem podemos ver la suma tanto de la memoria RAM como de la swap, haciendo un seguimiento de estos valores podemos tunear para escenarios concretos.

Usando zswap

zswap es un sistema de compresión de memoria RAM esto proporciona más memoria pero también un aumento del uso del procesador en el proceso de compresión, descompresión. Es útil en algunos escenarios donde los procesos consumen mucha memoria. Diría por las pruebas que he hecho que permite que ciertos procesos terminen y no se cuelguen pero no aumenta esencialmente la velocidad de los mismos. Los pasos serían:

Tenemos que añadir zswap.enabled = 1 al archivo /boot/cmdline.txt (/boot/firmware/cmdline.txt en bookworm) para habilitar zswap.

Otra opción sería usar zram, aquí y aquí un listado de ventajas e inconvenientes.

Habilitando z3fold & lz4 para zswap

Instalamos primero el paquete lz4, que es el compresor de menoría y z3fold es la forma en que se guardan estas páginas de memoria.

sudo apt-get install lz4

Después tenemos que comprobar si está habilitado initramfs, para eso editamos este fichero /etc/default/raspberrypi-kernel y comprobamos que está descomentada esta línea (No necesario para la version bookworm).

INITRD=Yes

En caso de que estuviese comentada posiblemente no tendríamos creado initramfs si quisiéramos verificarlo ejecutamos.

sudo update-initramfs -uv

Y la propia salida nos indicará si existe o no, en caso de que no exista ejecutamos el siguiente comando:

sudo update-initramfs -c -k $(uname -r)

Editamos el fichero /etc/initramfs-tools/modules y añadimos las siguientes líneas:

lz4
lz4_compress
z3fold

Y ejecutamos el siguiente comando para actualizar con los nuevos módulos:

update-initramfs -uv

Si da un error de hard link realmente es un aviso y no es un problema. Esta salida nos debería indicar también el nombre de la imagen, algo así como -este fichero se puede encontrar en /boot-:
‘/boot/initrd.img-6.6.20+rpt-rpi-v6‘ -> ‘/boot/firmware/initramfs’

(Este paso lo podemos omitir en bookworm) Después editamos config.txt y añadimos al final del fichero dentro de la sección general, es decir, ninguna sección que contenga un hardware concreto como [pi4] [pi3] [all]... siendo el valor el nombre de la imagen que nos ha sacado el comando anterior (no es necesario poner un «=»).

initramfs initrd.img-6.6.20+rpt-rpi-v6

Ahora editamos cmdline.txt y añadimos lo siguiente:

zswap.compressor=lz4 zswap.zpool=z3fold

Y reiniciamos, después para verificar que todo ha ido correcto ejecutamos:

grep -R . /sys/module/zswap/parameters

Que nos debería sacar como salida…

/sys/module/zswap/parameters/same_filled_pages_enabled:Y
/sys/module/zswap/parameters/enabled:Y
/sys/module/zswap/parameters/max_pool_percent:20
/sys/module/zswap/parameters/compressor:lz4
/sys/module/zswap/parameters/zpool:z3fold
/sys/module/zswap/parameters/accept_threshold_percent:90

En este punto la configuración del archivo config.txt auto_initramfs=1 se puede quitar por redundante pero tampoco afectará si se deja, esto asegura que al menos un initramfs se cargará al inicio, de hecho en Bookworm es la configuración recomendada y no usar la mencionada anteriormente.

Más información sobre las ventajas aquí y detalles de configuración aquí:

Usando ZRAM

A veces puede suceder que zswap no nos de el resultado esperado, podemos probar en ese caso con zram, para eso seguimos estos pasos:

sudo wget -O /usr/bin/zram.sh https://raw.githubusercontent.com/Bash-Projects/rpi_zram/master/zram.sh

Damos permisos para ejecutar el script que nos acabamos de descargar:

sudo chmod +x /usr/bin/zram.sh

Ahora lo vamos a programar para que se ejecute 50 segundos después del arranque, así que lanzamos crontab.

sudo crontab -e

y programamos la ejecución del script:

@reboot ( sleep 50 ; sudo /usr/bin/zram.sh &)

El comando lo podemos lanzar directamente para ver el resultado o podemos reiniciar y esperar 50 segundos para ver el resultado, podemos verificar el aumento de memoria usando:

free -h

para ver como ha aumentado la RAM y para poder ver el aumento en la swap usamos:

swapon -s

Recomendaciones

Recomendaciones para ciertas tareas que se suelen hacer con Raspberry de forma habitual.

Procesos de larga duración

Muchas veces tenemos que lanzar procesos en Raspberry que pueden tardar mucho tiempo, sobre todo en un sistema como Raspberry de capacidades limitadas, y queremos asegurarnos que el proceso termine aunque no vayamos a estar delante del terminal, aquí indico algunas formas de abordar este problema.

Usando comandos

A veces hay procesos que llevan mucho tiempo -por ejemplo compilar un contenedor-, la mejor opción es dejar ejecutando el comando build en el terminal pero a veces esta compilación se lanza desde una conexión ssh y al cerrar se puede terminar el proceso, para evitar esto se puede usar el comando nohup, por ejemplo:

nohup docker image build --tag user/imagen -f Dockerfile .

También se puede ejecutar en segundo plano añadiendo & al final del comando.

Si el comando ya se estaba ejecutando podemos usar el comando disown para conseguir el mismo efecto, en este caso primero pasamos el comando a segundo plano usando Ctrl+Z y usando el comando jobs veremos como el comando está detenido, ahora lo volvemos a arrancar pero esta vez en segundo plano usando:

bg %1

siendo 1 el número del trabajo y a continuación lo desasociamos del terminal usando:

disown -h %1

Si solo tenemos un trabajo podemos omitir %1 en los comandos ya que actuarán sobre el último proceso que se haya lanzado.

Con cualquiera de estos dos métodos el proceso seguirá ejecutándose aunque salgamos del terminal.

Un problema adicional es no saber que está haciendo el proceso, podemos guardar un log redireccionando a texto con > pero en ese caso no veremos por pantalla lo que está pasando para esto tenemos el comando tee que nos permite ver por pantalla y a la vez emitir a fichero lo que está pasando.

Este comando es un resumen de lo explicado, lanza el proceso en segundo plano y permite ver la salida por pantalla a la vez que se guarda en un fichero.

nohup docker image build --tag user/image -f Dockerfile . | tee out.txt &

Si cerramos la sesión y volvemos a conectar más adelante la forma cómoda de ver que está haciendo el trabajo sería usando el comando tail con el fichero que hemos indicado, usando el parámetro -f podemos ir viendo las actualizaciones a medida que el proceso escriba.

tail -f out.txt

Usando screen

Otra opción posiblemente más cómoda sea usando screen, permite tener una sesión de la que se pueda salir sin cerrar el proceso, tiene el problema que no es un paquete instalado por defecto así que primero lo instalamos.

sudo apt-get install screen

Para lanzarlo simplemente usamos el comando screen

screen

Aceptamos el mensaje y veremos un shell en el que podemos trabajar, algunos comandos útiles de screen.

# Listado de las sesiones activas
screen -ls
# Reanudar sesión, si solo hay una activa
screen -r
# Reanudar sesión, donde ID es el número que aparece antes del punto
screen -r ID

También tenemos unos atajos útiles:

Ctrl+A,D Desconectamos temporalmente de la sesión (esto NO interrumpe los procesos activos).

Ctrl+A,A Permite cambiar entre diferentes sesiones activas.

Capturar errores en comandos encadenados

Muchas veces en encadenamos comandos con && y puede ser que fallé alguno de esos comandos pero realmente no se dará la ejecución como fallida a no ser que falle el último, si queremos que cualquier comando produzca un error en toda la ejecución podemos usar pipefail:

set -eo pipefail

Referencias

Enabling & Increasing Raspberry Pi Swap – Neblio
ZRAM

Optimizaciones Raspberry Pi

Pensamiento lateral en pruebas unitarias: ¿y el resto?

Introducción y puesta en marcha
Servicios para todos
Servicios para unos pocos
¿Y el resto?

Sí, es cierto que no todo son servicios en pruebas unitarias de hecho elementos comunes que se suelen encontrar aparte de servicios -que generalmente es lo más complicado de reemplazar- sería la propia verificación del resultado para lo cual existen interesantes librerías como FluentValidation aunque adolece de alguna carencia sobre todo en lo relacionado con recuperación de información de otras fuentes, aunque es verdad que este no es es su principal cometido.

Este escenario es muy habitual y tiene múltiples soluciones, por ejemplo:

private DbSet<T> Set<T>() where T : class
{
	var context = this.factory.Services.GetRequiredService<ReplicaDbContext>();
	return context.Set<T>();
}

Este es un ejemplo muy sencillo de crear un Set<T> de una base de datos para realizar consultas sobre la misma, también igualmente se puede usar para borrar datos enteros o volverlos a generar para pruebas concretas, existen librerías más especializadas como Respawn que cubren muy bien estos escenarios y con unos pocos métodos de extensión cubren la mayoría de los escenarios.

Otro caso distinto son las entidades que se usan para generar la prueba, principalmente los objetos que tienen que estar disponibles para la prueba, como entidades en base de datos o la propia petición que se envía a una API, en este caso volvemos al problema que comentamos antes, el equilibrio entre no tener pruebas demasiado largas y tampoco generar demasiado código reutilizable.

Estaría bien tener una forma cómoda y rápida de generar objetos sin tener que crear muchos ficheros y evitar generar excesivo código, o incluso código que genera los objetos necesarios para hacer las pruebas, -sí, esto existe-.

Podríamos intentar algo como:

var person = FluentBuilder
               .Create<Killer>()
	       .With(n => n.Name).Set("Hola")
	       .With(n => n.Surname).Set("Caracola")
               .Build();

No deja de ser una forma rápida de asignar valores a propiedades, con la comodidad de poder añadir algo de código de ser necesario, esto es especialmente útil para evitar crear objetos directamente y también en la propia prueba unitaria se puede ver que tipo de objeto se está creando con que propiedades en lugar de una referencia a un objeto común. De igual forma evita generar enormes clases fluent para poder generar cualquier objeto en cualquier escenario posible.

Este sería un ejemplo de implementación:

using System;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

namespace A.Builders
{
    public static class FluentBuilder
    {
        public static FluentBuilderFeatures<TEntity> Create<TEntity>()
            where TEntity : class, new()
        {
            TEntity entity = new TEntity();
            return new FluentBuilderFeatures<TEntity>(entity);
        }

        public static FluentBuilderFeatures<TEntity> Create<TEntity>(
          params object[] parameters) where TEntity : class
        {
            var constructor = typeof(TEntity)
                .GetConstructors()
                .Where(c => c.GetParameters().Length == parameters.Length
                &&
                Enumerable.SequenceEqual(
                    parameters.Select(p => p.GetType()), 
                    c.GetParameters().Select(c => c.ParameterType))
                    )
                .Single();
            var entity = (TEntity)constructor.Invoke(parameters);
            return new FluentBuilderFeatures<TEntity>(entity);
        }
    }

    public class FluentBuilderFeatures<TEntity>
        where TEntity : class
    {
        private TEntity entity = default(TEntity);

        private FluentBuilderFeatures() { }

        internal FluentBuilderFeatures(TEntity entity)
        {
            this.entity = entity;
        }

        public FluentBuilderProperty<TEntity, TProperty> With<TProperty>(Expression<Func<TEntity, TProperty>> propertyExpression)
        {
            return new FluentBuilderProperty<TEntity, TProperty>(
              this, entity, propertyExpression);
        }

        public TEntity Build()
        {
            return entity;
        }
    }

    public class FluentBuilderProperty<TEntity, TProperty>
        where TEntity : class
    {
        private TEntity entity = default(TEntity);
        private FluentBuilderFeatures<TEntity> builder = default(FluentBuilderFeatures<TEntity>);
        private Expression<Func<TEntity, TProperty>> propertyExpression = default(Expression<Func<TEntity, TProperty>>);
        private FluentBuilderProperty() { }

        internal FluentBuilderProperty(
            FluentBuilderFeatures<TEntity> builder,
            TEntity entity,
            Expression<Func<TEntity, TProperty>> propertyExpression)
        {
            this.entity = entity;
            this.builder = builder;
            this.propertyExpression = propertyExpression;
        }

        public FluentBuilderFeatures<TEntity> Set<TValue>(TValue value)
        {
            var property = GetPropertyInfo();
            property.SetValue(entity, value, null);
            return builder;
        }

        public FluentBuilderFeatures<TEntity> Get<TValue>(ref TValue value)
        {
            var property = GetPropertyInfo();
            value = (TValue)property.GetValue(entity);
            return builder;
        }

        private PropertyInfo GetPropertyInfo()
        {
            var memberSelectorExpression = 
              propertyExpression.Body as MemberExpression;
            if (memberSelectorExpression != null)
            {
                var property = memberSelectorExpression.Member as PropertyInfo;
                if (property != null)
                {
                    return property;
                }
            }
            throw new InvalidOperationException($"Action on a non-property {memberSelectorExpression.Member.ToString()}");
        }
    }
}

La idea en general de estos capítulos es poder tener pruebas unitarias lo más auto contenidas posible y que no implique la generación de muchos ficheros, que sean fáciles de mantener y también que sean sencillo crear nuevas para alguien que no esté acostumbrado a crearlas o que acabe de entrar al proyecto.

Es verdad que los frameworks actuales de pruebas ayudan a simplificar la generación de tests y son válidos en muchas ocasiones, pero también es verdad que poder usar el mismo estilo de código en la prueba que en el código de la aplicación ayuda a crear pruebas más rápido y de forma más intuitiva sin tener que consultar en la documentación formas de poder hacer una prueba unitaria por un caso concreto.

Posiblemente estos problemas se mitiguen con la práctica pero también alejan un poco al desarrollador del código original, poder crear una prueba como si fuera código de la aplicación con las mismas librerías y estilo hace una transición más fácil del código de la aplicación al código de las pruebas y ayuda a crear pruebas de de forma fácil y rápida en el que se vea mejor la intención de la prueba sin estar rodeada de código de librerías o de infraestructura que complican ver realmente que casos cubre esa prueba.

Bibliografía

En este caso ChatGPT me ha ayudado para el código Emit, aunque no ha sido capaz de sacarme todos los casos -habrá que probar la versión 4 cuando sea gratis-.

Lyrics

Face The PainStemm
Faster Within Temptation
Soul CreatorCinderella

Pensamiento lateral en pruebas unitarias: ¿y el resto?

Pensamiento lateral en pruebas unitarias: Servicios para unos pocos

Introducción y puesta en marcha
Servicios para todos
Servicios para unos pocos
¿Y el resto?

Bien, estamos en el supuesto que queremos cambiar solo la implementación de un método concreto de una interfaz, un caso de uso concreto podría ser querer cambiar la implementación del método SendNotificationAsync(…) de la interfaz INotificationHubClient del paquete Microsoft.Azure.NotificationHubs.

Es verdad que algunos servicios de Azure se pueden emular con Azurite o en caso de bases de datos se puede crear una, pero no siempre es posible, no todos los servicios tienen una versión para test o no queremos lidiar con infrastructura compleja pero si puede interesar que el método final al que no tenemos acceso SendNotificationAsync(…) no intente nada raro, es decir, no queremos que se ejecute y lance una notificación o lo intente, queremos que tenga un comportamiento concreto.

Afortunadamente la mayoría de los métodos de las librerías de Azure implementan algún tipo de interfaz y muchas veces se puede inyectar esa librería en el propio host lo que también da opción a que se puede trabajar con ella a nivel de prueba unitaria.

Esto último es importante porque no nos interesa solo conque devuelva unos ciertos valores fijos de salida en base a unos valores de entrada como sería un caso habitual, ni queremos usar el método comentado anteriormente sobrescribiendo la interfaz con una implementación personalizada, esto nos condicionaría el resto de las pruebas y solo nos interesa en un test concreto.

Bien, esto… es bastante complicado, porque interesa hacerlo de una forma demasiado genérica y dinámica, queremos poder implementar cualquier método de una interfaz cualquiera sin tener que crear la clase expresamente -lo recalco en este punto para que cada cual se sienta libre de pensar en como solucionar esto-.

Llegados a este punto posiblemente la mayoría ha pensado en usar reflection, no es una tecnología que suela usar porque casi siempre hay una forma mejor y más sencilla de hacer las cosas, -lo siento por los fans de reflection-.

Pero en este caso es inevitable, de hecho incluso el uso habitual de reflection se queda corto, porque reflection permite hacer muchas cosas pero la mayoría son de modo lectura, por ejemplo podemos sacar mucha información de clases, métodos, etc… incluso ejecutar algunos, pero no podemos modificarlos a nivel de estructura y en este caso nos interesa, porque queremos implementar una interfaz I en una clase C cuando esta clase no la implementa de serie ni queremos hacerlo de manera habitual por código.

A partir de aquí el campo imaginativo se expande con inmensa felicidad para aquellos que gustan de reinventar la rueda (Dinamyc Proxy, Source Generators, Expression Trees…) y cualquier otra idea… creativa. Todas tienen sus pros y sus contras pero en este caso vamos a rizar el rizo y vamos a usar la librería Emit (que también pertenece a reflection) -no hay miedo-.

Esta librería casualmente -o no- también es utilizada por los principales paquetes de pruebas unitarias y no sin razón porque permite realizar casi cualquier cosa sin saber muy bien de donde vienen los tiros, se pueden crear tipos dinámicos con métodos dinámicos sacando la información para crearlos mediante reflection en otro tipo que tampoco sabemos en tiempo de compilación que estructura tiene -una fiesta-.

Pero en este caso es muy útil, porque vamos a hacer lo siguiente, vamos a crear una clase dinámicamente en tiempo de ejecución y le vamos a decir que implemente al vuelo una interfaz cualquiera, y además le vamos a indicar que para un método concreto de esa interfaz va a ejecutar con unos parámetros específicos un código que hemos diseñado a tal efecto, es decir, algo como esto:

public async void A_Cool_Test_Returns_Always()
{ … Code
  var notificationAsync = (Notification notification, string tagExpression) =>
    {  
      if (String.IsNullOrEmpty(tagExpression))    
        throw new Exception("test failed!!");  
      return new NotificationOutcome()  
      {
        Success = 0,    
        Failure = 1,    
        NotificationId = "Good morning with id 1",  
      };
   };  
   Scenario
     .WithService<INotificationHubClient>()
     .For(svm => svm.SendNotificationAsync(
                   n, 
                   $"user:{userId}"))    
     .Use(notificationAsync)    
     .Build();
... More code
}

En esta prueba estamos haciendo exactamente eso:

  • (WithService<T>) queremos que el servicio T INotificationHubClient de Azure…
  • (For(…)) para el método SendNotificationAsync con los parámetros dados…
  • (Use()) use la implementación que le pasamos en el delegado que hemos creado.

Para hacer esto vamos guardando toda la información tanto de las firmas como de los delegados, pero también falta la parte más importante, hacer que funcione.

public class ScenarioFixtureBuilder
{
  public ServiceFixtureBuilder<TS> WithService<TS>()
  	where TS: class
  {
  	var service = factory.Services.GetService<TS>();
  	ServiceFixtureBuilder<TS> sf = new ServiceFixtureBuilder<TS>(this);
  	return serviceFixture;
  }

  public ScenarioFixtureBuilder Use(Delegate method)
  {
  	this.Body = method;
  	serviceFixture.scenarioFixture.AddService<T>(this.serviceFixture.Service);
  	return serviceFixture.scenarioFixture;
  }
  
  public MethodFixtureBuilder For(Expression<Action<T>> expression)
  {
  	MethodFixtureBuilder method = new MethodFixtureBuilder(this, expression);
  	this.Service.AddMethod(method);
  	return method;
  }
  
  protected internal void Execute([CallerMemberName] string callerName = "")
  {
  	var del = this.Methods.Where(m => m.MethodName == callerName).First();
  	if (del == null)
  	{
  		return;
  	}
  	var act = del.Signature;
  	if (act.Body is MethodCallExpression lambdaBody)
  	{
  		// Obtener los parámetros de la expresión lambda
  		var parameters = lambdaBody.Arguments.ToArray();
  		var pars = new List<object>(parameters.Length);
  		foreach (var param in parameters)
  		{
  	          pars.Add(GetValueFromExpression(param));
  		}
  		var a = del.Body.DynamicInvoke(pars.ToArray());
  	}
  }
}

Este método Execute() usa el parámetro callerName que contiene el nombre del método invocador (el método de la interfaz) para recuperar la firma del método que hemos indicado en For() y va a usar estos parámetros para invocar al código que hemos pasado al método Use(), esto hará que cuando el servicio que tengamos que lanza notificaciones (siendo originales un INotificationService) acceda al método de INotificationHubClient lance el código del delegado con los parámetros que le hemos indicado y no use el código de la librería de Azure.

¿Cómo se hace esto? La idea es la siguiente, con Emit vamos a coger una clase que tenemos ya creada (siempre la misma para todas las pruebas) y dinámicamente le vamos a añadir una interfaz (la de Azure) esto claramente implica implementar todos sus métodos pero como no vamos a crear código para cada método porque no interesa ni tiene sentido vamos a hacer que todos los métodos llamen a un método común Execute() que como hemos indicado solo acepta un parámetro, el nombre del método que lo va a invocar (sí, el método de la interfaz) -bueno, en realidad necesitamos dos métodos Execute() uno que devuelve valor y otro que no ,pero para simplificar hablaremos solo de uno-.

Ahora viene la parte de Emit, es un tanto complicada y es de muy bajo nivel pero aquí estarían las partes más destacadas.

private DynamicServiceFixture CreateDynamicClass()
{
  // Crear un nuevo ensamblado dinámico
  var nombreEnsamblado = new AssemblyName("DynamicAssembly");
  var ensambladoBuilder = AssemblyBuilder.DefineDynamicAssembly(nombreEnsamblado, AssemblyBuilderAccess.Run);
  
  // Crear un nuevo módulo en el ensamblado
  var moduloBuilder = ensambladoBuilder.DefineDynamicModule("DynamicModule");
  
  // Crear un nuevo tipo dinámico que hereda de la clase e implementa la interfaz
  var tipoBuilder = moduloBuilder.DefineType(
  	"DynamicClassWithInterface",
  	TypeAttributes.Public | TypeAttributes.Class,
  	typeof(DynamicServiceFixture),
  	new Type[] { typeof(T) }
  );
  
  // Información del método ejecutar
  var executeMethod = typeof(DynamicServiceFixture)
    .GetMethod(
      nameof(ExecuteNoReturn), 
      BindingFlags.NonPublic | BindingFlags.Instance);
  
  var methods = typeof(T).GetMethods();
  foreach (var method in methods)
  {
  	MethodBuilder method = null;
  	// Implementar el método de la interfaz
  	method = tipoBuilder.DefineMethod(
  		method.Name,
  		MethodAttributes.Public | MethodAttributes.Virtual,
  		method.ReturnType,
  		method.GetParameters().Select(m => m.ParameterType).ToArray()
  	);
  	var generadorIL = method.GetILGenerator();
        // Cargar la instancia en la pila
  	generadorIL.Emit(OpCodes.Ldarg_0); 
        // Nombre del método invocador
  	generadorIL.Emit(OpCodes.Ldstr, method.Name);
  if (method.ReturnType != typeof(void))
  {   // El método tiene valor de retorno
      // Variable local donde guardar el resultado
      var local = generadorIL.DeclareLocal(method.ReturnType); 
      // Crear el genérico con el mismo tipo de retorno que el llamador
      generadorIL.Emit(
        OpCodes.Call, 
        executeMethod.MakeGenericMethod(method.ReturnType)); 
  }
  generadorIL.Emit(OpCodes.Ret);
  tipoBuilder.DefineMethodOverride(method, method);

  // Crear el tipo y devolver
  dynamic instance = Activator.CreateInstance(tipoBuilder.CreateType());
  return instance;
}

La primera parte es fácil, simplemente crea un tipo dinámico DynamicClassWithInterface, para eso necesitamos crear un ensamblado y un módulo que lo contengan, y vamos a hacer que herede de la clase que hemos comentado que hemos creado a tal efecto DynamicServiceFixture (que sirve para almacenar la información de las firmas y delegados y que usará Execute()) y también vamos a hacer que implemente la interfaz que nos interesa.

Ahora viene la parte interesante, de esta interfaz obtenemos todos los métodos de la misma y en el tipo que hemos definido anterior DynamicClassWithInterface vamos creando uno por uno cada uno de estos métodos, el código aquí implica bastantes más cosas sobre todo relacionadas con genéricos y valores de retorno, pero el código es ilustrativo de lo que se pretende.

Como un método implementado en una clase vía interfaz tiene que tener código -añeja pregunta de entrevista de trabajo- creamos ese código al vuelo, un código muy sencillo, lo único que va a hacer es invocar al método Execute() con el parámetro que hemos comentado que es el nombre del propio método de la interfaz que será el que se utilice para encontrar la información que necesita para ejecutar el delegado como hemos comentado antes.

Con esto lo tendríamos, inyectamos esta interfaz usando el objeto creado de esta forma y cuando se vaya a ejecutar el método SendNotificationAsync() ejecutará realmente el método Execute(«SendNotificacionAsync») y con los parámeros ejecutará el delegado que hemos indicado y devolverá el resultado al código que se está probando.

Este sistema es totalmente compatible con los anteriores ya que se ejecuta de una forma muy local al test y al servicio concreto, otras pruebas no se verán afectadas y podrán usar cualquiera de los métodos anteriores.

Obviamente este sistema vale tanto para métodos de interfaces de terceros como propias aunque requiriendo que la interfaz se puede inyectar a través del host, normalmente es lo habitual y en caso de que no lo sea no es mala idea envolverlo en una interfaz sobre todo sin son librerías de terceros.

Pensamiento lateral en pruebas unitarias: Servicios para unos pocos

Pensamiento lateral en pruebas unitarias: Servicios para todos

Introducción y puesta en marcha
Servicios para todos
Servicios para unos pocos
¿Y el resto?

¿Cómo haríamos para cambiar la implementación de un servicio concreto? Bueno puede ser que queramos mantener algunos servicios con la implementación original pero también poder sobrescribir durante la inicialización algunos de estos, ampliamos el ejemplo anterior:

builder.ConfigureTestServices((services) =>
{
  services.AddScoped<INotificationService, NotificationServiceFake>();
  services.AddScoped<IBlobService, BlobServiceFake>();
});

En este caso con el método ConfigureTestServices() pisamos la inyección de ConfigureServices() sobrescriendo la configuración de esta, ConfigureTestServices() se ejecuta después de ConfigureServices() así que de esta forma podríamos tener las dos opciones, mantener algunas con la implementación original y sobrescribir otras que nos interesen.

El problema aquí queda en que la implementación fake es la misma para todas las pruebas unitarias y eso puede que no siempre interese, se puede realizar lógica en el código fake para evitar esto por ejemplo creando identificadores concretos de pruebas y en base a esto se devuelva una u otra información -sí, un poco rebuscado-.

También existe la opción de crear una especia de almacén compartido entre esta clase y la prueba unitaria que permite acceder solo a la información que necesita una prueba concreta, pero es un código poco apropiado para pruebas unitarias, fácil que aumente su complejidad y perdería en cierta medida el concepto de unitario -sí, aún más rebuscado-.

En cualquiera de las opciones anteriores esto terminará con una implementación muy larga, es mejor mantener estas clases (las que indicamos en ConfigureServices() y ConfigureTestServices()) con un código común que puedan usar la gran mayoría y enfocarse en solucionar el problema de las pocas pruebas que no encajen en este patrón.

Una forma sencilla de lidiar con esto sería sobrescribir las implementaciones bajo petición, por ejemplo podríamos tener una prueba que hiciera algo como:

public async void A_Cool_Test_Returns_Always()
{
  Scenario
    .AddService<INotificationService, NotificationServiceCustomFake>()
    .Build();

  User user = Scenario.AUser();
  Model model = new Model();
  var id = Guid.NewGuid().ToString();
  model.Id = id;
  model.UserId = user.Id;
  
  var httpClient = Scenario
    .UseHttpClient
    .WithZumoVersion3()
    .WithValidAuthHeaders()
    .WithRelativeUri(Uri.Quick.CreateSomething());

  var response = await httpClient.SendPostAsync(model);
  response.StatusCode.Should().Be(HttpStatusCode.Created);
  var result = await response.ReadAsAsync();

 ... Verificación
}

Lo que estamos haciendo es usar el método AddService<T,U>() para añadir una implementación bajo demanda de un test concreto, esto provocará un cambio parcial en el host concreto de este test, lo cual supone una cierta merma de rendimiento pero en general no supone un grave problema y vale para este escenario -como siempre la clave es no abusar-.

Para poder hacer esto nos apoyamos en el propio método ConfigureTestServices() , un ejemplo podría ser:

public class ScenarioFixtureBuilder
{
  private ReplicaWebApplicationFactory factory;
  private readonly Dictionary<Type, Type> dependencies;
  
  public ScenarioFixtureBuilder AddService<TService, TImplementation>()
  	where TService : class
  	where TImplementation : class, TService
  {
  	this.dependencies.Add(typeof(TService), typeof(TImplementation));
  	return this;
  }
  
  public HttpClientBuilder UseHttpClient
  {
  	get
  	{
  	  if (this.httpClientBuilder == null)
  	    throw new InvalidOperationException("Build() not called!");
  	  return httpClientBuilder;
  	}
  }	
  
  public void Build()
  {
    var builtfactory = this.factory.WithWebHostBuilder(builder =>
    {
  	  builder.ConfigureTestServices((services) =>
  	  {
  	    foreach (var dependency in dependencies)
  	    {
  	      services.AddScoped(dependency.Key, dependency.Value);
  	    }
  	  });
  	});
  	this.httpClientBuilder = new HttpClientBuilder(builtfactory.CreateClient());
    }
}	

Aquí añadimos los servicios a un diccionario que luego usaremos al construir el escenario usando el método ConfigureTestServices() en el método Build().

Lo único que hay que tener en cuenta es que si vamos a crear un HttpClient para realizar pruebas contra una Api este HttpClient tiene que ser el generado en el método Build() ya que cualquier otro apuntará a otra versión del host y no funcionará (en este caso lo guardamos en la variable httpClientBuilder).

Vamos ahora con el tercer supuesto, solo interesa cambiar un método concreto y el resto no nos interesa ni lo vamos a implementar, ni siquiera queremos crear la clase que lo implemente aunque sea para devolver valores por defecto, es algo que solo queremos de una forma puntual para un test concreto y queremos una implementación concreta.

También queremos poder combinarlo con los métodos anteriores, es decir, el clásico todo lo anterior y además… bueno este caso es bastante más complicado y se comenta en el siguiente capítulo.

Pensamiento lateral en pruebas unitarias: Servicios para todos

Pensamiento lateral en pruebas unitarias: Introducción y puesta en marcha

Introducción y puesta en marcha
Servicios para todos
Servicios para unos pocos
¿Y el resto?

Existen multitud de frameworks para desarrollar pruebas unitarias (Moq, Rhino Mocks, NSubstitute,…) y unos cuantos blogs que realizan las pertinentes comparaciones comentando los pros y contras de cada uno de ellos, realmente son en gran medida similares y solo difieren en unos pocos aspectos que en general son más dependientes de gustos personales que funcionales, en este punto quizás solo destacaría Microsoft Fakes como el más completo, aunque lamentablemente es de pago -por eso probablemente no sea el más conocido-.

A la hora de hacer pruebas unitarias más allá del framework particular existen las buenas prácticas como el patrón AAA (Arrange, Act, Assert) del cual solo puedo decir que pertenece a esa época en la que cualquier idea envuelta en un acrónimo le llamaban patrón -pero bueno ahí esta para el que no sepa donde van los saltos de línea en una prueba unitaria-.

En general se considera buena idea, -de hecho por propio concepto- que las pruebas unitarias estén auto contenidas, es decir que todo lo que implica ejecutar esa prueba unitaria esté dentro de la misma, esto es interesante pero también a veces puede generar pruebas demasiado largas o un tanto difíciles de entender y mantener, los frameworks actuales intentan hacerlo más cómodo usando técnicas fluent pero aun así a veces es difícil de evitar.

Algunas veces para evitar esto se generan pequeños trozos de código reutilizable, como por ejemplo las clásicas entidades de base de datos que se usan en muchas pruebas (Given.A.User…) o pequeños trozos de invocación de métodos (autenticación, llamadas a handlers…) en este punto cada cual puede estar más o menos a favor o en contra de esto, al final el problema reside en cuantos ficheros de código quiero tener o como de grandes puedo tolerar que sean mis pruebas.

Las pruebas unitarias generalmente tienen a degradarse más rápido que el resto del código a fin de cuentas si se generan algunos ficheros de código reutilizable puede que la nueva prueba unitaria no haga uso de los mismos por desconocimiento o que diferentes pruebas según quién las haga tengan un formato diferente, seguro que ahora algunos pensarán que para eso se inventaron las pull request, pero… ¿Quién revisa realmente bien las pruebas unitarias de una pull request?.

De hecho uno de los problemas añadidos es que a veces interesa enfocar las pruebas de diferentes formas, por ejemplo las pruebas unitarias sobre una base de datos (ya sea creada ex profeso o en memoria) tienden a ser más cómodas de realizar porque se puede usar el mismo código que se usaría normalmente para el código de la aplicación y evita tener que lidiar con el framework correspondiente averiguando como hacerlo.

El problema es que suele acarrear una mayor complejidad a la hora de realizar las pruebas y puede proporcionar un peor rendimiento, una de la razones -entre otras muchas- por las que se creo el concepto flaky tests, una forma de resignarse a que algunas pruebas nunca funcionarán bien del todo pero aun así hay que desplegar.

Otro de los problemas clásicos es como hacer un fake de los servicios, si el servicio se ha creado en la propia aplicación no es un problema pero si es un servicio de terceros quizás las cosas se compliquen un poco aunque sea un servicio que realiza una tarea concreta y de poco riesgo (por ejemplo Azure emitiendo una notificación) aun así puede ser interesante a veces poder realizar algún tipo de procesamiento.

Es verdad que un buen sistema de pruebas unitarias puede hacer mucho de este trabajo pero el problema es que el framework tiene que ser capaz de ver la interfaz del servicio y poder actuar sobre ella y eso a veces no es trivial, así que para el resto de los conceptos que se van a explicar vamos a suponer que no se usan estos frameworks -no es un truco fácil, tiene su sentido que se verá más adelante-.

Podríamos decir que hay tres escenarios posibles según el nivel al que queramos trabajar.

  1. No hacer nada, simplemente confiamos en la implementación original del servicio que hemos creado y es lo que realmente queremos probar, esto que puede parecer trivial no lo es tanto si el servicio no está preparado, por ejemplo por tener acceso a bases de datos u otros elementos externos.
  2. Crear una clase que implemente la interfaz con código útil para las pruebas unitarias, en este caso solo quedaría inyectar la interfaz o usar el framework correspondiente para indicárselo.
  3. Cambiar solo la implementación de un método, esto puede sonar raro pero hay interfaces que pueden tener muchos métodos, por ejemplo la interfaz INotificationHubClient de Azure tiene más de 130 métodos y puede que solo usemos unos pocos y no nos interese añadirlos todos.

Y por supuesto queda la combinación, es decir, en algunas pruebas igual solo nos interesa el punto 2 o el 3, pero el resto deberían usar el 1 y para esto aunque xUnit de algunas facilidades no siempre es tan fácil y tampoco suele ser interesante crear diferentes clases y agruparlas en base a razones técnicas, se suele preferir agrupar por criterios de características o de escenarios.

También estaría bien poder crear estas pruebas de forma cómoda y rápida sin tener que revisar la documentación cada vez que haya un caso un poco diferente y como no también estaría bien no tener muchos ficheros con los que trabajar y que la prueba unitaria fuese fácil de entender y con poco margen de error para que la mayor cantidad de pruebas tuvieran un aspecto lo más similar posible.

En este caso vamos a comentar brevemente el punto 1, el 2 y el 3 quedan para los siguientes capítulos.

.Net tiene una clase que se llama WebApplicationFactory<T> que ayuda bastante en este trabajo, incorpora entre otros un método ConfigureServices(), por ejemplo:

public class AWebApplicationFactory : WebApplicationFactory<Startup>
{
  protected override void ConfigureWebHost(IWebHostBuilder builder)
  {
  	builder.ConfigureAppConfiguration((hostingContext, configurationBuilder) =>
  	{
  	  configurationBuilder.Sources.Clear();
  	  configurationBuilder
  		.SetBasePath(AppDomain.CurrentDomain.BaseDirectory)
  	  .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);
  	});
  
  	builder.ConfigureServices(services =>
  	{
  	  services.AddScoped<INotificationService, NotificationService>();
  	  services.AddScoped<IBlobService, BlobService>();  
  	  var sp = services.BuildServiceProvider();
  	  using (var scope = sp.CreateScope())
  	  {
  		var scopedServices = scope.ServiceProvider;
  		var db = scopedServices.GetRequiredService<ADbContext>();
  		db.Database.EnsureCreated();
          }
  	}).UseEnvironment("Test");
  }
}

En este caso hemos creado un host de test que tiene un método ConfigureServices() que emula prácticamente la misma funcionalidad que un host real, aquí podemos añadir servicios e implementaciones concretas como haríamos en el host, este método se ejecutará después del ConfigureServices() del host real. De igual forma tenemos un ConfigureAppConfiguration() para poder adaptar la configuración.

Este es un ejemplo sencillo que se puede ampliar consultando la documentación relacionada. Esto cubriría el primer caso pero no el caso en el que queremos que pruebas concretas utilicen otra implementación de la interfaz del servicio, esto queda para el próximo capítulo.

Pensamiento lateral en pruebas unitarias: Introducción y puesta en marcha

Kodi y plataformas de streaming (Netflix, HBO, Amazon, Disney+)

Actualizado a 13 de Octubre de 2022

Es habitual ver centros multimedia en Raspberry Pi, lo que no es tan habitual es montarlos en una imagen Docker, con las mejoras que ha habido en hardware en Raspberri ya es posible tener varios servicios a la vez en una sola Rpi, por ejemplo servidores de vídeo, audio etc..

En este caso se van a detallar los pasos para tener una imagen Docker con Kodi (v19.x) en la última versión Debian disponible en este momento (Bullseye).

Esta imagen Docker contendrá los plugins de HBO, Netflix, Amazon y Disney+ (aunque es posible que se puedan configurar otros).

Configuración Host

Antes de empezar con la imagen Docker necesitaremos una Raspberry operativa, como versión de Raspberry Pi OS podemos usar cualquiera (incluso Lite) una vez instalado Raspberry Pi OS actualizamos el sistema con:

sudo apt-get update
sudo apt-get upgrade

También hace falta instalar el paquete lirc.

apt-get install lirc

Y tendremos también que instalar Docker siguiendo estos pasos sino está ya instalado, en ese mismo enlace se dan algunas recomendaciones sobre rendimiento que pueden ser útiles.

Vídeo

Hay que hacer una pequeña modificación en el fichero /boot/config.txt para que el vídeo se reproduzca correctamente, tenemos que tener esto en el fichero:

[all]
dtoverlay=vc4-fkms-v3d

Y después solo queda reiniciar. A partir de aquí es posible que queramos hacer algunas tareas adicionales, por ejemplo igual queremos añadir como biblioteca de vídeo o audio un USB conectado o crear carpetas de unidad de red para acceder a ficheros remotos.

Docker

Aquí tenemos la imagen Docker, solo hay un parámetro para build que indica que salida de audio se va a utilizar, 1 para HDMI y 0 para la salida Jack.

FROM balenalib/raspberrypi3-debian:bullseye

ARG DEBIAN_FRONTEND=noninteractive
# 1 for HDMI, 0 for headphones
ARG     AUDIO=1

RUN	apt-get update				&& \
	apt-get -y purge openssl		&& \
	apt-get -y --purge autoremove		&& \
	apt-get dist-upgrade			&& \
	apt-get install	-y			\			
	uuid-dev		\
	upower			\
	alsa-base		\
	alsa-utils		\
	alsa-tools		\
	dbus-x11		\
	libraspberrypi0		\
	xterm			\
	xinput			\
	xinput-calibrator	\
	evemu-tools		\	
	libnspr4		\
	libwidevinecdm0		\
	libc6			\
	avahi-daemon		\
	libnss-mdns		\
	nano			\
	lirc			\
	lirc-compat-remotes	\
	bluez			\
	dumb-init		\
# Required for AirPlay Mirroring
	cmake					\
        libavahi-compat-libdnssd-dev		\
        libplist-dev				\
        libgstreamer1.0-dev			\
        libx264-dev				\
        libjpeg-dev				\
        libgstreamer-plugins-base1.0-dev	\
        libgstreamer-plugins-bad1.0-dev		\
        gstreamer1.0-plugins-ugly		\
        gstreamer1.0-tools			\
        gstreamer1.0-gl				\
        gstreamer1.0-gtk3			\
        git					\
        git-svn					\
        libssl-dev				\
# Python required for Netflix, Amazon...
	python3-pip		\
	build-essential		&& \
# Install some Python packages
	pip install setuptools pycryptodome pycryptodomex wheel pycrypto				&& \
	apt-get -y --purge autoremove									&& \
	rm -rf /var/lib/apt/lists/*

# besides kodi, we will install a few extra packages:
#  - ca-certificates              allows Kodi to properly establish HTTPS connections
#  - kodi-eventclients-kodi-send  allows us to shut down Kodi gracefully upon container termination
#  - kodi-game-libretro           allows Kodi to utilize Libretro cores as game add-ons
#  - kodi-game-libretro-*         Libretro cores
#  - kodi-inputstream-*           input stream add-ons
#  - kodi-peripheral-*            enables the use of gamepads, joysticks, game controllers, etc.
#  - kodi-pvr-*                   PVR add-ons
#  - kodi-screensaver-*           additional screensavers
#  - lirc,lirc-compat-remotes     enables the use of IR Remotes
#  - locales                      additional spoken language support (via x11docker --lang option)
#  - pulseaudio                   in case the user prefers PulseAudio instead of ALSA
#  - tzdata                       necessary for timezone selection
RUN packages="                                               \
    fbset                                                    \
    ca-certificates                                          \
    kodi                                                     \
    kodi-eventclients-kodi-send                              \
    kodi-inputstream-adaptive                                \
    kodi-inputstream-rtmp                                    \
    kodi-peripheral-joystick                                 \
    kodi-pvr-argustv                                         \
    kodi-pvr-dvblink                                         \
    kodi-pvr-dvbviewer                                       \
    kodi-pvr-filmon                                          \
    kodi-pvr-hdhomerun                                       \
    kodi-pvr-hts                                             \
    kodi-pvr-iptvsimple                                      \
    kodi-pvr-mediaportal-tvserver                            \
    kodi-pvr-mythtv                                          \
    kodi-pvr-nextpvr                                         \
    kodi-pvr-njoy                                            \
    kodi-pvr-pctv                                            \
    kodi-pvr-sledovanitv-cz                                  \
    kodi-pvr-stalker                                         \
    kodi-pvr-teleboy                                         \
    kodi-pvr-vbox                                            \
    kodi-pvr-vdr-vnsi                                        \
#    kodi-pvr-vuplus                                          \ # Not yet available
    kodi-pvr-wmc                                             \
    kodi-pvr-zattoo                                          \
    kodi-screensaver-biogenesis                              \
    kodi-screensaver-matrixtrails                            \
    kodi-screensaver-pyro                                    \
    kodi-screensaver-stars                                   \
    lirc                                                     \
    lirc-compat-remotes                                      \
    locales                                                  \
    libnss3                                                  \
    tzdata"                                               && \
                                                             \
    apt-get update                                        && \
    apt-get install -y $packages                          


# Audio settings
RUN     echo            \
"pcm.!default {         \n\
  type asym             \n\
  playback.pcm {        \n\
  type plug             \n\
  slave.pcm "output"    \n\
}                       \n\
capture.pcm {           \n\
  type plug             \n\
  slave.pcm "input"     \n\
  }                     \n\
}                       \n\
pcm.output {            \n\
  type hw               \n\
  card $AUDIO           \n\
}                       \n\
ctl.!default {          \n\
  type hw               \n\
  card $AUDIO           \n\
}" >> /etc/asound.conf

# Adding user
ENV	KODI_ID=1000
RUN     adduser \
	--uid $KODI_ID \
	--disabled-password \
	--gecos '' \
	--ingroup sudo \
	kodi 
RUN     echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
RUN	usermod -a -G audio,video,input,dialout,plugdev,netdev,users,cdrom,tty kodi

# Install RpiPlay
WORKDIR /opt/vc
RUN     git svn clone https://github.com/raspberrypi/firmware/trunk/opt/vc/include
RUN     git svn clone https://github.com/raspberrypi/firmware/trunk/opt/vc/src
RUN	git svn clone https://github.com/raspberrypi/firmware/trunk/opt/vc/lib

WORKDIR /repos
COPY    rpiplay.tar.gz .
RUN     tar -zxvf rpiplay.tar.gz
RUN	ls -la
WORKDIR /repos/rpiplay/build
RUN	ls -la
RUN	cmake ..
RUN	make -j 4
WORKDIR /
RUN	rm -rf /repos
RUN	rm -rf /rpiplay.tar.gz

# Plugins

WORKDIR	/plugins
ADD	--chown=kodi:sudo https://github.com/castagnait/repository.castagnait/raw/kodi/repository.castagnait-2.0.0.zip .
ADD	--chown=kodi:sudo https://k.slyguy.xyz/repository.slyguy.zip .
ADD	--chown=kodi:sudo https://github.com/Sandmann79/xbmc/releases/download/Repository/repository.sandmann79.plugins-1.0.4.zip .

# Configuration
EXPOSE	8080 9777/udp

VOLUME	/home/kodi
USER	kodi

COPY entrypoint.sh /usr/local/bin
CMD ["/usr/local/bin/entrypoint.sh"]

También usaremos el siguiente fichero Docker compose:

version: "3.7"
services:
  rpi-kodi:
    image: joursain/rpi-kodi:buster
    build:
      context: .
      dockerfile: Dockerfile
    container_name: "kodi"
    user: kodi
    network_mode: host
    restart: "no"
    privileged: true
    devices:
      - /dev/fb0:/dev/fb0
      - /dev/vchiq:/dev/vchiq
      - /dev/tty0:/dev/tty0
      - /dev/tty2:/dev/tty2
      - /dev/input:/dev/input
      - /dev/snd:/dev/snd
      - /dev/shm:/dev/shm
    volumes:
      - /var/run/dbus:/var/run/dbus
      - /mnt/media/kodi:/home/kodi
      - "/etc/timezone:/etc/timezone:ro"
      - "/etc/localtime:/etc/localtime:ro"
      - "/usr/bin/tvservice:/usr/bin/tvservice:ro"
    tmpfs:
      - /tmp
    environment:
      - DBUS_SYSTEM_BUS_ADDRESS=unix:path=/var/run/dbus/system_bus_socket

Para arrancar lo haremos de la forma habitual (opcionalmente podemos añadir -d si queremos que arranque en segundo plano)

docker-compose up [-d]

Veremos la salida (si no hemos seleccionado -d) de la ejecución y en cuanto haya terminado aparecerá Kodi en pantalla. Esta imagen permite el uso de teclado así que en cuanto esté operativa podremos usar el teclado y escuchar sonidos a medida que se seleccionen diferentes opciones, esto verificará que el sonido está bien configurado.

La carpeta home del usuario kodi (/home/kodi) que contiene la configuración de Kodi está compartida en un volumen por si hiciera falta hacer una copia de seguridad de la configuración o para añadir algún plugin externo se puede dejar en esta carpeta para poder instalarlo desde Kodi.

Al arrancar posiblemente saldrán varias ventanas relacionadas con plugins pidiendo activación, no hace falta hacerlo en un primer momento y se puede hacer más adelante.

Servicios

Es posible configurar algunos servicios de Kodi como por ejemplo UPnP o AirPlay de la siguiente forma:

  1. Vamos a configuración (el icono de la rueda)
  2. Icono System
  3. Services
  4. Aquí podemos habilitar UPnP/DLNA o AirPlay

En la parte de abajo hay una opción para poder ampliar las opciones de configuración (el nivel máximo es Expert)

Sonido

La imagen Docker por defecto al arrancar ya tiene el sonido habilitado, la configuración que deberíamos ver es la que está en:

  1. Vamos a configuración (el icono de la rueda)
  2. Icono System
  3. Opción audio
  4. Audio output device
  5. Marcamos Default (bcm2835 Headphones bcm2835 Headphones) ()

Como detalle si se están compartiendo los altavoces entre Raspberry y otro dispositivo es posible que la imagen arranque sin sonido, solo un dispositivo puede estar habilitado a la vez.

Plugins

Ahora vamos a configurar los diferentes plugins, la imagen Docker los coloca en la carpeta /plugins, antes de instalarlos hay que realizar algunas acciones en la imagen Docker.

  1. Vamos a configuración (el icono de la rueda).
  2. Add-ons.
  3. My add-ons.
  4. VideoPlayer InputStream.
  5. InputStream Adaptive.
  6. Enable.

Ahora tenemos que habilitar la opción de orígenes desconocidos para poder instalar los plugins

  1. Vamos a configuración (el icono de la rueda).
  2. System.
  3. Add-ons.
  4. Habilitamos Unknown sources y confirmamos el mensaje.

Para instalar los diferentes plugins tendremos que seguir estos pasos.

  1. Vamos a configuración (el icono de la rueda).
  2. Add-ons.
  3. Install from zip file (igual tenemos que usar los .. para subir a un nivel superior y poder ver la opción) y confirmamos el mensaje que saldrá, este mensaje solo nos indica que estos plugins no se actualizan automáticamente y habrá que hacerlo manualmente.
  4. Nos movemos a la carpeta /plugins (Root filesystem/plugins).
  5. Aquí se encuentran todos los plugins que iremos instalando.

Instalación

Todos los plugins se instalan de la misma forma, el procedimiento es:

  1. Desde la carpeta plugins del paso anterior buscamos el plugin que queramos instalar, habrá los siguientes:
    1. CastagnaIT para Netflix
    2. SlyGuy para HBO y Disney+
    3. Sandman para Amazon
  2. Una vez seleccionado e instalado, vamos a la opción Install From Repository (al mismo nivel que Install from zip File) y veremos varios repositorios, seleccionamos por el mismo nombre que antes el que interese.
  3. Si solicita componentes adicionales los instalamos también.

Al ejecutar algunos plugins es posible que solicite instalar Widevine CDM, es necesario instalarlo para que funcione el vídeo. Este proceso puede llevar un rato. Es posible también que falle la instalación o de un error al abrir el vídeo, en ese caso hay que apagar el contenedor y volverlo a levantar.

Configuración

Ahora que están los plugins instalados vamos a configurarlos, simplemente hay que acceder a ellos a través de:

  1. Vamos a configuración (el icono de la rueda)
  2. Add-ons
  3. My addons / Video add ons / (plug-in de vídeo)

Básicamente todos los plugins tienen el mismo sistema, algunos pedirán los datos de la cuenta al principio y otros tendrán una sección especial. Cada plugin tiene varias opciones de configuración, por ejemplo idioma o subtítulos, merece la pena dedicarle un rato.

Televisión

Para poder ver la televisión tenemos que ir al apartado de Add-Ons en Sistema y luego en My-AddOns y finalmente instalamos PVR IPTV Simple Client, en la parte de Configure donde dice M3U playlist URL ponemos https://www.tdtchannels.com/lists/tv.m3u8 o https://www.tdtchannels.com/lists/tvradio.m3u8 si también queremos radio.

Ahora solo tenemos que ir al icono de TV que tenemos en la página principal en un lateral y esperar a que cargue los canales, saldrán muchos así que usar el buscador es una buena opción.

Notas

El código fuente aquí.

Como dato adicional se puede usar este mismo docker compose para cambiar Plex por Kodi.

No parece que de buen resultado usar zswap aquí, produce algunos errores al menos en Rpi3, es mejor usar zram.

De igual forma no podemos tener la memoría gráfica por debajo de 160, así que lo dejamos en 256GB.

Bonus

Esta imagen Docker admite AirPlay y Bluetooth, también el uso de infrarrojos si hay uno conectado aunque habría que incluirlo en docker-compose añadiendo – dev/lirc:/dev/lirc o – dev/lirc0:/dev/lirc0

Referencias

https://forum.kodi.tv/showthread.php?tid=365714
https://mundokodi.com/addon-hbo-max-en-kodi/
https://support.zoom.us/hc/en-us/articles/203680359-Protocols-for-Apple-screen-mirroring-AirPlay-
https://forums.raspberrypi.com/viewtopic.php?t=295008
https://github.com/CodaFog/kodi-rpi
https://github.com/rimago/rpi-kodi
https://kodi.wiki/view/AirPlay
https://www.genbeta.com/multimedia/como-ver-netflix-kodi-que-puede-ser-mejor-que-hacerlo-sus-aplicaciones-oficiales
https://www-genbeta-com.cdn.ampproject.org/v/s/www.genbeta.com/multimedia/netflix-hbo-pluto-tv-twitch-youtube-todas-grandes-plataformas-que-puedes-ver-kodi-como-hacerlo/amp?amp_gsa=1&amp_js_v=a6&usqp=mq331AQIKAGwASCAAgM%3D#amp_tf=De%20%251%24s&aoh=16301408042642&csi=0&referrer=https%3A%2F%2Fwww.google.com&ampshare=https%3A%2F%2Fwww.genbeta.com%2Fmultimedia%2Fnetflix-hbo-pluto-tv-twitch-youtube-todas-grandes-plataformas-que-puedes-ver-kodi-como-hacerlo
https://www.balena.io/docs/learn/more/masterclasses/services-masterclass/#5-running-systemd-in-a-service
https://wiki.ubuntu.com/LircHowto
https://www.linuxquestions.org/questions/linux-from-scratch-13/blfs-svn-dbus-won%27t-start-886338/
https://forums.balena.io/t/dbus-failed-to-connect-to-socket-host-run-dbus-system/3107
https://www.balena.io/docs/learn/develop/runtime/#dbus-communication-with-host-os

Kodi y plataformas de streaming (Netflix, HBO, Amazon, Disney+)

Documenta, que no es poco

La creencia popular suele asociar documentación con aburrimiento, no es que se pueda decir que documentar es la parte más creativa del desarrollo sin embargo poca gente se da cuenta de las bondades ocultas de la documentación. En este artículo voy a comentar algunas que es posible mucha gente no se haya dado cuenta así como algunos consejos para hacerlo más ameno (sí, es posible).

Una de las razones principales que no hay que olvidar sobre la documentación es que más allá de su utilidad o necesidad se realiza también por un concepto de solidaridad, todos hemos entrado por primera vez en una empresa y nos hemos enfrentado a un proyecto del que no sabemos nada, siempre se agradece que alguien se haya acordado de esa gente que entra nueva en el proyecto y haya preparado una documentación con la que empezar para no tener que golpearse contra una pared o estar preguntando cada 5 minutos.

Herramientas

Hay muchas herramientas para documentar, no me voy a detener en ellas porque no es el propósito de este artículo pero en general con el tiempo si me he dado cuenta que es más cómodo trabajar con aquellas herramientas de edición en línea tipo blog como puede ser Confluence o Github que otras estilo Sharepoint en las que creas un documento con una tercera herramienta (como un Word) y luego lo subes a la plataforma, las razones son varías:

  • Necesitas herramientas de terceros que pueden no ser gratuitas, además necesitas que estén instaladas en el equipo que estés usando, normalmente siempre será el mismo pero puede que necesites hacer un pequeño cambio con un móvil o tablet.
  • Es más incómodo porque hace falta tener varias herramientas abiertas a la vez, por ejemplo un Word y la web de Sharepoint, lo cual produce cierta pereza a la hora de generar documentación.
  • Por la misma razón para editar hay que abrir el Word y bajarse el archivo para editarlo y volverlo a subir, o que el navegador permita abrir la aplicación embebida en el navegador, si lo soporta, en general no es muy directo y cómodo.
  • Discrepancias de estilo, documentación hecha por varias personas con varios formatos, si se hace en línea es fácil ver que los formatos no son coherentes y es más fácil aplicar herramientas automatizadas.
  • Los diferentes ficheros pueden ser de diferente tipo Word, Excel… lo que complica mucho la búsqueda y también la explotación de la información.
  • A no ser que se haga con cuidado (y esto suele llevar mucho trabajo y detalle) los documentos se acaban versionando (mal) por nombre y se acaba teniendo Manual-1.0, Manual-2.0, etc… al final es fácil perder la pista de cual es el último o cual hay que modificar y se acaba llenando la aplicación de ficheros, es mejor editar el mismo documento y tener un historial para poder ver los cambios, las versiones anteriores y poder comparar.

Esto no quita que la herramienta deba permitir visualizar cualquier fichero, por ejemplo a veces un vídeo o una presentación pueden ser útiles. En este caso verlas directamente desde el navegador sería la opción preferida.

Pero independientemente de cual sea, una buena práctica es mantener siempre esa aplicación o web abierta, para que en cualquier momento simplemente saltando a esa aplicación se añada un comentario o se haga una pequeña corrección. Para simplificar esto la aplicación debe de tener un índice y debe de ser fácil encontrar lo que se quiere modificar o actualizar de tal forma que con un par de clicks te coloque en el sitio que necesites para actualizar el contenido y después continúes con tu trabajo.

Algunos extras interesantes que suelen tener ya presentes muchas plataformas es la opción de insertar comentarios, ya sea directamente en los textos como anotaciones o en una sección aparte -usualmente en la parte inferior-, con esto último hay que tener cuidado ya que la página se puede hacer excesivamente grande debido a los comentarios, generalmente esta sección es mejor usarla para perfiles que no tengan permisos para editar el documento.

Otros detalles como una sección de feedback o la posibilidad de recibir notificaciones cuando el contenido se ha actualizado también suele ser bastante útil.

Costumbres

Realmente la documentación no esconde grandes secretos es sobre todo una cuestión de practicar y para eso es bueno coger una costumbre, por ejemplo escribir pequeñas cosas durante el día a cierta hora o apuntarlas aparte y al final de un sprint -si se usa agile- o cada cierto tiempo dedicar un rato a escribirlas en la documentación.

La palabra documentar tiende a generar cierto agobio porque cada uno piensa en todo lo que tiene que escribir (es decir, todo lo que no ha documentado que tenía que haber documentado) y se le cae el mundo encima, no es necesario abarcarlo todo, sino empezar por pequeñas cosas, como si fueran pequeñas funciones de programación, solo que en este caso explican una concepto determinado o una idea y poco a poco con el tiempo todas esas piezas van encajando para construir un documento más completo.

En esta línea no hace falta inicialmente -sobre todo si no hay ninguna documentación o no se tiene experiencia y/o ganas- volverse loco con frases o palabras o como escribirlo, es mejor simplemente ponerse a escribir, a medida que se escribe se van viendo las formas de cambiar el texto para que quede mejor. La idea es escribirlo aunque no sean grandes documentos, al menos que quede reflejado.

Las costumbres se pueden coger de varias formas, por ejemplo en un despliegue a producción si no hay ningún documento que explique el proceso se puede ir realizando mientras se desarrolla el despliegue así se tiene un documento verificado y actualizado del proceso. Otro caso podría ser durante una reunión de equipo, si hay una charla para explicar un nuevo concepto o idea en lugar de decirlo o dibujarlo malamente en una pizarra o papel se pueda dibujar en un PowerPoint o similar y usar ese mismo PowerPoint como punto de entrada a una documentación más elaborada.

Finalmente una buena costumbre es actualizar la documentación cuando se detecta una discrepancia, generalmente no suelen ser grandes cambios -porque si lo fueran es mejor directamente crear un documento nuevo- pero permite mantener la documentación actualizada, en este sentido es bueno coger la práctica de que cualquier persona que detecte esa discrepancia tenga la capacidad de poder cambiarla y no delegar esa responsabilidad en una tercera persona que puede no tenga tiempo de hacerlo en ese momento y correr el riesgo de que al final ese cambio se pierda y no se haga.

Escribir no es pintar

Una buena opción es, primero escribir y luego estructurar, es más fácil estructurar porque al fin y al cabo no es más que copiar y pegar los textos ya escritos en un sitio u otro y darle forma y esa forma de trabajar es más afín a la mentalidad de un desarrollador que simplemente escribir hojas y hojas con principio y final que es más un trabajo literario.

Por ejemplo, se puede documentar simplemente un proceso de subida a producción y otro día documentar el proceso de desarrollo, con el tiempo esas secciones se pueden ir homogeneizando dándoles un formato similar y finalmente crear un elemento padre que sea Despliegues en entornos.

Es verdad que aunque la idea no es crear un libro escribir documentación si requiere un cierto trabajo literario pero sobre todo más en el sentido de ser capaz de explicar las cosas de tal forma que cualquiera las pueda entender aunque sea un texto técnico, en lugar de escribir código para desarrolladores que sea fácil de entender y mantener, la idea es escribir texto que sea comprensible por un mayor grupo de personas y esto es interesante porque en cierta forma cuando hay que escribir documentación para personas no técnicas ayuda a entender como esos perfiles ven la aplicación.

En este sentido no es malo documentar para escapar un poco de la disciplina del código y practicar otras habilidades, en un momento de ofuscación puede ser una vía de escape para hacer otra actividad diferente, y si hay que realizar una presentación documentar ayuda a asentar las ideas e ir practicando como se va a explicar una aplicación.

Depurando por escrito

Una de las mayores ventajas que he encontrado en documentar es que es mucho más fácil encontrar tus propios errores y también descubrir las cosas que no sabías y las posibles lagunas que puedes tener, a fin de cuentas documentar es como explicarte a ti mismo lo que has hecho y como lo has hecho y en ese proceso es fácil darse cuenta de errores, imprecisiones o duplicidades que habrá que corregir en código o incluso fallos de concepto.

De hecho una forma de descubrir que el código tiene buena calidad es al documentar, una documentación imprecisa o larga y tediosa y que no surge fluida y es difícil de entender suele ser un síntoma de que la aplicación puede tener fallos conceptuales o ser innecesariamente complicada, esto ayuda a simplificar el código por ejemplo detectando piezas de código que ya no son necesarias. Funciona igual en el sentido inverso, al escribir código uno se puede dar cuenta de que la documentación es incorrecta yendo entonces a actualizarla o sino hay tiempo en ese momento simplemente marcarla como inválida o moverla a otra sección a la espera de que más adelante se pueda arreglar.

Es interesante también de cara a una nueva funcionalidad, si una idea que se pretende desarrollar es complicada de documentar, explicar o crear ejemplos en base a ella es un claro síntoma de que puede que no sea una buena idea, quizás es demasiado complicada (idea feliz) o demasiado arriesgada (mejor empezar con algo más terrenal).

Finalmente…

Sobre los idiomas, esto es es muy dependiente del proyecto y de la empresa pero la costumbre generalizada suele ser documentar en inglés que para mucha gente suele ser una razón más para no hacerlo pero también puede ser una razón más para meterse en el idioma, no solo para entenderlo mejor sino también para asimilar y aprender nuevos términos en inglés que son útiles de cara al desarrollo, por ejemplo para buscar información o usar una mejor nomenclatura.

Es curioso que mucha gente diga que no tiene tiempo para documentar pero si para explicar lo mismo a varias personas, es más rentable escribirlo como si se lo estuvieras contando a alguien que repetirlo varias veces, de hecho una forma de comprobar que se entiende es dar ese documento a alguien y si es capaz de aplicarlo satisfactoriamente con las mínimas preguntas es que el documento es util.

La famosa frase la virtud está en el término medio se puede aplicar también aquí, tiene tan poco sentido documentarlo todo como no documentar nada, en un lado no documentar nada no es útil pero documentarlo todo no es práctico porque harían falta constantes correcciones y se invertiría demasiado tiempo, es fácil que al final la documentación acabase siendo un pequeño monstruo de imprecisiones y desactualizado que nadie quiera modificar.

Como medida subjetiva una o dos veces por sprint suele ser suficiente, puede que al principio se haga más pero tampoco dedicarle demasiado tiempo, hay que tener en cuenta que un código bien hecho también ayuda, además el código siempre cambiará más y más rápido que la documentación así que no tiene sentido documentar código como tal. En general la documentación debería responder a la mayoría de las preguntas pero el objetivo no es responder a todas las preguntas.

Espero que todos estos consejos ayuden a todos aquellos reacios a documentar y vean las bondades de la documentación más allá de la manida frase de es algo que hay que hacer.

Documenta, que no es poco

Servidor DHCP y DNS

(Actualizado a 13 de Febrero de 2024)

En algunos casos puede resultar interesante tener configurado un servidor DNS en una red local especialmente si tenemos servicios montados en una Raspberry y queremos acceder a ellos por nombre en lugar de aprender la IP y el puerto.

Un servidor DHCP también nos permite personalizar el rango de direcciones y poder asignar direcciones específicas basadas en MAC, útiles para servidores o sistemas NAS.

Existen paquetes que agrupan estos dos servicios y permiten usarse con una configuración mínima pero en este caso vamos a usar dos paquetes diferentes basado en dos clásicos isc dhcp server y bind9.

DHCP y DNS

Servidor DHCP

Primero instalamos el servidor DHCP

sudo apt-get install isc-dhcp-server

Es posible que al instalar este paquete aparezcan algunos errores, no hay que preocuparse porque es debido a que el servicio todavía no está configurado.
Ahora vamos a editar algunos valores el fichero /etc/dhcpcd.conf, en la parte de Example static IP configuration ponemos la siguiente configuración, vamos a suponer que el servidor tiene de IP 192.168.1.2 que el router de la red tiene de IP 192.168.1.1 y que el servidor DNS es este mismo (192.168.1.2) así como uno adicional 8.8.8.8

# Example static IP configuration:
interface eth0
static ip_address=192.168.1.2/24
#static ip6_address=fd51:42f8:caae:d92e::ff/64
static routers=192.168.1.1
static domain_name_servers=192.168.1.2 8.8.8.8 fd51:42f8:caae:d92e::1

Ahora editamos el fichero /etc/dhcp/dhcpd.conf quitamos el comentario de la opción authoritative y también de la opción log-facility local7; indicamos opciones para el nombre de la zona y aumentamos el tiempo de concesión (lease time), en una red doméstica es más práctico para no perder la IP en caso de que el servidor DHCP se caiga.

Al final del fichero agregamos la red que en nuestro caso es 192.168.1.0 y también creamos el rango en el que se entregarán IPs vamos a usar el rango 192.168.1.100-192.168.1.254.

En esta misma sección vamos a poder asignar las IP estáticas, por ejemplo máquinas que siempre tienen que recibir la misma IP, vamos a suponer por ejemplo que tenemos un servidor NAS en la IP 192.168.1.10, y por si acaso añadimos también el router aunque posiblemente ya tenga IP fija.

La configuración con todos estos cambios sería:

option domain-name "acme.local";
option domain-name-servers 192.168.1.2, 8.8.8.8;

default-lease-time 259200;
max-lease-time 604800;

authoritative;
log-facility local7;

subnet 192.168.1.0 netmask 255.255.255.0 {
range 192.168.1.100 192.168.1.254;
  option routers 192.168.1.1;
  option subnet-mask 255.255.255.0;
  option broadcast-address 192.168.1.255;
}

host router {
  hardware ethernet 38:72:c0:30:26:6e;
  fixed-address 192.168.1.1;
}

host NAS {
  hardware ethernet 00:d0:4b:95:06:c0;
  fixed-address 192.168.1.10;
}

Finalmente indicamos que solo procesaremos peticiones IPv4, para esto editamos el fichero /etc/default/isc-dhcp-server y al final dejamos solo lo siguiente comentando el resto:

INTERFACESv4="eth0"

Para poder empezar igual necesitamos establecer la dirección IP manualmente. Esto no interrumpirá la conexión existente.

sudo ifconfig eth0 192.168.1.1

También habilitamos algunos servicios para que el arranque sea correcto.

sudo systemctl enable dhcpcd
sudo systemctl enable isc-dhcp-server

Al terminar es aconsejable reiniciar los servicios para detectar cualquier posible error en la configuración:

sudo service dhcpcd restart
sudo service isc-dhcp-server restart

Servidor DNS

En este caso vamos a usar el servidor bind9, vamos a crear las zonas que servirá para identificar nuestra red y las maquinas que se tendrán que resolver cuando se acceda a ellas por nombre. Primero instalamos el servidor:

sudo apt-get install bind9

Una opción de configuración para habilitar bind9 solo para direcciones IPV4, editamos el fichero /etc/default/named y añadimos la opción -4 al final.

OPTIONS="-u bind -4"

Vamos a definir estas zonas en el fichero /etc/bind/named.conf.local a la zona la llamaremos acme

zone "acme.local" {
  type master;
  file "/etc/bind/db.acme.local";
};

zone "1.168.192.in-addr.arpa" {
  type master;
  file "/etc/bind/db.192.168.1";
};

En la primera sección creamos la zona acme que definiremos en la ruta indicada /etc/bind/db.acme.local en la segunda sección definiremos la zona inversa que permite hacer consultas en base a la IP para obtener el nombre, esta zona se suele nombrar usando la IP de red en sentido inverso.

Ahora editamos la opciones a través del fichero /etc/bind/named.conf.options

options {
  directory "/var/cache/bind";
  auth-nxdomain no;    # conform to RFC1035

# Reenviadores
  forwarders { # Reenviamos las consultas a:
    8.8.8.8; # DNS de Google.
    8.8.4.4; # DNS de Google.
    192.168.128.1; # IP del router.
  };

# Opciones de seguridad
  listen-on port 53 { # Escuchamos en el puerto 53 (Puerto por defecto)...
    127.0.0.1; # ... por la interfaz de loopback...
    192.168.1.2; # ... y por nuestra IP de red.
  };

# Otras opciones
  listen-on-v6 { none; }; # No escuchamos tráfico IPv6;

  allow-query { # Permitimos consultas DNS desde...
    127.0.0.1; # ... la interfaz de loopback ...
    192.168.1.0/24; # ... y nuestra red interna.
  };

  allow-transfer { none; }; # Prohibimos la transferencia de zonas puesto que este   es nuestro único servidor DNS de la LAN

  allow-recursion { # Permitimos consultas recursivas desde...
    127.0.0.1; # ... la interfaz de loopback...
    192.168.1.0/24; # ... y la red interna.
  };
};

Ahora creamos las zonas usando la configuración indicada al principio así que usaremos la carpeta /etc/bind, es importante respetar el formato del fichero con el punto al final (.) y los tabuladores.

  • db.acme.local
    • Contiene los nombres de la zona, aquí indicamos el nombre y su IP, al servidor DNS lo vamos a llamar server
$ORIGIN acme.local.
$TTL 86400 ; 1 dia
@       IN      SOA     server.acme.local        admin.acme.local (
        2    ; serie
        6H   ; refresco (6 horas)
        1H   ; reintentos (1 hora)
        2W   ; expira (2 semanas)
        3H   ; mínimo (3 horas)
)
; name servers - NS records
                        IN      NS      server.acme.local.

; name servers - A records
router.etxe.local.              IN      A       192.168.1.2
nas.etxe.local.                 IN      A       192.168.1.3
  • db.192.168.128
    • Este fichero contiene la zona inversa que se ha comentado antes
$ORIGIN 1.168.192.in-addr.arpa.
$TTL 86400     ; 1 dia
@       IN      SOA     server.acme.local        admin.acme.local (
        2      ; serie
        6H     ; refresco (6 horas)
        1H     ; reintentos (1 hora)
        2W     ; expire (2 semanas)
        3H     ; mínimo (3 horas)
)
; name servers - NS records
      IN      NS      server.acme.local.

; PTR Records
1.128   IN      PTR     router.etxe.local.       ; 192.168.128.1
2.128   IN      PTR     nas.etxe.local.          ; 192.168.128.2

Finalmente establecemos el servicio para que arranque automáticamente

sudo systemctl enable named

Para verificar que la configuración es correcta:

sudo named-checkconf

También podemos verificar la configuración de las zonas usando este comando donde el primer parámetro es el nombre de la zona y el segundo el nombre del fichero.

sudo named-checkzone acme.local db.acme.local

Y comprobamos que todo es correcto con:

sudo service named restart
Servidor DHCP y DNS

Básicos Redis: Lua

Redis

Lua es un lenguaje de script de lado servidor, se podría decir que es de alguna forma los procedimientos almacenados de Redis, se ejecutan de forma atómica dentro de una transacción.

Un script se puede ejecutar durante 5 segundos antes de que Redis empiece a aceptar la ejecución de otros comandos, este parámetro se puede modificar pero el comportamiento es similar, los otros comandos empezarán a recibir mensajes de que el sistema está ocupado, se puede cerrar el script usado la instrucción SCRIPT KILL lo que hará un cierre seguro siempre que el script sea de solo lectura y no haya modificado ningún dato en caso de que lo haya hecho habrá que usar SHUTDOWN NOSAVE que provocará que pueda haber inconsistencias

Conversión de tipos

Redis valueLua value
IntegerNumber
Bulk replyString
Multi bulk replyTable (con otros tipos anidados)
Status replyTabla con campo «ok» indicando el estado
Redis errorTabla con campo «err» indicando el error
nil bulk reply and nil multi replyFalso (booleano)
Conversión tipos

Lua no usa números de punto flotante por lo que cualquier número con decimales se tiene que guardar como string. Una tabla Lua se puede devolver por ejemplo usando el comando HSCAN, también se pueden crear tablas al vuelo haciendo por ejemplo:
eval «return {KEYS[1], {ARGV[1], ARGV[2]}}» 1 hash-key field1 field2
Devolvería:
1) «hash-key»
2) 1) «field1»
2) «field2»

Elementos básicos

Algunos elementos básicos que se pueden usar en LUA parecidos a la programación tradicional.

Variables

Declarar variables locales

eval «local val=42 return val» 0
Este comando devolverá 42

if – else – then

if ARGV[1] == «sum» and ARGV[0] == «op» then
return k1 + k2
elseif ARGV[1] == «max» and ARGV[2] <= 10 then
return math.max(k1, k2)
else
return nul
end

Comentarios

Usando dos guiones —

— Convert arguments to numbers

Concatenar cadenas

Usando ..

local hold_key = ‘holds:’ .. KEYS[1]

Bucles

for _, hold_key in ipairs(hold_keys) do
local count = redis.call(‘HGET’, hold_key, ‘qty’)
— If the return is nil, then remove the hold from the list
if (count == nil) then
redis.call(‘SREM’, ticket_hold_key, hold_key)
else
tickets_held = tickets_held + count
end
end
return tickets_held

Comandos

Algunos comandos básicos asociados o necesarios para LUA

EVAL

Evalúa la expresión dada

Sintaxis

EVAL script numkeys key1 [keyN …] arg1 [argN…]

  • script: Script que se ejecutará.
  • numkeys: El número de key names que se utilizarán.
  • key: Las keys que se van a utilizar.
  • arg: Cualquier argumento extra necesario.

Ejemplo

eval «return redis.call(‘HGET, KEYS[1|], ARGV[1])» 1 hash-key field2

KEYS[1] es el primer elementos de keys (hash-key)
ARGV[1] es el primer argumento (field2)

Redis.call Redis.pcall

call devuelve el error y provoca que falle EVAL mientras que pcall devolverá devuelve una estructura representando la respuesta del error.

SCRIPT LOAD

Carga el script en Redis y devuelve un hash así se puede invocar el script usando el hash evitando tener que enviarlo cada vez optimizando el proceso.

Sintaxis

SCRIPT LOAD script

  • script: Script que se cargará.

Ejemplo

eval «local val=redis.call(‘GET’, KEYS[1|]) return val»

EVALSHA

Evalua un script pero usando su hash que se obtiene a través del comando SCRIPT LOAD.

Sintaxis

EVALSHA sha1 numkeys key1 [keyN …] arg1 [argN…]

  • sha1: Digest del script que se ejecutará.
  • numkeys: El número de key names que se utilizarán.
  • key: Las keys que se van a utilizar.
  • arg: Cualquier argumento extra necesario.

SCRIPT EXISTS

Comprueba si un script existe

Sintaxis

script exists sha1… [shaN]

SCRIPT FLUSH

Elimina todos los scripts que haya en caché.

SCRIPT KILL

Cierra el script en ejecución, por ejemplo si está tardando demasiado.

SCRIPT DEBUG [YES|SYNC|NO]

No se recomienda usar en producción porque durante la depuración no se acepta la ejecución de otro comando.

SHUTDOWN NOSAVE

Reinicia sin guardar.

Básicos Redis: Lua

Básicos Git (VI): Stash

Stash es una acción que ya lleva bastante tiempo dentro de Git y ahora está disponible dentro de Visual Studio, stash lo que permite es aparcar los cambios actuales para poder retomarlos después, es similar a usar shelve en TFS.

Un escenario típico de uso es cuando se está trabajando en una rama y es necesario cambiar a otra pero sin perder el trabajo realizado pero tampoco sin querer hacer un commit, (por ejemplo la correción de un bug).

Stash

En este escenario sobre los archivos que seleccionemos usamos la opción stash que encontraremos en el botón Commit marcando una de las dos opciones disponibles.

Se puede hacer un stash moviendo los cambios a un área apartada quitando los cambios del area de stage (–include-untracked) o se puede hacer esto pero manteniendo los archivos en stage (–keep-index).

Apply and Pop

Si más adelante después de terminar con la otra rama queremos volver al trabajo guardado no tendremos más ir a la sección Stashes y tendremos dos posibles opciones:

  • Apply Esto vuelve a colocar los cambios en la rama y los sigue manteniendo en stashes
  • Pop Es lo mismo que Apply pero además lo elimina de la lista de stash.

Ambas opciones permiten especificar al igual que al hacer un stash si queremos que estos cambios se procesen y se coloquen en el area de stage (–index) o no.

Stash tiene el mismo comportamiento que si se intentase hacer un merge, es decir no se puede restaurar código si el código sobre el que se va a actuar está modificado, habrá que hacer un commit o deshacer los cambios.

De igual forma si el código tiene una versión más actualizada y se intenta efectuar un stash con código anterior se producirá un conflicto que habrá que resolver.

También es posible eliminar ese conjunto de cambios con Drop o ver los cambios en ese conjunto (View changes).

Básicos Git (VI): Stash