msx

Habilitando ordenadores MSX al cloud con Docker, .NET Core, GNU/Linux y GR8cloud Server

No es frecuente que pueda enlazar mis tres grandes aficiones: Docker, Azure y los ordenadores MSX; pero gracias a las espectaculares posibilidades del GR8NET, ¡el disco duro de nuestro MSX puede estar ahora en la nube!

11 min de lectura
Habilitando ordenadores MSX al cloud con Docker, .NET Core, GNU/Linux y GR8cloud Server

Ya he comentado en muchas ocasiones lo enamorado que he estado siempre de los ordenadores MSX, un estándar de computación creado en 1983 por Microsoft y ASCII, y que siguen espectacularmente vivos con múltiples nuevos desarrollos hardware y software a lo largo de cada año. En la siguiente imagen se puede ver a Kazuhiko Nishi, padre del estándar MSX y vice-presidente de Microsoft Japón durante la década de los 80; seguido de Bill Gates y Paul Allen.

Kazuhiko Nishi, Bill Gates and Paul Allen

Entre estos desarrollos destaca en especial el GR8NET, un adaptador Ethernet con pila TCP/IP por hardware, interfaz SD y múltiples extensiones multimedia. Este adaptador habilita un ordenador MSX 2 o superior a conectarse a redes TCP/IP y por tanto... al cloud...

GR8NET

MSX turboR with GR8NET

Tanto el GR8NET como el GR8cloud server que voy a comentar en este artículo están desarrollados por Eugeny Brychkov.

Hechas las presentaciones, ¡entremos en materia!

GR8cloud Server

El GR8cloud Server es un software escrito en .NET Core que habilita a los ordenadores MSX equipados con un GR8NET a montar un volumen virtual a través de red como si de un disco duro local se tratase, ¡incluso a través de enlaces WAN como Internet! De esta manera un MSX puede arrancar desde red en una unidad que además es de lectura/escritura. ¡Nada mal para un venerable MSX a 3,5 MHz!

¿Cómo lo hace? Mediante una arquitectura cliente servidor que recuerda a los volúmenes de red de Novell NetWare o al iSCSI si me apuráis.

msx_gr8cloud_arch

Como se puede ver, apenas tenemos dos componentes:

  • El servidor GR8cloud. Desarrollado en .NET Core 2.1, expone el puerto 684/tcp (podemos configurarlo) que será utilizado por los ordenadores MSX que se quieran conectar. Tanto la autenticación como la transferencia de datos se realiza a través de este puerto. En cuanto a configuración sólo necesita de dos tipos de archivo:
    • Un archivo llamado passwd (no confundir con /etc/passwd), que es donde se almacena la autenticación de los MSX que se conectan al servidor. Sigue un esquema muy sencillo: MacAddress Password, habiendo un GR8NET por línea. Ejemplo: 10160004051E i*love+gr8net.
    • Un archivo IMG por cada disco virtual que vamos a servidor. El nombre debe ser macaddress.img, sustituyendo en cada caso. Por ejemplo, para dar acceso al GR8NET del punto anterior, el archivo debería llamarse 10160004051E.img.
  • El cliente GR8NET. El cliente se encuentra en la ROM del GR8NET para MSX por lo que no se precisa instalación. La orden CALL NETSETCLOUD de MSX-BASIC se utiliza para la configuración de la dirección a la que apuntar.

Se pueden consultar todos los detalles en el capítulo 13.5 del manual del GR8NET.

Estupendo todo, pero... ¿qué es lo que vamos a hacer?

Al estar hecho GR8cloud en .NET Core 2.1 pensé inmediatamente que era un estupendo candidado a contenerizar en Docker y explorar que tal es la experiencia. Por otro lado me encontraba con dos necesidades adicionales:

  • GR8cloud Server es por diseño... inseguro. No por mala praxis, sino porque los ordenadores MSX no tienen potencia suficiente para procesar algoritmos de cifrado modernos, por lo que las comunicaciones no se establecen cifradas. Utilizar proxies TLS como el versátil stunnel puede ayudarnos a subsanar este tema, que definitivamente dejaré para añadir a la imagen Docker más adelante.
  • Establecer algún método parar cargar y descargar fácilmente imágenes de volúmenes, para lo cual opté por vsftpd, un servidor FTP minimalista, seguro y altamente eficiente.

