Skip to content

Latest commit

 

History

57 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

piccolo-perf fleet install

Ansible project that idempotently rolls piccolo-perf out across a fleet of Unix/Linux hosts and brings every host's Prometheus to a hardened baseline: bound to the mesh VPN interface only, TLS-terminated with a centrally-issued Let's Encrypt wildcard certificate, and firewalled as defense-in-depth.

Architecture

Control node (cert_issuer_hosts)
  ├── certbot DNS-01  →  *.mesh.example.net wildcard cert
  └── cert-distribute.yml  →  pushes cert to fleet on renewal

Fleet hosts
  ├── piccolo-perf-exporter   [probe_address]:9862   HTTPS/TLS
  ├── nginx config server     [probe_address]:8443   HTTPS/TLS
  └── prometheus (selected)   [probe_address]:9090   HTTPS/TLS + basic auth

nftables on every host: 9862, 8443, 9090 blocked on all interfaces except the mesh VPN

All network services bind exclusively to each host's mesh-VPN IPv6 address, never to 0.0.0.0 or ::. The wildcard cert is issued once centrally and distributed to every host that needs it, so no host carries DNS credentials.

Repository layout

piccolo-perf-install/
├── ansible.cfg
├── inventory/
│   ├── hosts.example.ini          # copy to hosts.ini and edit
│   └── group_vars/
│       ├── all.yml                # mesh topology & fleet config
│       ├── prometheus_hosts.yml   # prometheus version & auth vars
│       └── config_server_hosts.yml
├── roles/
│   ├── piccolo_perf/              # install + harden piccolo-perf exporter
│   ├── config_server/             # nginx serving topology JSON over HTTPS
│   ├── prometheus_hardened/       # install or adopt-and-harden Prometheus
│   └── cert_issuer/               # DNS-01 ACME issuance + renewal timer
└── playbooks/
    ├── site.yml                   # deploys everything in one run
    ├── piccolo-perf.yml
    ├── config-server.yml
    ├── prometheus.yml
    ├── cert-issue.yml             # run once to issue the wildcard cert
    ├── cert-distribute.yml        # push renewed cert to fleet
    └── verify-fleet.yml           # HTTPS smoke-test across all services

Requirements

Control node

  • Ansible ≥ 2.15
  • Python ≥ 3.9
  • A DNS provider supported by a certbot DNS-01 plugin (Cloudflare, Route53, etc.)
  • The project checked out somewhere the certbot deploy hook can reach: cert_issuer_playbook_dir (default /opt/piccolo-fleet)

Fleet hosts

  • Debian/Ubuntu family (uses apt). Other families are a documented follow-up.
  • A mesh VPN (WireGuard or similar) with a consistent interface name across the entire fleet, providing IPv6 ULA addressing.
  • SSH access from the control node via the mesh VPN.
  • Python 3 (Ansible dependency).

Ports used

Service Port Protocol Bound to
piccolo-perf exporter 9862 TCP mesh IPv6 only
config server (nginx) 8443 TCP mesh IPv6 only
Prometheus 9090 TCP mesh IPv6 only

Initial setup

1. Clone and configure

git clone <this-repo> /opt/piccolo-fleet
cd /opt/piccolo-fleet

Copy the example inventory and edit it to match your fleet:

cp inventory/hosts.example.ini inventory/hosts.ini
$EDITOR inventory/hosts.ini

The inventory uses short hostnames as Ansible host names; ansible_host carries the mesh-VPN FQDN used for SSH:

[piccolo_perf]
probe-a ansible_host=probe-a.mesh.example.net
probe-b ansible_host=probe-b.mesh.example.net

[prometheus_hosts]
probe-a          # hosts that should run (or already run) Prometheus

[config_server_hosts]
probe-a          # exactly one host serves the topology JSON

[cert_issuer_hosts]
localhost ansible_connection=local   # the control node issues certs

2. Edit fleet topology

Open inventory/group_vars/all.yml and fill in your actual values:

mesh_iface: wg0                    # WireGuard interface name, same on every host
mesh_domain: mesh.example.net      # internal DNS domain for the fleet
mesh_vpn_service: "wg-quick@wg0.service"  # systemd unit for the mesh VPN

piccolo_perf_hosts:
  - { name: probe-a, address: "fd00:dead:beef::1", site: site-a }
  - { name: probe-b, address: "fd00:dead:beef::2", site: site-b }

