Automatizar mi red casera con Salt

Translations: en - Tags: salt

No soy un buen administrador de sistemas.

Por muchos años, mi estrategia para administrar mis máquinas en casa ha sido una de negligencia máxima:

  • Evitar actualizar la distro o reinstalarla lo más posible, porque las cosas se rompen o pierden su configuración.

  • Mantener mi $HOME intacto entre cada actualización, para conservar mi configuración personal aunque se acumule la basura vieja.

  • Llorar cuando instalo un servicio en mi servidorcito casero, como directorios compartidos con SMB o un servicio de música, porque sé que se va a romper cuando tenga que reinstalar.

Hace unos dos años escribí unos scripts para automatizar al menos la parte de instalar los paquetes que necesito, y configurar lo más indispensable como el firewall. Esos scripts me ayudaron a perder parte del miedo a reinstalar o poner actualizaciones; al correrlos, ya sólo tenía que hacer un poco de trabajo manual para dejar todo funcionando. También, el tener esos scripts me hizo pensar en qué cosas puedo dejar como defaults en la distro, y qué necesito cambiar de deveras.

Salt

En mi cabeza pienso que existe un universo tenebroso de herramientas para administración de sistemas a gran escala. Por supuesto que querrías automatización para manejar una granja de servidores. Por supuesto que nadie administraría 3000 estaciones de trabajo a mano. Pero para mi redecita minúscula en casa, con un servidor y dos estaciones de trabajo, ¿no es como matar hormigas con aplanadora? ¿No es demasiado usar esas herramientas gigantescas?

¡Por fortuna no es así!

Mi colega Richard Brown ha estado hablando sobre el Proyecto Salt por varios años. Es un sistema similar a Ansible o Puppet, para instalar y configurar compus de forma automática.

Lo que me gusta de Salt hasta ahora es que su documentación es excelente, y me ha permitido traducir mi modesta configuración casera a un sistema automático, mientras que aprendo algunas buenas prácticas.

Comencé con el Salt walkthrough, que es bastante sencillo.

Resumen: el salt-master es la máquina central que mantiene y distribuye la configuración a otras máquinas. Esas otras máquinas se llaman salt-minions. Escribes YAML más o menos declarativo en el salt-master, y propagas esa configuración a los minions. Salt tiene operaciones de alto nivel como "crear un usuario" o "instalar un paquete" o "reiniciar un servicio cuando cambian archivos de configuración" sin que tengas que saber cómo lo hace tu distro en específico.

Mi red casera y cómo quiero que sea

pambazo - Tiene un RAID con música y películas y almacena respaldos de datos. Este es mi servidor casero.

tlacoyo - Compu de escritorio; es mi estación de trabajo principal.

torta - Mi laptop, que he usado muy poco durante la pandemia. En principio debería tener una configuración idéntica a mi estación de trabajo.

Tengo abierto el puerto de MDNS en esas tres máquinas para que pueda usar nombre-de-host.local para accesarlas, sin tener que configurar un servicio de DNS. Tal vez debería aprender a hacer esto último.

Todas las compus necesitan mis archivos básicos de configuración (Emacs, prompt del shell), y unos cuantos programas indispensables (Emacs, Midnight Commander, git, podman).

Mis estaciones de trabajo necesitan tener mis llaves de SSH para gitlab/github, mis llaves de la VPN de Suse, y algo de infraestructura básica para desarrollo. Necesito poder reinstalar y reconstruir esas máquinas rápidamente.

Todas las compus deben tener la misma configuración para las dos impresoras que hay en casa (una laser que imprime por dos lados, y una de inyección de tinta para fotos).

Mi servidor casero por supuesto que necesita la configuración para todos sus servicios.

También tengo unas cuantas máquinas virtuales más bien efímeras, para probar imágenes de distros. En esas sólo necesito algunos paquetes y configuración básica.

Instalar el salt-master

