Skip to content
Merged
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
53 changes: 39 additions & 14 deletions ansible/roles/decdn_node/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
# roles/decdn_node

Provisions a **public deCDN node** (`decdn-node` daemon) from a pinned GitHub
Release tarball, under a hardened systemd unit. This is the repo's **primary**
deployment (`playbooks/site.yml`); the anvil devnet is separate internal tooling.
Provisions a **public deCDN node** (`decdn-node` daemon) under a hardened systemd
unit. Two install methods (`decdn_node_install_method`): the default `release`
pulls a pinned GitHub Release tarball, and `manual` copies locally-built binaries
from the Ansible control machine — the pre-release path for when no release exists
yet. This is the repo's **primary** deployment (`playbooks/site.yml`); the anvil
devnet is separate internal tooling.

## What this role does (and does not)

Expand All @@ -18,11 +21,22 @@ Per `decdn/adr/019-node-onboarding.md`, a node only serves paid traffic after

## Prerequisites

1. **A published release.** The role downloads
`decdn-node-<ver>-<target>.tar.gz` (and the `decdn` CLI) from
`github.com/decdn/decdn/releases`. Set `decdn_node_version` to a real
`v<version>` release. *(No release exists yet — cut one with the upstream
`release.yml` workflow first.)*
1. **The binaries.** Pick an install method with `decdn_node_install_method`:

- **`release`** (default) — the role downloads
`decdn-node-<ver>-<target>.tar.gz` (and the `decdn` CLI) from
`github.com/decdn/decdn/releases`. Set `decdn_node_version` to a real
`v<version>` release. *(No release exists yet — either cut one with the
upstream `release.yml` workflow, or use `manual` below in the meantime.)*
- **`manual`** — the role copies the two binaries **verbatim** from the paths
you give it (`decdn_node_manual_bin_src` + `decdn_cli_manual_bin_src`) on the
Ansible control machine; it does *not* consult `decdn_node_target`, so you are
responsible for building for the host's architecture (the default target is
`x86_64-unknown-linux-gnu`). Build both `decdn-node` and `decdn` from the
upstream `decdn` repo, then set the two paths. `decdn_node_version` is **not**
required in this mode — but if it is set (e.g. left over from a `release`
deploy) the `--version` backstop still enforces it, so clear it when switching
to `manual` unless you intend that binary to report that exact version.

2. **Eth wallet (operator-provisioned).** Generate the node identity + eth
keystore on the host, as the `decdn` user, into the data dir:
Expand All @@ -46,9 +60,10 @@ Per `decdn/adr/019-node-onboarding.md`, a node only serves paid traffic after

## Required variables (set in `host_vars/<node>/`)

