Skip to content
23 changes: 16 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ A single command line quickstart to spin up lean node(s)
- Uses PK's `eth-beacon-genesis` docker tool (not custom tooling)
- Generates PQ keys based on specified configuration in `validator-config.yaml`
- Force regen with flag `--forceKeyGen` when supplied with `generateGenesis`
- ✅ Integrates zeam, ream, qlean, lantern (and more incoming...)
- ✅ Integrates zeam, ream, qlean, lantern, lighthouse, grandine
- ✅ Configure to run clients in docker or binary mode for easy development
- ✅ Linux & Mac compatible & tested
- ✅ Option to operate on single or multiple nodes or `all`
Expand Down Expand Up @@ -114,8 +114,8 @@ NETWORK_DIR=local-devnet ./spin-node.sh --node all --generateGenesis --metrics
- If not specified, uses the current user (whoami) for SSH connections
- If specified, uses `root` user for SSH connections
- Example: `--useRoot` to connect as root user
10. `--tag` specifies the Docker image tag to use for zeam, ream, qlean, and lantern containers.
- If provided, all clients will use this tag (e.g., `blockblaz/zeam:${tag}`, `ghcr.io/reamlabs/ream:${tag}`, `qdrvm/qlean-mini:${tag}`, `piertwo/lantern:${tag}`)
10. `--tag` specifies the Docker image tag to use for zeam, ream, qlean, lantern, lighthouse, and grandine containers.
- If provided, all clients will use this tag (e.g., `blockblaz/zeam:${tag}`, `ghcr.io/reamlabs/ream:${tag}`, `qdrvm/qlean-mini:${tag}`, `piertwo/lantern:${tag}`, `hopinheimer/lighthouse:${tag}`, `sifrai/grandine:${tag}`)
- If not provided, defaults to `latest` for zeam, ream, and lantern, and `dd67521` for qlean
- The script will automatically pull the specified Docker images before running containers
- Example: `--tag devnet0` or `--tag devnet1`
Expand All @@ -129,6 +129,8 @@ Current following clients are supported:
2. Ream
3. Qlean
4. Lantern
5. Lighthouse
6. Grandine

However adding a lean client to this setup is very easy. Feel free to do the PR or reach out to the maintainers.

Expand Down Expand Up @@ -615,16 +617,23 @@ ansible/
│ └── group_vars/ # Group variables
│ └── all.yml # Global variables
├── playbooks/
│ ├── site.yml # Main playbook (copy genesis + deploy)
│ ├── site.yml # Main playbook (clean + copy genesis + deploy)
│ ├── clean-node-data.yml # Clean node data directories
│ ├── generate-genesis.yml # Generate genesis files
│ ├── copy-genesis.yml # Copy genesis files to remote hosts
│ ├── deploy-nodes.yml # Node deployment playbook
│ └── deploy-single-node.yml # Helper for single node deployment
│ ├── stop-nodes.yml # Stop and remove nodes
│ └── helpers/ # Helper task files
│ └── deploy-single-node.yml # Single node deployment tasks
└── roles/
├── common/ # Common setup (Docker, yq, directories)
├── genesis/ # Genesis file generation
├── zeam/ # Zeam node role
├── ream/ # Ream node role
└── qlean/ # Qlean node role
├── qlean/ # Qlean node role
├── lantern/ # Lantern node role
├── lighthouse/ # Lighthouse node role
└── grandine/ # Grandine node role
```

### Remote Deployment
Expand Down Expand Up @@ -673,7 +682,7 @@ NETWORK_DIR=local-devnet ./spin-node.sh --node all --generateGenesis --deploymen

The inventory generator will automatically:
- Detect remote IPs (non-localhost) and configure remote connections
- Group nodes by client type (zeam_nodes, ream_nodes, qlean_nodes, lantern_nodes)
- Group nodes by client type (zeam_nodes, ream_nodes, qlean_nodes, lantern_nodes, lighthouse_nodes, grandine_nodes)
- Set appropriate connection parameters
- Apply SSH key file if provided via `--sshKey` parameter

Expand Down
7 changes: 5 additions & 2 deletions ansible/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ docker ps | grep zeam_0
- `ansible.cfg` - Ansible configuration
- `inventory/` - Host inventory and variables
- `playbooks/` - Main playbooks
- `roles/` - Reusable role modules (zeam, ream, qlean, lantern, genesis, common)
- `roles/` - Reusable role modules (zeam, ream, qlean, lantern, lighthouse, grandine, genesis, common)
- `requirements.yml` - Ansible Galaxy dependencies

## Configuration Source
Expand Down Expand Up @@ -177,8 +177,11 @@ cd ansible

# Check all playbooks
ansible-playbook --syntax-check playbooks/site.yml
ansible-playbook --syntax-check playbooks/clean-node-data.yml
ansible-playbook --syntax-check playbooks/generate-genesis.yml
ansible-playbook --syntax-check playbooks/copy-genesis.yml
ansible-playbook --syntax-check playbooks/deploy-nodes.yml
ansible-playbook --syntax-check playbooks/stop-nodes.yml
```

