Rebelión en la granja — investigación y laboratorio de dispositivos
Investigación sobre granjas de dispositivos y automatización completa para levantar un laboratorio Android sobre un chasis de blades Debian. Conclusión que ordena el repositorio: el cuello de botella nunca fue el cómputo sino la identidad. Desde que la atestación de dispositivo pasó de estadística a criptográfica, un servidor no puede hacerse pasar por un teléfono — le falta una clave grabada en fábrica que ningún software produce. Contenido: - 01_investigacion/ — capa de identidad (verificación telefónica, eSIM, reputación de IP, atestación por hardware, correlación entre cuentas), mercado y players, stack técnico, marco legal en la UE - 02_device_lab/ansible/ — Ansible para 16 blades: preflight de solo lectura que identifica el hardware por DMI, despliegue de redroid en Docker y control de flota por adb. Solo ansible.builtin, sin colecciones externas - docs/ — requisitos, costes y repaso de seguridad con 11 hallazgos - 03_charla/ — guion para HackMadrid / Hackmeeting València Estado: la automatización está escrita pero SIN ejecutar contra hardware. YAML validado, plantillas renderizadas en ambos modos de red y fleet.sh comprobado con bash -n. El modelo de blade sigue sin confirmar.
This commit is contained in:
commit
74bfbe9437
42 changed files with 2989 additions and 0 deletions
302
02_device_lab/ansible/README.md
Normal file
302
02_device_lab/ansible/README.md
Normal file
|
|
@ -0,0 +1,302 @@
|
|||
# Ansible — provisionar la granja Android sobre los blades Debian
|
||||
|
||||
Automatiza el montaje completo: de blades Debian pelados a una flota de Android
|
||||
virtuales controlables por `adb` desde un único sitio.
|
||||
|
||||
**No hay móviles físicos en esta granja.** Cada "dispositivo" es un contenedor
|
||||
[redroid](https://github.com/remote-android/redroid-doc) — Android real corriendo sobre el kernel
|
||||
del blade. Es la única forma sensata de hacer esto con el hardware que hay.
|
||||
|
||||
## La arquitectura
|
||||
|
||||
```
|
||||
Dell PowerEdge M1000e — 16 blades
|
||||
│
|
||||
├── blade-01 ................ COORDINADOR
|
||||
│ └── adb + fleet.sh único punto de control de toda la flota
|
||||
│
|
||||
└── blade-02 .. blade-16 .... NODOS DE CARGA (15)
|
||||
└── Debian 12 pelado
|
||||
└── Docker
|
||||
├── redroid-blade-NN-01 Android 13 ~2 GB
|
||||
├── redroid-blade-NN-02 Android 13 ~2 GB
|
||||
└── redroid-blade-NN-03 Android 13 ~2 GB
|
||||
|
||||
Con 8 GB por blade → 3 instancias → ~45 Android en total
|
||||
```
|
||||
|
||||
Debian va **directo sobre el blade**. Sin Proxmox, sin VMs, sin LXC anidado: con 8 GB por blade,
|
||||
cada capa de hipervisor se come una instancia Android entera.
|
||||
|
||||
---
|
||||
|
||||
## Requisitos
|
||||
|
||||
**Nodo de control** (tu sobremesa):
|
||||
|
||||
```bash
|
||||
sudo apt install ansible
|
||||
```
|
||||
|
||||
**Blades**:
|
||||
|
||||
- Debian 12 bookworm instalado
|
||||
- Acceso SSH desde el nodo de control, con sudo o como root
|
||||
- Salida a internet, o un mirror local de `download.docker.com` y `deb.debian.org`
|
||||
- Kernel con `binder_linux.ko` (el estándar de Debian lo trae)
|
||||
|
||||
**Ejecuta siempre desde este directorio** (`ansible/`) — `ansible.cfg` y las rutas de roles son
|
||||
relativas a él.
|
||||
|
||||
---
|
||||
|
||||
## Puesta en marcha
|
||||
|
||||
### 1. Inventario
|
||||
|
||||
```bash
|
||||
cp inventory/hosts.yml.example inventory/hosts.yml
|
||||
$EDITOR inventory/hosts.yml # pon las IPs reales de los blades
|
||||
```
|
||||
|
||||
### 2. Preflight — esto va primero, siempre
|
||||
|
||||
```bash
|
||||
ansible-playbook playbooks/preflight.yml
|
||||
```
|
||||
|
||||
**Es de solo lectura, no toca nada.** Contesta lo que ahora mismo no sabemos:
|
||||
|
||||
- **Qué blade hay dentro del chasis** (lo lee de DMI — no hace falta ir a mirar el frontal)
|
||||
- Si esas CPUs tienen las instrucciones que Android x86_64 necesita
|
||||
- Si el kernel trae binder
|
||||
- Cuántas instancias caben en cada blade según su RAM real
|
||||
|
||||
Deja un informe en `reports/preflight-<fecha>.md` con la tabla de toda la flota y el total de
|
||||
capacidad. Si algún blade sale **NO APTO**, lo dice ahí y no cuando estés a medio despliegue.
|
||||
|
||||
### 3. Montar la granja
|
||||
|
||||
```bash
|
||||
ansible-playbook site.yml
|
||||
```
|
||||
|
||||
Idempotente: relanzable sin romper nada.
|
||||
|
||||
### 4. Comprobar
|
||||
|
||||
```bash
|
||||
ansible-playbook playbooks/status.yml
|
||||
```
|
||||
|
||||
### 5. Instalar un APK en toda la flota
|
||||
|
||||
```bash
|
||||
ansible-playbook playbooks/apk.yml -e apk_local_path=~/COFRE/CODERS/OASIS_APK/oasis.apk
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Control diario desde el coordinador
|
||||
|
||||
Entra por SSH al coordinador y usa `fleet`:
|
||||
|
||||
```bash
|
||||
fleet connect # conecta adb a toda la flota
|
||||
fleet list # qué hay conectado
|
||||
fleet install app.apk # instala en todos
|
||||
fleet shell 'getprop ro.build.version.release'
|
||||
fleet ip # IP de cada Android — útil para el caso de dos APKs
|
||||
fleet reboot
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Los dos modos de red
|
||||
|
||||
Se controla con `redroid_network_mode` en `group_vars/all.yml`.
|
||||
|
||||
### `portmap` (por defecto)
|
||||
|
||||
Cada Android detrás de NAT de Docker; adb en `<ip_blade>:<puerto>`, empezando en 5555.
|
||||
Funciona en cualquier sitio sin saber nada de la LAN. Es el modo con el que empezar.
|
||||
|
||||
### `macvlan` — el que quieres para `oasis_mobile`
|
||||
|
||||
Cada Android coge una **IP real de la LAN** a través del switch FlexIO del chasis. Los dos APKs
|
||||
que se tienen que ver entre sí se ven como **pares de red de verdad**, no a través de traducciones.
|
||||
Es lo más cerca de "dos móviles en la misma wifi" sin tener dos móviles.
|
||||
|
||||
```yaml
|
||||
redroid_network_mode: macvlan
|
||||
redroid_macvlan_parent: eno1
|
||||
redroid_macvlan_subnet: 10.0.0.0/24
|
||||
redroid_macvlan_gateway: 10.0.0.1
|
||||
redroid_macvlan_ip_prefix: "10.0.0."
|
||||
```
|
||||
|
||||
Y en el inventario, un `redroid_macvlan_ip_offset` por blade (el ejemplo ya trae 20, 30, 40…).
|
||||
|
||||
> **El gotcha de macvlan.** Un anfitrión **no puede hablar con sus propios contenedores macvlan**.
|
||||
> Es una limitación del driver, no un fallo de configuración. Por eso el coordinador es un blade
|
||||
> **distinto**: desde fuera sí se llega. Si algún día pones adb en un nodo de carga y no ve sus
|
||||
> propios Android, es esto.
|
||||
>
|
||||
> Reserva también un rango de IPs para los Android en el DHCP de la LAN, o acabarás con colisiones.
|
||||
|
||||
---
|
||||
|
||||
## El binder, que es donde esto se atasca
|
||||
|
||||
Debian 12 compila binder como módulo pero **sin binderfs**, así que redroid no puede crear sus
|
||||
devices solo. El rol `binder` lo resuelve: carga `binder_linux` con los tres nombres que Android
|
||||
espera (`binder`, `hwbinder`, `vndbinder`) y lo deja persistente para los siguientes arranques.
|
||||
|
||||
Los contenedores de un blade **comparten ese binder del kernel anfitrión**. Si no está, no arranca
|
||||
ni una instancia.
|
||||
|
||||
**Si el playbook falla con "faltan devices binder"**: el módulo estaba ya cargado y en uso, así que
|
||||
no se pudo recargar con los parámetros nuevos. La configuración persistente ya quedó escrita —
|
||||
reinicia ese blade y relanza. Es un fallo de una sola vez.
|
||||
|
||||
---
|
||||
|
||||
## Ajustes que vas a querer tocar
|
||||
|
||||
Todo en `group_vars/all.yml`.
|
||||
|
||||
| Variable | Por defecto | Para qué |
|
||||
|---|---|---|
|
||||
| `redroid_image` | `redroid/redroid:13.0.0-latest` | Versión de Android. Hay 11, 12, 13 y 14 |
|
||||
| `redroid_instances` | `0` (auto) | Instancias por blade. 0 = calculado desde la RAM real |
|
||||
| `redroid_mem_mb` | `2048` | RAM por Android |
|
||||
| `redroid_host_reserve_mb` | `1500` | RAM que se le deja a Debian + Docker |
|
||||
| `redroid_gpu_mode` | `guest` | Sin GPU en los blades → render por software |
|
||||
| `redroid_network_mode` | `portmap` | `portmap` o `macvlan` |
|
||||
|
||||
Cambiar la versión de Android en toda la flota, sin editar nada:
|
||||
|
||||
```bash
|
||||
ansible-playbook playbooks/devices.yml -e redroid_image=redroid/redroid:14.0.0-latest
|
||||
```
|
||||
|
||||
### La palanca real: la RAM
|
||||
|
||||
El cuello de botella son **los 8 GB por blade, no la CPU**. Si los blades resultan ser M620
|
||||
(DDR3 ECC), ampliar a 32 GB cuesta poco en segunda mano:
|
||||
|
||||
```bash
|
||||
ansible-playbook playbooks/devices.yml -e redroid_instances=14
|
||||
```
|
||||
|
||||
De ~45 Android a **~210**, por menos de lo que cuesta un mes de luz del chasis.
|
||||
|
||||
---
|
||||
|
||||
## Playbooks
|
||||
|
||||
| Playbook | Qué hace | Toca algo |
|
||||
|---|---|---|
|
||||
| `playbooks/preflight.yml` | Reconocimiento: modelo, CPU, RAM, binder, capacidad | **No** |
|
||||
| `site.yml` | La granja entera de cero | Sí |
|
||||
| `playbooks/provision.yml` | Solo el hierro: base + binder + Docker | Sí |
|
||||
| `playbooks/devices.yml` | Solo los contenedores Android | Sí |
|
||||
| `playbooks/coordinator.yml` | Regenera el listado de flota y `fleet` | Sí |
|
||||
| `playbooks/apk.yml` | Instala un APK en toda la flota | Sí |
|
||||
| `playbooks/status.yml` | Estado real de la granja | **No** |
|
||||
| `playbooks/destroy.yml` | Para y borra los contenedores | **Sí, destructivo** |
|
||||
|
||||
`destroy.yml` exige `-e confirmar=si`, y conserva los `/data` salvo que añadas `-e borrar_datos=si`.
|
||||
|
||||
---
|
||||
|
||||
## Probarlo antes de tocar el chasis
|
||||
|
||||
`inventory/hosts.yml.example` trae un bloque comentado de **piloto de un nodo**: el sobremesa hace
|
||||
de coordinador y de nodo de carga a la vez. Descoméntalo, comenta el bloque `granja`, y tienes el
|
||||
flujo entero contra una sola máquina. Con 62 GB de RAM salen unas 8 instancias (tope
|
||||
`redroid_max_instances`).
|
||||
|
||||
Es la forma sensata de empezar: valida todo el pipeline sin encender 2 kW.
|
||||
|
||||
---
|
||||
|
||||
## Estructura
|
||||
|
||||
```
|
||||
ansible/
|
||||
├── ansible.cfg
|
||||
├── site.yml playbook maestro
|
||||
├── inventory/hosts.yml.example 16 blades + piloto de un nodo
|
||||
├── group_vars/all.yml los mandos del operador
|
||||
├── playbooks/
|
||||
│ ├── preflight.yml reconocimiento (solo lectura)
|
||||
│ ├── provision.yml base + binder + docker
|
||||
│ ├── devices.yml contenedores Android
|
||||
│ ├── coordinator.yml listado de flota + fleet
|
||||
│ ├── apk.yml instalar APK en la flota
|
||||
│ ├── status.yml estado (solo lectura)
|
||||
│ ├── destroy.yml desmontar (destructivo)
|
||||
│ └── templates/preflight-report.md.j2
|
||||
├── roles/
|
||||
│ ├── preflight/ DMI, flags de CPU, binder, capacidad
|
||||
│ ├── base/ paquetes, hora
|
||||
│ ├── binder/ los tres devices, persistentes
|
||||
│ ├── docker/ Docker CE + compose v2
|
||||
│ ├── redroid/ genera el compose y levanta Android
|
||||
│ └── coordinator/ adb + devices.txt + fleet.sh
|
||||
└── reports/ informes de preflight (se genera solo)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Estado de verificación
|
||||
|
||||
Escrito el 2026-08-08. **No se ha ejecutado contra hardware real** — el chasis todavía no está
|
||||
accesible y el sobremesa no tiene Docker instalado.
|
||||
|
||||
Lo que sí está comprobado:
|
||||
|
||||
- Los 18 ficheros YAML parsean correctamente
|
||||
- La plantilla de `docker-compose` renderiza YAML válido **en los dos modos de red**, con los
|
||||
puertos y las IPs bien asignados
|
||||
- `fleet.sh` pasa `bash -n` una vez renderizado, sin Jinja pendiente
|
||||
|
||||
Lo que **no** está comprobado y solo dirá la realidad:
|
||||
|
||||
- Que las imágenes x86_64 de Android arranquen en la generación de CPU de estos blades
|
||||
(el riesgo real si resultan ser M610 — preflight lo detecta antes de gastar tiempo)
|
||||
- El comportamiento de macvlan sobre los switches FlexIO concretos del chasis
|
||||
- Que 3 instancias por blade sean cómodas con 8 GB en carga real, no solo en el papel
|
||||
|
||||
---
|
||||
|
||||
## Siguiente capa: GADS
|
||||
|
||||
[GADS](https://github.com/shamanec/GADS) da UI web y Appium integrado encima de esto. No está
|
||||
automatizado aquí a propósito: montar un despliegue de Node que no puedo probar sería añadir
|
||||
complejidad sin verificar. Merece la pena cuando la flota pase de una decena de dispositivos o
|
||||
cuando haga falta lanzar tests automatizados en vez de trastear con `fleet shell`.
|
||||
|
||||
---
|
||||
|
||||
## Seguridad — lee esto antes de encender
|
||||
|
||||
El repaso completo está en **[../../docs/seguridad.md](../../docs/seguridad.md)** (11 hallazgos).
|
||||
Lo que no se puede saltar:
|
||||
|
||||
**adb no tiene autenticación**, las imágenes de redroid permiten `adb root`, y los contenedores son
|
||||
`privileged` porque redroid lo exige. Encadenado: **quien alcance el puerto 5555 de un blade acaba
|
||||
siendo root en ese blade**, sin exploit ninguno. Con 45 dispositivos son 45 puertas.
|
||||
|
||||
Mitigación parcial ya en el código — ata adb solo a la interfaz interna del chasis:
|
||||
|
||||
```yaml
|
||||
blade-02: { ansible_host: 10.0.0.12, redroid_adb_bind_ip: 10.0.0.12 }
|
||||
```
|
||||
|
||||
Lo que lo cierra de verdad es una **VLAN aislada** para la granja. En modo `macvlan`,
|
||||
`redroid_adb_bind_ip` no aplica (adb escucha en la IP de LAN del propio Android) y la VLAN es la
|
||||
única defensa.
|
||||
|
||||
Y antes de nada: **cambiar las credenciales de fábrica del CMC y de los 16 iDRAC** (`root`/`calvin`).
|
||||
18
02_device_lab/ansible/ansible.cfg
Normal file
18
02_device_lab/ansible/ansible.cfg
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
[defaults]
|
||||
inventory = inventory/hosts.yml
|
||||
roles_path = roles
|
||||
host_key_checking = False
|
||||
retry_files_enabled = False
|
||||
stdout_callback = yaml
|
||||
interpreter_python = auto_silent
|
||||
# 20 > 16 blades: toda la flota en paralelo
|
||||
forks = 20
|
||||
timeout = 30
|
||||
|
||||
[ssh_connection]
|
||||
pipelining = True
|
||||
ssh_args = -o ControlMaster=auto -o ControlPersist=120s
|
||||
|
||||
[privilege_escalation]
|
||||
become = True
|
||||
become_method = sudo
|
||||
80
02_device_lab/ansible/group_vars/all.yml
Normal file
80
02_device_lab/ansible/group_vars/all.yml
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
# ---------------------------------------------------------------------------
|
||||
# Mandos del operador. Lo que se toca normalmente está aquí.
|
||||
# Los defaults internos de cada rol están en roles/<rol>/defaults/main.yml
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# --- Android ---------------------------------------------------------------
|
||||
|
||||
# Imagen de redroid. Tags: 11.0.0-latest, 12.0.0-latest, 13.0.0-latest, 14.0.0-latest
|
||||
# Android 13 es el punto dulce: memfd en vez de ashmem (un módulo de kernel menos)
|
||||
# y todavía ligero.
|
||||
redroid_image: "redroid/redroid:13.0.0-latest"
|
||||
|
||||
# RAM por instancia. 2 GB es el mínimo cómodo para Android 13.
|
||||
redroid_mem_mb: 2048
|
||||
|
||||
# RAM que se le deja al anfitrión (Debian + Docker + margen).
|
||||
redroid_host_reserve_mb: 1500
|
||||
|
||||
# Instancias por nodo. 0 = automático a partir de la RAM real del blade:
|
||||
# (RAM_total - reserva) / redroid_mem_mb
|
||||
# Con 8 GB por blade eso da 3. Súbelo a mano solo si sabes lo que haces.
|
||||
redroid_instances: 0
|
||||
|
||||
# Tope duro por si un blade tiene mucha RAM y te comes los cores.
|
||||
redroid_max_instances: 8
|
||||
|
||||
# Pantalla de las instancias.
|
||||
redroid_width: 720
|
||||
redroid_height: 1280
|
||||
redroid_dpi: 320
|
||||
|
||||
# Sin GPU en los blades -> render por software. Con GPU pasable: "host".
|
||||
redroid_gpu_mode: guest
|
||||
|
||||
|
||||
# --- Red -------------------------------------------------------------------
|
||||
|
||||
# portmap -> cada Android detrás de NAT, adb en <ip_blade>:<puerto>
|
||||
# Funciona en cualquier sitio. Es el default sensato.
|
||||
#
|
||||
# macvlan -> cada Android coge una IP REAL de la LAN a través del switch
|
||||
# FlexIO del chasis. Los APKs se ven como pares de red de verdad.
|
||||
# Es lo que quieres para el caso de los dos APKs de oasis_mobile.
|
||||
# Ojo al gotcha documentado en el README.
|
||||
redroid_network_mode: portmap
|
||||
|
||||
# --- solo si redroid_network_mode == macvlan ---
|
||||
redroid_macvlan_network: redroid-lan
|
||||
redroid_macvlan_parent: "" # p.ej. eno1 (interfaz física del blade)
|
||||
redroid_macvlan_subnet: "" # p.ej. 10.0.0.0/24
|
||||
redroid_macvlan_gateway: "" # p.ej. 10.0.0.1
|
||||
redroid_macvlan_ip_prefix: "" # p.ej. "10.0.0." (el offset va por host en el inventario)
|
||||
|
||||
# --- solo si redroid_network_mode == portmap ---
|
||||
# Primer puerto adb en el blade. Las instancias ocupan base, base+1, base+2...
|
||||
redroid_adb_base_port: 5555
|
||||
|
||||
# A qué interfaz se ata adb. 0.0.0.0 = TODAS, y adb no tiene autenticación:
|
||||
# quien llegue al puerto es root en el Android, y desde un contenedor privilegiado
|
||||
# eso es una vía hacia el anfitrión. Ver docs/seguridad.md (hallazgo S1).
|
||||
#
|
||||
# Pon aquí la IP interna del blade en la red del chasis para que adb no escuche
|
||||
# en nada más. Por host, en el inventario:
|
||||
# blade-02: { ansible_host: 10.0.0.12, redroid_adb_bind_ip: 10.0.0.12 }
|
||||
redroid_adb_bind_ip: "0.0.0.0"
|
||||
|
||||
|
||||
# --- Rutas -----------------------------------------------------------------
|
||||
|
||||
redroid_base_dir: /opt/redroid
|
||||
|
||||
|
||||
# --- Preflight -------------------------------------------------------------
|
||||
|
||||
# Flags de CPU que Android x86_64 necesita. Si un blade no las tiene,
|
||||
# preflight lo marca como NO APTO en vez de dejarte descubrirlo a medio despliegue.
|
||||
preflight_required_cpu_flags:
|
||||
- sse4_2
|
||||
- ssse3
|
||||
- popcnt
|
||||
70
02_device_lab/ansible/inventory/hosts.yml.example
Normal file
70
02_device_lab/ansible/inventory/hosts.yml.example
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
# Inventario de la granja. Copia esto a hosts.yml y ajusta IPs.
|
||||
#
|
||||
# cp inventory/hosts.yml.example inventory/hosts.yml
|
||||
#
|
||||
# Reparto: 1 blade coordinador (adb + control) + 15 blades de carga.
|
||||
# El coordinador NO corre instancias Android, solo dirige.
|
||||
|
||||
all:
|
||||
children:
|
||||
granja:
|
||||
vars:
|
||||
ansible_user: root
|
||||
# Si entras con usuario normal + sudo, cambia a:
|
||||
# ansible_user: sito
|
||||
# ansible_become_password: "{{ vault_sudo_password }}"
|
||||
|
||||
children:
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# Coordinador: adb, scripts de flota, punto único de control
|
||||
# ---------------------------------------------------------------
|
||||
coordinator:
|
||||
hosts:
|
||||
blade-01:
|
||||
ansible_host: 10.0.0.11
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# Nodos de carga: aquí corren los contenedores redroid
|
||||
# ---------------------------------------------------------------
|
||||
redroid_nodes:
|
||||
hosts:
|
||||
blade-02: { ansible_host: 10.0.0.12, redroid_macvlan_ip_offset: 20 }
|
||||
blade-03: { ansible_host: 10.0.0.13, redroid_macvlan_ip_offset: 30 }
|
||||
blade-04: { ansible_host: 10.0.0.14, redroid_macvlan_ip_offset: 40 }
|
||||
blade-05: { ansible_host: 10.0.0.15, redroid_macvlan_ip_offset: 50 }
|
||||
blade-06: { ansible_host: 10.0.0.16, redroid_macvlan_ip_offset: 60 }
|
||||
blade-07: { ansible_host: 10.0.0.17, redroid_macvlan_ip_offset: 70 }
|
||||
blade-08: { ansible_host: 10.0.0.18, redroid_macvlan_ip_offset: 80 }
|
||||
blade-09: { ansible_host: 10.0.0.19, redroid_macvlan_ip_offset: 90 }
|
||||
blade-10: { ansible_host: 10.0.0.20, redroid_macvlan_ip_offset: 100 }
|
||||
blade-11: { ansible_host: 10.0.0.21, redroid_macvlan_ip_offset: 110 }
|
||||
blade-12: { ansible_host: 10.0.0.22, redroid_macvlan_ip_offset: 120 }
|
||||
blade-13: { ansible_host: 10.0.0.23, redroid_macvlan_ip_offset: 130 }
|
||||
blade-14: { ansible_host: 10.0.0.24, redroid_macvlan_ip_offset: 140 }
|
||||
blade-15: { ansible_host: 10.0.0.25, redroid_macvlan_ip_offset: 150 }
|
||||
blade-16: { ansible_host: 10.0.0.26, redroid_macvlan_ip_offset: 160 }
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# PILOTO DE UN NODO
|
||||
# -------------------------------------------------------------------------
|
||||
# Para probar todo esto en el sobremesa antes de tocar el chasis, comenta
|
||||
# el bloque `granja` de arriba y descomenta este. El sobremesa hace de
|
||||
# coordinador y de nodo de carga a la vez.
|
||||
#
|
||||
# ansible-playbook playbooks/preflight.yml
|
||||
# ansible-playbook site.yml
|
||||
#
|
||||
# all:
|
||||
# children:
|
||||
# granja:
|
||||
# children:
|
||||
# coordinator:
|
||||
# hosts:
|
||||
# localhost:
|
||||
# ansible_connection: local
|
||||
# redroid_nodes:
|
||||
# hosts:
|
||||
# localhost:
|
||||
# ansible_connection: local
|
||||
# redroid_macvlan_ip_offset: 20
|
||||
60
02_device_lab/ansible/playbooks/apk.yml
Normal file
60
02_device_lab/ansible/playbooks/apk.yml
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
---
|
||||
# Instala un APK en toda la flota, desde el coordinador.
|
||||
#
|
||||
# ansible-playbook playbooks/apk.yml -e apk_local_path=~/COFRE/CODERS/OASIS_APK/oasis.apk
|
||||
#
|
||||
# Se ejecuta desde el coordinador a propósito: en modo macvlan un blade NO puede
|
||||
# hablar con sus propios contenedores, así que instalar desde cada nodo fallaría.
|
||||
# Desde el coordinador funcionan los dos modos de red.
|
||||
|
||||
- name: Instalar APK en la flota
|
||||
hosts: coordinator
|
||||
gather_facts: false
|
||||
vars:
|
||||
apk_local_path: ""
|
||||
tasks:
|
||||
|
||||
- name: Exigir la ruta del APK
|
||||
ansible.builtin.assert:
|
||||
that: apk_local_path | length > 0
|
||||
fail_msg: "Falta -e apk_local_path=/ruta/al/app.apk"
|
||||
|
||||
- name: Comprobar que el APK existe en el nodo de control
|
||||
ansible.builtin.stat:
|
||||
path: "{{ apk_local_path }}"
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
register: apk_local
|
||||
|
||||
- name: Abortar si no está
|
||||
ansible.builtin.assert:
|
||||
that: apk_local.stat.exists
|
||||
fail_msg: "No existe: {{ apk_local_path }}"
|
||||
|
||||
- name: Copiar el APK al coordinador
|
||||
ansible.builtin.copy:
|
||||
src: "{{ apk_local_path }}"
|
||||
dest: "{{ coordinator_dir }}/apks/{{ apk_local_path | basename }}"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
|
||||
- name: Conectar adb a la flota
|
||||
ansible.builtin.command:
|
||||
cmd: "{{ coordinator_dir }}/fleet.sh connect"
|
||||
register: apk_connect
|
||||
changed_when: false
|
||||
|
||||
- name: Resultado de la conexión
|
||||
ansible.builtin.debug:
|
||||
var: apk_connect.stdout_lines
|
||||
|
||||
- name: Instalar el APK en todos los dispositivos
|
||||
ansible.builtin.command:
|
||||
cmd: "{{ coordinator_dir }}/fleet.sh install {{ coordinator_dir }}/apks/{{ apk_local_path | basename }}"
|
||||
register: apk_install
|
||||
changed_when: true
|
||||
|
||||
- name: Resultado de la instalación
|
||||
ansible.builtin.debug:
|
||||
var: apk_install.stdout_lines
|
||||
19
02_device_lab/ansible/playbooks/coordinator.yml
Normal file
19
02_device_lab/ansible/playbooks/coordinator.yml
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
---
|
||||
# Regenera solo el coordinador (listado de flota + script fleet).
|
||||
#
|
||||
# ansible-playbook playbooks/coordinator.yml
|
||||
#
|
||||
# La primera play recoge facts de los nodos de carga sin tocarlos: el listado
|
||||
# de dispositivos se calcula a partir de su RAM real.
|
||||
|
||||
- name: Recoger facts de los nodos de carga
|
||||
hosts: redroid_nodes
|
||||
gather_facts: true
|
||||
become: false
|
||||
tasks: []
|
||||
|
||||
- name: Configurar el coordinador
|
||||
hosts: coordinator
|
||||
gather_facts: true
|
||||
roles:
|
||||
- role: coordinator
|
||||
48
02_device_lab/ansible/playbooks/destroy.yml
Normal file
48
02_device_lab/ansible/playbooks/destroy.yml
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
---
|
||||
# DESTRUCTIVO. Para y borra los contenedores Android.
|
||||
#
|
||||
# ansible-playbook playbooks/destroy.yml -e confirmar=si
|
||||
#
|
||||
# Por defecto conserva los volúmenes /data (el estado de cada Android).
|
||||
# Para borrarlos también — esto no tiene vuelta atrás:
|
||||
#
|
||||
# ansible-playbook playbooks/destroy.yml -e confirmar=si -e borrar_datos=si
|
||||
|
||||
- name: Desmontar la flota Android
|
||||
hosts: redroid_nodes
|
||||
gather_facts: false
|
||||
vars:
|
||||
confirmar: "no"
|
||||
borrar_datos: "no"
|
||||
tasks:
|
||||
|
||||
- name: Exigir confirmación explícita
|
||||
ansible.builtin.assert:
|
||||
that: confirmar == "si"
|
||||
fail_msg: >-
|
||||
Esto para y borra los contenedores Android de {{ inventory_hostname }}.
|
||||
Si es lo que quieres, relanza con -e confirmar=si
|
||||
|
||||
- name: Comprobar que hay un compose desplegado
|
||||
ansible.builtin.stat:
|
||||
path: "{{ redroid_base_dir }}/docker-compose.yml"
|
||||
register: destroy_compose
|
||||
|
||||
- name: Parar y eliminar los contenedores
|
||||
ansible.builtin.command:
|
||||
cmd: docker compose down
|
||||
chdir: "{{ redroid_base_dir }}"
|
||||
when: destroy_compose.stat.exists
|
||||
changed_when: true
|
||||
|
||||
- name: Borrar los volúmenes /data
|
||||
ansible.builtin.file:
|
||||
path: "{{ redroid_base_dir }}/data"
|
||||
state: absent
|
||||
when: borrar_datos == "si"
|
||||
|
||||
- name: Qué ha quedado
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
{{ inventory_hostname }}: contenedores eliminados.
|
||||
Datos {{ 'BORRADOS' if borrar_datos == 'si' else 'conservados en ' ~ redroid_base_dir ~ '/data' }}.
|
||||
23
02_device_lab/ansible/playbooks/devices.yml
Normal file
23
02_device_lab/ansible/playbooks/devices.yml
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
# Despliega o redespliega solo los contenedores Android.
|
||||
# Da por hecho que provision.yml ya pasó.
|
||||
#
|
||||
# ansible-playbook playbooks/devices.yml
|
||||
#
|
||||
# Cambiar la versión de Android en toda la flota:
|
||||
# ansible-playbook playbooks/devices.yml -e redroid_image=redroid/redroid:14.0.0-latest
|
||||
#
|
||||
# Subir instancias por nodo (si has ampliado la RAM):
|
||||
# ansible-playbook playbooks/devices.yml -e redroid_instances=6
|
||||
|
||||
- name: Contenedores Android
|
||||
hosts: redroid_nodes
|
||||
gather_facts: true
|
||||
roles:
|
||||
- role: redroid
|
||||
|
||||
- name: Regenerar el listado de la flota en el coordinador
|
||||
hosts: coordinator
|
||||
gather_facts: true
|
||||
roles:
|
||||
- role: coordinator
|
||||
37
02_device_lab/ansible/playbooks/preflight.yml
Normal file
37
02_device_lab/ansible/playbooks/preflight.yml
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
---
|
||||
# SOLO LECTURA. No cambia nada en los blades.
|
||||
#
|
||||
# ansible-playbook playbooks/preflight.yml
|
||||
#
|
||||
# Contesta lo que ahora mismo no sabemos: qué modelo de blade hay dentro del
|
||||
# chasis, si esas CPUs pueden con Android x86_64, y cuántas instancias salen.
|
||||
# Lánzalo ANTES de site.yml.
|
||||
|
||||
- name: Reconocimiento de la granja
|
||||
hosts: granja
|
||||
gather_facts: true
|
||||
become: true
|
||||
roles:
|
||||
- role: preflight
|
||||
|
||||
- name: Escribir informe en el nodo de control
|
||||
hosts: localhost
|
||||
connection: local
|
||||
gather_facts: true
|
||||
become: false
|
||||
tasks:
|
||||
- name: Crear directorio de informes
|
||||
ansible.builtin.file:
|
||||
path: "{{ playbook_dir }}/../reports"
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Generar informe
|
||||
ansible.builtin.template:
|
||||
src: preflight-report.md.j2
|
||||
dest: "{{ playbook_dir }}/../reports/preflight-{{ ansible_date_time.date }}.md"
|
||||
mode: "0644"
|
||||
|
||||
- name: Dónde ha quedado
|
||||
ansible.builtin.debug:
|
||||
msg: "Informe en ansible/reports/preflight-{{ ansible_date_time.date }}.md"
|
||||
18
02_device_lab/ansible/playbooks/provision.yml
Normal file
18
02_device_lab/ansible/playbooks/provision.yml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
# Deja los blades listos, pero sin levantar Android todavía.
|
||||
# Útil para separar "preparar el hierro" de "desplegar la flota".
|
||||
#
|
||||
# ansible-playbook playbooks/provision.yml
|
||||
|
||||
- name: Base del sistema
|
||||
hosts: granja
|
||||
gather_facts: true
|
||||
roles:
|
||||
- role: base
|
||||
|
||||
- name: Binder y Docker en los nodos de carga
|
||||
hosts: redroid_nodes
|
||||
gather_facts: true
|
||||
roles:
|
||||
- role: binder
|
||||
- role: docker
|
||||
53
02_device_lab/ansible/playbooks/status.yml
Normal file
53
02_device_lab/ansible/playbooks/status.yml
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
---
|
||||
# Estado real de la granja. Solo lectura.
|
||||
#
|
||||
# ansible-playbook playbooks/status.yml
|
||||
|
||||
- name: Estado de los nodos de carga
|
||||
hosts: redroid_nodes
|
||||
gather_facts: true
|
||||
tasks:
|
||||
|
||||
- name: Contenedores en marcha
|
||||
ansible.builtin.command:
|
||||
cmd: docker ps --filter "name=redroid-" --format '{% raw %}{{.Names}}\t{{.Status}}{% endraw %}'
|
||||
register: status_ps
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Devices binder presentes
|
||||
ansible.builtin.stat:
|
||||
path: "/dev/{{ item }}"
|
||||
loop: [binder, hwbinder, vndbinder]
|
||||
register: status_binder
|
||||
|
||||
- name: Memoria libre
|
||||
ansible.builtin.command:
|
||||
cmd: free -m
|
||||
register: status_mem
|
||||
changed_when: false
|
||||
|
||||
- name: Resumen del nodo
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- "--- {{ inventory_hostname }} ---"
|
||||
- "binder : {{ 'ok' if (status_binder.results | rejectattr('stat.exists') | list | length == 0) else 'FALTAN devices' }}"
|
||||
- "ram libre : {{ status_mem.stdout_lines[1].split()[6] | default('?') }} MB disponibles de {{ ansible_memtotal_mb }} MB"
|
||||
- "contenedores:"
|
||||
- "{{ status_ps.stdout_lines | default(['(ninguno)']) }}"
|
||||
|
||||
- name: Estado de la flota adb
|
||||
hosts: coordinator
|
||||
gather_facts: false
|
||||
tasks:
|
||||
|
||||
- name: Conectar y listar
|
||||
ansible.builtin.shell:
|
||||
cmd: "{{ coordinator_dir }}/fleet.sh connect >/dev/null 2>&1; {{ coordinator_dir }}/fleet.sh list"
|
||||
register: status_fleet
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Flota
|
||||
ansible.builtin.debug:
|
||||
var: status_fleet.stdout_lines
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
# Informe de preflight — {{ ansible_date_time.date }}
|
||||
|
||||
Generado por `ansible-playbook playbooks/preflight.yml`. Solo lectura: no se tocó nada.
|
||||
|
||||
## Flota
|
||||
|
||||
| Nodo | Modelo | CPU | vCPU | RAM | Kernel | binder | Flags CPU | Capacidad | Veredicto |
|
||||
|---|---|---|---|---|---|---|---|---|---|
|
||||
{% for h in groups['granja'] | sort %}
|
||||
{% set hv = hostvars[h] %}
|
||||
| {{ h }} | {{ hv.preflight_vendor | default('?') }} {{ hv.preflight_model | default('?') }} | {{ hv.ansible_processor[2] | default('?') }} | {{ hv.ansible_processor_vcpus | default('?') }} | {{ hv.ansible_memtotal_mb | default('?') }} MB | {{ hv.ansible_kernel | default('?') }} | {{ 'sí' if hv.preflight_binder_ko.stat.exists | default(false) else 'NO' }} | {{ 'ok' if (hv.preflight_missing_flags | default([]) | length) == 0 else 'faltan: ' ~ (hv.preflight_missing_flags | join(', ')) }} | {{ hv.preflight_capacity | default('?') }} | {{ 'APTO' if hv.preflight_ok | default(false) else '**NO APTO**' }} |
|
||||
{% endfor %}
|
||||
|
||||
## Totales
|
||||
|
||||
{% set aptos = groups['granja'] | map('extract', hostvars) | selectattr('preflight_ok', 'defined') | selectattr('preflight_ok') | list %}
|
||||
{% set nodos_carga = groups['redroid_nodes'] | default([]) | map('extract', hostvars) | selectattr('preflight_ok', 'defined') | selectattr('preflight_ok') | list %}
|
||||
- Nodos aptos: **{{ aptos | length }}** de {{ groups['granja'] | length }}
|
||||
- Capacidad total en nodos de carga: **{{ nodos_carga | map(attribute='preflight_capacity') | map('int') | sum }} instancias Android**
|
||||
- RAM total de la flota: {{ groups['granja'] | map('extract', hostvars) | selectattr('ansible_memtotal_mb', 'defined') | map(attribute='ansible_memtotal_mb') | map('int') | sum }} MB
|
||||
|
||||
## Cómo leer esto
|
||||
|
||||
- **binder = NO** → ese blade no puede correr redroid con su kernel actual. Ver `roles/binder/tasks/main.yml`.
|
||||
- **Flags CPU faltantes** → CPU demasiado antigua para las imágenes x86_64 de Android. Es el riesgo
|
||||
real si los blades resultan ser M610 (Xeon 2009–2011).
|
||||
- **Capacidad** sale de `(RAM - {{ redroid_host_reserve_mb }} MB de reserva) / {{ redroid_mem_mb }} MB por instancia`.
|
||||
Si el número es bajo, la palanca es ampliar RAM por blade, no añadir blades.
|
||||
38
02_device_lab/ansible/roles/base/tasks/main.yml
Normal file
38
02_device_lab/ansible/roles/base/tasks/main.yml
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
---
|
||||
- name: Actualizar índice de apt
|
||||
ansible.builtin.apt:
|
||||
update_cache: true
|
||||
cache_valid_time: 3600
|
||||
|
||||
- name: Paquetes base
|
||||
ansible.builtin.apt:
|
||||
name:
|
||||
- ca-certificates
|
||||
- curl
|
||||
- gnupg
|
||||
- htop
|
||||
- iproute2
|
||||
- python3
|
||||
- rsync
|
||||
state: present
|
||||
|
||||
- name: Leer zona horaria actual
|
||||
ansible.builtin.command:
|
||||
cmd: timedatectl show -p Timezone --value
|
||||
register: base_tz
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Fijar zona horaria
|
||||
ansible.builtin.command:
|
||||
cmd: "timedatectl set-timezone {{ granja_timezone | default('Europe/Madrid') }}"
|
||||
when: base_tz.stdout | default('') != (granja_timezone | default('Europe/Madrid'))
|
||||
changed_when: true
|
||||
failed_when: false
|
||||
|
||||
- name: Sincronización horaria activa
|
||||
ansible.builtin.systemd:
|
||||
name: systemd-timesyncd
|
||||
enabled: true
|
||||
state: started
|
||||
failed_when: false
|
||||
84
02_device_lab/ansible/roles/binder/tasks/main.yml
Normal file
84
02_device_lab/ansible/roles/binder/tasks/main.yml
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
---
|
||||
# El punto donde esto se atasca en Debian.
|
||||
#
|
||||
# Debian 12 compila binder como módulo (CONFIG_ANDROID_BINDER_IPC=m) pero SIN
|
||||
# binderfs (CONFIG_ANDROID_BINDERFS no está activado). Sin binderfs, redroid no
|
||||
# puede crear los devices por sí mismo: hay que cargarlos a mano y con los TRES
|
||||
# nombres que Android espera — binder, hwbinder, vndbinder.
|
||||
#
|
||||
# Los contenedores comparten este binder del kernel anfitrión. Si no está, no
|
||||
# arranca ni una instancia.
|
||||
|
||||
- name: Comprobar que el kernel trae binder_linux
|
||||
ansible.builtin.stat:
|
||||
path: "/lib/modules/{{ ansible_kernel }}/kernel/drivers/android/binder_linux.ko"
|
||||
register: binder_ko
|
||||
|
||||
- name: Abortar si no hay módulo binder
|
||||
ansible.builtin.assert:
|
||||
that: binder_ko.stat.exists
|
||||
fail_msg: >-
|
||||
Este kernel ({{ ansible_kernel }}) no trae binder_linux.ko.
|
||||
Sin binder no hay redroid. Salidas: instalar el kernel estándar de Debian,
|
||||
o el módulo DKMS de Waydroid (binder_linux-dkms), que además aporta binderfs.
|
||||
|
||||
- name: Cargar binder_linux en cada arranque
|
||||
ansible.builtin.copy:
|
||||
content: "binder_linux\n"
|
||||
dest: /etc/modules-load.d/redroid.conf
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
|
||||
- name: Fijar los tres devices como parámetro del módulo
|
||||
ansible.builtin.copy:
|
||||
content: "options binder_linux devices=binder,hwbinder,vndbinder\n"
|
||||
dest: /etc/modprobe.d/redroid.conf
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
|
||||
- name: Ver qué devices binder existen ya
|
||||
ansible.builtin.stat:
|
||||
path: "/dev/{{ item }}"
|
||||
loop: [binder, hwbinder, vndbinder]
|
||||
register: binder_devs_before
|
||||
|
||||
- name: ¿Falta alguno?
|
||||
ansible.builtin.set_fact:
|
||||
binder_needs_load: "{{ binder_devs_before.results | rejectattr('stat.exists') | list | length > 0 }}"
|
||||
|
||||
# Si el módulo ya está cargado pero con los parámetros equivocados, modprobe no
|
||||
# lo recarga solo. Hay que descargarlo primero.
|
||||
- name: Descargar binder_linux si está cargado mal
|
||||
ansible.builtin.command:
|
||||
cmd: modprobe -r binder_linux
|
||||
when: binder_needs_load
|
||||
register: binder_rmmod
|
||||
failed_when: false
|
||||
changed_when: binder_rmmod.rc == 0
|
||||
|
||||
- name: Cargar binder_linux con los tres devices
|
||||
ansible.builtin.command:
|
||||
cmd: modprobe binder_linux devices=binder,hwbinder,vndbinder
|
||||
when: binder_needs_load
|
||||
register: binder_modprobe
|
||||
failed_when: false
|
||||
changed_when: binder_modprobe.rc == 0
|
||||
|
||||
- name: Verificar los devices binder
|
||||
ansible.builtin.stat:
|
||||
path: "/dev/{{ item }}"
|
||||
loop: [binder, hwbinder, vndbinder]
|
||||
register: binder_devs_after
|
||||
|
||||
- name: Confirmar que los tres devices están
|
||||
ansible.builtin.assert:
|
||||
that: binder_devs_after.results | rejectattr('stat.exists') | list | length == 0
|
||||
fail_msg: >-
|
||||
Faltan devices binder tras el modprobe:
|
||||
{{ binder_devs_after.results | rejectattr('stat.exists') | map(attribute='item') | join(', ') }}.
|
||||
Causa habitual: el módulo estaba cargado y en uso, así que no se pudo recargar
|
||||
con los parámetros nuevos. La configuración persistente YA está escrita, así que
|
||||
un reinicio del blade lo arregla. Reinicia y relanza este playbook.
|
||||
success_msg: "binder, hwbinder y vndbinder presentes."
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
---
|
||||
coordinator_dir: /opt/granja
|
||||
61
02_device_lab/ansible/roles/coordinator/tasks/main.yml
Normal file
61
02_device_lab/ansible/roles/coordinator/tasks/main.yml
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
---
|
||||
# El coordinador es el único punto desde el que se habla con toda la flota.
|
||||
# No corre instancias Android: corre adb y los scripts.
|
||||
#
|
||||
# Importante en modo macvlan: un anfitrión NO puede hablar con sus propios
|
||||
# contenedores macvlan. Por eso adb vive aquí, en un blade distinto — desde
|
||||
# fuera sí se llega. Ver el README.
|
||||
|
||||
- name: Instalar herramientas de control
|
||||
ansible.builtin.apt:
|
||||
name:
|
||||
- adb
|
||||
- jq
|
||||
state: present
|
||||
|
||||
- name: Crear directorios del coordinador
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0755"
|
||||
loop:
|
||||
- "{{ coordinator_dir }}"
|
||||
- "{{ coordinator_dir }}/apks"
|
||||
|
||||
- name: Escribir el listado de dispositivos de la flota
|
||||
ansible.builtin.template:
|
||||
src: devices.txt.j2
|
||||
dest: "{{ coordinator_dir }}/devices.txt"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
|
||||
- name: Instalar el script de flota
|
||||
ansible.builtin.template:
|
||||
src: fleet.sh.j2
|
||||
dest: "{{ coordinator_dir }}/fleet.sh"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0755"
|
||||
|
||||
- name: Enlazar fleet en el PATH
|
||||
ansible.builtin.file:
|
||||
src: "{{ coordinator_dir }}/fleet.sh"
|
||||
dest: /usr/local/bin/fleet
|
||||
state: link
|
||||
|
||||
- name: Contar dispositivos de la flota
|
||||
ansible.builtin.command:
|
||||
cmd: "grep -cv '^#' {{ coordinator_dir }}/devices.txt"
|
||||
register: coordinator_count
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Resumen
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- "Coordinador listo en {{ inventory_hostname }}."
|
||||
- "Flota declarada: {{ coordinator_count.stdout | default('0') | trim }} dispositivos."
|
||||
- "Uso: fleet connect · fleet list · fleet install <apk> · fleet shell '<cmd>'"
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
# GENERADO POR ANSIBLE — no editar a mano.
|
||||
# Una dirección adb por línea: <host>:<puerto>
|
||||
# Regenerar con: ansible-playbook playbooks/coordinator.yml
|
||||
{% for h in groups['redroid_nodes'] | sort %}
|
||||
{% set hv = hostvars[h] %}
|
||||
{% set count = hv.redroid_instance_count | default(
|
||||
[ (((hv.ansible_memtotal_mb | int) - (redroid_host_reserve_mb | int))
|
||||
/ (redroid_mem_mb | int)) | int,
|
||||
redroid_max_instances | int ] | min ) %}
|
||||
# --- {{ h }} ({{ count }} instancias) ---
|
||||
{% for i in range(count | int) %}
|
||||
{% if (hv.redroid_network_mode | default(redroid_network_mode)) == 'macvlan' %}
|
||||
{{ hv.redroid_macvlan_ip_prefix | default(redroid_macvlan_ip_prefix) }}{{ (hv.redroid_macvlan_ip_offset | int) + i }}:5555
|
||||
{% else %}
|
||||
{{ hv.ansible_host | default(h) }}:{{ (hv.redroid_adb_base_port | default(redroid_adb_base_port) | int) + i }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
135
02_device_lab/ansible/roles/coordinator/templates/fleet.sh.j2
Normal file
135
02_device_lab/ansible/roles/coordinator/templates/fleet.sh.j2
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
#!/usr/bin/env bash
|
||||
# GENERADO POR ANSIBLE — no editar a mano.
|
||||
# Fuente: roles/coordinator/templates/fleet.sh.j2
|
||||
#
|
||||
# Control de la flota Android de la granja desde un único sitio.
|
||||
set -euo pipefail
|
||||
|
||||
DEVICES_FILE="{{ coordinator_dir }}/devices.txt"
|
||||
APK_DIR="{{ coordinator_dir }}/apks"
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
fleet — control de la flota Android
|
||||
|
||||
fleet connect conecta adb a toda la flota
|
||||
fleet disconnect desconecta todo
|
||||
fleet list dispositivos conectados y su estado
|
||||
fleet install <apk> instala un APK en toda la flota
|
||||
fleet uninstall <pkg> desinstala un paquete de toda la flota
|
||||
fleet shell '<cmd>' ejecuta un comando de shell en toda la flota
|
||||
fleet ip IP de cada dispositivo (útil para el caso de dos APKs)
|
||||
fleet reboot reinicia toda la flota
|
||||
|
||||
El listado de dispositivos sale de devices.txt, que genera Ansible.
|
||||
USAGE
|
||||
}
|
||||
|
||||
declared_devices() {
|
||||
grep -v '^#' "$DEVICES_FILE" | grep -v '^[[:space:]]*$'
|
||||
}
|
||||
|
||||
connected_devices() {
|
||||
adb devices | awk '/\tdevice$/ {print $1}'
|
||||
}
|
||||
|
||||
require_connected() {
|
||||
local n
|
||||
n=$(connected_devices | wc -l)
|
||||
if [[ "$n" -eq 0 ]]; then
|
||||
echo "No hay dispositivos conectados. Lanza 'fleet connect' primero." >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_connect() {
|
||||
local ok=0 fail=0
|
||||
while read -r dev; do
|
||||
if adb connect "$dev" 2>&1 | grep -qE 'connected to'; then
|
||||
ok=$((ok + 1))
|
||||
else
|
||||
echo " fallo: $dev" >&2
|
||||
fail=$((fail + 1))
|
||||
fi
|
||||
done < <(declared_devices)
|
||||
echo "conectados: $ok · fallidos: $fail"
|
||||
}
|
||||
|
||||
cmd_disconnect() {
|
||||
adb disconnect || true
|
||||
}
|
||||
|
||||
cmd_list() {
|
||||
local declared connected
|
||||
declared=$(declared_devices | wc -l)
|
||||
connected=$(connected_devices | wc -l)
|
||||
echo "declarados: $declared · conectados: $connected"
|
||||
echo
|
||||
adb devices -l
|
||||
}
|
||||
|
||||
cmd_install() {
|
||||
local apk="${1:?uso: fleet install <apk>}"
|
||||
[[ -f "$apk" ]] || { echo "no existe: $apk" >&2; exit 1; }
|
||||
require_connected
|
||||
|
||||
local ok=0 fail=0
|
||||
while read -r dev; do
|
||||
if adb -s "$dev" install -r "$apk" >/dev/null 2>&1; then
|
||||
echo " ok $dev"
|
||||
ok=$((ok + 1))
|
||||
else
|
||||
echo " FALLO $dev" >&2
|
||||
fail=$((fail + 1))
|
||||
fi
|
||||
done < <(connected_devices)
|
||||
echo "instalado en $ok · fallos $fail"
|
||||
[[ "$fail" -eq 0 ]]
|
||||
}
|
||||
|
||||
cmd_uninstall() {
|
||||
local pkg="${1:?uso: fleet uninstall <paquete>}"
|
||||
require_connected
|
||||
while read -r dev; do
|
||||
printf ' %s: ' "$dev"
|
||||
adb -s "$dev" uninstall "$pkg" || true
|
||||
done < <(connected_devices)
|
||||
}
|
||||
|
||||
cmd_shell() {
|
||||
local command="${1:?uso: fleet shell '<comando>'}"
|
||||
require_connected
|
||||
while read -r dev; do
|
||||
echo "== $dev =="
|
||||
adb -s "$dev" shell "$command" || true
|
||||
done < <(connected_devices)
|
||||
}
|
||||
|
||||
cmd_ip() {
|
||||
require_connected
|
||||
while read -r dev; do
|
||||
printf '%-24s %s\n' "$dev" \
|
||||
"$(adb -s "$dev" shell ip -4 -o addr show scope global 2>/dev/null \
|
||||
| awk '{print $4}' | paste -sd, - || echo '?')"
|
||||
done < <(connected_devices)
|
||||
}
|
||||
|
||||
cmd_reboot() {
|
||||
require_connected
|
||||
while read -r dev; do
|
||||
echo " reiniciando $dev"
|
||||
adb -s "$dev" reboot || true
|
||||
done < <(connected_devices)
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
connect) cmd_connect ;;
|
||||
disconnect) cmd_disconnect ;;
|
||||
list) cmd_list ;;
|
||||
install) shift; cmd_install "$@" ;;
|
||||
uninstall) shift; cmd_uninstall "$@" ;;
|
||||
shell) shift; cmd_shell "$@" ;;
|
||||
ip) cmd_ip ;;
|
||||
reboot) cmd_reboot ;;
|
||||
*) usage; exit 1 ;;
|
||||
esac
|
||||
53
02_device_lab/ansible/roles/docker/tasks/main.yml
Normal file
53
02_device_lab/ansible/roles/docker/tasks/main.yml
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
---
|
||||
# Docker desde el repo oficial. El `docker.io` de Debian 12 va con un compose
|
||||
# antiguo (v1, sintaxis `docker-compose`), y las plantillas de aquí usan
|
||||
# `docker compose` (v2). Requiere que los blades lleguen a internet o a un
|
||||
# mirror local de download.docker.com.
|
||||
|
||||
- name: Directorio de keyrings de apt
|
||||
ansible.builtin.file:
|
||||
path: /etc/apt/keyrings
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0755"
|
||||
|
||||
- name: Clave GPG de Docker
|
||||
ansible.builtin.get_url:
|
||||
url: https://download.docker.com/linux/debian/gpg
|
||||
dest: /etc/apt/keyrings/docker.asc
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
|
||||
- name: Repositorio de Docker
|
||||
ansible.builtin.apt_repository:
|
||||
repo: >-
|
||||
deb [arch={{ 'amd64' if ansible_architecture == 'x86_64' else ansible_architecture }}
|
||||
signed-by=/etc/apt/keyrings/docker.asc]
|
||||
https://download.docker.com/linux/debian {{ ansible_distribution_release }} stable
|
||||
filename: docker
|
||||
state: present
|
||||
update_cache: true
|
||||
|
||||
- name: Instalar Docker Engine y el plugin de compose
|
||||
ansible.builtin.apt:
|
||||
name:
|
||||
- docker-ce
|
||||
- docker-ce-cli
|
||||
- containerd.io
|
||||
- docker-compose-plugin
|
||||
state: present
|
||||
|
||||
- name: Servicio de Docker activo y arrancado
|
||||
ansible.builtin.systemd:
|
||||
name: docker
|
||||
enabled: true
|
||||
state: started
|
||||
|
||||
- name: Añadir el usuario de despliegue al grupo docker
|
||||
ansible.builtin.user:
|
||||
name: "{{ docker_user }}"
|
||||
groups: docker
|
||||
append: true
|
||||
when: docker_user | default('') | length > 0
|
||||
75
02_device_lab/ansible/roles/preflight/tasks/main.yml
Normal file
75
02_device_lab/ansible/roles/preflight/tasks/main.yml
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
---
|
||||
# Preflight es de SOLO LECTURA. No cambia nada en los blades.
|
||||
# Su trabajo es contestar dos preguntas antes de gastar un minuto:
|
||||
# 1. ¿Qué blade es esto exactamente?
|
||||
# 2. ¿Puede correr Android x86_64, y cuántas instancias?
|
||||
|
||||
- name: Leer identidad DMI del blade
|
||||
ansible.builtin.slurp:
|
||||
src: "/sys/class/dmi/id/{{ item }}"
|
||||
loop:
|
||||
- sys_vendor
|
||||
- product_name
|
||||
register: preflight_dmi
|
||||
failed_when: false
|
||||
changed_when: false
|
||||
|
||||
- name: Registrar modelo de blade
|
||||
ansible.builtin.set_fact:
|
||||
preflight_vendor: "{{ (preflight_dmi.results[0].content | default('') | b64decode | trim) or 'desconocido' }}"
|
||||
preflight_model: "{{ (preflight_dmi.results[1].content | default('') | b64decode | trim) or 'desconocido' }}"
|
||||
|
||||
- name: Leer flags de CPU
|
||||
ansible.builtin.shell:
|
||||
cmd: "set -o pipefail; grep -m1 '^flags' /proc/cpuinfo | cut -d: -f2"
|
||||
executable: /bin/bash
|
||||
register: preflight_cpuflags
|
||||
changed_when: false
|
||||
|
||||
- name: Detectar flags de CPU que faltan
|
||||
ansible.builtin.set_fact:
|
||||
preflight_missing_flags: >-
|
||||
{{ preflight_required_cpu_flags
|
||||
| reject('in', preflight_cpuflags.stdout.split())
|
||||
| list }}
|
||||
|
||||
- name: Comprobar módulo binder en el kernel
|
||||
ansible.builtin.stat:
|
||||
path: "/lib/modules/{{ ansible_kernel }}/kernel/drivers/android/binder_linux.ko"
|
||||
register: preflight_binder_ko
|
||||
changed_when: false
|
||||
|
||||
- name: Comprobar si el kernel trae binderfs
|
||||
ansible.builtin.shell:
|
||||
cmd: "set -o pipefail; grep -c '^CONFIG_ANDROID_BINDERFS=y' /boot/config-{{ ansible_kernel }} || true"
|
||||
executable: /bin/bash
|
||||
register: preflight_binderfs
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Calcular instancias que caben en este blade
|
||||
ansible.builtin.set_fact:
|
||||
preflight_capacity: >-
|
||||
{{ [ (((ansible_memtotal_mb | int) - (redroid_host_reserve_mb | int))
|
||||
/ (redroid_mem_mb | int)) | int,
|
||||
redroid_max_instances | int ] | min }}
|
||||
|
||||
- name: Veredicto del nodo
|
||||
ansible.builtin.set_fact:
|
||||
preflight_ok: >-
|
||||
{{ (preflight_missing_flags | length == 0)
|
||||
and preflight_binder_ko.stat.exists
|
||||
and (preflight_capacity | int) >= 1 }}
|
||||
|
||||
- name: Resumen del blade
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- "modelo : {{ preflight_vendor }} {{ preflight_model }}"
|
||||
- "cpu : {{ ansible_processor[2] | default('?') }} ({{ ansible_processor_vcpus }} vcpus)"
|
||||
- "ram : {{ ansible_memtotal_mb }} MB"
|
||||
- "kernel : {{ ansible_kernel }} / {{ ansible_distribution }} {{ ansible_distribution_version }}"
|
||||
- "binder .ko : {{ 'sí' if preflight_binder_ko.stat.exists else 'NO — este blade no puede correr redroid' }}"
|
||||
- "binderfs : {{ 'sí' if (preflight_binderfs.stdout | default('0') | int) > 0 else 'no (se cargan devices a mano)' }}"
|
||||
- "flags CPU : {{ 'ok' if preflight_missing_flags | length == 0 else 'FALTAN ' ~ (preflight_missing_flags | join(', ')) }}"
|
||||
- "capacidad : {{ preflight_capacity }} instancias"
|
||||
- "veredicto : {{ 'APTO' if preflight_ok else 'NO APTO' }}"
|
||||
27
02_device_lab/ansible/roles/redroid/defaults/main.yml
Normal file
27
02_device_lab/ansible/roles/redroid/defaults/main.yml
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
# Defaults del rol. Lo que se toca normalmente vive en group_vars/all.yml,
|
||||
# que tiene precedencia sobre esto.
|
||||
|
||||
redroid_image: "redroid/redroid:13.0.0-latest"
|
||||
redroid_mem_mb: 2048
|
||||
redroid_host_reserve_mb: 1500
|
||||
redroid_instances: 0
|
||||
redroid_max_instances: 8
|
||||
|
||||
redroid_width: 720
|
||||
redroid_height: 1280
|
||||
redroid_dpi: 320
|
||||
redroid_gpu_mode: guest
|
||||
|
||||
redroid_base_dir: /opt/redroid
|
||||
|
||||
redroid_network_mode: portmap
|
||||
redroid_adb_base_port: 5555
|
||||
redroid_adb_bind_ip: "0.0.0.0"
|
||||
|
||||
redroid_macvlan_network: redroid-lan
|
||||
redroid_macvlan_parent: ""
|
||||
redroid_macvlan_subnet: ""
|
||||
redroid_macvlan_gateway: ""
|
||||
redroid_macvlan_ip_prefix: ""
|
||||
redroid_macvlan_ip_offset: 20
|
||||
115
02_device_lab/ansible/roles/redroid/tasks/main.yml
Normal file
115
02_device_lab/ansible/roles/redroid/tasks/main.yml
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
---
|
||||
# Despliega N contenedores Android en el blade. N sale de la RAM real salvo
|
||||
# que se fije redroid_instances a mano.
|
||||
|
||||
- name: Calcular cuántas instancias caben
|
||||
ansible.builtin.set_fact:
|
||||
redroid_instance_count: >-
|
||||
{{ (redroid_instances | int) if (redroid_instances | int) > 0
|
||||
else [ (((ansible_memtotal_mb | int) - (redroid_host_reserve_mb | int))
|
||||
/ (redroid_mem_mb | int)) | int,
|
||||
redroid_max_instances | int ] | min }}
|
||||
|
||||
- name: Abortar si no cabe ni una instancia
|
||||
ansible.builtin.assert:
|
||||
that: (redroid_instance_count | int) >= 1
|
||||
fail_msg: >-
|
||||
{{ inventory_hostname }} tiene {{ ansible_memtotal_mb }} MB de RAM. Con
|
||||
{{ redroid_host_reserve_mb }} MB de reserva y {{ redroid_mem_mb }} MB por
|
||||
instancia no cabe ninguna. Baja redroid_mem_mb, baja la reserva, o amplía
|
||||
la RAM del blade (que es la palanca buena — ver hardware_m1000e.md).
|
||||
|
||||
- name: Validar configuración de macvlan
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- redroid_macvlan_parent | length > 0
|
||||
- redroid_macvlan_subnet | length > 0
|
||||
- redroid_macvlan_gateway | length > 0
|
||||
- redroid_macvlan_ip_prefix | length > 0
|
||||
- redroid_macvlan_ip_offset is defined
|
||||
fail_msg: >-
|
||||
redroid_network_mode es 'macvlan' pero faltan datos de red. Necesito
|
||||
redroid_macvlan_parent / _subnet / _gateway / _ip_prefix en group_vars/all.yml
|
||||
y redroid_macvlan_ip_offset por host en el inventario.
|
||||
when: redroid_network_mode == 'macvlan'
|
||||
|
||||
- name: Crear árbol de directorios
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0755"
|
||||
loop:
|
||||
- "{{ redroid_base_dir }}"
|
||||
- "{{ redroid_base_dir }}/data"
|
||||
- "{{ redroid_base_dir }}/apks"
|
||||
|
||||
- name: Crear el volumen /data de cada instancia
|
||||
ansible.builtin.file:
|
||||
path: "{{ redroid_base_dir }}/data/device-{{ '%02d' % (item + 1) }}"
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0755"
|
||||
loop: "{{ range(redroid_instance_count | int) | list }}"
|
||||
|
||||
# --- red macvlan -----------------------------------------------------------
|
||||
|
||||
- name: Listar redes docker existentes
|
||||
ansible.builtin.command:
|
||||
cmd: docker network ls --format '{% raw %}{{.Name}}{% endraw %}'
|
||||
register: redroid_networks
|
||||
changed_when: false
|
||||
when: redroid_network_mode == 'macvlan'
|
||||
|
||||
- name: Crear la red macvlan
|
||||
ansible.builtin.command:
|
||||
cmd: >-
|
||||
docker network create -d macvlan
|
||||
--subnet={{ redroid_macvlan_subnet }}
|
||||
--gateway={{ redroid_macvlan_gateway }}
|
||||
-o parent={{ redroid_macvlan_parent }}
|
||||
{{ redroid_macvlan_network }}
|
||||
when:
|
||||
- redroid_network_mode == 'macvlan'
|
||||
- redroid_macvlan_network not in redroid_networks.stdout_lines
|
||||
changed_when: true
|
||||
|
||||
# --- contenedores ----------------------------------------------------------
|
||||
|
||||
- name: Escribir el docker-compose del nodo
|
||||
ansible.builtin.template:
|
||||
src: docker-compose.yml.j2
|
||||
dest: "{{ redroid_base_dir }}/docker-compose.yml"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
register: redroid_compose
|
||||
|
||||
- name: Descargar la imagen de redroid
|
||||
ansible.builtin.command:
|
||||
cmd: docker compose pull
|
||||
chdir: "{{ redroid_base_dir }}"
|
||||
register: redroid_pull
|
||||
changed_when: "'Downloaded' in redroid_pull.stderr or 'Pulled' in redroid_pull.stderr"
|
||||
|
||||
- name: Levantar las instancias Android
|
||||
ansible.builtin.command:
|
||||
cmd: docker compose up -d --remove-orphans
|
||||
chdir: "{{ redroid_base_dir }}"
|
||||
register: redroid_up
|
||||
changed_when: "'Started' in redroid_up.stderr or 'Created' in redroid_up.stderr or 'Recreated' in redroid_up.stderr"
|
||||
|
||||
- name: Contar contenedores en marcha
|
||||
ansible.builtin.command:
|
||||
cmd: docker compose ps --status running --format '{% raw %}{{.Name}}{% endraw %}'
|
||||
chdir: "{{ redroid_base_dir }}"
|
||||
register: redroid_running
|
||||
changed_when: false
|
||||
|
||||
- name: Resumen del nodo
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
{{ inventory_hostname }}: {{ redroid_running.stdout_lines | length }}/{{ redroid_instance_count }}
|
||||
instancias en marcha ({{ redroid_network_mode }})
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
# GENERADO POR ANSIBLE — no editar a mano.
|
||||
# Fuente: roles/redroid/templates/docker-compose.yml.j2
|
||||
# Nodo: {{ inventory_hostname }} · {{ ansible_memtotal_mb }} MB RAM · {{ redroid_instance_count }} instancias
|
||||
|
||||
name: redroid-{{ inventory_hostname }}
|
||||
|
||||
services:
|
||||
{% for i in range(redroid_instance_count | int) %}
|
||||
{% set idx = '%02d' % (i + 1) %}
|
||||
device-{{ idx }}:
|
||||
image: {{ redroid_image }}
|
||||
container_name: redroid-{{ inventory_hostname }}-{{ idx }}
|
||||
privileged: true
|
||||
restart: unless-stopped
|
||||
mem_limit: {{ redroid_mem_mb }}m
|
||||
volumes:
|
||||
- {{ redroid_base_dir }}/data/device-{{ idx }}:/data
|
||||
{% if redroid_network_mode == 'macvlan' %}
|
||||
networks:
|
||||
redroid_lan:
|
||||
ipv4_address: {{ redroid_macvlan_ip_prefix }}{{ (redroid_macvlan_ip_offset | int) + i }}
|
||||
{% else %}
|
||||
ports:
|
||||
- "{{ redroid_adb_bind_ip }}:{{ (redroid_adb_base_port | int) + i }}:5555"
|
||||
{% endif %}
|
||||
command:
|
||||
- androidboot.redroid_width={{ redroid_width }}
|
||||
- androidboot.redroid_height={{ redroid_height }}
|
||||
- androidboot.redroid_dpi={{ redroid_dpi }}
|
||||
- androidboot.redroid_gpu_mode={{ redroid_gpu_mode }}
|
||||
{% endfor %}
|
||||
{% if redroid_network_mode == 'macvlan' %}
|
||||
|
||||
networks:
|
||||
redroid_lan:
|
||||
external: true
|
||||
name: {{ redroid_macvlan_network }}
|
||||
{% endif %}
|
||||
29
02_device_lab/ansible/site.yml
Normal file
29
02_device_lab/ansible/site.yml
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
# Playbook maestro: provisiona la granja entera de cero.
|
||||
#
|
||||
# ansible-playbook site.yml
|
||||
#
|
||||
# Antes de esto, SIEMPRE:
|
||||
# ansible-playbook playbooks/preflight.yml
|
||||
#
|
||||
# Es idempotente: se puede relanzar sin romper nada.
|
||||
|
||||
- name: Base del sistema en toda la granja
|
||||
hosts: granja
|
||||
gather_facts: true
|
||||
roles:
|
||||
- role: base
|
||||
|
||||
- name: Nodos de carga — binder, Docker y contenedores Android
|
||||
hosts: redroid_nodes
|
||||
gather_facts: true
|
||||
roles:
|
||||
- role: binder
|
||||
- role: docker
|
||||
- role: redroid
|
||||
|
||||
- name: Coordinador — adb y scripts de flota
|
||||
hosts: coordinator
|
||||
gather_facts: true
|
||||
roles:
|
||||
- role: coordinator
|
||||
Loading…
Add table
Add a link
Reference in a new issue