Básicos Redis: Estructuras de datos

Redis

Aquí indico unas notas sobre estructuras básicas, si un comando acepta varias opciones los comento en la sección de ejemplos.

Claves

Parejas clave valor:

  • Son únicas.
  • Por defecto si no existe se crea.
  • Almacenadas en formato binario (se puede cualquier elemento binario como clave).
  • Valores de clave de 512 MB de tamaño máximo.
  • Sensible a mayúsculas / minúsculas.
SET key value [EX seconds] [PX milliseconds|EX seconds] [NX|XX]Crea la clave de nombre key con valor valueO(1)
MSET key1 value1 key2 value2 … keyN valueNInserta varias claves a la vez con sus valores correspondientesO(N)
GET keyObtiene la clave de nombre keyO(1)
KEYS [pattern]Recupera todas las clavesO(N)
SCAN slot [MATCH pattern] [COUNT count]Recupera las claves por gruposO(1)/O(N)
DEL key1 key2Borra las claves indicadas, es un comando bloqueanteO(1)/O(N)/(OM)
UNLINK keyBorra la clave indicada de forma asíncronaO(1)/O(N)
EXISTS keyIndica si la clave existe (0 indica que no existe)O(1)
TTL keyTiempo restante de la clave antes de expirar en segundosO(1)
PTTL keyTiempo restante de la clave antes de expirar en milisegundosO(1)
INCR keyAumenta en 1 el valor de key (tiene que ser un número)O(1)
INCRBY key [-]incrAumenta o decrementa (-) en incr el valor de key (tiene que ser un número)O(1)
DECRBY key decrDecrementa en decr el valor de key (tiene que ser un número)O(1)
PEXPIRE key secondsCambia el tiempo de expiración de key en segundosO(1)
EXPIREAT key timestampIndica el momento en que expirará en tiempo unix en segundosO(1)
PEXPIREAT key timestamp_in_millisecondsIndica el momento en que expirará en tiempo unix en milisegundosO(1)
TYPE keyDevuelve el tipo de dato de keyO(1)
OBJECT [ENCODING| REFCOUNT| IDLETIME| FREQ| HELP] keyDevuelve información internal de keyO(1)
Claves

Aunque KEYS y SCAN sirven para lo mismo (devolver todas las claves) tienen algunas diferencias.

KEYSSCAN
Bloque hasta que terminaUsa un cursor para iterar, útil para producción
No es para producciónDevuelve una referencia a un grupo
Útil para depuraciónDevolverá 0 o más claves por llamada
Keys vs Scan

Ejemplos

SET claves:ejemplo:clave1 valor_nuevo
– Crea una clave de nombre claves:ejemplo:clave1 con valor valor_nuevo al no tener Redis espaciones de nombres suele ser interesante separar los elementos con «:» del elemento más general al específico
SET claves:ejemplo:clave1 valor_nuevo NX
– Crea la clave solo si no existía previamente (NX=No exist)
SET claves:ejemplo:clave1 valor_nuevo XX
– Crea la clave solo si exístia previamente
SET claves:ejemplo:clave1 valor_nuevo EX 7200
– Crea la clave claves:ejemplo:clave1 que expirará en 7200 segundos
SET claves:ejemplo:clave1 PX 50000
– Crea una clave que expirará en 50000 ms
SET claves:ejemplo:clave1 EX 50
– Crea una clave que expirará en 50 s (igual que el anterior)

GET claves:ejemplo:clave1
– Devuelve el valor de esta clave, en este caso valor_nuevo

KEYS claves:ejemplo:cl*
– Devuelve todas las claves que empiecen por cl*

SCAN 0 MATCH claves:ejemplo:cl*
– Devuelve todas las claves que empiecen por cl* empezando desde el princpio 0 devolverá un nuevo valor que se usará para la siguiente llamada, por ejemplo la salido puede ser:
1) «14848»
Entonces la siguiente llamada sería:
SCAN 14848 MATCH claves:ejemplo:1*
SCAN 14848 MATCH claves:ejemplo:1* 1000
– Indicamos que queremos buscar en bloques más grandes (1000)

Rendimiento

O(1): HDEL cuando borra un solo elemento, UNLINK, SCAN en una llamada
O(N): HDEL cuando borra varios elementos, siendo N el número de elementos, UNLINK en su hilo siendo N el número de elementos, SCAN para la iteración completa siendo N el número de elementos en la colección
O(M): HDEL cuando la clave eliminada contiene una estructura como lista, conjunto, conjunto ordenado o Hash, siendo M el número de elementos dentro de la esctructura

Hashes

Conjuntos de campos clave/valor.

  • Así como existe un comando para borrar campos (HDEL) no existe uno para añadir, tanto la creación como la adición de campos se hace con HSET
  • HMGET recomendado para pocas claves, si van a recuperarse muchas es mejor HSCAN
  • No existe un comando para decrementar
HSET key field1:value1 fieldN:valueN…Creación de un hashsetO(N)
HSETNX key field valueEstablece el valor value al compo field en key solo si no existía previamente, 1 indica que se cambió el valor y 0 lo contrarioO(1)
HGETALL keyDevuelve todos los campos del conjunto keyO(N)
HDEL key field1… fieldNElimina el campo field del conjunto keyO(N)
HGET key fieldObtiene el valor del campo field del conjunto keyO(1)
HMGET key field…fieldNObtiene las claves de los valores indicadosO(N)
HINCRBY key field valueIncrementa el campo field con la cantidad value (tiene que ser numérico)O(1)
HINCRBYFLOAT key field valueIncrementa el campo field con la cantidad value (tiene que ser numérico)O(1)
HSCAN key slot [MATCH pattern] [COUNT count]Devuelve todos los campos de key O(1)/O(N)
HEXISTS key fieldComprueba si existe el campo field en key (1 existe 0 no)O(1)
HKEYS keyObtiene todas las claves (N) de un keyO(N)
HVALS keyObtiene todos los valores (N) de una keyO(N)
Hashes

Ejemplos

HSCAN hash:full 0 MATCH claves:ejemplo:cl*
– Tiene el mismo efecto que SCAN para claves pero sobre el hash hash:full

Rendimiento

O(1): HSCAN en una llamada
O(N): HGETALL (siendo n el número de campos), HSCAN para la iteración completa siendo N el número de elementos en la colección

Listas

Se pueden implentar como colas o como pilas añadiendo (PUSH) y quitando (POP) elementos de los extremos. Admite hasta 4 billones de elementos.

RPUSH list value1 …valueNInserta por la derecha el elemento value en la lista listO(N)
LPUSH list value1 …valueNInserta por la izquierda el elemento value en la lista listO(N)
LPOP list [N]Devuelve y elimina N elementos por la izquierda de la lista listO(N)
RPOP listDevuelve y elimina N elementos por la derecha de la lista listO(N)
LRANGE list start [-]stopDevuelve los N elementos de la lista list desde el índice start (S) hasta stop (índice basado en 0) empezando por la izquierda si stop es negativo empezará desde la derechaO(S+N)
LLEN listDevuelve el número de elementos de la lista listO(1)
LTRIM list start [-]stopElimina los N elementos de la lista list excepto los contenidos entre los índices start y stop (índice basado en 0) empezando por la izquierda y si stop es negativo contará desde la derechaO(N)
LINSERT key [after|before] key1 valueInserta en la lista key antes (before) o después (after) del emento key en la posición N el valor valueO(N)
LINDEX key indexDevuelve el elemento en el índice (index) especificado de la lista de tamaño N keyO(N)
LSET key index valueInserta el elemento value en el índice (index) especificado de la lista de tamaño N keyO(N)
LREM key count elementElimina las count (M) primeras ocurrencias del elemento element en la lista de tamaño N keyO(M+N)
Lists

Ejemplos

Teniendo una lista a,b,c,d,e,f llamada list
LTRIM list 0 4
Devuelve todos los elementos desde a hasta e
LTRIM list 1 -2
Devuelve los elementos b,c,d,e (f es el elemento -1 y e es el elemento -2)
LRANGE list 0 -1
Devuelve todos los elementos de la lista

Rendimiento

O(1): LPOP, RPUSH, LLEN
O(s+n): LRANGE (donde s es la distancia al primer elemento y n el número total de elementos)

Conjuntos

Colección no ordenada de elementos no duplicados

SADD key element1 …elementNAgrega los elementos elementX al conjunto de clave keyO(N)
SCARD keyDevuelve el número de elementos en el conjunto de clave keyO(1)
SISMEMBER key elementIndica si el elemento element está dentro del conjunto (devuelve 1) o no (devolverá 0)O(1)
SINTER key1 key2 key3 …Realiza la intersección y devuelve los elementos de los conjuntos de claves keyXO(N*M)
SDIFF key1 …keyXRealiza la diferencia de los conjuntos keyXO(N)
SREM key element …elementNQuita los N elementos element al conjunto de clave keyO(N)
EXPIREAT key timeEl conjunto de clave key expirará en el tiempo indicado en time (Unix timestamp)O(1)
SSCAN key slot [MATCH pattern] [COUNT count]Devuelve todos los campos de keyO(1)/O(N)
SPOP key [count]Devuelve count (N) elementos aleatorios de keyO(1)
SUNION key1 … keyXRealiza la unión de todos los elementosO(N)
Conjuntos

Ejemplos

Teniendo un conjunto A = {A, b, C} y B = {a, b, C}:
SDIFF A B
– Devolverá la diferencia de conjuntos, los elementos que están en A y no están en en B, es decir «A»
SSCAN set:full 0 MATCH claves:ejemplo:cl*
– Tiene el mismo efecto que SCAN para claves pero sobre el hash hash:full

Rendimiento

O(N*M): SINTER, siendo N la cardinalidad del conjunto pequeño y M el número de conjuntos
O(N): SDIFF y SUNION donde N es el número total de elementos de todos los conjuntos