Mi servidor casero funciona como el "salt master", que es la máquina que mantiene la configuración que se va a distribuir a las demás. Allí uso el paquete de salt-master que viene con openSUSE. Lo único que le moví a la configuración por default son las rutas a los archivos de configuración para los minions, para poder tenerlos en un árbol de git en mi directorio personal en vez de bajo /etc/salt. Lo siguiente va en /etc/salt/master:

# configuración para los minions
file_roots:
  base:
    - /home/federico/src/salt-states

# datos privados para distribuir a los minions
pillar_roots:
  base:
    - /home/federico/src/salt-pillar

Instalar los minions

Es fácil instalar el paquete por default de salt-minion y configurarlo para que sepa ubicar al salt-master, pero quería una forma más directa de haerlo en un solo paso Salt-bootstrap permite hacer eso. Puedo correr esto en una máquina recién instalada:

curl -o bootstrap-salt.sh -L https://bootstrap.saltproject.io
sudo sh bootstrap-salt.sh -w -A 192.168.1.10 -i mi-hostname stable

La primera línea descarga el script de bootstrap-salt.sh.

La segunta línea:

  • -w - usar el paquete de la distro de salt-minion, no el oficial.

  • -A 192.168.1.10 la dirección IP del salt-master. En este punto la máquina que se va a convertir en minion todavía no tiene abierto el puerto de DNS en el firewall, entonces no puede encontrar a pambazo.local. Entonces, el ponemos su IP.

  • -i mi-hostname - Nombre para el minion. Salt te permite tener un nombre diferente para el minion y para el host, pero yo quiero que sean iguales. Es decir, quiero que el host tlacoyo.local sea un minion llamado tlacoyo.

  • stable - Usar la versión estable de salt-minion, no la desarrollo.

Cuando se ejecuta ese script, éste crea un par de llaves y le pide al salt-master que registre la llave pública del minion. Luego, en el salt-master hago esto:

salt-key -a mi-hostname

Esto hace que el master acepte la llave pública del minion, y ya está listo para configurarse.

Lo primero: poner el nombre del host, abrir el puerto de MDNS en el firewall

Quiero que el nombre del host sea igual al nombre del minion:

'poner el nombre del host igual al nombre del minion:
  network.system:
    - hostname: {{ grains['id'] }}
    - apply_hostname: True
    - retain_settings: True

Salt usa plantillas de Jinja para pre-procesar sus archivos de configuración. Una de las variables disponibles es grains, que contiene información específica a cada minion: su arquitectura de CPU, memoria, distribución, y el identificador del minion. Aquí uso {{ grains['id'] }} para sacar el identificador del minion y ponerlo como el hostname.

Para configurar el firewall de las estaciones de trabajo, usé YaST y luego copié la configuración resultante a Salt:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
/etc/firewalld/firewalld.conf:
  file.managed:
    - source: salt://opensuse/desktop-firewalld.conf
    - mode: 600
    - user: root
    - group: root

/etc/firewalld/zones/desktop.xml:
  file.managed:
    - source: salt://opensuse/desktop-firewall-zone.xml
    - mode: 644
    - user: root
    - group: root

firewalld:
  service.running:
    - enable: True
    - watch:
      - file: /etc/firewalld/firewalld.conf
      - file: /etc/firewalld/zones/desktop.xml

file.managed es lo que se usa en Salt para copiar archivos a las máquinas de destino. En las líneas 1 a 6, se copia salt://opensuse/desktop-firewalld.conf a /etc/firewalld/firewalld.conf. El prefijo salt:// se refiere a una ruta bajo el lugar donde tienes los salt-states; este es el árbol de git que mencioné más arriba.

Las líneas 15 a 20 le dicen a salt que habilite el servicio firewalld service, y que lo reinicien cuando cambie cualquiera de esos archivos de configuración.

Paquetes indispensables

No puedo vivir sin estos:

'paquetes indispensables:
  pkg.installed:
    - pkgs:
      - emacs
      - git
      - mc
      - ripgrep