`decdn_node_version`, `decdn_rpc_url` (sensitive — may embed an API key; goes in the
git-ignored `secret.yml`, everything else in the committed `main.yml`),
`decdn_payment_channel_address`, `decdn_capacity_bond_address`,
`decdn_node_version` (`release` mode only) **or** `decdn_node_manual_bin_src` +
`decdn_cli_manual_bin_src` (`manual` mode), `decdn_rpc_url` (sensitive — may embed an
API key; goes in the git-ignored `secret.yml`, everything else in the committed
`main.yml`), `decdn_payment_channel_address`, `decdn_capacity_bond_address`,
`decdn_slash_judge_address` (all `0x`+40-hex; SlashJudge non-zero),
`decdn_region` (ISO 3166-1 alpha-2). Contract addresses/chain-id are protocol
facts — source them from the deployment / an ADR, never guess.
Expand Down Expand Up @@ -106,6 +121,16 @@ decdn node peers
decdn appeal slash <SLASH_ID> <EVIDENCE_BUNDLE_HASH>
```

Upgrades: bump `decdn_node_version` (+ `decdn_node_sha256`) and re-deploy — the
version stamp triggers re-install + restart; the persistent `node.secret` and
`keystore.json` are untouched.
Upgrades (`release` mode): bump `decdn_node_version` (+ `decdn_node_sha256`) and
re-deploy — the version stamp triggers re-install + restart; the persistent
`node.secret` and `keystore.json` are untouched.

Upgrades (`manual` mode): there is **no** version stamp — rebuild the binaries
locally and re-deploy. `copy` compares checksums and re-pushes (and restarts) only
when the control-machine binary actually changed.

Switching methods: a `manual` deploy clears the release version stamp on the host,
so returning to `release` afterwards always re-fetches and re-installs the official
tarball — even when `decdn_node_version` is unchanged. (This is separate from the
`--version` backstop: a `decdn_node_version` left set in `manual` mode is still
enforced against the local build — see the `manual` prerequisite above.)
12 changes: 10 additions & 2 deletions ansible/roles/decdn_node/defaults/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,16 @@
# non-secret config (contract addresses, region, version) in the committed main.yml,
# and the sensitive rpc_url in a git-ignored secret.yml (see secret.yml.example).

# --- Release to install (REQUIRED — a v<version> GitHub Release must exist) ---
decdn_node_version: "" # e.g. "0.1.0" (asserted non-empty)
# --- Install method -----------------------------------------------------------
# "release" (default): download the pinned GitHub Release tarballs (a v<version>
# release must exist). "manual": copy locally-built binaries from the Ansible
# control machine — for pre-release / dev deploys where no release exists yet.
decdn_node_install_method: release # "release" | "manual"
decdn_node_manual_bin_src: "" # manual: control-machine path to the decdn-node daemon binary
decdn_cli_manual_bin_src: "" # manual: control-machine path to the decdn CLI binary

# --- Release to install (REQUIRED in "release" mode — a v<version> GitHub Release must exist) ---
decdn_node_version: "" # e.g. "0.1.0" (asserted non-empty in "release" mode)
decdn_node_target: x86_64-unknown-linux-gnu
decdn_node_sha256: "" # optional "abc123..." to pin the daemon tarball
decdn_cli_sha256: "" # optional, for the decdn CLI tarball
Expand Down
4 changes: 3 additions & 1 deletion ansible/roles/decdn_node/meta/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
galaxy_info:
role_name: decdn_node
author: deCDN Contributors
description: Public deCDN node — release-tarball install, hardened systemd unit, loopback metrics/admin, public QUIC.
description: >-
Public deCDN node — release-tarball or manual local-binary install, hardened
systemd unit, loopback metrics/admin, public QUIC.
license: MIT
min_ansible_version: "2.15"
galaxy_tags: [decdn, cdn, node, systemd, web3, debian]
Expand Down
152 changes: 97 additions & 55 deletions ansible/roles/decdn_node/tasks/install.yml
Original file line number Diff line number Diff line change
@@ -1,74 +1,116 @@
---
# Install the pinned decdn-node daemon + decdn CLI from GitHub Release tarballs.
# Idempotent and upgrade-aware via a version stamp; re-extracts only on change.
# NOTE: requires a published v<version> release (see role README).
# Install the decdn-node daemon + decdn CLI. Two methods, selected by
# decdn_node_install_method:
# "release" (default): download the pinned GitHub Release tarballs. Idempotent
# and upgrade-aware via a version stamp; re-extracts only on change. NOTE:
# requires a published v<version> release (see role README).
# "manual": copy locally-built binaries from the Ansible control machine (for
# pre-release / dev deploys). copy's checksum idempotency replaces the stamp.
# A --version backstop runs in both modes.

- name: Build the release tarball list
ansible.builtin.set_fact:
decdn_release_tarballs:
- {archive: decdn-node, sha: "{{ decdn_node_sha256 }}"}
- {archive: decdn, sha: "{{ decdn_cli_sha256 }}"}
- name: Install from pinned GitHub Release tarballs
when: decdn_node_install_method == "release"
block:
- name: Build the release tarball list
ansible.builtin.set_fact:
decdn_release_tarballs:
- {archive: decdn-node, sha: "{{ decdn_node_sha256 }}"}
- {archive: decdn, sha: "{{ decdn_cli_sha256 }}"}

- name: Read the installed-version stamp
ansible.builtin.slurp:
src: /usr/local/lib/decdn/installed-version
register: decdn_stamp
failed_when: false
changed_when: false
- name: Read the installed-version stamp
ansible.builtin.slurp:
src: /usr/local/lib/decdn/installed-version
register: decdn_stamp
failed_when: false
changed_when: false

- name: Decide whether (re)install is needed
ansible.builtin.set_fact:
decdn_need_install: >-
{{ (decdn_stamp.content is not defined)
or ((decdn_stamp.content | b64decode | trim) != decdn_node_version) }}
- name: Decide whether (re)install is needed
ansible.builtin.set_fact:
decdn_need_install: >-
{{ (decdn_stamp.content is not defined)
or ((decdn_stamp.content | b64decode | trim) != decdn_node_version) }}

- name: Install decdn-node + decdn CLI {{ decdn_node_version }}
when: decdn_need_install
block:
- name: Download release tarballs
ansible.builtin.get_url:
url: "{{ decdn_node_release_base }}/v{{ decdn_node_version }}/\
{{ item.archive }}-{{ decdn_node_version }}-{{ decdn_node_target }}.tar.gz"
dest: "/tmp/{{ item.archive }}-{{ decdn_node_version }}.tar.gz"
mode: "0644"
checksum: "{{ ('sha256:' + item.sha) if (item.sha | length > 0) else omit }}"
loop: "{{ decdn_release_tarballs }}"
loop_control:
label: "{{ item.archive }}"
- name: Install decdn-node + decdn CLI {{ decdn_node_version }}
when: decdn_need_install
block:
- name: Download release tarballs
ansible.builtin.get_url:
url: "{{ decdn_node_release_base }}/v{{ decdn_node_version }}/\
{{ item.archive }}-{{ decdn_node_version }}-{{ decdn_node_target }}.tar.gz"
dest: "/tmp/{{ item.archive }}-{{ decdn_node_version }}.tar.gz"
mode: "0644"
checksum: "{{ ('sha256:' + item.sha) if (item.sha | length > 0) else omit }}"
loop: "{{ decdn_release_tarballs }}"
loop_control:
label: "{{ item.archive }}"

- name: Extract binaries into /usr/local/bin
ansible.builtin.unarchive:
src: "/tmp/{{ item.archive }}-{{ decdn_node_version }}.tar.gz"
dest: /usr/local/bin
remote_src: true
- name: Extract binaries into /usr/local/bin
ansible.builtin.unarchive:
src: "/tmp/{{ item.archive }}-{{ decdn_node_version }}.tar.gz"
dest: /usr/local/bin
remote_src: true
owner: root
group: root
mode: "0755"
loop: "{{ decdn_release_tarballs }}"
loop_control:
label: "{{ item.archive }}"
notify: Restart decdn-node

- name: Ensure the stamp directory exists
ansible.builtin.file:
path: /usr/local/lib/decdn
state: directory
owner: root
group: root
mode: "0755"

- name: Record the installed version
ansible.builtin.copy:
dest: /usr/local/lib/decdn/installed-version
content: "{{ decdn_node_version }}\n"
owner: root
group: root
mode: "0644"

- name: Install locally-built binaries from the control machine
when: decdn_node_install_method == "manual"
block:
- name: Copy the decdn-node daemon + decdn CLI from the control machine
ansible.builtin.copy:
src: "{{ item.src }}"
dest: "{{ item.dest }}"
owner: root
group: root
mode: "0755"
loop: "{{ decdn_release_tarballs }}"
loop:
- {src: "{{ decdn_node_manual_bin_src }}", dest: "{{ decdn_bin }}"}
- {src: "{{ decdn_cli_manual_bin_src }}", dest: "{{ decdn_cli_bin }}"}
loop_control:
label: "{{ item.archive }}"
label: "{{ item.dest }}"
notify: Restart decdn-node
Comment thread
thiras marked this conversation as resolved.

- name: Ensure the stamp directory exists
# Manual mode never reads the release version stamp, but a leftover stamp from
# a prior release deploy would make a later release re-deploy of the SAME
# version skip the download (decdn_need_install stays false). Clear it so a
# release <-> manual round-trip always re-fetches the official tarball.
- name: Clear the release version stamp in manual mode
ansible.builtin.file:
path: /usr/local/lib/decdn
state: directory
owner: root
group: root
mode: "0755"

- name: Record the installed version
ansible.builtin.copy:
dest: /usr/local/lib/decdn/installed-version
content: "{{ decdn_node_version }}\n"
owner: root
group: root
mode: "0644"
path: /usr/local/lib/decdn/installed-version
state: absent

# Integrity backstop: runs EVERY converge (not just on install), so a host whose
# binary drifted from the pin is caught loudly rather than trusted via the stamp.
- name: Verify the installed decdn-node reports the pinned version
# rc != 0 is included because a custom failed_when suppresses the module's default
# non-zero-exit check — else a broken manual binary (rc != 0) would pass silently.
# In "release" mode decdn_node_version is always set (asserted), so the version
# match is enforced; in "manual" mode with no version it degrades to a liveness
# check (rc == 0).
- name: Verify the installed decdn-node runs (version backstop)
ansible.builtin.command: "{{ decdn_bin }} --version"
register: decdn_installed_version
changed_when: false
failed_when: decdn_node_version not in decdn_installed_version.stdout
failed_when: >-
decdn_installed_version.rc != 0
or (decdn_node_version | length > 0
and decdn_node_version not in decdn_installed_version.stdout)
20 changes: 20 additions & 0 deletions ansible/roles/decdn_node/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,33 @@
# roles/decdn_node/README.md. This role covers host prep + Phase-3 startup.

# --- Fail loud on missing required config -------------------------------------
- name: Require a valid install method
ansible.builtin.assert:
that:
- decdn_node_install_method in ["release", "manual"]
fail_msg: >-
decdn_node_install_method must be "release" (pinned GitHub Release tarballs)
or "manual" (copy locally-built binaries from the control machine).

- name: Require a pinned release version
ansible.builtin.assert:
that:
- decdn_node_version | length > 0
fail_msg: >-
decdn_node_version must be set to a published release (e.g. "0.1.0"). A
matching v<version> GitHub Release must exist. Set it in host_vars.
when: decdn_node_install_method == "release"

- name: Require manual binary sources when install method is manual
ansible.builtin.assert:
that:
- decdn_node_manual_bin_src | length > 0
- decdn_cli_manual_bin_src | length > 0
fail_msg: >-
decdn_node_install_method "manual" requires decdn_node_manual_bin_src and
decdn_cli_manual_bin_src — control-machine paths to the locally-built
decdn-node daemon and decdn CLI binaries. Set them in host_vars.
Comment thread
Copilot marked this conversation as resolved.
when: decdn_node_install_method == "manual"

- name: Require chain + contract configuration
ansible.builtin.assert:
Expand Down Expand Up @@ -261,7 +281,7 @@
# --- Readiness ----------------------------------------------------------------
- name: Wait for the node metrics endpoint
ansible.builtin.uri:
url: "http://127.0.0.1:{{ decdn_metrics_port }}/metrics"

Check warning on line 284 in ansible/roles/decdn_node/tasks/main.yml

View workflow job for this annotation

GitHub Actions / kics

[MEDIUM] Communication Over HTTP

Using HTTP URLs (without encryption) could lead to security vulnerabilities and risks
status_code: 200
register: decdn_metrics_probe
retries: 30
Expand Down
Loading