piccolo_perf_measurements:
  - { type: twamp, interval: 60s, targets: all, burst_size: 5,
      burst_interval: 200ms, packet_timeout: 5s }
  - { type: dns, interval: 120s,
      resolvers: ["2620:fe::fe", "9.9.9.9"],
      names: ["example.com"] }

The piccolo_perf_hosts list is the single source of truth for fleet topology. The config_server role serves it as JSON over HTTPS; the prometheus_hardened role uses it to build the Prometheus scrape config. Add or remove hosts here and re-run site.yml to propagate changes everywhere.

3. Configure secrets with Ansible Vault

Two group_vars files hold secrets that must be vault-encrypted before committing.

Cert issuer credentials

inventory/group_vars/cert_issuer_hosts.yml is created for you with a placeholder. Edit it to add your real DNS API token, then encrypt:

$EDITOR inventory/group_vars/cert_issuer_hosts.yml
ansible-vault encrypt inventory/group_vars/cert_issuer_hosts.yml

The file should contain:

cert_issuer_acme_email: "admin@mesh.example.net"

# For Cloudflare (python3-certbot-dns-cloudflare):
cert_issuer_dns_credentials_content: |
  dns_cloudflare_api_token = <your-token-here>

The Cloudflare token needs Zone:Zone:Read and Zone:DNS:Edit permissions for the zone that contains your mesh_domain.

DNS plugin selection: The default plugin is cloudflare. To use a different provider, override cert_issuer_dns_plugin_name in cert_issuer_hosts.yml:

cert_issuer_dns_plugin_name: route53
# cert_issuer_certbot_dns_package and cert_issuer_certbot_authenticator
# are derived automatically from cert_issuer_dns_plugin_name.

Prometheus credentials

Edit inventory/group_vars/prometheus_hosts.yml to add the password variables, then encrypt:

# Generate a bcrypt hash
htpasswd -nbBC 10 admin '<password>'

$EDITOR inventory/group_vars/prometheus_hosts.yml
# Add:
#   prometheus_basic_auth_password_hash: "$2y$10$..."
#   prometheus_basic_auth_password: "<your-password-here>"

ansible-vault encrypt inventory/group_vars/prometheus_hosts.yml

Pass --ask-vault-pass or --vault-password-file ~/.vault_pass on every ansible-playbook invocation that touches encrypted files.


First deployment

Run these four commands in order. Each is idempotent; re-running is always safe.

Step 1: Issue the wildcard certificate

This runs on the control node (cert_issuer_hosts) and issues *.mesh.example.net via DNS-01 ACME. On success, certbot's deploy hook automatically calls cert-distribute.yml to push the cert to every host in inventory.

ansible-playbook -i inventory/hosts.ini playbooks/cert-issue.yml \
  --ask-vault-pass

Expected outcome: /etc/letsencrypt/live/mesh.example.net/fullchain.pem and privkey.pem exist on the control node, and the cert files appear at /etc/piccolo-perf/tls/ and /etc/prometheus/tls/ on every fleet host.

Step 2: Deploy the full stack

ansible-playbook -i inventory/hosts.ini playbooks/site.yml \
  --ask-vault-pass

This is equivalent to running piccolo-perf.yml, config-server.yml, and prometheus.yml in sequence. On first run it will:

  • Download and install piccolo-perf v1.0.7 on every piccolo_perf host.
  • Deploy the piccolo-perf-exporter systemd unit bound to the mesh IPv6 address.
  • Install nginx on config_server_hosts and serve the topology JSON over HTTPS on port 8443.
  • Install or upgrade Prometheus to v3.13.1 on every prometheus_hosts host, applying TLS + basic auth and rebinding from any existing 0.0.0.0:9090 listener to the mesh interface only.
  • Apply nftables rules on every host restricting the relevant ports to the mesh interface.

Step 3: Verify

ansible-playbook -i inventory/hosts.ini playbooks/verify-fleet.yml \
  --ask-vault-pass

This hits every deployed service over HTTPS through the mesh interface:

  • piccolo-perf /metrics on each piccolo_perf host — expects HTTP 200.
  • Config server /piccolo-config.json — expects HTTP 200.
  • Prometheus /-/healthy with basic auth — expects HTTP 200.
  • Prometheus API query up{job="piccolo_perf"} — asserts every target reports 1.

Host management

inventory/hosts.ini is the single source of truth for fleet membership. Every host in [piccolo_perf] requires two inline variables:

[piccolo_perf]
probe-a  ansible_host=probe-a.mesh.example.net  probe_address=fd00:dead:beef::1  site="site-a"
probe-b  ansible_host=probe-b.mesh.example.net  probe_address=fd00:dead:beef::2  site="site-b"
Variable Required Description
ansible_host yes Hostname or IP used for SSH from the control node
probe_address yes The address Prometheus scrapes and piccolo-perf uses for peer discovery. Use a native IPv6 address for non-VPN deployments or a VPN-assigned IPv6 for mesh deployments. IPv4 is accepted when IPv6 is unavailable. Deprecated alias: mesh_ipv6 (still works, emits a warning).
site yes Short label for the host's physical or logical site. Always quote numeric-looking values: site="809"
piccolo_name no Override the name piccolo-perf uses to identify this host. Defaults to the inventory hostname. Set when the OS hostname differs from the inventory name.

The fleet topology JSON served to piccolo-perf nodes and the Prometheus scrape targets are both generated automatically from this single inventory. There is no separate piccolo_perf_hosts list to maintain.

Tip: verify a new host's OS hostname before adding it: ssh user@host hostname. If it differs from the name you plan to use in inventory, set piccolo_name=<os-hostname> on the host line.

Adding a host

  1. Add the host to inventory/hosts.ini with probe_address= and site=:

    [piccolo_perf]
    new-probe  ansible_host=new-probe.mesh.example.net  probe_address=fd00:dead:beef::3  site="site-c"

    Add to [prometheus_hosts] too if it should run Prometheus.

  2. Run the add-host playbook:

    ansible-playbook -i inventory/hosts.ini playbooks/add-host.yml \
      -e new_host=new-probe --ask-vault-pass

    Use -e new_host= (not --limit) — the playbook must fetch the cert from cert_issuer_hosts (localhost) regardless of which host is being added, and --limit would prevent that play from running.

    This automatically: distributes the wildcard cert, installs piccolo-perf, regenerates the fleet config JSON (now including the new host), and updates all Prometheus scrape configs.

Removing a host

  1. Run the decommission playbook before editing hosts.ini:

    ansible-playbook -i inventory/hosts.ini playbooks/decommission.yml \
      --limit old-probe --ask-vault-pass

    This stops and disables piccolo-perf-exporter, removes the systemd unit, nftables rules, TLS certs, and binary from the target host.

  2. Delete the host line from inventory/hosts.ini.

  3. Re-run site.yml to regenerate the fleet config JSON and Prometheus scrape configs without the removed host:

    ansible-playbook -i inventory/hosts.ini playbooks/site.yml --ask-vault-pass

Day-2 operations

Adding or changing measurement types

Measurement configuration lives in piccolo_perf_measurements in inventory/group_vars/all.yml. After editing it:

  1. Redeploy the fleet config JSON so nodes pick up the new configuration:

    ansible-playbook -i inventory/hosts.ini playbooks/config-server.yml \
      --ask-vault-pass
  2. Restart piccolo-perf on all fleet hosts to reload immediately (otherwise each node waits up to the config_refresh interval, default 5 minutes):

    ansible -i inventory/hosts.ini piccolo_perf -b --ask-vault-pass \
      -m systemd -a "name=piccolo-perf-exporter state=restarted"
  3. Add panels to Grafana for any new measurement types. New measurement types produce new metric names — they do not automatically appear in existing dashboard panels. piccolo-perf metrics follow the naming convention piccolo_<type>_*. For example:

    • TWAMP: piccolo_twamp_rtt_avg_ms, piccolo_twamp_loss_pct, etc.
    • DNS: piccolo_dns_dns_rtt_ms, piccolo_dns_dns_success

    To find what metrics a new measurement type is emitting, query Prometheus directly:

    curl -sk --user admin:<password> \
      'https://[<prometheus-mesh-ipv6>]:9090/api/v1/label/__name__/values' | \
      python3 -m json.tool | grep piccolo

    Then add a new panel to the Grafana dashboard using the relevant metric name. Use source, target, and site label filters to match the dashboard variable selectors already in use by existing panels.

Certificate renewal

Certbot's renewal timer (piccolo-cert-renew.timer) fires daily on the control node. When a cert is actually renewed (within 30 days of expiry), the deploy hook automatically runs cert-distribute.yml and config-server.yml, which copy the new cert to every host and restart affected services only if the cert content changed.

To force an immediate renewal check:

ansible-playbook -i inventory/hosts.ini playbooks/cert-issue.yml \
  --ask-vault-pass