pkg.installed toma un arreglo de nombres de paquetes. Aquí pongo los nombres que se usan en openSUSE. Salt te permite hacer mucha magia con Jinja y el salt-pillar para tener nombres de paquetes específicos para cada distribución de Linux, si es que tienes un entorno con distros diferentes. Como aquí tengo todo con openSUSE, no necesito hacer eso.

Mi usuario, y archivos de configuración personales

Esto crea mi usuario:

federico:
  user.present:
    - fullname: Federico Mena Quintero
    - home: /home/federico
    - shell: /bin/bash
    - usergroup: False
    - groups:
      - users

Esto copia algunos archivos de configuración:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
{% set managed_files = [
  [ 'bash_profile',  '.bash_profile',         644 ],
  [ 'bash_logout',   '.bash_logout',          644 ],
  [ 'bashrc',        '.bashrc',               644 ],
  [ 'starship.toml', '.config/starship.toml', 644 ],
  [ 'gitconfig',     '.gitconfig',            644 ],
] %}

{% for file_in_salt, file_in_homedir, mode in managed_files %}

/home/federico/{{ file_in_homedir }}:
  file.managed:
    - source: salt://opensuse/federico-config-files/{{ file_in_salt }}
    - user: federico
    - group: users
    - mode: {{ mode }}

{% endfor %}

Aquí uso un arreglo de Jinja para definir una lista de archivos y sus rutas de destino, y un ciclo for para reducir la duplicación de código en el YAML.

Instalar algunos flatpaks

Instalar los flatpaks que necesito:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
'Añadir repositorio de flathub:
  cmd.run:
    - name: flatpak remote-add --if-not-exists --user flathub https://flathub.org/repo/flathub.flatpakrepo
    - runas: federico

{% set flatpaks = [
   'com.github.tchx84.Flatseal',
   'com.microsoft.Teams',
   'net.ankiweb.Anki',
   'org.gnome.Games',
   'org.gnome.Solanum',
   'org.gnome.World.Secrets',
   'org.freac.freac',
   'org.zotero.Zotero',
] %}

install-flatpaks:
  cmd.run:
    - name: flatpak install --user --or-update --assumeyes {{ flatpaks | join(' ') }}
    - runas: federico
    - require:
      - 'Añadir repositorio de flathub'

Poner uno de los archivos de configuración de esas aplicaciones:

/home/federico/.var/app/org.freac.freac/config/.freac/freac.xml:
  file.managed:
    - source: salt://opensuse/federico-config-files/freac.xml
    - user: federico
    - group: users
    - mode: 644
    - makedirs: True

Este último es el archivo de configuración para fre:ac, una aplicación para ripear CDs de audio. Los Flatpaks guardan su configuración bajo ~/.var/app/nombre-del-flatpak. Para ésta, configuré la aplicación una vez desde su interfaz gráfica y luego copié el archivo de configuración a los salt-states.

Etcétera

Esto de arriba no es mi configuración completa; por supuesto que las cosas como el servidor casero necesitan muchos más paquetes y archivos. Sin embargo, esos patrones de arriba son casi todo lo que necesito para configurar lo demás.

Cómo funciona en la práctica

Tengo un árbol de git con mis salt-states. Cuando hago cambios y quiero distribuirlos a mis compus, hago un git push al repositorio en mi servidor casero y luego corro este script:

#!/bin/sh
set -e
cd /home/federico/src/salt-states
git pull
sudo salt '*' state.apply

La línea con salt '*' state.apply hace que todas las compus obtengan una configuración actualizada. Salt te dice qué cambió en cada máquina y qué se quedó igual. La parte más lenta parece ser cuando zypper actualiza la información sobre sus repositorios de paquetes; fuera de eso, Salt es suficientemente rápido para mis propósitos.

Cómo jugar con esto por primera vez