Puedo decir que la experiencia de contenerización fue muy satisfactoria además de bastante sencilla.

Docker y .NET Core

Microsoft tiene un repositorio dedicado a .NET Core en Docker Hub donde encontramos un buen número de imágenes con el runtime listo para ejecutar tanto para Windows Nano Server como para GNU/Linux.

Entre las imágenes de GNU/Linux encontramos también varias distribuciones: Debian, Ubuntu y Alpine. Como se trata de un servidor muy sencillo que apenas ocupa unos escasos kilobytes, quería minimizar el tamaño de la imagen todo lo posible y por tanto opté por Alpine Linux, concretamente microsoft/dotnet:2.1-runtime-alpine3.7.

Repositorio de Github

Todo lo que voy a contar a continuación está publicado en este repositorio de Github, que a su vez está enlazado con este otro de Docker Hub, por lo que se activa la característica de build automática.

La imagen Docker

Vamos una vez más a saltarnos las buenas práticas de Docker y construir una imagen que ejecutará tres procesos: GR8cloud Server, vsftpd y el fantástico supervisord para asegurarnos que se maneja bien el signaling de los dos procesos.

Tendría el siguiente aspecto:

FROM microsoft/dotnet:2.1-runtime-alpine3.7

ARG DOWNLOAD_URL=http://www.gr8bit.ru/software/gr8cloudserver/gr8cloudserver.rar
ARG RAR_PWD=gr8net

LABEL title="GR8cloud server" \
  author="AGE Labs / Eugeny Brychkov" \
  maintainer="Carlos Milán Figueredo" \
  version="20181007" \
  url1="https://github.com/cmilanf/docker-gr8cloudserver" \
  url2="http://www.gr8bit.ru/software/gr8cloudserver" \
  url3="http://rs.gr8bit.ru/Documentation/GR8NET-manual.pdf" \
  twitter="@cmilanf" \
  thanksto1="Beatriz Sebastián Peña" \
  usage="docker run -it -p 684:684 -p 20:20 -p 21:21 -p 34000-34010:34000-34010 --rm --name gr8cloud -e \"FTP_PWD=gr8net\" -e \"PASSWD_CSV=101600040501 MyPassword,10160004051E i*love+gr8net,10160004057A msx_is_the_best\" -e \"FTP_PASV_ADDRESS=127.0.0.1\" cmilanf/gr8cloudserver:latest"

LABEL FTP_PWD="The FTP user password" \
    PASSWD_URL="URL for downloading a passwd file for the GR8 Cloud Server. Please note exsiting file will be overwriten!" \
    PASSWD_CSV="MAC-password pair for the GR8 Cloud Server passwd file delimited by comma. Example: 101600040501 MyPassword,10160004051E i*love+gr8net,10160004057A msx_is_the_best" \
    FTP_PASV_ADDRESS="FTP pasive mode address. If not present, it will be autodetected."