### Phase 3: Test Genesis File Copying
Expand Down Expand Up @@ -511,7 +514,7 @@ This section covers how to test the latest changes that extract docker images an

- Docker images and deployment modes are now automatically extracted from `client-cmds/*-cmd.sh` files
- This ensures consistency between `spin-node.sh` (local) and Ansible (remote) deployments
- All client roles (zeam, ream, qlean, lantern) now use this extraction mechanism
- All client roles (zeam, ream, qlean, lantern, lighthouse, grandine) now use this extraction mechanism

### Quick Test

Expand Down
125 changes: 125 additions & 0 deletions ansible/playbooks/clean-node-data.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
---
# Clean data playbook: Clean node data directories

- name: Parse and validate node names
hosts: localhost
connection: local
gather_facts: no
vars:
validator_config_file: "{{ genesis_dir }}/validator-config.yaml"
tags:
- zeam
- ream
- qlean
- lantern
- lighthouse
- grandine
- deploy

tasks:
- name: Validate validator-config.yaml exists
stat:
path: "{{ validator_config_file }}"
register: validator_config_stat

- name: Fail if validator-config.yaml missing
fail:
msg: "validator-config.yaml not found at {{ validator_config_file }}"
when: not validator_config_stat.stat.exists

- name: Verify yq is available
command: yq --version
register: yq_version
changed_when: false
failed_when: false
ignore_errors: true

- name: Fail if yq is not available
fail:
msg: "yq is required but not installed. Install on macOS: brew install yq, or see https://github.com/mikefarah/yq"
when: yq_version.rc != 0

- name: Extract all node names
shell: |
yq eval '.validators[].name' {{ validator_config_file }}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a nit, but can we please add a check to validate if yq is installed before using it here?

register: all_node_names_raw
changed_when: false

- name: Set all node names
set_fact:
all_node_names: "{{ all_node_names_raw.stdout_lines }}"

- name: Fail if node_names is not specified
fail:
msg: "node_names must be specified. Provide one or more node names (comma or space separated)."
when: node_names is not defined or node_names == ""

- name: Handle "all" node names - expand to all nodes
set_fact:
clean_nodes: "{{ all_node_names }}"
when:
- node_names is defined
- node_names == "all"

- name: Parse node names if provided as comma-separated string
set_fact:
clean_nodes: "{{ node_names.split(',') | map('trim') | list }}"
when:
- node_names is defined
- node_names is string
- node_names != "all"
- '("," in node_names)'

- name: Parse node names if provided as space-separated string
set_fact:
clean_nodes: "{{ node_names.split(' ') | map('trim') | select('length') | list }}"
when:
- node_names is defined
- node_names is string
- node_names != "all"
- '("," not in node_names)'
- '(" " in node_names)'

- name: Handle single node name
set_fact:
clean_nodes: "{{ [node_names] }}"
when:
- node_names is defined
- node_names is string
- node_names != "all"
- '"," not in node_names'
- '" " not in node_names'

- name: Display nodes to clean
debug:
msg: "Cleaning data directories for nodes: {{ clean_nodes | join(', ') }}"

- name: Add nodes to clean_targets group
add_host:
name: "{{ item }}"
groups: clean_targets
loop: "{{ clean_nodes }}"

- name: Clean node data directories on remote hosts
hosts: clean_targets
gather_facts: no
vars:
# Use remote paths on remote hosts
data_dir: "{{ remote_data_dir | default('/opt/lean-quickstart/data') }}"
node_name: "{{ inventory_hostname }}"
tags:
- zeam
- ream
- qlean
- lantern
- lighthouse
- grandine
- deploy

tasks:
- name: Clean node data directory
raw: rm -rf {{ data_dir }}/{{ node_name }}
Copy link
Member

@bomanaps bomanaps Jan 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One issue i found berfore merge

  1. raw: rm -rf can corrupt data if container is running add before the rm command:
- name: Stop container before cleaning
  raw: docker rm -f {{ node_name }} 2>/dev/null || true

Otherwise LGTM


- name: Display cleaned directory
debug:
msg: "Cleaned data directory: {{ data_dir }}/{{ node_name }}"
16 changes: 15 additions & 1 deletion ansible/playbooks/deploy-nodes.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,20 @@
node_name: "{{ inventory_hostname }}"