Después de instalar el salt-master nuevo en mi servidor casero, me hice una máquina virtual, la configuré como minion e inmediatamente le hice un snapshot a la VM. Así quería poder regresar al estado "recién instalado, nada configurado" para probar mis salt-states como si fuera una compu nuevecita. Ya que tuve la confianza de que las cosas funcionaban, instalé salt-minion en mi máquina de escritorio y en mi laptop de la misma manera. Esto me fue muy útil para no romperlas desde el principio.

Un proceso de actualización personal

Poco a poco he estado mudando mi configuración acumulada de mi $HOME histórico a Salt. Esto me hizo darme cuenta de toda la basura que seguía cargando ahí; todavía tenía archivos como ~/.red-carpet y ~/.realplayerrc para software que no existe desde hace muchos años. ¡Mis archivos de configuración están mucho más limpios ahora!

También pude quitar mi scripts viejos y que ya no uso de ~/bin (todavía tenía el que usaba para conectarme al túnel SSH de Ximian, y el script para conetarse a los modems con PPP de la universidad), darme cuenta de cuáles aun necesitaba, moverlos a ~/.local/bin y manejarlos bajo Salt.

Para limpiar archivos viejos en varias máquinas al mismo tiempo, tengo algo así:

/home/federico/.dotfile-que-ya-no-se-usa:
  file.absent

Eso borra el archivo.

Al final de cuentas, sí pude alcanzar mi meta inicial: puedo reinstalar una compu, y luego dejarla bien configurada con un solo comando.

Esto me ha dado más confianza para instalar juguetes lindos en mi servidor casero... como un servidor de música, que me llena de felicidad. ¡Mira todo esto!

Algo que sería lindo en Flatpak

Salt te permite saber qué cambió al aplicar una configuración, o también puedes hacer una ejecución simulada donde te dice qué va a cambiar sin que modifique nada. Por ejemplo, Salt sabe cómo revisar la base de datos de paquetes en cada distribución, para avisarte si un paquete tiene actualizaciones, o si un archivo de configuración existente difiere de uno que vas a propagar.

Sería lindo si el comando flatpak regresara esa información. En los ejemplos de arriba, utilizo flatpak remote-add --if-not-exists y flatpak install --or-update para que sean idempotentes, pero Salt no se entera de lo que Flatpak hizo realmente; nada más ejecuta los comandos y regresa si fueron exitosos o no.

Cosillas pendientes

Algunos programas mezclan información de estado con información de configuración en el mismo archivo controlado por el usuario, y este estado se pierde cada vez que Salt sobreescribe el archivo:

  • fre:ac, un programa para ripear CDs de audio, tiene uno de esos "consejos del día" al iniciarlo. Cada vez que lo ejecutas, se actualiza el "número del último consejo visto", pero lo hace en el mismo archivo donde se guarda la información de las rutas a donde va a escribir los archivos de audio y la configuración que le pusiste para los codecs. Cada vez que Salt reescribe este archivo, se resetea el "número del último consejo" al inicial.

  • Cuando cargas un tema en Emacs, digamos, con custom-enabled-theme en custom.el, Emacs guarda un checksum del tema después de preguntarte si quieres ejecutar el código del tema — para prevenir temas maliciosos o algo así. Sin embargo, ese checksum también se guarda en custom.el. Si Salt sobreescribe ese archivo, Emacs te vuelve a preguntar sobre el tema la vez siguiente.

En términos de Salt, tal vez necesito usar una forma más detallada de cambiar un archivo que copiarlo enterito. Salt permite cambiar líneas individuales y cosas así. ¿Copiar todo el archivo si no existe, o sólo cambiar unas líneas si ya está?

Necesito destilar la información de dconf para los programas de GNOME instalados en el sistema, a diferencia de los flatpaks, para poder restaurarla después.

Estoy seguro que todavía tengo detritus bajo ~/.config que debería manejarse con Salt... tal vez tengo que hacer otra pasada de actualización personal y limpiar eso.

En casa tenemos algunas máquinas Windows para cosas de la escuela. Salt también funciona en Windows, y me encataría configurarlo para que los respaldos al servidor casero se configuren automáticamente.