RUN mkdir -p /srv/gr8cloudserver/data \
    && mkdir -p /var/log/supervisord \
    && apk update \
    && apk add --no-cache unrar supervisor vsftpd bash openssl \
    && cd /srv/gr8cloudserver \
    && wget ${DOWNLOAD_URL} \
    && unrar e ${DOWNLOAD_URL##*/} -p${RAR_PWD} \
    && rm -f ${DOWNLOAD_URL##*/} \
    && adduser -h /srv/gr8cloudserver/data -s /sbin/nologin -D gr8ftp \
    && chown -R gr8ftp:gr8ftp /srv/gr8cloudserver/data

RUN mkdir -p /etc/ssl/private \
    && echo 'gr8ftp' >> /etc/vsftpd/vsftpd.allowed_users

VOLUME /srv/gr8cloudserver/data
COPY docker-entrypoint.bash /
COPY supervisord.conf /etc/
COPY vsftpd.conf /etc/vsftpd/

EXPOSE 684/tcp 20/tcp 21/tcp 34000-34010/tcp

ENTRYPOINT ["/docker-entrypoint.bash"]
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"]

Como se puede ver por la metadata, vamos a escuchar una serie de variables para configurar automáticamente vsftpd así como la autenticación de nuestros MSX.

Un buen detalle de diseño es que GR8cloud Server no lee ninguna imagen de disco virtual ni su archivo passwd hasta que un MSX solicita acceso. Eso nos permite cargar por FTP una nueva versión del passwd así como nuevas imágenes sin estar arrancando de nuevo el contenedor.

Lo único que se puede ver que no haya comentado en otras ocasiones es la línea 36, VOLUME /srv/gr8cloudserver/data.

El servidor quedará instalado en /srv/gr8cloudserver.

Persistiendo los datos en imágenes Docker

Siempre debemos tener presente que nuestros contenedores deben crearse y destruirse dinámicamente, e imagino que nadie quiere perder el disco de su MSX, por lo que la palabra clave VOLUME juega un papel crucial.

Básicamente le estamos indicando a Docker que en esta imagen la carpeta /srv/gr8cloudserver/data debe montarse como un volumen separado de nuestro contenedor y por tanto sobrevivirá al ciclo de vida de este.

Docker puede implementar un volumen de muchas maneras, tal como se explica en su documentación oficial, como puede ser una carpeta en el host o una cabina de discos a través de un plugin de terceros.

¿Suena familiar? Desde luego, ya hablé de este tema en relación a Kubernetes. ¿Es el mismo concepto? Sí. ¿Es el mismo mecanismo? No. Kubernetes no usa ninguna capacidad de Docker para la gestión de volúmenes persistentes y por tanto su arquitectura es distinta; diría que más complicada, pero bastante más potente. Recordemos que Kubernetes puede funcionar también con otros motores de contenerización como rkt, por lo que abstraer esta funcionalidad es buena idea.

Si queremos persistir los datos del GR8cloud Server en Kubernetes, debemos hacer uso de su modelo de volúmenes persistentes.

El docker-entrypoint.bash

Alpine Linux no viene con bash por defecto, sino con una versión ultra-ligera llamada ash (Alquimist Shell). En este entrypoint vamos a necesitar una característica específica de bash y por eso lo agregamos a la instalación.

El script es el siguiente:

#!/bin/bash
echo "Welcome to the dockerized GR8cloud server"
echo "========================================="
cat /srv/gr8cloudserver/readme.txt
echo ""
echo "Downloading default GR8cloud cloud volume..."
wget http://www.gr8bit.ru/software/gr8cloudserver/default-gr8cloud-volimg.rar -O /srv/gr8cloudserver/data/default-gr8cloud-volimg.rar
echo ""

if [ -z ${FTP_PWD} ]; then
  FTP_PWD=$(< /dev/urandom tr -dc A-Za-z0-9 | head -c${1:-8};echo;)
  echo "Generated password for user 'gr8ftp': ${FTP_PWD}"
fi
echo "gr8ftp:${FTP_PWD}" | /usr/sbin/chpasswd
echo ""

echo "Creating TLS self-signed certificate..."
openssl req -x509 -nodes -days 730 -newkey rsa:2048 -keyout /etc/ssl/private/vsftpd.key -out /etc/ssl/certs/vsftpd.crt -subj "/C=ES/ST=Self-signed/L=Certificate/O=HispaMSX/OU=org/CN=hispamsx.org"
echo ""

if ! [ -z ${FTP_PASV_ADDRESS} ]; then
    echo "Setting pasv_address to ${FTP_PASV_ADDRESS}"
    echo "pasv_address=${FTP_PASV_ADDRESS}" >> /etc/vsftpd/vsftpd.conf
fi

if ! [ -z ${PASSWD_URL} ]; then
    echo "Downloading passwd from: ${PASSWD_URL}"
    wget ${PASSWD_URL} -O /srv/gr8cloudserver/data/passwd
    echo ""
fi

if ! [ -z "${PASSWD_CSV}" ]; then
    echo "PASSWD_CSV variable found: ${PASSWD_CSV}"
    IFS=',' read -ra PASSWD_ARRAY <<< "$PASSWD_CSV"
    for i in "${PASSWD_ARRAY[@]}"; do
        echo "Adding ${i} to /srv/gr8cloudserver/data/passwd"
        echo ${i} >> /srv/gr8cloudserver/data/passwd
    done
fi

echo ""
echo "All done! Running services now, entering supervisord..."
echo ""
exec "$@"

A groso modo el entrypoint realiza las siguientes funciones:

  1. Descarga un volumen por defecto de GR8cloud para que tengamos dónde empezar rápidamente, dado que el proceso de crear una imagen para servir a nuestro MSX es un pelín tedioso.
  2. Genera una contraseña aleatoria para el FTP si no hemos especificado una con la variable FTP_PWD.
  3. Crea un certificado TLS autofirmado que utilizaremos para cifrar las conexiones FTP.
  4. Si la hemos especificado con FTP_PASV_ADDRESS, declara la dirección IP pública que el servidor mostrará al cliente en el modo pasivo.
  5. Si hemos especificado la variable PASSWD_URL, descargará el archivo passwd de la dicha dirección y sobrescribirá el existente.
  6. De lo más interesante de todo, podemos pasar por la variable PASSWD_CSV tantas entradas de autenticación como longitud de variable acepte nuestra shell. Así por ejemplo esta variable puede ser PASSWD_CSV=101600040501 MyPassword,10160004051E i*love+gr8net,10160004057A msx_is_the_best. Utilizo una coma para separar cada línea en el archivo.

El punto 6 merece especial atención. ¿Cómo podemos en bash leer una cadena de texto y trocearla para después escribirla en un archivo? Esta operación sería trivial en PowerShell... ¡pero en bash tampoco es muy complicada!

Para ello volvemos a un viejo conocido: IFS o Internal Field Separator. Se trata de una variable especial donde definimos el delimitador que queremos aplicar a nuestro procedimiento. Lo utilizamos de esta manera:

if ! [ -z "${PASSWD_CSV}" ]; then
    echo "PASSWD_CSV variable found: ${PASSWD_CSV}"
    IFS=',' read -ra PASSWD_ARRAY <<< "$PASSWD_CSV"
    for i in "${PASSWD_ARRAY[@]}"; do
        echo "Adding ${i} to /srv/gr8cloudserver/data/passwd"
        echo ${i} >> /srv/gr8cloudserver/data/passwd
    done
fi
  • En la línea 3 especifico que el separador es la coma: ,, leo el contenido de la variable PASSWD_CSV y usando el separador del IFS ponemos el resultado en PASSWD_ARRAY (efectivamente, el formato array me pareció muy conveniente).
  • En el resto del miniscript iteramos por todo el array y agregamos cada entrada como línea en el archivo /srv/gr8cloudserver/data/passwd.

Quizás no muy intuitivo en primera instancia, pero desde luego fácil. EL IFS no está presente en la Alquimist Shell y es el motivo por el que necesitaba bash en esta imagen de Alpine.

El vsftpd.conf

Algo que me ha encantado de vsftpd es lo fácil y rápido que es de configurar. Si es sencillo, altamente eficiente y muy seguro, ¿qué más le puedo pedir?

La configuración es la siguiente:

background=NO
listen=YES
listen_ipv6=NO
dirmessage_enable=YES
anonymous_enable=NO
log_ftp_protocol=YES
xferlog_enable=YES
local_enable=YES
write_enable=YES
chroot_local_user=YES
local_umask=022
ftpd_banner=GR8cloud server FTP service.
passwd_chroot_enable=YES
allow_writeable_chroot=YES
seccomp_sandbox=NO
connect_from_port_20=YES
pasv_enable=YES
pasv_min_port=34000
pasv_max_port=34010
userlist_deny=NO
userlist_enable=YES
userlist_file=/etc/vsftpd/vsftpd.allowed_users
ssl_enable=YES
allow_anon_ssl=NO
force_local_data_ssl=NO
force_local_logins_ssl=NO
ssl_tlsv1=YES
ssl_sslv2=NO
ssl_sslv3=NO
require_ssl_reuse=YES
ssl_ciphers=HIGH
rsa_cert_file=/etc/ssl/certs/vsftpd.crt
rsa_private_key_file=/etc/ssl/private/vsftpd.key

Algunos puntos a comentar:

  • background=NO se debe a que estamos ejecutándonos en un contenedor y por tanto no queremos procesos de fondo. Todo será gestionado como foreground por supervisord.
  • seccomp_sandbox=NO me dió mucho dolor de cabeza, ya que hasta que lo especifiqué obtenía siempre un incomprensible 500 OOPS: priv_sock_get_cmd. Hay detalles del bug aquí y parece que está solucionado en la versión 3.0.2.
  • Especifico el rango de puertos para el FTP pasivo que después el administrador de sistemas puede mapear desde Docker como estime oportuno.
  • Activo el TLS de forma opcional. La única razón por la que mantengo un acceso sin cifrado es para poder entrar directamente con el MSX.

Arquitectura final dockerizada

Queda de la siguiente manera:
msx_gr8cloud_arch_docker

Ejecutando la imagen

¡Una vez visto todo sólo nos queda saber cómo arrancar el servidor! Aquí facilito un ejemplo donde mapeo los puertos y paso unos valores de autenticación por defecto:

docker run -it -p 684:684 -p 20:20 -p 21:21 -p 34000-34010:34000-34010 --rm --name gr8cloud -e "FTP_PWD=gr8net" -e "PASSWD_CSV=101600040501 MyPassword,10160004051E i*love+gr8net,10160004057A msx_is_the_best" -e "FTP_PASV_ADDRESS=127.0.0.1" cmilanf/gr8cloudserver:latest

Construyendo nuestra propia imagen de disco virtual

Si no queremos usar la imagen por defecto del GR8cloud Server y queremos construirnos la nuestra propia, he de decir que el proceso es un poquito tedioso, así que he pensado que es buena idea detallarlo aquí. Para ello nos vamos a valer del mejor emulador de MSX hasta la fecha, el openMSX. El emulador es capaz de ejecutarse en infinidad de plataformas y sistemas operativos.

Los pasos son los siguientes:

  1. Configurar openMSX para ejecutarse como un MSX 2 o superior y una interfaz de disco Nextor como el MegaflashROM SCC+ SD
  2. Estable el tamaño de la unidad en MegaFlashROM_SCC+_SD.xml. Esto nos creará las imágenes en el disco duro.
  3. Si la ROM de MegaflashRM SCC+ SD cargó correctamente, podremos llamar al comando CALL FDISK desde MSX-BASIC para particionar el disco.
  4. Reinicia openMSX para que la nueva unidad particionada y formateada esté disponible. Ahora podemos empezar a copiar todos los archivos que queramos.
  5. Cierra openMSX.
  6. Sube la imagen de disco generada a /srv/gr8cloudserver/data/. ¡El servidor FTP de la imagen Docker es excelente para ello!
  7. Renombra la imagen con la dirección MAC de tu MSX, por ejemplo: 10160004051E.img.
  8. Asegúrate de que también está configurado correctamente el archivo passwd.
  9. ¡Ahora volvamos a nuestro MSX con GR8NET!
  10. Configuramos desde MSX-BASIC el acceso al GR8cloud Server: CALL NETSETCLOUD("myserver.mydomain.org:684","MyPassword")
  11. Asegúrate de que el GR8cloud está activado en el MSX: CALL NETSETCLOUD(1).
  12. Arranca tu MSX con el GR8NET en un modo mapper que incluya Nextor, por ejemplo con CALL NETSETMAP(30).
  13. El MSX debería reiniciarse, conectar al contendor Docker, montar el volumen... ¡y arrancar desde él!
  14. Puedes usar el volumen como si de un disco normal se tratase.

A continuación se pueden ver capturas de pantalla de mi MSX arrancando desde el contenedor Docker:

msx_config
Configuración del GR8NET para acceder a nuestro servidor.

msx_network_bootup
Reiniciamos y vemos como la BIOS se prepara para conectar.

msx_network_mm
¡MSX-DOS 2 y Multimente arrancdos desde red!

¡Happy MSXing!