Conjuntos ordenados

Una colección ordenada de elementos únicos, para ordenarlos a cada elemento se le asocia una puntuación (score) que será el valor usado para la ordenación.
Si esta puntuación es igual se realiza un orden lexicográfico basado en el nombre del key. Útil para:

  • Colas con prioridad
  • Mostrar paneles de puntuación con baja latencia
  • Índices secundarios
ZADD key [NX|XX] [CH] [INCR] score memberAgrega un elemento a key (tamaño N) con la puntuación score y el campo memberO(log(N))
ZINCRBY key [-]increment memberIncrementa o decrementa [-] el valor del campo member a key (tamaño N) en un valor increment.O(log(N))
ZRANGE key [BYSCORE|BYLEX] [REV] [LIMIT offset count] [WITHSCORES]Devuelve M elementos desde el score más bajo al más alto de key (tamaño N)O(log(N)+M)
ZRANK key memberDevuelve la posición de member dentro de key ordenada de menor a mayor (si devuelve 0 tendría el score más bajo)O(log(N))
ZREVRANK key memberDevuelve la posición de member dentro de key ordenada de mayor a menor (si devuelve 0 tendría el score más alto)O(log(N))
ZSCORE key memberDevuelve el score del elemento member dentro del conjunto de clave keyO(1)
ZCARD keyDevuelve el número de elementos en el conjunto de clave keyO(1)
ZREM key value1 …valueNElimina M elementos del key (tamaño N) por nombreO(M*log(N))
ZREMRANGEBYRANK key min maxElimina M elementos de key (tamaño N) entre los índices start y stop (índice basado en 0) empezando por la izquierda y si stop es negativo contará desde la derechaO(log(N)+M)
ZREMRANGEBYLEX key min maxElimina M elementos de key (tamaño N) en el rango lexicográfico entre start y stop (índice basado en 0) empezando por la izquierda y si stop es negativo contará desde la derecha O(log(N)+M)
ZREMRANGEBYSCORE key min maxElimina M elementos de key (tamaño N) cuya puntuación esté entre start y stop (índice basado en 0) empezando por la izquierda y si stop es negativo contará desde la derechaO(log(N)+M)
ZCOUNT key min maxDevuelve el número de elementos con una puntuación entre min y max en key (tamaño N) O(log(N))
ZINTERSTORE destination numkeys key1..keyN [WEIGHTS weight [weight …]] [AGGREGATE SUM|MIN|MAX]Realiza la intersección de numkeys conjuntos key1… y almacena el valor en destinationO(N*K)+O(M*log(M)) 
ZUNIONSTORE destination numkeys key1…keyN [WEIGHTS weight [weight …]] [AGGREGATE SUM|MIN|MAX]Realiza la union de numkeys conjuntos key1… y almacena el valor en destinationO(N)+O(M log(M))
Sorted Sets

Ejemplos