To distribute a cert that was renewed outside Ansible (e.g., manually via certbot):

ansible-playbook -i inventory/hosts.ini playbooks/cert-distribute.yml \
  --ask-vault-pass

Updating piccolo-perf or Prometheus

Change piccolo_perf_version in roles/piccolo_perf/defaults/main.yml or prometheus_version in roles/prometheus_hardened/defaults/main.yml and re-run site.yml. The install tasks check the currently installed version and only download and replace the binary if the version doesn't match.

Rotating the Prometheus password

  1. Generate a new bcrypt hash: htpasswd -nbBC 10 admin '<new-password>'
  2. Update prometheus_basic_auth_password_hash and prometheus_basic_auth_password in the vault.
  3. Run ansible-playbook -i inventory/hosts.ini playbooks/prometheus.yml --ask-vault-pass. The web.config.yml template will be redeployed and Prometheus restarted.

Re-running after config changes

All roles are fully idempotent. Re-running site.yml at any time is safe — hosts already at the correct state see zero changes. Use targeted playbooks to limit scope:

# Only update piccolo-perf across the fleet
ansible-playbook -i inventory/hosts.ini playbooks/piccolo-perf.yml --ask-vault-pass

# Only update Prometheus scrape config and auth
ansible-playbook -i inventory/hosts.ini playbooks/prometheus.yml --ask-vault-pass

# Only regenerate the fleet config JSON
ansible-playbook -i inventory/hosts.ini playbooks/config-server.yml --ask-vault-pass

Non-VPN (native IPv6) deployment

For hosts reachable via native public IPv6 with no VPN mesh:

  1. In inventory/hosts.ini, use probe_address= with the host's public IPv6 address:

    [piccolo_perf]
    probe-a  ansible_host=probe-a.example.net  probe_address=2001:db8::1  site="site-a"
  2. In inventory/group_vars/all.yml, set firewall_mode and allowed_source_prefixes:

    firewall_mode: source_prefix
    allowed_source_prefixes:
      - 2001:db8::/32   # your fleet's IPv6 prefix
    mesh_vpn_services: []  # no VPN daemons to wait for
  3. Run site.yml normally — all other configuration is unchanged.

Mixed fleets (some hosts on VPN, some on native IPv6) are not supported in a single inventory. Use separate inventories with different firewall_mode values.


Configuration reference

inventory/group_vars/all.yml — fleet-wide

Variable Default Description
mesh_iface wt0 Primary mesh interface used to resolve this host's mesh IPv6 address
mesh_domain mesh.example.net Internal DNS domain
allowed_mesh_ifaces [wt0, tailscale0] All interfaces whose inbound traffic should reach fleet services. Accepts any interface type: wt0, tailscale0, eth0, zt0, wg0, etc. nftables allows traffic from any listed interface and drops all others on fleet ports.
mesh_vpn_services [tailscaled.service, netbird.service] systemd units listed in After=/Wants= for fleet services. Use Wants= (not BindsTo=) so services survive a VPN restart.
firewall_mode interface Controls nftables port restriction. interface: allow from interfaces in allowed_mesh_ifaces (VPN). source_prefix: allow from IPv6 prefixes in allowed_source_prefixes (non-VPN). none: skip nftables entirely. mtls: skip nftables (future).
allowed_source_prefixes [] IPv6 CIDRs or addresses allowed when firewall_mode: source_prefix. Example: [2001:db8::/32, 2001:db8::1]
piccolo_perf_config_url https://config.<mesh_domain>:8443/... URL piccolo-perf fetches its config from. Override if the default DNS name doesn't exist.
piccolo_perf_measurements see file List of measurement configs served to all piccolo-perf nodes

Fleet host topology (names, addresses, sites) is defined entirely in inventory/hosts.ini via probe_address= and site= host variables on each [piccolo_perf] entry — there is no separate piccolo_perf_hosts list.

roles/piccolo_perf/defaults/main.yml

Variable Default Description
piccolo_perf_version 1.0.7 Release tag to install
piccolo_perf_binary_path /usr/local/bin/piccolo-perf Install path
piccolo_perf_metrics_port 9862 Port the exporter listens on
piccolo_perf_twamp_port 862 Port for TWAMP measurements
piccolo_perf_probe_mode background piccolo-perf probe mode
piccolo_perf_tls_dir /etc/piccolo-perf/tls Where the cert and key live
piccolo_perf_cert_group piccolo-perf-cert Group with read access to TLS files
piccolo_perf_service_name piccolo-perf-exporter systemd unit name
piccolo_perf_config_url https://config.<mesh_domain>:8443/piccolo-config.json URL piccolo-perf fetches its config from

