Servidor VPN en Raspberry Pi

Un uso muy clásico de usar una raspberry es configurarla como VPN, prácticamente cualquier modelo da un rendimiento aceptable, siempre y cuando sea para uso personal y no requiera una gran transferencia de datos, en este caso se va a dar un ejemplo con una Raspberry Pi Model B, una de las primeras que salió al mercado que tiene solo 256MB (sí, con M) de RAM y una tarjeta SD (sí, no es micro) de 8GB.

Primero como siempre actualizar el sistema, en este caso como la fundación Raspberry son muy retrocompatibles (algo que no se agradece lo suficiente) vamos a usar la última distribución disponible Bookworm.

Se recomienda esta optimización.

NoIP

Primero vamos a crear un DNS dinámico de esta forma aunque nuestra dirección IP pública cambie siempre podremos alcanzar al servidor VPN si nos estamos conectando desde el exterior, hay varios proveedores y en este caso vamos a usar NoIP.

Lo primero será crear una cuenta, gratuitamente se dan hasta 3 dominios, cada mes hay que confirmar que estamos usando el servicio, poca molestia por el servicio gratuito.

Después de crear una cuenta tendremos que usar el cliente NoIP este cliente sirve para actualizar dinámicamente la dirección IP con el servidor NoIP, esto es necesario porque si el ISP cambia la IP del router NoIP no lo sabrá a menos que se lo notifiquemos.

NoIP tiene una guia detallada de como instalar el cliente aquí.

En este caso vamos a usar la compilación manual por el simple hecho que en ciertas Raspberry podemos obtener el error Segmentation fault un equivalente al BSOD de Windows.

Las instrucciones para compilar desde fuente también se comentan en el propio documento, esta es una guía de ejemplo:

Vamos a crear una carpeta para almacenar el código y los binarios:

mkdir noip
cd noip

Ahora descargar los fuentes (revisar esta dirección si no descarga puede haber una versión nueva):

wget https://dmej8g5cpdyqd.cloudfront.net/downloads/noip-duc_3.1.0.tar.gz

Extraer los fuentes y posicionarnos en la carpeta:

tar xf noip-duc_3.1.0.tar.gz
cd noip-duc_3.1.0

Necesitamos Rust para poder compilar así que lo instalamos, la instalación dura un poco pero la compilación es muy muy larga, así que usamos screen para que no se pare:

Instalamos Rust primero:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Se recomienda aumentar la memoria swap, al menos a 512MB, esto está explicado aquí, sino se corre el riesgo (altamente probable) de que la Raspberry se quede colgada en el último paso, también se recomienda configurar zswap.

Y ahora lanzamos la compilación, primero recargamos el shell como nos indica Rust y luego el comando propio de Rust para la compilación.

. "$HOME/.cargo/env"
cargo build --release

Ahora solo queda configurar el binario que tendremos en /target/release para que se inicie al arrancar, así que lo configuramos como un servicio usando systemd.

Primero copiamos el binario que tenemos en /target/release a la carpeta /usr/bin

sudo cp target/release/noip-duc /usr/bin/

Ahora creamos un fichero en /etc/default/

sudo nano /etc/default/noip-duc

Vamos a añadir en este fichero la configuración, -la configuración completa está en la ayuda-, estamos usando el usuario y grupo de pi para arrancar el servicio por comodidad (que no seguridad).

NOIP_USERNAME=Raspberry
NOIP_PASSWORD=drN01p
NOIP_DAEMON_USER=pi
NOIP_DAEMON_GROUP=pi
NOIP_DAEMON_PID_FILE=/var/run/noip.pid
NOIP_HOSTNAMES=uno.ddns.net,dos.ddns.net

El parámetro NOIP_HOSTNAMES puede ser tanto una lista de hosts como un grupo que se puede crear para actualizar todos los hosts en bloque, la creación de un grupo se hace desde el portal de noip.

Ahora creamos un fichero noip.service in /etc/systemd/system:

sudo nano /etc/systemd/system/noip.service

y vamos a poner lo siguiente (el contenido también está en la carpeta debian):

[Unit]
Description=No-IP Dynamic Update Client
After=network.target auditd.service

[Service]
EnvironmentFile=/etc/default/noip-duc
ExecStart=/usr/bin/noip-duc
Restart=on-failure
Type=simple

[Install]
WantedBy=multi-user.target

Ahora recargamos los servicios:

sudo systemctl daemon-reload

Ahora vamos a hacer que el servicio se inicie al arrancar:

sudo systemctl enable noip.service

Finalmente vamos a arrancarlo manualmente a ver si funciona todo bien.

sudo systemctl start noip.service

Y si queremos saber el estado en el que está usamos:

sudo systemctl status noip.service

PiVPN

Vamos a usar PiVPN como herramienta software para la instalación, bastante sencilla de usar, primero la instalación:

curl -L https://install.pivpn.io | bash

Aparecerá un instalador que hará algunas preguntas:

PiVPN Automated Installer es un mensaje informativo, OK para continuar.

Static IP Needed otro mensaje informativo, OK para continuar.

IPv6 leak elegimos OK, es para que el cliente use el tunel y evitar que su dirección IPv6 se filtre a los servidores, esto no implica que hay que realizar ninguna configuración adicional relacionada con IPv6.

DHCP Reservation para usar la dirección IP del servidor DHCP que tengamos en la red, es verdad que esto es poco habitual, generalmente los servidores suelen tener una IP estática, así que indicamos No.

Static IP Address: si ya tenemos una IP asignada por un servidor DHCP podemos usar la que indica sino pulsamos No y en la siguiente pantalla IPv4 address indicamos la IP y máscara de red, después en la pantalla IPv4 gateway (router) indicamos la IP del router.

Local Users es un mensaje informativo, OK para continuar.

Choose A User sirve para indicar que usuario ejecutará el servicio, el usuario por defecto pi es una buena opción, lo normal es que en un servidor VPN no existan cuentas de usuario.

Installation Mode (1) seleccionamos OpenVPN, la siguiente opción marcamos No para poder usar el protocolo UDP, más rápido en este caso.

Installation Mode (2) usamos UDP por defecto, que en este caso da mejor rendimiento. La opción custom search domain es para indicar si en las peticiones DNS del cliente hay que adjuntar este nombre de dominio, lo normal es que no sea el caso. Seleccionamos No en esta pantalla.

Default openvpn Port el puerto que usará OpenVPN, por defecto es el 1194 (¡UDP!) importante recordarlo para configurarlo en un firewall o router.

Confirm Custom Port Number la simple confirmación de que estamos de acuerdo con el número de puerto.

DNS Provider aquí podemos elegir uno externo como Google o CloudFare, o podemos usar el propio servidor PiVPN-is-local-DNS si también es servidor DNS (algo poco habitual y recomendable), con la opción Custom podemos elegir otro, esto lo veremos más adelante pero para una configuración estándar las dos primeras opciones son correctas.

Public IP or DNS aquí a no ser que tengamos una IP pública que no cambie (no suele ser así) elegiremos DNS Entry e indicaremos un nombre de dominio como el que hemos configurado anteriormente.

Confirm DNS Name simple confirmación que el nombre elegido es correcto.

Server information seleccionamos OK para poder generar la clave HMAC, necesaria para la autenticación TLS de OpenVPN.

Unattended Upgrades seleccionamos OK de esta forma OpenVPN se actualizará automáticamente con parches, seleccionamos Yes.

Installation Complete! si todo ha ido bien llegaremos aquí, presionamos OK para poder reiniciar y confirmamos el reinicio.

Después de este paso podemos actualizar la Raspberry y después reiniciar.

sudo apt-get -y update && sudo apt-get -y upgrade

Optimizaciones

A continuación algunas mejoras de rendimiento para una Raspberry Pi 1.

Deshabilitamos algunos servicios del arranque:

# Deshabilitamos el servicio de modem
sudo systemctl disable ModemManager.service
# Deshabilitamos el servicio Avahi
sudo systemctl disable avahi-daemon.service
# Deshabilitamos triggerhappy
sudo systemctl disable triggerhappy.service

Ajustamos el tamaño de los buffers de red en /etc/sysctl.conf añadimos estas líneas

net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 87380 16777216
net.core.netdev_max_backlog = 2000

Y recargamos los parámetros con:

sudo sysctl -p

Ahora vamos a cambiar algunos valores de OpenVPN, para esto editamos el fichero /etc/openvpn/server.conf

#Bajamos el cifrado a 128 para reducir carga en CPU
cipher AES-128-CBC
#Optimizar TUN/TAP/UDP y escrituras I/O 
fast-io
#Cambiamos tamaño de buffer en cliente y servidor
sndbuf 524288
rcvbuf 524288
#Tamaño de la longitud de la cola, mismo valor que net.core.netdev_max_backlog
txqueuelen 2000

Crear clientes

Ahora solo queda crear los clientes, OpenVPN tiene aplicación cliente para prácticamente todos los sistemas, IOS, Android, Windows, Linux…. así que la opción preferida es crear una por dispositivo que vayamos a usar.

Para crear un cliente solamente tenemos que usar este comando:

pivpn add

Simplemente rellenamos las preguntas con nombre del certificado, contraseña y duración que tiene que estar entre 1 y 3650 días.

Por defecto el certificado lo creará en la carpeta /home/pi/ovpns lo recomendable es sacarlo de aquí y llevarlo al dispositivo destino usando el comando scp, por ejemplo:

scp pi@192.168.1.2:/home/pi/ovpns/client.ovpn .

Donde 192.168.128.1 es la ip del servidor y /home/pi/ovpns/client.ovpn es la ruta al certificado.

Si quisiéramos revocar un certificado, ya sea para que ese cliente no se pueda conectar o para cambiar la contraseña tendríamos que hacer.

pivpn -r client

Done client es el nombre del cliente que queremos revocar, es decir el mismo nombre que usamos al crearlo. Después de hacer esto solo tenemos que volver a crearlo usando pivpn add. Y si queremos saber que clientes están instalados podemos usar pivpn -l

Referencias

https://www.industrialshields.com/es_ES/blog/raspberry-pi-para-la-industria-26/configurar-un-servidor-vpn-en-una-raspberry-pi-566
https://linuxblog.io/improving-openvpn-performance-and-throughput

Servidor VPN en Raspberry Pi

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

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+)

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

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

Jupyter notebook en Raspberry Pi con Docker

Actualizado el 31 de Octubre de 2021

Docker

Introducción

Últimamente he estado usando Azure notebooks para mis notas en Jupyter pero parece que finalmente Microsoft ha decidido abandonarlo y da varias alternativas para la migración, pero ninguna me ha terminado de convencer, ya sea por estar atado a otro componente de Azure o por ser de pago, así que decidí incluir Jupyter dentro de una de mis Raspberry.

Uno de los problemas es la mala compatibilidad de Jupyter con Git, apenas existen extensiones que sean fiables así que decidí usar JupyterLab con su extensión de Git que es bastante completa.

Finalmente continuando con lo que parece que será la senda a seguir en un futuro cercano he optado a hacerlo en Docker, principalmente porque así lo puedo mover cómodamente entre mis Raspberry sin tener que instalarlo todo de nuevo (bueno, para lo que se supone que es Docker).

Estos son los pasos a seguir, en este caso use una Raspberry Pi 3 con 1 GB de RAM para comprobar el rendimiento, aunque Docker tiene más sentido en una Raspberry Pi 4 con al menos 4GB de RAM.

Configuración del sistema

Es recomendable leer este enlace antes de seguir, da instrucciones e ideas para instalar Docker.

DockerFile

Este es el fichero Dockerfile necesario para crear la imagen, vamos a usar como imagen base la versión ARM de Debian, posiblemente con Alpine el tamaño de la imagen sería menor (actualmente 1.2GB) pero puede haber algunos problemas de rendimiento.

# 0. Image and labels
FROM debian:latest
LABEL "guru.raraavis.creator"="blog@raraavis.guru"
LABEL "guru.raraavis.version"="1.1.0"
LABEL "guru.raraavis.release-date"="31/10/2021"
LABEL "guru.raraavis.description"="Raspberry Pi image with JupyterLab"

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

# 2. Install packages
# 2.1 Update system and install necessary packages
RUN     apt-get update && \
        apt-get install -y --no-install-recommends \
        python3-dev \
        python3-venv \
        libatlas-base-dev \
        libblas-dev \
        liblapack-dev \
        python-dev \
        gfortran \
        tzdata \
        software-properties-common \
        sudo \
        gosu \
        bzip2 \
        git \
        dumb-init \
        curl && \
        curl -sL https://deb.nodesource.com/setup_$NODE_VERSION.x | bash - && \
        apt-get install -y --no-install-recommends nodejs && \
        rm -rf /var/lib/apt/lists/* && \
        rm -rf /tmp/*
# 2.2 Install pip
ADD     https://bootstrap.pypa.io/get-pip.py get-pip.py
RUN     python3 get-pip.py
RUN     python3 -m pip config --global set global.extra-index-url https://www.piwheels.org/simple
# 2.3 Install pip packages and Jupyter
RUN     python3 -m pip install --upgrade \
        conda \
        virtualenv \
        ipykernel \
        jupyter \
        jupyterlab \
        jupyterlab-git \
        jupyter_contrib_nbextensions \
        autopep8

RUN     jupyter lab build
RUN     jupyter contrib nbextension install --system

# 3. Add jupyter user
RUN     adduser --uid $JUPYTER_ID --disabled-password --gecos '' jupyter
RUN     adduser jupyter sudo
RUN     echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers

# 4. Install Miniconda
WORKDIR /home/jupyter
ADD --chown=jupyter     http://repo.continuum.io/miniconda/Miniconda3-latest-Linux-armv7l.sh Miniconda3.sh
RUN     chmod 755 Miniconda3.sh
RUN     md5sum Miniconda3.sh
RUN     ./Miniconda3.sh -b -p /home/jupyter/miniconda3
RUN     ./miniconda3/bin/conda config --add channels rpi
RUN     rm ./Miniconda3.sh

# 5. Configure container
RUN     mkdir -p /home/jupyter/notebooks
RUN     chown -R jupyter:jupyter /home/jupyter
ONBUILD SHELL   ["/bin/bash","-c"]
VOLUME /home/jupyter/notebooks
EXPOSE 8888
USER jupyter

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

Variables de entorno (1)

El paquete tzdata cuando se instala necesita especificar una zona horaria, así que establecemos la variable de entorno TZ, si se establece en tiempo de ejecución indicará la zona horaria a usar para configurar el sistema, se puede volver a configurar usando dpkg-reconfigure, tzdata también necesitará establecer DEBIAN_FRONTEND=noninteractive para evitar preguntas durante la instalación.
Enlazamos ARG con ENV, ya que ARG solo se puede usar en tiempo de compilación la enlazamos con ENV para que se pueda establecer también en tiempo de ejecución.
NODE_OPTIONS se usará más adelante para establecer parámetros en NodeJs, principalmente en el uso de la memoria, ya que a no ser que la Raspberry tenga más de 2GB fallará durante la creación de la extensión de Git, para una RPI4 se podría eliminar o ampliar este parámetro.
Existe también la opción de parametrizar que versión de Node se quiere instalar -necesario para JupyterLab- por defecto la 16 que es la última disponible actualmente.
La variable JUPYTER_ID se usa para indicar que id de usuario tendrá el usuario Jupyter que luego crearemos, esto es muy útil para poder establecer permisos en el host y evitar algunos problemas.

Instalar paquetes (2)

El proceso de instalación consta de un solo comando RUN para evitar crear capas intermedias. También crea e instala la extensión de Git, hay que tener en cuenta que no existe una forma oficial de generar la extensión y luego instalarla, de ser así una opción habría sido crearla en un host y luego copiarla en otro.
También se incluyen varias librerías de sistema, muchas de ellas son necesarias para instalar ciertos paquetes Python (principalmente librerías científicas) así que es bueno tenerlas ya instaladas.

Actualizar el sistema e instalar Jupyter (2.1)

Los comandos básicos para actualizar el sistema e instalar Jupyter con sus dependencias, se usa –no-install-recommends y finalmente se elimina de la cache los paquetes para aligerar el tamaño del fichero Docker.

Instalar pip (2.2)

Aquí descargamos e instalamos pip, también añadimos el repositorio de paquetes Python piwheels, este repositorio contiene muchos de los paquetes Python preparados para Raspberry lo que ahorra mucho tiempo de compilación e instalación, también indicamos que este repositorio sea global para que cualquier usuario o entorno virtual se beneficie del mismo.

Instalar librerías Python y Jupyter (2.3)

Aquí se instalan todos los paquetes Python necesarios para JupyterLab incluyendo algunos que son útiles -como virtualenv- extensiones a JupyterLab -como la propia de Git– y finalmente el propio Jupyterlab, después solo hay que llamar al comando build. Aprovechamos también para instalar algunas extensiones útiles para JupyterLab.

Agregar usuario Jupyter (3)

Ahora creamos el usuario jupyter que usaremos a partir de ahora, lo configuramos sudo y configuramos la carpeta de trabajo.

Instalar Miniconda (4)

En este paso vamos a instalar Miniconda para el usuario Jupyter, se hace una descarga e instalación desatendida dentro de la carpeta que hemos creado previamente. Miniconda instala principalmente el gestor de paquetes Conda y el resto de paquetes habría que instalarlos por separado, hay que tener en cuenta que el soporte de Conda para Raspberry no es muy bueno.
Finalmente se agrega el canal (repositorio) de rpi a conda para poder descargar paquetes orientados a Raspberry Pi (similar a como hemos hecho antes para pip).
Al instalar Conda de manera desatendida no se incluyen los binarios de Conda en el PATH por lo tanto para usar los binarios de conda hay que ubicarse en la carpeta bin de la carpeta miniconda como se ve al usar el comando conda config, esta opción la he preferido para no tener que usar la versión de Python de Conda que generalmente es inferior a la instalada en el sistema.

Contenedor (5)

Establecemos una carpeta de trabajo donde crearemos los notebooks y la usaremos también para clonar repositorios, establecemos los permisos adecuados para el usuario Jupyter.

También usamos ONBUILD para indicar que las imágenes que hereden de esta usarán bash como shell, esto es importante porque sino algunos comandos como source muy usados para crear entornos virtuales no estarán disponibles.

Añadimos la configuración de los puertos y la carpeta que se usará como volumen para mapear con el host que es donde residen los cuadernos.

Ejecutar Jupyter

Establecemos el comando que arrancará Jupyter, algunas consideraciones:

  • –no-browser: para evitar abrir el navegador al arrancar.
  • –allow-root: permite ejecutar Jupyter como root.
  • –notebook-dir: carpeta raíz de Jupyter, es decir de la que leerá los ficheros, que a su vez es la misma que usamos para Git.
  • –NotebookApp.token y NotebookApp.password lo establecemos con cadenas vacías para indicar que no queremos usar autenticación, esto para un entorno particular como suele ser el de una Raspberry es muy adecuado, en entornos compartidos quizás tenga más sentido no usar estos valores y establecerlos a través del archivo de configuración.

Compilación

Ejecutamos con el siguiente comando para compilar, previamente hacemos una limpieza para evitar posibles errores (errores GPG con las claves de los orígenes de los paquetes por ejemplo). Es posible que de algún error al compilar por falta de memoría, quitar algunos servicios o programas activos para ganar memoria y reintentar puede ayudar.

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

Ejecución

Una vez terminada la compilación con el siguiente comando arrancamos la imagen:

docker container run --init --publish 8888:8888 --detach --volume /home/pi/jupyter:/home/jupyter/notebooks user/image_name

Estamos mapeando el puerto 8888 al mismo en puerto en el host y también estamos mapeando la carpeta en Rasperry como un volumen en el host en la carpeta home del usuario pi, en id_imagen indicamos el id de la imagen a ejecutar. El parámetro –init es un valor recomendado para Jupyter que le proporciona más estabilidad.
Se pueden establecer también las variables de entorno con –env para JUPYTER_ID, NODE_VERSION o TZ o se pueden dejar los valores por defecto.

Push & Pull

Para subir la imagen solo tenemos que ejecutar el siguiente comando e introducir nuestras credenciales de Docker.

docker login

Para subir la imagen usamos la información del repositorio que hemos creado, normalmente usamos como user/name lo mismo que hayamos especificado en el tag que hemos creado con el comando docker build.

docker push user/image_name

Y para descargarlo en otra máquina simplemente hacemos:

docker pull user/image_name

Aunque si lo invocamos directamente con docker run tendrá el mismo efecto.

Configurar Git

Si arrancamos un navegador y vamos a la dirección de la Raspberry podremos ver el entorno de JupyterLab, vamos a configurar Git desde aquí.

La mayoría de las tareas básicas se pueden hacer desde los menús pero para comandos más avanzados podemos abrir un terminal y lanzar comandos desde ahí o para verificar que todo ha sido correcto, por ejemplo vamos a configurar Git con nuestro usuario e email, (nota, pegar se realiza con mayus+Insert).

git config user.email "coyote@acme.com"
git config user.name "El Coyote"

Para evitar que nos pregunte usuario y contraseña para los commit vamos a configurarlo también, desde el terminal hacemos:

git config credential.helper store
git pull

Introducimos el usuario y contraseña cuando lo pregunte, con esto quedarán almacenadas y no lo preguntará para el resto de acciones que hagamos desde la consola o desde la interfaz.

Ahora para clonar el repositorio usamos la interfaz, desde el menú Git -> Clone a Repository introducimos la URL del repo, nos preguntará por unas credenciales que tendremos que tener previamente configuradas en el servidor Git.

Esto nos descargará el código en la carpeta que hemos indicado, ahora podemos probar a crear cualquier archivo y realizar un commit, en la parte de la izquierda pulsemos el icono de Git y veremos los archivos modificados, seleccionamos los que queremos subir y en la parte inferior escribimos un resumen y una descripción opcional, a continuación pulsamos Commit, como ya hemos configurado el nombre y el correo en Git el commit no dará ningún error, sino que saldrá una ventana pidiendo esos datos.

Para hacer Push hay dos formas, se puede hacer directamente desde esta misma ventana usando el icono o desde el menú Git -> Push to remote.

Desde esta misma ventana también podemos cambiar de ramas o ver el historial.

(Opcional) Proxy Apache

Podemos usar Apache como proxy para acceder y así configurar estas URL por algo más fácil de recordar, para ello en Apache creamos un virtual host que nos haga de proxy con el servidor de JupyterLab, creamos un fichero en la carpeta /etc/apache2/sites-available/005-jupyterlab.conf con el siguiente contenido, obviamente deberemos tener un sistema DNS que nos resuelva correctamente el nombre jupyterlab, vamos a suponer que el servidor está en la dirección 192.168.1.4

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

a2enmod headers
a2enmod proxy
a2enmod proxy_http
a2enmod proxy_wstunnel
a2enmod headers

Y ahora configuramos el servidor virtual

<VirtualHost spotify:80>
  ServerName jupyterlab.domain
  SererAlias jupyterlab
  Header set Access-Control-Allow-Origin "*"
  ProxyPreserveHost On
  ProxyPass / http://192.168.1.4:8888/
  ProxyPassReverse / http://192.168.1.4:8888/
  <Location "terminals/websocket">
      ProxyPass "ws://192.168.1.4:8888/terminals/websocket"
  </Location>
  <Location "/api/kernels/">
      ProxyPass "ws://192.168.1.4:8888/api/kernels/"
  </Location>
  CustomLog /var/log/apache2/jupyterlab.log combined
  ErrorLog /var/log/apache2/jupyterlab.error.log
</VirtualHost>

Para habilitar el sitio ejecutamos:

sudo a2ensite 005-jupyterlab.conf

Referencias

Configurar tzdata en Docker
Jupyter Notebook Dockerfile

Jupyter notebook en Raspberry Pi con Docker

USB externo en Raspberry Pi

Actualizado el 22 de Noviembre de 2020

Una forma rápida y sencilla de añadir un disco duro externo por USB y asociarlo a una carpeta, primero como siempre actualizar:

sudo apt-get update
sudo apt-get upgrade
sudo apt-get dist-upgrade

Antes de empezar tenemos que tener en cuenta que hay unidades USB con alimentación externa y otros sin ella, con alimentación externa no habrá problemas pero debido a las limitaciones de la RaspberryPi en términos de alimentación es posible que no pueda arrancar un disco que requiera alimentación por USB, así que lo primero será encontrar las unidades que tenemos conectadas.

sudo blkid

Si no vemos nuestra unidad y no tiene alimentación externa podemos probar a hacer los siguientes pasos:

Archivos sin fuente de alimentación externa

Editamos el fichero config.txt

sudo nano /boot/config.txt

Agregamos esta línea al final del fichero

max_usb_current=1

Reiniciamos

sudo reboot

Ahora volveríamos a probar el comando blkid y ver si aparece nuestra unidad, de no ser así podemos probar lo siguiente.

Instalamos wringPi desde aquí y comprobamos si tenemos el pin GPIO 38 activo que es el que nos permitirá doblar la potencia de la RaspberryPi y alimentar dispositivos externos:

gpio -g read 38

Si devuelve 1 lo tenemos habilitado, si vemos un 0 lo habilitamos con:

gpio -g write 38 1

En caso de no funcionar hay que usar una fuente de alimentación de más calidad, para verificar si el problema es la fuente de alimentación al arrancar la RaspberryPi tenemos que fijarnos en la cantidad de iconos de frambuesa que veamos, cuantos más mejor y sobre todo fijarnos de no ver ningún símbolo de un rayo lo que querría decir que no hay potencia suficiente.

Configurando unidad externa

Cuando veamos nuestra unidad usamos el siguiente comando:

sudo lsblk -o UUID,NAME,FSTYPE,SIZE,MOUNTPOINT,LABEL,MODEL

Veremos un listado de unidades conectadas, una buena pista suele ser mirar la columna LABEL y MODEL también por el tamaño podemos averiguar cual es nuestra unidad.

La columna FSTYPE muestra el tipo del sistema de archivos, hay que tener instalado el driver adecuado para que se pueda leer, por ejemplo para exFAT:

sudo apt install exfat-fuse

O para NTFS (poder leer y escribir):

sudo apt install ntfs-3g

Tenemos que crear la carpeta en la que queramos mapear la unidad de red, por ejemplo:

mkdir /media/usb/acme

Ahora localizamos el valor UUID (la primera columna) lo guardamos y editamos el fichero /etc/fstab, incluyendo esta línea:

UUID=12345678ABCDEFGH /media/usb/acme ntfs-3g default,uid=pi,gid=pi,nofail 0 0

Donde UUID es el valor que teníamos guardado, luego tenemos la ruta a la carpeta que hemos creado /media/usb/acme, ntfs-3g es el driver necesario para leer la unidad, en este caso ntfs-3g porque es una unidad con sistema de archivos ntfs, uid es el nombre de usuario que se usará para acceder a la unidad (en este ejemplo el usuario pi) y gid es el nombre del grupo con permiso para acceder (en este caso el grupo pi), nofail sirve para indicar que si la unidad no se puede montar en el arranque no se produzca un error.

En este caso hemos usado el usuario pi que viene por defecto en la instalación, si quisiéramos usar otro haríamos:

sudo adduser acme

Igual puede interesar crear un usuario solo para validarse por red pero no para poder iniciar sesión como un usuario normal, algo parecido a una cuenta de servicio.

sudo adduser -shell /bin/false --no-create-home acme

Ahora solo nos queda montar la unidad USB con:

sudo mount -a

Si accedemos a la carpeta que hemos creado veremos el contenido del disco, también podemos compartirla por unidad de red siguiendo los detalles de este post.

Referencias

https://www.htpcguides.com/power-2-5-hard-drive-with-raspberry-pi-b/
https://www.raspberrypi.org/documentation/configuration/external-storage.md
https://www.raspberrypi.org/forums/viewtopic.php?t=238095

USB externo en Raspberry Pi

Samba en Raspberry Pi

Actualizado el 6 de Junio de 2021

Una forma rápida y sencilla de instalar Samba, primero como siempre actualizar:

sudo apt-get update
sudo apt-get upgrade
sudo apt-get dist-upgrade

Servidor

Instalamos los paquetes samba necesarios

sudo apt-get install samba-common smbclient samba

En adelante vamos a usar el usuario pi para los ejemplos que es el que viene por defecto en la instalación, si quisiéramos usar otro podríamos crearlo con:

sudo adduser acme

Igual puede interesar crear un usuario solo para validarse por red pero no para poder iniciar sesión como un usuario normal, algo parecido a una cuenta de servicio.

sudo adduser -shell /bin/false --no-create-home acme

Y ahora establecemos una contraseña de red para que este usuario se pueda conectar a la carpeta compartida:

sudo smbpasswd -a acme

Ahora creamos la carpeta de red en el fichero /etc/samba/smb.conf

[acme]
   comment = La carpeta secreta del grupo Acme
   path = /home/pi/acme
   read only = No
   valid users = acme
   write list = acme # Usuarios que pueden escribir
   create mask = 0660 # Mascara de creación de archivos
   directory mask = 0770 # Máscara de creación de directorios

Con este comando verificamos que todo está correcto:

testparm

Hay que dar los permisos adecuados al usuario que hemos creado sobre la carpeta compartida, por ejemplo:

sudo chown -R pi:pi /home/pi/acme

Si todo es correcto reiniciamos Samba:

sudo service smbd restart

Si tenemos algún problema al configurar o conectar al recurso remoto podemos usar el comando smbclient para poder ver los recursos compartidos de un servidor, este comando nos pedirá una contraseña para poder acceder y nos sacará los recursos compartidos.

smbclient -L coyote.acme.hell

Cliente

Ahora hay que configurar y probar el lado cliente, ya sea desde Linux o desde Windows.

Raspberry Pi

Desde el lado cliente en una Raspberry se pueden establecer valores de permisos a ficheros y directorios a través de las opciones file_mode y dir_mode en el fichero /etc/fstab usando los valores a establecer en octal, estos serán los permisos que se establecerán al montar la unidad, un ejemplo de entrada fstab sería:

//servidor/carpeta       /mnt/samba/servidor/carpeta      cifs     vers=1.0,file_mode=0775,dir_mode=0775,uid=pi,gid=pi,username=sambauser,password=acme,nofail,iocharset=utf8,auto 0 0

La opción vers sirve para indicar la versión del protocolo SMB a usar, si da errores al montar por parámetros incorrectos una opción puede ser establecer una versión más baja, 1.0, 2.0 o 3.0

Windows

Ahora conectamos al recurso remoto de la forma que elijamos y puede ser que obtengamos el siguiente error en Windows.

Es una advertencia de seguridad indicando que no se permite acceder a recursos remotos que permitan invitados, este comportamiento empezó en la versión 2.0 del protocolo SMB.

Es importante intentar acceder al servidor con el nombre completo, es decir, si tenemos un servidor que se llama server en un dominio home.local deberíamos usar \\server.home.local

Hay dos formas de evitar este error:

Una que es la más práctica es usando la opción de mapear unidades de red de Windows indicando explícitamente usuario y contraseña.

Es necesario también indicar la ruta completa el servidor a la hora de mapear la unidad, es decir \\servidor\carpeta

Si no se quiere usar el dominio por defecto se puede indicar como usuario .\pi al poner .\ se usará como dominio la máquina local.

La otra opción es deshabilitando este error, a través de Directiva de equipo local (gpedit.msc), en esta ruta:

Configuración de equipo local\plantillas administrativas\Red\Estación de trabajo Lanman\Habilitar inicios de sesión de invitado no seguros

Habilitamos esta opción lo que evitará el mensaje.

Samba en Raspberry Pi