ZREMRANGEBYRANK list:cash 9 -1
– Elimina desde el elemento 9 hacia la derecha, es decir, el último (j)
ZRANGEBYSCORE list:cash (3 +inf
– Devuelve los elementos que tenga un score superior a 3 (3 hasta el final de la lista +inf
ZADD claves:ejemplo:clave1 30 value1 NX
– Crea la clave solo si no existía previamente (NX=No exist)
ZADDclaves:ejemplo:clave1 30 value1 XX
– Actualiza la clave solo si exístia previamente
ZADD claves:ejemplo:clave1 30 value1 CH
– Devuelve solo los elementos que hayan cambiado, por defecto devuelve los creados
ZADD claves:ejemplo:clave1 30 value1 INC
– Se comporta como ZINCRBY
ZRANGE claves:ejemplo 10 20 BYLEX
– Devuelve los elementos con score entre 10 y 20, los que tengan el mismo score estarán ordenados lexicograficamente
ZRANGE claves:ejemplo 10 20 REV
– Devuelve los elementos en orden inverso
ZRANGE claves:ejemplo 10 20 LIMIT 1 9
– Devuelve los elementos desde el 1 hasta el 19 (si count es negativo devuelve todos los elementos desde offset).

Rendimiento

O(N*K)+O(M*log(M)): ZINTERSTORE siendo N el conjunto más pequeño, K el número de conjuntos y M el número de elementos en el conjunto resultante

Bit data

Conjuntos de de bits, el offset se entiende como el desplazamiento dentro del mapa de bits de tamaño, por ejemplo un offset de 9 indicaría la novena columna de la primera fila quedando así 0000 0001 en un caso más general usaremos la fórmula offset = Y * MAX_WIDTH + X por lo que si quisiéramos marcar la primera fila de la novena columna en un mapa de tamaño N (=MAX_WIDTH) tendríamos: (suponemos un tamaño de N=20)
offset = 1 * 20 + 9 = 29 y el y el mapa de bits sería 0000 0000 0000 0000 0000 000 0000 1, si juntamos las dos coordenadas (9,0) y (9,1) y lo apilamos en líneas de 20 (N) queda:

00000000000100000000
00000000000100000000

Offset se puede indicar también usando # en este caso Redis calcula la posición del bit dependiendo del tamaño del tipo, por ejemplo #1 para u8 sería el elemento 8*1 de esta forma se tiene un número de elementos cada uno con un valor y tamaño fijo.

SETBIT key offset valueEstablece value (1 o 0) al elemento offset dentro del mapa de bits keyO(1)
GETBIT key [-]offsetRecupera el valor del bit para el mapa key y offset especificado, si es negativo se excluye la parte especificadaO(1)
BITCOUNT offset keyDevuelve el número de bits a 1 en el mapa key a partir de offsetO(N)
BITFIELD key
[get|set|incrby] type offset value [overflow wrap| sat|fail]
Devuelve (get) se establece (set) o incrementa (incrby) un valor value a un elemento type, definido como u para unsigned e i para signed seguido del tamaño en bits, por ejemplo u5, los comandos get and set se puede acumular en la misma instrucción bitfieldO(1)
BITOP [and|or|xor|not] key key1 key2Ejecuta la operación [or] sobre key1 y key2 y almacena el resultado en keyO(N)
BITPOS key [0|1|start end]Busca en key el valor 0 o 1 según se indique, si se especifican dos valores indica un intervalo a buscar, en este caso el valor 0.O(N)
Mapa de bits

Datos geoespaciales

Permite guardar datos usando coordenadas (latitud, longitud), se almacenan codificados en GeoHash (52 bits), se almacenan como un sorted set donde GeoHash es el score por lo que los mismos comandos de sorted set se pueden usar también aquí.

GEOADD key longitude1 latitude1 member … [longitudeN latitudeN]Añade una ubicación name al conjunto key de N elementos usando la longitud y latitud especificada, si se le llama con los mismos datos actualizará la posición existente.O(log(N))
GEOSEARCH key [FROMLONLAT longitude latitude] [BYRADIUS radius m|km|ft|mi] [BYBOX width height m|km|ft|mi][WITHDIST] [WITHCOORD] [ASC | DESC] [COUNT n]Devuelve los N sitios en el radio (byradius) o caja (bybox) indicado de los M existentes, usando como centro la longitud y latitud especificada o el ancho y alto, WITHDIST devuelve la distancia, WITHCOORD devuelve también las coordenadas, usando ASC se devolverán los resultados en orden empezando por la más cercana o viceversa usando DESC, se puede limitar el número de resultados a devolver con COUNT, unit es la unidad de medida a devolver: m (metros) km (kilometros) mi (millas) ft (pies).O(N+log(M))
GEOSEARCHSTORE store source [FROMLONLAT longitude latitude] [BYRADIUS radius m|km|ft|mi] [BYBOX width height m|km|ft|mi] [ASC | DESC] [COUNT n]Este comando es como GEOSEARCH pero almacena los resultados en el store indicado, si se usa la opción STOREDIST almacena los resultados en un conjunto ordenado con la distancia al centro desde el círculo o caja.O(N+log(M))
GEOHASH key member1 … [memberN]Devuelve el valor de GeoHash para el elemento indicado dentro del sorted set de N elementosO(log(N))
GEOPOS key member1 … [memberN]Devuelve la longitud y latitud de todos los N elementos solicitados dentro del conjunto keyO(N)
GEODIST key member1… [memberN] [unit]Devuelve la distancia entre dos elementos en la unidad indicada: m (metros) km (kilometros) mi (millas) ft (pies).O(log(N))
Geoposición
Básicos Redis: Estructuras de datos

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 Redis: Características

Redis

Instalación

Hay varias opciones de instalar Redis, tanto en la nube como en local, así como un par de opciones interesantes de clientes para acceder a hacer consultas.

Comandos de sistema

Una pequeña lista de comandos básicos de sistema:

DBSIZEDevuelve el número de keys en la base de datos seleccionada
OBJECT ENCODING KEYDevuelve el tipo de dato de key
Comandos básicos

Transacciones

Manejo de transacciones en Redis, Redis cumple ACID.

Detalles:

  • No se permiten transacciones anidadas.
  • DISCARD no ejecuta ningún comando, no existe el concepto de rollback porque la cola de comandos no se ejecuta hasta usar EXEC.
  • Redis implementa concurrencia optimista.
MULTIComienzo de una transacción
EXECEjecuta la cola de comandos
DISCARDDescarta los comandos
WATCH keyObserva una key, si esta cambia entonces no se ejecuta la transacción, este comando se ejecuta antes de MULTI, este comando es local no afecta a transaccciones de otros procesos
UNWATCH keyDeja de observar todas las keys, si la transacción es correcta se ejecuta un UNWATCH automático de todas las keys que se estuvieran observando
Transacciones

Básicos Redis: Características

Docker Raspberry: Servidor multimedia con Compose

Actualizado el 19 de Octubre de 2021

Con la llegada de Raspberry surgieron muchas distribuciones multimedia como Kodi, OSMC, OpenELEC,… todas tienen en común que vienen preparadas para ser utilizadas sin ninguna instalación y con su propia interfaz gráfica y proporcionan plugins para ampliar la funcionalidad, por ejemplo emisoras de radio o clientes Torrent.

Estas distribuciones al estar dedicadas en exclusiva a funciones multimedia no permiten realizar otras acciones. Puede ser que no estemos todo el tiempo usándola de este modo y queramos darle más usos, sobre todo con los últimos modelos que son más potentes así que una alternativa es usar contenedores Docker, en este caso se van a crear tres contenedores:

  • Transmission: Un cliente torrent personalizable.
  • Mopidy: Servidor de audio compatible con Spotify.
  • Plex: Un servidor de streaming de video.
Docker

Dockerfile

Todos los contenedores se pueden ejecutar por separado usando los siguientes ficheros Dockerfile o usando el fichero compose que se indica al final. El código fuente está aquí. Vamos a suponer que las carpetas locales están bajo el raíz /mnt/media

Docker Mopidy

El fichero Docker para Mopidy está basado en este artículo, tiene consejos y recomendaciones sobre la configuración de sonido, la variable de entorno AUDIO maneja la salida de audio. Hay que tener en cuenta que si los altavoces están compartidos, por ejemplo tanto un ordenador como una Raspberry están conectados solo uno puede estar usándolo a la vez y el otro dispositivo tendrá que cambiar su salida de audio para liberar el canal.

# 0. Official armv32 image
FROM debian:latest

LABEL "guru.raraavis.creator"="blog@raraavis.guru"
LABEL "guru.raraavis.version"="1.0.0"
LABEL "guru.raraavis.release-date"="20/03/2021"
LABEL "guru.raraavis.description"="Raspberry Pi 3 image with Mopidy"

# 1. Environment vars
ARG DEBIAN_FRONTEND=noninteractive
ARG TZ=Europe/Madrid
ENV TZ=$TZ
        # Use 1 for headphones, 0 for HDMI
ARG AUDIO=1

# 2. Update system and install necessary packages
RUN     apt-get update && \
        apt-get install -y \
                wget \
                lsb-release \
                gnupg2 \
                gnupg \
                gnupg1 \
                python3-dev \
                python3-pip \
                libffi-dev \
                sudo \
                gosu \
                dumb-init && \
                cron && \
                rm -rf /var/lib/apt/lists/* && \
                rm -rf /tmp/*

# 3. Add mopidy repository
RUN     wget -q -O - https://apt.mopidy.com/mopidy.gpg | apt-key add -
RUN     DEBIAN_CODENAME=`lsb_release -sc` && \
        wget -vO /etc/apt/sources.list.d/mopidy.list "https://apt.mopidy.com/$DEBIAN_CODENAME.list"

# 4. Install pip packages and Mopidy
RUN     apt-get update && \
        apt-get install -y \
                mopidy \
                libspotify-dev && \
        rm -rf /var/lib/apt/lists/* && \
        rm -rf /tmp/*
RUN     python3 -m pip install \
                pyspotify \
                Mopidy-Local \
                Mopidy-Iris \
                Mopidy-Mobile \
                Mopidy-Spotify \
                Mopidy-Mopify

# 5. Configure user
RUN     usermod -d /mnt/mopidy mopidy
RUN     echo "mopidy ALL=NOPASSWD: /usr/local/lib/python3.7/dist-packages/mopidy_iris/system.sh" >> /etc/sudoers

# 6. Create folders
WORKDIR /mnt/mopidy
RUN     mkdir /mnt/mopidy/data
RUN     mkdir /mnt/mopidy/cache
COPY    scripts/ scripts/

# 7. Configuration local scan
RUN     (crontab -l 2>/dev/null || true; echo "0 */1 * * * mopidy --config  /mnt/mopidy/config/mopidy.conf local scan") | crontab -
RUN     ln -sf /mnt/mopidy/config/mopidy.conf /etc/mopidy/mopidy.conf

# 8. 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


# 9. Container
EXPOSE  6680
VOLUME  /mnt/mopidy/config
VOLUME  /mnt/mopidy/local

ENTRYPOINT ["/usr/bin/dumb-init","--"]
CMD ["/bin/sh", "scripts/entry_point.sh", "mopidy", "--config", "/mnt/mopidy/config/mopidy.conf"]

Instalamos algunos paquetes de sistema necesarios para poder instalar Mopidy, el paquete curl se instala para poder usar el comando HEALTHCHECK en el fichero docker-compose.yml

Instalamos Mopidy usando el repositorio oficial, en este caso usamos Debian porque el repositorio está orientado a esta distribución de tal forma que al usar el comando lsb_release podemos parametrizar el acceso al repositorio.

Una vez instalado Mopidy instalamos algunos plugins (se podrían instalar más). Usamos el usuario creado mopidy para arrancar el servicio y configuramos las carpetas donde guardar la configuración (/config) y la carpeta de archivos locales (/local) para el plugin local.

Como el plugin local tiene que escanear si hay cambios en las carpetas añadimos una tarea cron que se ejecutará cada hora para refrescar carpetas. Este mismo comando se puede ejecutar desde la opción Start local scan para eso añadimos al usuario mopidy al fichero sudoers para que pueda lanzar el comando. Este comando lanzado a través de la web utilizar el fichero de configuración por defecto así que creamos un enlace simbólico al que vamos a usar.

En este caso el usuario mopidy no hace falta que tenga derechos de root por seguridad, aunque es verdad que sería necesario para actualizar algunos plugins en este caso es más práctico actualizar el contenedor.

También se usa el paquete dump-init, el proceso mopidy no está orientado a ser usado como proceso padre así que es difícil gestionar su ciclo de vida si sucede algo y es necesario reiniciarlo, por eso gestionamos el contenedor a través de dumb-init que es el proceso padre del contenedor y el que arranca el proceso de mopidy.

Para obtener un id de cliente y un secret de Spotify podemos ir aquí.

Compilación y ejecución

Para compilar podemos usar:
docker build –tag user/mopidy -f Dockerfile.mopidy .

Para ejecutar podemos usar:
docker container run \
–init \
–publish 6680:6680 \
–device /dev/snd \
–volume /mnt/media/mopidy/config:/mnt/mopidy/config \
–volume /mnt/media/mopidy/local:/mnt/mopidy/local \
–env RA_UUID=1000 \
–env RA_GUID=1000 \
–env RA_SRVC=mopidy \
–env RA_FLDR=/mnt/mopidy \
–detach \
user/mopidy

Un detalle importante es usar –device /dev/snd para que el dispositivo de sonido sea utilizado dentro del contenedor. Una vez que arranquemos el contenedor tendremos que hacer login en Spotify y puede que también refrescar el token y la lista de reproducción.

Migración

Si ya tenemos una instalación de Mopidy podemos coger el fichero de configuración buscándolo con:
sudo find / -name mopidy*
Normalmente estará en el home del usuario en la carpeta .config, por ejemplo /home/pi/.config/mopidy/mopidy.conf

Docker Transmission

El fichero Docker para Transmission está basado en este artículo aunque también existe una variante de linuxserver.

# 0. Official armv32 image
FROM ubuntu:latest

LABEL "guru.raraavis.creator"="blog@raraavis.guru"
LABEL "guru.raraavis.version"="1.0.0"
LABEL "guru.raraavis.release-date"="20/03/2021"
LABEL "guru.raraavis.description"="Raspberry Pi 3 image with Transmission"

# 1. Environment vars
ARG DEBIAN_FRONTEND=noninteractive
ARG TZ=Europe/Madrid
ENV TZ=$TZ

# 2. Update system and install necessary packages
RUN     apt-get update && \
        apt-get install -y \
                gosu \
                software-properties-common \
                gnupg2 \
                gnupg \
                gnupg1 && \
                rm -rf /var/lib/apt/lists/* && \
                rm -rf /tmp/*

# 3. Add transmission repository and install Transmission
RUN     add-apt-repository ppa:transmissionbt/ppa && \
        apt-get update && \
        apt-get install -y \
                transmission-cli \
                transmission-common \
                transmission-daemon && \
                rm -rf /var/lib/apt/lists/* && \
                rm -rf /tmp/*

# 4. Configure user
RUN     usermod -d /mnt/transmission debian-transmission

# 5. Create folder
WORKDIR /mnt/transmission
COPY    scripts/ scripts/

# 6. Container
EXPOSE  9091
VOLUME  /mnt/transmission/config
VOLUME  /mnt/transmission/watch
VOLUME  /mnt/transmission/download
VOLUME  /mnt/transmission/temp

ENTRYPOINT ["/bin/sh", "scripts/entry_point.sh"]
CMD ["/usr/bin/transmission-daemon", "-g", "/mnt/transmission/config", "-f", "-x", "/mnt/transmission/config/trans.PID"]

Para evitar un error parecido a este:
UDP Failed to set receive buffer: requested 4194304, got 425984 (tr-udp.c:84)

Tenemos que editar algunos valores del kernel en el host estos valores no se pueden modificar en el contenedor al ser valores compartidos. Hay varias formas de editar estos valores, una forma sería la siguiente:

Primero editamos el fichero sysctl.conf y añadimos lo siguiente (hay varias formas de editar sysctl).

net.core.rmem_max = 16777216
net.core.wmem_max = 4194304

Y refrescamos los valores con sysctl -p

Tenemos también varios volúmenes configurados:

  • /mnt/transmission/config para la configuración
  • /mnt/transmission/watch la carpeta watch donde buscar nuevos ficheros torrent para descargar
  • /mnt/transmission/download carpeta de descargas
  • /mnt/transmission/temp la carpeta temporal

Se usa un script especial para arrancar el contenedor y configurar los permisos, los detalles aquí.

Compilación y ejecución

Para compilar podemos usar:
docker build –tag user/transmission -f Dockerfile.transmission .

Para ejecutar podemos usar:
docker container run –init -p 9091:9091 -p 51413:51413 \
–volume /mnt/media/transmission/config:/mnt/transmission/config \
–volume /mnt/media/transmission/download:/mnt/transmission/download \
–volume /mnt/media/transmission/temp:/mnt/transmission/temp \
–volume /mnt/media/transmission/watch:/mnt/transmission/watch \
–env RA_UUID=1000 \
–env RA_GUID=1000 \
–env RA_SRVC=debian-transmission \
–env RA_FLDR=/mnt/transmission
\
–detach user/transmission

Migración

La carpeta de Transmission tiene no solo la configuración en el fichero settings.json también otras carpetas donde se guardan los ficheros torrent descargados y las estadísticas, se puede encontrar con sudo find / -name settings.json y normalmente estará ubicada en /var/lib/transmission-daemon
Hay que copiar todo este contenido dentro de la carpeta /mnt/transmission/config y actualizar el fichero settings.json para que apunte a las carpetas del contenedor (/mnt/transmission/download, ….)

Docker Plex

En este caso se ha usado un contenedor ya existente de Plex suministrado por linuxserver.

Compilación y ejecución

En este caso no hace falta compilar al ser una imagen sacada del registro de contenedores. Para ejecutar podemos ver los detalles aquí.

Migración

Si tenemos cuenta en Plex con hacer login se puede recuperar gran parte de la configuración, la carpeta con el resto de la información se puede sacar de aquí /var/lib/plexmediaserver/Library/Application Support/Plex Media Server tal y como se cuenta aquí.

Docker Compose

Ahora todos los contenedores se arrancan a la vez usando este fichero:

version: "3"
services:  
  mopidy:    
    container_name: mopidy    
    image: raraavis/mopidy    
    build:      
      context: ..
      dockerfile: media/mopidy/Dockerfile.mopidy
    ports:      
      - 6680:6680    
    volumes:      
      - /mnt/media/mopidy/config:/mnt/mopidy/config
      - /mnt/media/mopidy/local:/mnt/mopidy/local
    devices:
      - /dev/snd
    environment:      
      - RA_UUID=1000      
      - RA_GUID=1000      
      - RA_SRVC=mopidy
      - RA_FLDR=/mnt/mopidy
    healthcheck:
      test: ["CMD-SHELL", "curl -f http://localhost:6680 || bash -c 'kill -s 15 -1 && (sleep 10; kill -s 9 -1)'"]      
      interval: 30m
      timeout: 30s
      retries: 3
      start_period: 30s
    restart: unless-stopped
  transmission:    
    container_name: transmission    
    image: raraavis/transmission    
    build:      
      context: ..
      dockerfile: media/transmission/Dockerfile.transmission
    ports:      
      - 9091:9091      
      - 51413:51413    
    volumes:      
      - /mnt/media/transmission/config:/mnt/transmission/config
      - /mnt/media/temp:/mnt/transmission/temp
      - /mnt/media/download:/mnt/transmission/download
      - /mnt/media/watch:/mnt/transmission/watch
    environment:      
      - RA_UUID=1000      
      - RA_GUID=1000      
      - RA_SRVC=debian-transmission
      - RA_FLDR=/mnt/transmission
  plex:    
    container_name: plex    
    image: ghcr.io/linuxserver/plex    
    container_name: plex    
    network_mode: host    
    environment:      
      - PUID=105      
      - PGID=106      
      - VERSION=docker      
    #- PLEX_CLAIM= see .env file
    volumes:      
      - /mnt/media/plex/config:/config      
     #- /mnt/media/plex/tv:/tv      
      - /mnt/media/movies:/movies    
  restart: unless-stopped

Algunos aspectos a destacar son que el servidor Mopidy usa un health check para verificar si sigue activo, esto es debido a un bug reportado a Mopidy, el comando lo que hace es verificar si el servidor sigue activo y sino lo está mata el contenedor para que se reinicie usando el comando restart, más información aquí.

Para rellenar PLEX_CLAIM puede usarse esta página, esta variable está en un fichero de configuración separado .env otra opción sería usar secrets pero esto condiciona a la creación de un swarm.

Para docker transmission se pueden configurar varias carpetas pero si algunas de ellas está anidadas dentro de otras pueden ocultarse entre ellas de tal forma que no verá el contenedor los ficheros que dejemos, esto no es algo que suceda usando la línea de comandos de docker. La forma de evitarlo es indicar solo la carpeta raíz.

La estructura de carpetas es la misma que aparece en el código fuente.

Una vez que las imágenes han sido creadas y subidas al repositorio se puede comentar la sección de build.

Compose: compilación y ejecución

El fichero está configurado para compilar los ficheros dockerfile indicados usando docker-compose build el nombre de la imagen será el indicado en image.

Para ejecutar simplemente usamos docker-compose up y podremos ir viendo los mensajes o si lo queremos en segundo plano agregamos la opción –detach y quedaría docker-compose up –detach

Puede pasar que algunos valores del contenedor se mantengan entre ejecuciones y veamos mensajes como:
WARNING: Service «transmission» is using volume «/watch» from the previous container. Host mapping «/watch» has no effect. Remove the existing containers (with docker-compose rm transmission) to use the host volume mapping.

Solo hay que fijarse en el mensaje y ejecutar la instrucción que indica docker-compose rm transmission

Posible arquitectura

Se puede plantear lo siguiente:

  • Los contenedores Docker se colocan en la misma Rpi usando Docker compose.
  • Las carpetas de descarga apuntan a un almacenamiento externo, para saber como hacerlo se puede ver aquí.
  • La carpeta watch se puede crear como un recurso compartido vía Samba para poder dejar los ficheros torrent desde cualquier otra máquina y que se vayan descargando sin tener que hacerlo a través del cliente web.
  • Haría falta un cliente Plex que puede ser una SmartTv, u otra Raspberry Pi con esta distribución.
  • Opcionalmente se puede colocar por delante el proxy Apache como se indica más abajo.

(Opcional) Proxy Apache

Es posible usar un servidor Apache que sirva de proxy para poder acceder a los servicios de forma más cómoda usando una URL con nombre en lugar de a través de la IP y del puerto, en cualquier caso tiene que haber un DNS que pueda resolver los nombres.

Tenemos que habilitar algunos módulos, para permitir que funcionen los websockets cors.

a2enmod headers
a2enmod proxy
a2enmod proxy_http
a2enmod proxy_wstunnel
a2enmod headers

Vamos a suponer que la IP del contenedor es 192.168.1.2, y que el dominio es rpi.local, hay que crear los siguientes ficheros de configuración de apache y colocarlos en la carpeta /etc/apache2/sites-available

001-transmission.conf

<VirtualHost *:80>
      ServerName torrent.rpi.local
      ServerAlias torrent
      ProxyPreserveHost On
      ProxyPass / http://192.168.1.2:9091/
      ProxyPassReverse / http://192.168.1.2:9091/
      CustomLog /var/log/apache2/transmission.log combined
      ErrorLog /var/log/apache2/transmission.error.log
</VirtualHost>

Para habilitar el sitio ejecutamos:

sudo a2ensite 001-transmission.conf

002-mopidy.conf

<VirtualHost *:80>
      ServerName spotify.rpi.local
      ServerAlias spotify
      ProxyPreserveHost On
      ProxyPass / http://192.168.1.2:6680/
      ProxyPassReverse / http://192.168.1.2:6680/
      CustomLog /var/log/apache2/mopidy.log combined
      ErrorLog /var/log/apache2/mopidy.error.log
</VirtualHost>

Para habilitar el sitio ejecutamos:

sudo a2ensite 002-mopidy.conf

003-plex.conf

<VirtualHost *:80>
      ServerName plex.rpi.local
      ServerAlias plex
      ProxyPreserveHost On
      ProxyPass / http://192.168.1.2:32400/
      ProxyPassReverse / http://192.168.1.2:32400/
      CustomLog /var/log/apache2/plex.log combined
      ErrorLog /var/log/apache2/plex.error.log
</VirtualHost>

Para habilitar el sitio ejecutamos este comando para los diferentes virtual host, por ejemplo:

sudo a2ensite 003-plex.conf

Docker Raspberry: Servidor multimedia con Compose

Cambiando permisos en volúmenes Docker

Actualizado el 24 de Marzo de 2021

A veces es necesario que tanto el contenedor como el host accedan a la misma carpeta ya sea local al host o una carpeta externa, esto se puede configurar a través de los volúmenes Docker, para que ambos puedan tener los mismos permisos es necesario que compartan el mismo id de usuario o de grupo, esto es importante porque el nombre de usuario no servirá.

Normalmente esto se soluciona creando un usuario o grupo en el contenedor con un id específico y usando ese id para establecer los permisos en el host. El problema es que a veces el usuario es creado automáticamente por un paquete, como un usuario de servicio, y no se puede conocer por anticipado.

Una solución sencilla es arrancar el contenedor y ejecutar dentro de este el comando id por ejemplo docker exec #contenedor id -u que devolverá el id de usuario que se está usando en el contenedor docker.

Puede ser que esta opción no sea viable porque el contenedor necesite acceder a esa carpeta para arrancar o simplemente no interesa esta acción manual al ejecutarse Docker en un entorno automatizado (DevOps).

Permisos Docker

Script

Una posible solución es usar este script basado en la imagen base de linuxserver.

#!/bin/sh
RA_SRVC=${RA_SRVC:-$(id -un)}

if [ "$RA_SRVC" = $(id -un) ]; then
  echo "Executing as self: $RA_SRVC"
  echo "You will need to be root or sudo"
  $@
else
  RA_SRVC=${RA_SRVC:-$(id -un)}
  uid=$(id -u $RA_SRVC)
  gid=$(id -g $RA_SRVC)
  RA_UUID=${RA_UUID:-$uid}
  RA_GUID=${RA_GUID:-$gid}
  RA_FLDR=${RA_FLDR:-$(pwd)}
  echo "  Starting $RA_SRVC uid=$(id -u $RA_SRVC) gid=$(id -g $RA_SRVC)
  Setting user id:  $RA_UUID $(gosu root usermod -o -u $RA_UUID $RA_SRVC)
  -------------------------------------
  User:     $RA_SRVC $(id -u $RA_SRVC)
  Folder:   $RA_FLDR
  -------------------------------------
  Setting permissions on $RA_FLDR $(gosu root chown -R $RA_UUID:$RA_GUID $RA_FLDR)
  Running $@
  "
  exec gosu $RA_SRVC $@
fi

Usamos las siguientes variables de entorno:

  • RA_SRVC: el usuario que ejecutará el contenedor Docker, si no se indica se supondrá el usuario actual del contenedor Docker.
  • RA_UUID: el nuevo id de usuario.
  • RA_GUID: el id de grupo que se usará para cambiar los permisos.
  • RA_FLDR: la carpeta en la que se establecerá el nuevo owner.

La idea del script es establecer un nuevo id de usuario (RA_UUID) al usuario que va a ejecutar Docker (RA_SRVC) estas variables de entorno se pueden pasar a Docker vía línea de comandos o a través de un fichero docker-compose, después se usa el comando chown para cambiar el propietario en la carpeta indicada (RA_FLDR) al usuario (RA_UUID) y grupo indicado (RA_GUID).

Si no se indica un usuario se entiende que es el establecido en Docker via comando USER o el indicado en docker-compose usando user. Este usuario (RA_SRVC) tiene que ser otro diferente al que arranca el contenedor ya que no se puede cambiar el id así mismo sin reiniciar.

La única opción si se intenta cambiar el id del usuario que está activo en Docker sería hacer un login para refrescar el id del usuario, por ejemplo a través del comando su, lo que implicaría pasar la contraseña al script para poder hacerlo o reiniciar el contenedor pero dado que Docker no conserva el estado no sería útil.

Se está usando gosu en lugar de sudo porque es la opción recomendada para contenedores Docker, para detalles del uso de gosu ver aquí, sino queremos usar gosu porque no se puede instalar la opción comentada también funcionaría sudo -u «$RA_SRVC» $

Hay que tener en cuenta que si el volumen a compartir es una unidad de red entonces en /etc/fstab se indica el usuario que se establecerá como owner al montar la carpeta.

Uso

Una forma típica de uso es usando el comando ENTRYPOINT de Docker, la diferencia con CMD es que ENTRYPOINT no se puede sobreescribir, por ejemplo:

ENTRYPOINT [«/bin/sh», «/entry_point.sh»]
CMD [«/usr/bin/transmission-daemon», «-f», «-x», «/tmp/trans.PID»]

Un ejemplo más completo aquí.

Mejoras

En la opción sudo es necesario que el usuario pueda ejecutar sudo para poder establecer los permisos y lanzar el comando que ejecutará el contenedor, esto se podría cambiar quitando sudo antes de lanzar el comando si el usuario puede lanzar el comando sin usar sudo, también se puede usar s6-setuidguid.

Si se ejecuta el contenedor con el usuario por defecto root se pueden establecer los permisos usando también root en el host aunque puede que no sea la opción más segura.

Notas al pie

Detalles sobre instrucciones bash aquí.
Monta tu propio logo en ascii-art.
El comando cat es para mostrar el logo.

Cambiando permisos en volúmenes Docker

Docker Raspberry: Jupyter kernels

Actualizado el 29 de Octubre de 2022

Docker

Jupyter tiene un sistema basado en kernels, que son procesos de ejecución independientes en un lenguaje de programación concreto, de tal forma que en Jupyter pueden convivir kernels ejecutándose en el mismo o en varios lenguajes.

En Python es posible crear entornos virtuales con configuraciones concretas de versiones de Python y paquetes, podemos usar esta idea junto con los kernels de Jupyter para crear diferentes kernels de Python.

Por ejemplo, vamos a suponer para este caso que queremos crear cuatro entornos de Python, cada una de ellas orientado a un propósito diferente con diferentes paquetes.

  • Un entorno con Python 3.9 orientado a IA pero usando la versión de TensorFlow lite que es la recomendada para Raspberry PI incluyendo los paquetes más habituales en ML (torch, pandas, keras, scikit, numpy, …) y algunos paquetes de utilidad (split-folder, BeautifulSoup4, …)
  • Un segundo entorno similar al anterior pero con la versión de TensorFlow completa compilada para Raspberry PI junto con algunos paquetes adicionales como keras-tuner.
  • Un tercer entorno similar al anterior pero orientado a Reinforcement Learning (gym, keras-rl, etc…)
  • Un cuarto entorno de ámbito más matemático (plotly, sympy, …)

En general los comandos que se usarán a continuación están pensados para Python >= 3.3, más adelante se mostrará una configuración para Docker.

Configuración

La configuración incluye dos pasos, la creación del entorno virtual en Python y la creación del kernel asociado al mismo, en este ejemplo vamos a lanzar los comandos directamente desde la consola de Jupyter.

Entorno virtual

Si no lo estuviera ya instalamos el paquete venv para poder crear los entornos, esto hay que hacerlo para cada uno de las versiones de Python que queramos usar:

apt-get install python3-venv

Si usamos la consola de Jupyter es más útil arrancar el shell bash, simplemente ejecutando bash.

Hay que tener en cuenta que si queremos crear los entornos de Python con una versión específica, por ejemplo por compatibilidad de librerías tendremos que usar el binario exacto de esa versión, por ejemplo python3.7 o python3.8, etc…

Nos movemos a la carpeta donde queramos crear el entorno para el primer escenario, para crear el entorno virtual usamos:

python3 -m venv env

En general los ficheros .gitignore están configurados para ignorar la carpeta env por lo que usar este nombre es la opción recomendada.

Esto lo hacemos para todos los demás entornos, nos movemos a la carpeta donde queramos crear este entorno y ejecutamos el mismo comando que antes para que nos cree una carpeta env igual que antes.

Ahora si queremos activar un entornos concreto, dentro de la carpeta env correspondiente ejecutamos:

source env/bin/activate

El entorno se activa para cualquier código Python que se ejecute a partir de ese momento dentro de esa carpeta por lo que se puede crear un entorno y usarlo para varios proyectos.

Si intentamos lanzar Python veremos que se ejecuta con la versión que hemos configurado para esa carpeta, por ejemplo si queremos ver la versión podemos usar:

python3 --version

Cuando queramos desactivar un entorno hacemos:

deactivate

Para eliminar el entorno virtual simplemente tenemos que borrar la carpeta que se ha creado.

rm -r env

Kernel

Para cada uno de los kernels que vamos a crear tendremos que hacer los siguientes pasos, esto habrá que hacerlo dentro de la carpeta de cada entorno y habiendo activado previamente el entorno.

Instalamos el paquete ipykernel que nos proporcionará el kernel de Python para Jupyter, este paquete a su vez requiere del paquete wheel.

python3 -m pip install --user wheel
python3 -m pip install --user ipykernel

Con esta opción –user instalaríamos los paquetes dentro de la carpeta del usuario que es la opción recomendada.

Puede ser que tengamos un error al ejecutar desde el entorno de Jupyter (ERROR: Can not perform a ‘–user’ install. User site-packages are not visible in this virtualenv) en ese caso se puede ejecutar el mismo comando sin la opción –user.

Una vez hecho esto nos movemos a la carpeta de uno de ellos, en concreto vamos a empezar por el primer escenario y vamos a instalar el kernel correspondiente.

python3 -m ipykernel install --user --display-name='Python AI' --name=envpyai

–name se refiere al nombre del kernel que veremos al usar el comando list y –display-name se refiere al nombre que veremos en Jupyter Notebook, usamos –user para instalar el kernel en la carpeta del usuario y no tener problemas de permisos, si quisiéramos instalarlo a nivel global para todos los usuarios tendríamos que ejecutarlo como root.

El comando anterior emitirá una respuesta similar a la siguiente para indicar que todo ha ido bien, en la ruta que se muestra hay un archivo llamado kernel.json que contiene toda la configuración del kernel.

Installed kernelspec env in /home/jupyter/.local/share/jupyter/kernels/env

Ahora hacemos lo mismo para el resto de configuraciones, nos movemos a la carpeta y activamos el entorno correspondiente antes de ejecutar el comando anterior.

Hay que tener en cuenta que al ser entornos virtuales vienen vacíos por lo que habrá que instalar todos los paquetes que sean necesarios para poder desarrollar.

Para ver los kernel instalados podemos usar este comando, como detalle no se pueden tener dos kernel con el mismo nombre.

jupyter kernelspec list

SI queremos eliminar algún kernel, por ejemplo la versión para Python AI haríamos:

jupyter kernelspec uninstall envpyai

Verificación

Sí todo ha ido bien veremos algo como:

Si abrimos una consola de Python veremos la versión directamente, y para verificarla en un notebook podemos ejecutar el siguiente código:

import sys
print(sys.version)

No hay que usar el siguiente código que mostrará la versión global de Python.

!python3 --version

Desde la parte superior derecha o desde el menú podremos cambiar de kernel.

Dockerfile

Este sería un ejemplo de Dockerfile que resume todos los conceptos anteriores, puede que en Docker tener los entornos virtuales no tenga sentido, pero por comodidad vamos a hacerlo en este caso.

En este caso como estamos heredando de la imagen de Jupyter creada previamente cambiamos al usuario root ya que vamos a instalar algunos paquetes.

Como cada comando RUN se ejecuta de forma aislada de los demás una forma es emular lo que haría el entorno virtual de Python para poder activarlos, otra forma es activar los entornos usando el mismo comando que es lo que se ha hecho en este caso.

FROM joursain/rpi-jupyterlab:bullseye
LABEL "guru.raraavis.creator"="blog@raraavis.guru"
LABEL "guru.raraavis.version"="1.3.0"
LABEL "guru.raraavis.release-date"="23/05/2022"
LABEL "guru.raraavis.description"="Raspberry Pi image with JupyterLab Kernels"

# 0. Prepare system
# 0.1 Required for Matplotlib
USER    root
RUN     apt-get update -y && apt-get install -y \
                libopenjp2-7 \
                libjpeg62-turbo \
                libtiff5 \
                libhdf5-dev \
                libpng-dev \
                libavcodec-dev \
                libavformat-dev \
                libswscale-dev \
                libgtk-3-dev \
                unrar-free \
                # Required for nbconvert (to PDF)
                pandoc \
                libxslt1.1 \
                texlive-xetex \
                # Required for RL
                ffmpeg \
                mediainfo \
                xvfb \
                openmpi-bin \
                openmpi-common \
                libxcb1 \
                libopenmpi3 \
                libpomp-dev \
                libomp5 \
                libopenblas-dev \
                libopenmpi-dev && \
        rm -rf /var/lib/apt/lists/* && \
        rm -rf /tmp/*

# 1. Create virtual environments
WORKDIR /home/jupyter/ai
COPY --chown=jupyter    wheels/tensorflow-2.8.0-cp39-cp39-linux_aarch64.whl .
COPY --chown=jupyter    wheels/torch-1.11.0a0+gitbc2c6ed-cp39-cp39-linux_aarch64.whl .
COPY --chown=jupyter    wheels/ale_py-0.7.5+8f3bc3b-cp39-cp39-linux_aarch64.whl .

RUN     chown -R jupyter:jupyter /home/jupyter/ai
USER    jupyter

RUN     python3 -m venv envailite
RUN     source envailite/bin/activate && \
        python3 -m pip install --upgrade pip setuptools wheel && \
        python3 -m pip install --upgrade        \
                typing-extensions               \
                ipykernel                       \
                dask[complete]                  \
                keras                           \
                numpy                           \
                scipy                           \
                scikit-learn                    \
                scikit-image                    \
                pandas                          \
                matplotlib                      \
                split-folders                   \
                opencv-contrib-python==4.5.5.62 \
                theano                          \
                seaborn                         \
                scikit-fuzzy                    \
                mlflow                          \
                pyforest                        \
                html5lib                        \
                BeautifulSoup4                  \
                scrapy                          \
                requests                        \
                bokeh                           \
                plotly                          \
                lxml                            \
                graphviz                        \
                split-folders                   \
                nltk                            && \
        python3 -m pip install --extra-index-url https://google-coral.github.io/py-repo/ tflite_runtime && \
        python3 -m pip install torch-1.11.0a0+gitbc2c6ed-cp39-cp39-linux_aarch64.whl && \
        python3 -m ipykernel install --user --display-name='Python AI Lite' --name=envpyailite && \
        deactivate


RUN     python3 -m venv envai
RUN     source envai/bin/activate && \
        python3 -m pip install --upgrade pip setuptools wheel && \
        python3 -m pip install --upgrade        \
                typing-extensions               \
                ipykernel                       \
                dask[complete]                  \
                keras                           \
                numpy                           \
                scipy                           \
                scikit-learn                    \
                scikit-image                    \
                pandas                          \
                matplotlib                      \
                split-folders                   \
                opencv-contrib-python==4.5.5.62 \
                theano                          \
                seaborn                         \
                scikit-fuzzy                    \
                mlflow                          \
                pyforest                        \
                html5lib                        \
                BeautifulSoup4                  \
                scrapy                          \
                requests                        \
                bokeh                           \
                plotly                          \
                sympy                           \
                lxml                            \
                graphviz                        \
                h5py                            \
                kaggle                          \
                split-folders                   \
                keras-tuner                     \
                nltk                            && \
        python3 -m pip install  \
                torch-1.11.0a0+gitbc2c6ed-cp39-cp39-linux_aarch64.whl \
                tensorflow-2.8.0-cp39-cp39-linux_aarch64.whl && \
        python3 -m ipykernel install --user --display-name='Python AI' --name=envpyai && \
        deactivate

RUN     python3 -m venv envpyrl
RUN     source envpyrl/bin/activate && \
        python3 -m pip install --upgrade pip setuptools wheel && \
        python3 -m pip install  \
                torch-1.11.0a0+gitbc2c6ed-cp39-cp39-linux_aarch64.whl \
                tensorflow-2.8.0-cp39-cp39-linux_aarch64.whl && \
        python3 -m pip install --upgrade        \
                typing-extensions               \
                ipykernel                       \
                dask[complete]                  \
                keras                           \
                numpy                           \
                scipy                           \
                scikit-learn                    \
                scikit-image                    \
                pandas                          \
                matplotlib                      \
                split-folders                   \
                opencv-contrib-python==4.5.5.62 \
                theano                          \
                seaborn                         \
                scikit-fuzzy                    \
                mlflow                          \
                pyforest                        \
                html5lib                        \
                BeautifulSoup4                  \
                scrapy                          \
                requests                        \
                bokeh                           \
                plotly                          \
                sympy                           \
                lxml                            \
                graphviz                        \
                nltk                            \
                gym                             \
                h5py                            \
                pillow                          \
                keras-rl2                       \
                pyvirtualdisplay                \
                keras-tuner                     && \
        python3 -m pip install  \
                ale_py-0.7.5+8f3bc3b-cp39-cp39-linux_aarch64.whl \
                torch-1.11.0a0+gitbc2c6ed-cp39-cp39-linux_aarch64.whl \
                tensorflow-2.8.0-cp39-cp39-linux_aarch64.whl && \
        python3 -m ipykernel install --user --display-name='Python RL' --name=envpyrl && \
        deactivate


RUN     python3 -m venv envmaths
RUN     source envmaths/bin/activate && \
        python3 -m pip install --upgrade pip setuptools wheel && \
        python3 -m pip install --upgrade        \
                typing-extensions               \
                ipykernel                       \
                keras                           \
                numpy                           \
                scipy                           \
                scikit-learn                    \
                pandas                          \
                matplotlib                      \
                opencv-contrib-python==4.5.5.62 \
                theano                          \
                seaborn                         \
                scikit-fuzzy                    \
                requests                        \
                bokeh                           \
                plotly                          \
                sympy                           \
                lxml                            && \
        python3 -m ipykernel install --user --display-name='Python Maths' --name=envmaths && \
        deactivate

RUN     rm torch-1.11.0a0+gitbc2c6ed-cp39-cp39-linux_aarch64.whl
RUN     rm tensorflow-2.8.0-cp39-cp39-linux_aarch64.whl

Compilación

La compilación no tiene ninguna novedad, en este caso el proceso es un poco más largo.

docker image build --tag user/image_name -f Dockerfile.ai .

Ejecución

La ejecución tampoco tiene modificaciones.

docker container run --init -p 8888:8888 --detach --volume /home/pi/jupyter:/home/jupyter/notebooks id_imagen

Referencias

https://janakiev.com/blog/jupyter-virtual-envs/
https://queirozf.com/entries/jupyter-kernels-how-to-add-change-remove

Docker Raspberry: Jupyter kernels

Docker Raspberry: Jupyter TensorFlow

Docker

Es normal que al usar Jupyter se use Tensorflow, siguiendo con el ejemplo de Docker anterior voy a poner dos ejemplos, uno para usar una configuración de Docker con la última versión de TensorFlow y otra para tener dos versiones de TensorFlow, la correspondiente a la rama 1.x y otra con la versión 2.x.

TensorFlow 1.x y 2.x

El siguiente DockerFile está basado en el anterior así que solo se comentarán las diferencias.

FROM ubuntu:latest

# 1. Environment vars
ARG DEBIAN_FRONTEND=noninteractive
ARG TZ=Europe/Madrid
ENV TZ=$TZ
ENV NODE_OPTIONS=--max-old-space-size=768

# 2. Install packages
# 2.1 Update system and install Jupyter
RUN apt-get update && apt-get -y upgrade && \
        apt-get install -y --no-install-recommends libhdf5-dev && \
        apt-get install -y --no-install-recommends tzdata && \
        apt-get install -y --no-install-recommends libzbar-dev libzbar0 && \
        apt-get install -y --no-install-recommends build-essential python3-pip python3-dev python3-venv && \
        apt-get install -y --no-install-recommends git && \
        apt-get install -y --no-install-recommends software-properties-common && \
        python3 -m pip install --upgrade virtualenv && \
        python3 -m pip install --upgrade wheel && \
        python3 -m pip install --upgrade ipykernel && \
        python3 -m pip install --upgrade pip && \
        python3 -m pip install --upgrade setuptools && \
# 2.2 Build git extension
        python3 -m pip install jupyter && \
        apt-get install -y --no-install-recommends nodejs && \
        apt-get install -y --no-install-recommends npm && \
        python3 -m pip install jupyterlab && \
        python3 -m pip install --upgrade jupyterlab-git && \
        jupyter lab build --minimize=False && \
# 2.3 Clean
        apt-get clean && \
        rm -rf /var/lib/apt/lists/* && \
        rm -rf /tmp/*

# 3 Install TensorFlow
RUN python3 -m pip install --no-cache-dir --force-reinstall grpcio
# 3.1 Install Tensorflow 1 for Python 3.7
RUN add-apt-repository ppa:deadsnakes/ppa
RUN apt-get install -y python3.7 python3.7-dev python3.7-venv
RUN python3.7 -m pip install --upgrade pip && python3.7 -m pip install setuptools
COPY tensorflow-1.14.0-cp37-none-linux_armv7l.whl ./
RUN python3.7 -m pip install tensorflow-1.14.0-cp37-none-linux_armv7l.whl
# 3.2 Install Tensorflow 2 for Python 3.8
COPY tensorflow-2.4.0rc0-cp38-none-linux_armv7l.whl ./
RUN python3 -m pip install tensorflow-2.4.0rc0-cp38-none-linux_armv7l.whl

# 4. Add Jupyter user
RUN adduser --disabled-password --gecos '' jupyter
RUN adduser jupyter sudo
RUN echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
USER jupyter
WORKDIR /home/jupyter/
RUN chmod a+rwx /home/jupyter/

# 5. Folder to download git files
RUN mkdir /home/jupyter/notebooks

# 6. Execute jupyter
CMD ["jupyter", "lab", "--port=8888", "--no-browser", "--ip=0.0.0.0", "--allow-root", "--notebook-dir=/home/jupyter/notebooks", "--NotebookApp.token=''", "--NotebookApp.password]

Hay que instalar algunos paquetes nuevos como wheel, virtualenv o ipykernel para poder usar varias versiones de Python en Jupyter a través del uso de kernels, esto se comentará más adelante para configuraciones con varias versiones de TensorFlow.

Instalamos libhdf5-dev y actualizamos el paquete setuptools para evitar un error durante la instalación del paquete grpcio y para evitar posibles errores en la creación de entornos virtuales.

El paquete software-properties-common es necesario para poder agregar repositorios personalizados que harán falta más adelante.

Install TensorFlow (3)

Este código ejecuta las dos versiones de Python, la correspondiente a la 1.14 y 2.0rc4 en el caso de grpcio lo instalamos previamente ya que va a ser usado por ambas versiones de TensorFlow y además necesita una configuración especial para no producir un error durante la instalación.

Install TensorFlow 1 para Python 3.7 (3.1)

La versión Ubuntu de RaspberryPi Buster incorpora la versión 3.8 de Python pero la última versión disponible de TensorFlow 1.x (1.4) solo funciona con Python 3.7 que no se puede instalar a través de los repositorios oficiales de Ubuntu, por lo que configuramos un repositorio alternativo (3.1) donde se encuentran versiones de Python para varias versiones de Ubuntu.

Instalamos los mismos paquetes Python que hemos instalado para la versión 3.8 pero en este caso para la versión 3.7 usando el repositorio anterior y copiamos el paquete de TensorFlow 1.4 (3.1) para poder instalarlo, el paquete lo podemos compilar o cogerlo de aquí.

Install Tensorflow 2 for Python 3.8

En el caso de TensorFlow 2 solo tenemos que copiar (3.2) la versión correspondiente que podemos copiarla del mismo repositorio anterior o compilarla como se describe aquí.

TensorFlow 2.x

La configuración de Docker sería muy similar a la anterior, simplemente quitamos la parte relacionada con TensorFlow 1.x

FROM ubuntu:latest

# 1. Environment vars
ARG DEBIAN_FRONTEND=noninteractive
ARG TZ=Europe/Madrid
ENV TZ=$TZ
ENV NODE_OPTIONS=--max-old-space-size=768

# 2. Install packages
# 2.1 Update system and install Jupyter
RUN apt-get update && apt-get -y upgrade && \
        apt-get install -y --no-install-recommends libhdf5-dev && \
        apt-get install -y --no-install-recommends tzdata && \
        apt-get install -y --no-install-recommends libzbar-dev libzbar0 && \
        apt-get install -y --no-install-recommends build-essential python3-pip python3-dev python3-venv && \
        apt-get install -y --no-install-recommends git && \
        apt-get install -y --no-install-recommends software-properties-common && \
        python3 -m pip install --upgrade virtualenv && \
        python3 -m pip install --upgrade wheel && \
        python3 -m pip install --upgrade ipykernel && \
        python3 -m pip install --upgrade pip && \
        python3 -m pip install --upgrade setuptools && \
# 2.2 Build git extension
        python3 -m pip install jupyter && \
        apt-get install -y --no-install-recommends nodejs && \
        apt-get install -y --no-install-recommends npm && \
        python3 -m pip install jupyterlab && \
        python3 -m pip install --upgrade jupyterlab-git && \
        jupyter lab build --minimize=False && \
# 2.3 Clean
        apt-get clean && \
        rm -rf /var/lib/apt/lists/* && \
        rm -rf /tmp/*

# 3 Install TensorFlow
RUN python3 -m pip install --no-cache-dir --force-reinstall grpcio
COPY tensorflow-2.4.0rc0-cp38-none-linux_armv7l.whl ./
RUN python3 -m pip install tensorflow-2.4.0rc0-cp38-none-linux_armv7l.whl

# 4. Add Jupyter user
RUN adduser --disabled-password --gecos '' jupyter
RUN adduser jupyter sudo
RUN echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
USER jupyter
WORKDIR /home/jupyter/
RUN chmod a+rwx /home/jupyter/

# 5. Folder to download git files
RUN mkdir /home/jupyter/notebooks

# 6. Execute jupyter
CMD ["jupyter", "lab", "--port=8888", "--no-browser", "--ip=0.0.0.0", "--allow-root", "--notebook-dir=/home/jupyter/notebooks", "--NotebookApp.token=''", "--NotebookApp.password=''"]

Compilación

La compilación no tiene ninguna novedad, en este caso el proceso es mucho más largo y para la configuración con dos versiones de TensorFlow puede llevar entre 8-10 horas en una Raspberry Pi 3.

docker image prune -f
docker image build --tag user/image_name .

Ejecución

La ejecución tampoco tiene modificaciones.

docker container run --init -p 8888:8888 --detach --volume /home/pi/jupyter:/home/jupyter/notebooks id_imagen
Docker Raspberry: Jupyter TensorFlow

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

Docker en Raspberry Pi

Actualizado el 27 de Junio de 2024

Introducción

Es posible usar Docker con Raspberry Pi, puede parecer que en un dispositivo de prestaciones limitadas no tenga mucho sentido pero cada vez es más común verlo para dispositivos IoT y los dispositivos Raspberry han mejorado mucho con el tiempo sobre todo a partir de Raspberry Pi 4.

Aquí coloco algunas notas de como configurar Docker y algunas posibles mejoras de rendimiento.

Preparando el sistema

Lo primero es actualizar el sistema y reiniciar.

sudo apt-get update
sudo apt-get upgrade

Instalando Docker

Instalamos Docker usando el script proporcionado por ellos.

curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh

Después agregamos al usuario pi al grupo docker haciendo:

 sudo usermod -aG docker pi

Una vez agregado si estamos con el usuario tendremos que volver a iniciar sesión o reiniciar para que los cambios de pertenencia al nuevo grupo se hagan efectivos.

Reiniciamos con sudo reboot now y verificamos la versión Docker con docker version.
Probamos también a verificar el sistema usando este comando que descargará una imagen Docker y la ejecutará, se verá un mensaje como Hello from Docker!

docker run --rm hello-world

Instalando Docker-Compose

Ahora instalamos Docker Compose que nos hará falta para gestionar varios contenedores a la vez y la comunicación entre ellos, primero instalamos pip, el gestor de paquetes de Python, cualquier distribución de Raspberry ya viene con Python instalado así que no hará falta instalarlo.

sudo curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py && sudo python3 get-pip.py

Si tenemos algún error podemos probar a ejecutar este comando y después volvemos a ejecutar el comando anterior para actualizar pip.

sudo apt-get install python3-pip

Es importante que en el fichero /etc/pip.conf tengamos agregado el repositorio de piwheels

[global]
extra-index-url=https://www.piwheels.org/simple

Es posible también que tengamos que instalar distutils

sudo apt-get install python3-distutils

Ahora instalamos Docker Compose

python3 -m pip install docker-compose

Si nos da este error: command ‘arm-linux-gnueabihf-gcc’ failed with exit status 1 podemos probar lo siguiente y repetir el comando anterior:

sudo apt-get install libzbar-dev libzbar0

Si nos da un error relacionado con python.h instalamos este paquete:

sudo apt-get install python3-dev

Si tenemos un error relacionado con ffi.h instalamos este paquete:

apt install libffi-dev

Finalmente para verificar que docker-compose se ha instalado bien se puede usar:

docker-compose --version

Rendimiento

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

Una serie de recomendaciones y problemas frecuentes.

Usar gosu en lugar de sudo

Las imágenes Docker normalmente se ejecutan como root pero a veces hace falta ejecutar algunos comandos como un usuario concreto dentro del entry point en este caso la mejor opción es usar gosu en lugar de sudo.

Problemas

Job for docker.service failed because the control process exited with error code.
See «systemctl status docker.service» and «journalctl -xe» for details.

Si el servicio da errores al arrancar podemos usar journalctl -xe y si vemos un error como The process’ exit code is ‘exited’ and its exit status is 2, podemos intentar lo siguiente:

rm -rf /var/lib/docker
sudo curl -sSL https://get.docker.com | sh
service docker restart

Es decir, borramos la carpeta, volvemos a instalar y reiniciamos el servicio, es posible que este error indique la tarjeta SD está fallando.

Errores al subir imágenes

Si al hacer push da un error como file integrity checksum failed for «usr/lib/arm-linux-gnueabihf/libicui18n.so.66.1» ejecutamos este comando:

docker system prune -a

Si aún así continuamos con el error no nos quedará otra que borrar la imagen y volverla a crear con docker build.

At least one invalid signature was encountered

Si durante la creación de imágenes obtenemos un error «At least one invalid signature was encountered» al instalar paquetes via apt-get es necesario instalar la versión actualizada de libseccomp2 la opción recomendada es descargar l paquete (en este caso via wget) e instalarlo desde aquí aquí.
Es posible que la versión del paquete haya cambiado y no se encuentre en el servidor, en ese caso se puede acceder a más mirrors aquí.

wget  http://ftp.us.debian.org/debian/pool/main/libs/libseccomp/libseccomp2_2.4.4-1+b1_armhf.deb
sudo apt-get install ./libseccomp2_2.4.4-1+b1_armhf.deb

E: Release file for xxx is not valid yet (invalid for another 2h 45min 28s). Updates for this repository will not be applied.

Esto es por un problema con la hora del sistema, posiblemente el sistema no tiene la hora correcta, recordar que Raspberry no incluye un reloj de hardware, se puede solucionar configurando la zona regional a través de raspi-config pero si eso no funciona una forma cómoda es instalando el cliente de ntp:

sudo apt-get install ntpdate

Después solo nos queda ejecutarlo indicando el servidor de tiempo que queramos usar:

sudo ntpdate time.windows.com
Docker en Raspberry Pi

Compilando Tensorflow 2 para Raspberry Pi

Actualizado el 31 de Octubre de 2020

Introducción

Raspberry instala por defecto TensorFlow 1.x, existen varias opciones para instalar TensorFlow 2, una de ellas es acceder a la página de TensorFlow y descargarse la versión 2 para Python 3.5, en la parte inferior veremos el enlace actualmente con esta dirección:

https://storage.googleapis.com/tensorflow/raspberrypi/tensorflow-2.3.0-cp35-none-linux_armv6l.whl
https://storage.googleapis.com/tensorflow/raspberrypi/tensorflow-2.3.0-cp35-none-linux_armv7l.whl

Ambos paquetes instalarán tensorflow 2.3 para una versión de Python 3.5 el primero para una Raspberry 0 o 1 (arquitectura ARMv6) y el segundo para una Raspberry 2 o 3 (ARMv7).

Puede ser que junto con TensorFlow 2 queramos usar una versión de Python superior (actualmente está disponible la 3.7) en ese caso la mejor opción será crear la versión desde el propio código fuente. Este proceso se puede realizar desde la propia Raspberry pero el proceso es bastante largo debido a las limitaciones de la Raspberry así que vamos a apoyarnos en otra máquina (escritorio, portátil,…) para agilizar el proceso y también tenerlo preparado para futuras versiones.

El proceso se puede realizar de varias formas, en este caso nos apoyaremos en Windows Linux Subsytem.

Windows Linux Subsystem

Lo primero será instalar Windows Linux Subsystem, siguiendo los pasos que se dan en la página se puede preparar Windows 10 para ejecutar Linux sin problemas. Vamos a elegir como distribución a instalar Ubuntu 20.04 LTS.

Una vez arrancado y creado un usuario Linux seguimos los siguientes pasos:

Descargar el código fuente

El código fuente de TensorFlow lo podemos descargar de su repositorio oficial, vamos a trabajar con las ramas release en concreto para este caso la 2.4 que es la última disponible:

git clone https://github.com/tensorflow/tensorflow.git
git checkout r2.4

Compilación del código

TensorFlow 2 se compila a través de Docker, si estamos usando Windows 10 podemos habilitar la integración con WSL, la información completa esta aquí, pero con los siguientes pasos podemos verificar rápidamente si todo está correcto.

  1. Dentro de Settings -> General
    Habilitar Use the WSL 2 based engine (habilitado por defecto). Apply & restart.
  2. Verificar que WSL se está ejecutando en modo 2, ejecutamos este comando en una línea de comandos (cmd): wsl -l -v
    Tenemos que ver que la versión es la 2.
  3. En Settings -> Resources -> WSL Integration habilitamos Enable integration with my default WSL distro y marcamos todas las imágenes en las que queramos habilitar WSL 2. Después Apply & restart.
    Sino vemos la opción de WSL Integration es que estamos ejecutando Docker con contenedores Windows, tenemos que usar contenedores Linux esta opción la tenemos en el menú contextual de Docker Switch to Linux containers.

Ahora nos movemos a la carpeta donde lanzaremos la compilación, que típicamente estará en tensorflow/tensorflow/tools/ci_build (hay dos carpetas tensorflow, la de descarga del código y otra propia del repositorio).

En este caso tenemos que cambiar un par de detalles en el archivo Dockerfile.pi-python37 tenemos que quitar las librerías de Python 2.7, esto hará que no sea compatible con Python 2.7 pero no es un problema ya que Python 2 está en desuso y además la compilación será más rápida, también asociaremos la versión de Python de la imagen Docker con la 3.7, normalmente se hace con update-alternatives pero en este caso lo vamos a simplificar modificando el enlace simbólico.

El siguiente código lo vamos a colocar justo después de la instalación de Python.

...
# The following line installs the Python 3.7 cross-compilation toolchain.
RUN /install/install_pi_python3x_toolchain.sh "3.7"

# Code to remove Python 2.7 and update symbolic link
RUN ln -sf /usr/bin/python3.7 /usr/bin/python
RUN apt-get remove -y libpython2.7-dev

RUN /install/install_bazel.sh
...

Ahora lanzamos este comando y comenzará la compilación, este comando (ci_build.sh) acepta dos parámetros.

  • El primero sirve para indicar en que maquina Docker se ejecutará (PI-PYTHON37), hay otros valores para compilar para otras máquinas Raspberry, si se quiere compilar para todas se puede usar el parámetro PI_ONE.
  • El segundo parámetro (build_raspberry_pi.sh) indica el comando que se ejecutará dentro de la máquina virtual.
./ci_build.sh PI-PYTHON37 \
    tensorflow/tools/ci_build/pi/build_raspberry_pi.sh

Durante la compilación nos irá sacando información del proceso, el tiempo que tarda depende de la máquina en particular, en este caso concreto en una máquina i7-6700K@4Ghz con 16GB RAM y en un disco SSD tarda alrededor de 30′-45′.

El resultado de la compilación lo tendréis en la carpeta tensorflow/output-artifacts y el paquete a instalar tendrá este nombre:
tensorflow-2.4.0rc0-cp37-none-linux_armv7l.whl

Vemos que hemos compilado una release candidate #0 de la versión 2.4 (2.4.0rc0) para una versión de Python 3.7 (cp37) y una arquitectura ARMv7 (armv7l) el none simplemente indica que no está compilado pensando en optimizaciones para GPU (Raspberry no soporta CUDA) o versiones especiales de Python (las versiones m).

Si tarda más de 60′ es posible que el proceso no termine, en ese caso la mejor opción es detener el proceso (Ctrl+Z) buscar el trabajo parado y coger el id (jobs -l) y matar el proceso (kill -9 #id).

Después de esto probamos a volver a lanzar el comando pero antes es muy recomendable reiniciar Docker, volver a abrir la máquina Ubuntu y hacer una limpieza de ficheros con git clean -fxd desde el raíz del repositorio.

Instalación

Ahora solo tenemos que copiar el fichero a la Raspberry, la forma más cómoda posiblemente sea usando el comando scp, por ejemplo:

scp tensorflow-2.4.0rc0-cp37-none-linux_armv7l.whl pi@192.168.1.5:/home/pi

En este ejemplo estamos copiando el fichero a una máquina remota 192.168.1.5 en la carpeta /home/pi y para acceder a esa máquina usamos el usuario pi, al conectar nos pedirá contraseña.

Una vez en la máquina Raspberry nos movemos a la carpeta donde hayamos copiado el fichero (/home/pi) primero vamos a actualizar pip, después actualizaremos setuptools y finalmente instalaremos el paquete TensorFlow 2:

sudo python3 -m pip install --upgrade pip
sudo pip3 install --upgrade setuptools
sudo pip3 install tensorflow-2.4.0rc0-cp37-none-linux_armv7l.whl

También hace falta tener instalado la librería ATLAS

sudo apt-get install libatlas-base-dev

La instalación tarda un poco pero nos irá información del proceso, al terminar para verificar que está bien instalado podemos usar:

sudo python3 -c 'import tensorflow as tf; print(tf.__version__)'

Y deberíamos ver…


2.4.0-rc0

Bonus

Es posible también coger paquetes precompilados desde varios orígenes de terceros y evitar tener que hacer este proceso, pero al no ser repositorios oficiales no se puede asegurar el funcionamiento del mismo.

Otra opción sería usar TensorFlow lite una versión más apropiada para dispositivos de menores prestaciones.

Problemas

Durante el desarrollo de este artículo me he encontrado varios problemas, los enumero aquí para la posteridad (muchos de ellos relacionados con la rama master).

Cannot add PPA: ‘ppa:~openjdk-r/ubuntu/ppa’.
ERROR: ‘~openjdk-r’ user or team does not exist.
Es un problema de certificados, ejecutamos:

sudo apt-get install --reinstall ca-certificates

wget: unable to resolve host address ‘xxx’
Es un problema con los DNS, se puede editar el fichero resolv.conf (sudo nano /etc/resolv.conf) y añadir la siguiente línea para configurar el DNS de Google.

nameserver 8.8.8.8

sudo: pip3: command not found
Hay que instalar el gestor de paquetes de Python pip, en este caso estamos obligados a modificar la imagen de TensorFlow, en el fichero DockerFile que corresponda (en este caso Dockerfile.pi-python37) añadimos esta línea encima de la línea que haya fallado, por ejemplo en este caso ha fallado la instalación del paquete auditwheel

...
RUN /install/install_buildifier.sh
RUN apt install -y python3-pip
RUN pip install --upgrade pip
RUN /install/install_auditwheel.sh
...

ERROR: An error occurred during the fetch of repository ‘local_config_python’:

Problem getting numpy include path.

ModuleNotFoundError: No module named ‘numpy’

Hay que hacer dos cosas, por un lado es un problema con la versión de Python, por defecto la versión de Python es la 3.5 cuando tendría que ser la 3.7 y además también hay que instalar numpy

Referencias

https://github.com/tensorflow/tensorflow/issues/26947
http://www.itworkman.com/97618.html
https://devblogs.microsoft.com/commandline/a-guide-to-invoking-wsl/
https://github.com/tensorflow/tensorflow/issues/39340

Compilando Tensorflow 2 para Raspberry Pi