roles/prometheus_hardened/defaults/main.yml

Variable Default Description
prometheus_version 3.13.1 Release to install
prometheus_binary_path /usr/local/bin/prometheus Install path
prometheus_config_dir /etc/prometheus Config directory
prometheus_data_dir /var/lib/prometheus TSDB storage directory
tls_cert_dir_prometheus /etc/prometheus/tls Where the cert and key live
prometheus_web_port 9090 Port Prometheus listens on

Set in inventory/group_vars/prometheus_hosts.yml:

Variable Description
prometheus_basic_auth_username Username for basic auth (default admin)
prometheus_basic_auth_password_hash bcrypt hash — must be in Vault

roles/config_server/defaults/main.yml

Variable Default Description
config_server_port 8443 HTTPS port nginx listens on
config_server_www_dir /etc/piccolo-fleet/www Document root
config_server_tls_dir /etc/piccolo-fleet/tls TLS cert location
config_server_remove_default_site false Remove /etc/nginx/sites-enabled/default; set true only on hosts where no other service (e.g. Grafana) uses that vhost

Co-locating with Grafana or other nginx vhosts: the role deploys a single named vhost (piccolo-fleet-config-server.conf) on port 8443 bound exclusively to the mesh IPv6 address. It does not touch other vhosts. Set config_server_remove_default_site: false (the default) to leave any existing Grafana or default vhost in place. The systemd drop-in uses Wants= rather than BindsTo= for the mesh VPN service so that nginx stays up (and Grafana keeps serving) even if the mesh interface goes down.

First-run ordering: nginx is only started by the role when the TLS cert is already present at config_server_tls_dir. On a fresh host, run cert-issue.yml first; the deploy hook calls cert-distribute.yml, which copies the cert and reloads nginx automatically. If you run site.yml before issuing a cert, the role enables nginx but does not start it — re-run after cert distribution to bring it up.

roles/cert_issuer/defaults/main.yml

Variable Default Description
cert_issuer_acme_email admin@<mesh_domain> Email for Let's Encrypt registration
cert_issuer_dns_plugin_name cloudflare certbot DNS plugin name
cert_issuer_dns_credentials_path /etc/letsencrypt/dns-credentials.ini Where credentials file is written
cert_issuer_dns_credentials_content "" Must be in Vault — credentials file contents
cert_issuer_dns_propagation_seconds 60 Seconds to wait for DNS TXT record propagation before ACME validation
cert_issuer_deploy_hook_path /usr/local/sbin/piccolo-cert-deploy-hook.sh Script certbot calls on renewal
cert_issuer_playbook_dir /opt/piccolo-fleet Project root on the control node
cert_issuer_inventory_path inventory/hosts.ini Inventory path used by the deploy hook

Security design

Bind address as the primary control. Every service binds to a specific IPv6 address (mesh_ipv6, resolved from ansible_facts[mesh_iface]), not to 0.0.0.0 or ::. If the mesh interface goes down, services fail to bind rather than silently falling back to a public address.

nftables as defense-in-depth. Each role deploys a named nftables table that drops inbound traffic to its port(s) from any interface other than mesh_iface. This protects against a bind-address misconfiguration surviving a restart and against other processes opening the same ports on unintended interfaces. Loopback (lo) is always allowed for local health checks.

Single-credential ACME issuance. The DNS provider API token lives on the control node only, in Ansible Vault. Fleet hosts never hold credentials capable of issuing certificates. The cert is pushed to hosts after issuance; cert-distribute.yml restarts services only when the cert content actually changes (idempotent).

No root for piccolo-perf. The exporter unit uses DynamicUser=yes and NoNewPrivileges=true, acquiring only the capabilities it needs (CAP_NET_BIND_SERVICE, CAP_NET_RAW) via AmbientCapabilities.

Prometheus locked down on adopt. The prometheus_hardened role handles the "adopt an existing exposed install" case: if Prometheus is already running bound to 0.0.0.0:9090 with no TLS, the role replaces its systemd unit and restarts it. The Molecule scenario exercises exactly this path — prepare.yml intentionally installs a stale, insecure Prometheus, and the verify assertions confirm it is remediated.


Troubleshooting

Spot-check a host's metrics

Check what metric names piccolo-perf is currently emitting to a Prometheus instance:

curl -sk --user admin:<password> \
  'https://[<prometheus-mesh-ipv6>]:9090/api/v1/label/__name__/values' | \
  python3 -m json.tool | grep piccolo

Query a specific metric across all sources (replace piccolo_twamp_rtt_avg_ms with any metric name from the list above):

curl -sk --user admin:<password> \
  'https://[<prometheus-mesh-ipv6>]:9090/api/v1/query?query=piccolo_twamp_rtt_avg_ms' | \
  python3 -m json.tool | grep -E '(source|target|site|value)' | head -40

Query metrics for a specific source host:

curl -sk --user admin:<password> \
  'https://[<prometheus-mesh-ipv6>]:9090/api/v1/query?query=piccolo_twamp_rtt_avg_ms{source="attic-probe"}' | \
  python3 -m json.tool | grep -E '(target|value)'

Check a host's exporter directly (bypasses Prometheus entirely — useful to confirm piccolo-perf is running and collecting before Prometheus has scraped it):

curl -sk https://[<host-mesh-ipv6>]:9862/metrics | grep piccolo_twamp | head -20

Check which targets Prometheus is currently scraping and their health:

curl -sk --user admin:<password> \
  'https://[<prometheus-mesh-ipv6>]:9090/api/v1/targets' | \
  python3 -m json.tool | grep -E '(scrapeUrl|health|lastError)' | paste - - -

New measurements not appearing in Grafana

New measurement types produce new metric names (piccolo_<type>_*) that do not automatically appear in existing Grafana panels. After adding a new measurement type to piccolo_perf_measurements and redeploying:

  1. Confirm the metrics exist in Prometheus using the label query above.
  2. Add new panels to the Grafana dashboard manually using the new metric names.
  3. Use source, target, and site label filters to match existing dashboard variables.

Prometheus not scraping a host

# Check target health on a Prometheus host
curl -sk --user admin:<password> \
  'https://[<prometheus-mesh-ipv6>]:9090/api/v1/targets' | \
  python3 -m json.tool | grep -E '(scrapeUrl|health|lastError)' | paste - - -

# Check nftables is allowing traffic on the target host
ssh configbot@<host> sudo nft list table inet piccolo_fleet_perf

# Check piccolo-perf is running on the target host
ssh configbot@<host> sudo systemctl status piccolo-perf-exporter --no-pager | head -5

piccolo-perf exporter not starting

ssh configbot@<host> sudo journalctl -u piccolo-perf-exporter -n 20 --no-pager

Common causes:

  • Cannot load TLS cert/key — cert not distributed yet; run cert-distribute.yml --limit <host>
  • Cannot fetch initial config — config server unreachable or returning invalid JSON; check piccolo_perf_config_url and verify with curl -sk <url>
  • JSON parse error: cannot unmarshal number into sitesite= value in hosts.ini is unquoted integer; change to site="809"

Role testing

Each role ships a Molecule scenario using the Docker driver with a systemd-capable Debian 12 image. Tests include prepare → converge → idempotency check → verify.

# Run a single role's full suite
cd roles/piccolo_perf && molecule test
cd roles/config_server && molecule test
cd roles/prometheus_hardened && molecule test

# Or run all three in sequence from the project root
for r in piccolo_perf config_server prometheus_hardened; do
  (cd roles/$r && molecule test) || exit 1
done

The cert_issuer role has no Molecule scenario — DNS-01 issuance requires live DNS infrastructure and cannot run in CI containers. It is covered by ansible-lint and --syntax-check only. Test it manually against a real DNS zone (Let's Encrypt staging is a good choice) before relying on it in production.

Local environment notes

If Docker is managed by Colima on macOS, set DOCKER_HOST before running Molecule:

export DOCKER_HOST="unix://${HOME}/.colima/default/docker.sock"
molecule test

Linting

ansible-lint roles playbooks

Zero failures are expected. Warnings on handler naming and conditional tasks are acknowledged and suppressed in .ansible-lint.


Manual fallback

The upstream install.sh one-liner remains the fallback for any device outside Ansible's reach (OpenWrt routers, embedded hosts without Python):

curl -sSL https://raw.githubusercontent.com/retecolo/piccolo-perf/main/install.sh | sh

This installs the binary only. TLS, interface binding, and nftables hardening for such devices require manual follow-up and are outside the scope of this project.


Full design document

docs/superpowers/specs/2026-07-16-piccolo-perf-fleet-install-design.md

About

ansible work for mass deployment of piccolo-perf

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages