Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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