tasks:
- name: Clean node data directory
raw: rm -rf {{ data_dir }}/{{ node_name }}
when:
- clean_data | default(false) | bool
- not (skip_role_cleaning | default(false) | bool)
tags:
- zeam
- ream
- qlean
- lantern
- lighthouse
- grandine
- deploy

- name: Include common role
include_role:
name: common
Expand All @@ -104,7 +118,7 @@
- deploy

- name: Deploy this node
include_tasks: deploy-single-node.yml
include_tasks: helpers/deploy-single-node.yml
vars:
node_name: "{{ inventory_hostname }}"
tags:
Expand Down
16 changes: 13 additions & 3 deletions ansible/playbooks/site.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
---
# Main site playbook: Complete deployment workflow
# 1. Generate genesis files locally (including .key files)
# 2. Copy genesis files to remote hosts
# 3. Deploy nodes
# 1. Clean data directories (if clean_data=true)
# 2. Generate genesis files locally (including .key files)
# 3. Copy genesis files to remote hosts
# 4. Deploy nodes
#
# Usage:
# ansible-playbook -i inventory/hosts.yml playbooks/site.yml \
Expand All @@ -13,9 +14,17 @@
# network_dir - Path to network directory (genesis_dir derived from this)
# genesis_dir - Path to genesis directory (required)
# node_names - Nodes to deploy: "all" or comma-separated names
# clean_data - Set to true to clean data directories before deployment (default: false)
# skip_genesis - Set to true to skip genesis generation (default: false)
# genesis_offset - Seconds to add to current time for genesis (default: 360 for ansible mode)

- name: Clean Data Directories
import_playbook: clean-node-data.yml
vars:
genesis_dir: "{{ network_dir }}/genesis"
node_names: "{{ node_names }}"
when: clean_data | default(false) | bool

- name: Generate Genesis Files
import_playbook: generate-genesis.yml
vars:
Expand All @@ -32,3 +41,4 @@
import_playbook: deploy-nodes.yml
vars:
node_names: "{{ node_names }}"
skip_role_cleaning: "{{ clean_data | default(false) | bool }}"
6 changes: 0 additions & 6 deletions ansible/roles/lantern/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,6 @@
msg: "Node key file {{ node_name }}.key not found in {{ genesis_dir }}"
when: not (node_key_stat.stat.exists | default(false))

- name: Clean node data directory
file:
path: "{{ data_dir }}/{{ node_name }}"
state: absent
when: clean_data | default(false) | bool

- name: Create node data directory
file:
path: "{{ data_dir }}/{{ node_name }}"
Expand Down
6 changes: 0 additions & 6 deletions ansible/roles/lighthouse/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,6 @@
msg: "Node key file {{ node_name }}.key not found in {{ genesis_dir }}"
when: not (node_key_stat.stat.exists | default(false))

- name: Clean node data directory
file:
path: "{{ data_dir }}/{{ node_name }}"
state: absent
when: clean_data | default(false) | bool

- name: Create node data directory
file:
path: "{{ data_dir }}/{{ node_name }}"
Expand Down
6 changes: 0 additions & 6 deletions ansible/roles/qlean/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,6 @@
msg: "Node key file {{ node_name }}.key not found in {{ genesis_dir }}"
when: not (node_key_stat.stat.exists | default(false))

- name: Clean node data directory
file:
path: "{{ data_dir }}/{{ node_name }}"
state: absent
when: clean_data | default(false) | bool

- name: Create node data directory
file:
path: "{{ data_dir }}/{{ node_name }}"
Expand Down
6 changes: 0 additions & 6 deletions ansible/roles/ream/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,6 @@
msg: "Node key file {{ node_name }}.key not found in {{ genesis_dir }}"
when: not (node_key_stat.stat.exists | default(false))

- name: Clean node data directory
file:
path: "{{ data_dir }}/{{ node_name }}"
state: absent
when: clean_data | default(false) | bool

- name: Create node data directory
file:
path: "{{ data_dir }}/{{ node_name }}"
Expand Down
6 changes: 0 additions & 6 deletions ansible/roles/zeam/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,6 @@
msg: "Node key file {{ node_name }}.key not found in {{ genesis_dir }}"
when: not (node_key_stat.stat.exists | default(false))

- name: Clean node data directory
file:
path: "{{ data_dir }}/{{ node_name }}"
state: absent
when: clean_data | default(false) | bool

- name: Create node data directory
file:
path: "{{ data_dir }}/{{ node_name }}"
Expand Down