[refactor] moving cubes into own package"
Publish snapshot / snapshot (push) Successful in 1m2s

[fix] default parameter run records parameters in session for replay[fix] remove default parameters for several cubes
This commit is contained in:
Benjamin Diedrichsen
2026-07-28 12:18:10 +02:00
parent ac050c4459
commit 6ecb2c366f
130 changed files with 3386 additions and 520 deletions
@@ -0,0 +1,33 @@
# Tailscale Cube
Installs and authenticates the Tailscale client on a Linux host.
## Features
- **Automated Installation**: Adds the official Tailscale repository and installs the package.
- **Headless Authentication**: Uses a Tailscale Auth Key for zero-interaction setup.
- **Headscale Support**: Can be configured to connect to a custom login server.
- **Startup persistence**: Ensures the `tailscaled` daemon is enabled and running.
## Configuration
| Variable | Default | Description |
|----------|---------|-------------|
| `AUTH_KEY` | `""` | **Secret.** Tailscale Auth Key (recommended to use a 'reusable' or 'ephemeral' key). |
| `LOGIN_SERVER` | `https://controlplane.tailscale.com` | The coordination server URL. Set this to your Headscale instance URL if applicable. |
| `EXTRA_ARGS` | `""` | Additional flags to pass to `tailscale up` (e.g., `--advertise-exit-node`). |
| `FORCE_REAUTH` | `false` | If true, forces the client to re-authenticate. |
## Usage
```bash
nopy install tailscale
```
When prompted, provide your `AUTH_KEY`. If you are using Headscale, also provide the `LOGIN_SERVER` URL.
`AUTH_KEY` is declared in the manifest's `secrets`, so nopy keeps it out of session
and history files and masks it in any command it prints. It is asked for again on
replay, and a `--use-defaults` replay refuses rather than joining the tailnet with
an empty key. Prefer an ephemeral key regardless — the value is still on pyinfra's
command line while the deployment runs.
@@ -0,0 +1,45 @@
from pyinfra import host
from pyinfra.operations import server, apt
# Variables from manifest
AUTH_KEY = host.data.AUTH_KEY
LOGIN_SERVER = host.data.LOGIN_SERVER
EXTRA_ARGS = host.data.EXTRA_ARGS
FORCE_REAUTH = host.data.FORCE_REAUTH
# 1. Install Tailscale using the official one-liner script
server.shell(
name="Install Tailscale",
commands=["curl -fsSL https://tailscale.com/install.sh | sh"],
_sudo=True
)
# 2. Ensure Tailscale is enabled and running
# Manually (Linux): sudo systemctl enable --now tailscaled
server.service(
name="Ensure tailscaled is running and enabled on boot",
service="tailscaled",
running=True,
enabled=True,
_sudo=True
)
# 3. Authenticate and bring Tailscale up
# We use --authkey for headless mode
# We use --login-server if it's different from the default
up_command = f"tailscale up --authkey {AUTH_KEY}"
if LOGIN_SERVER and LOGIN_SERVER != "https://controlplane.tailscale.com":
up_command += f" --login-server {LOGIN_SERVER}"
if FORCE_REAUTH:
up_command += " --force-reauth"
if EXTRA_ARGS:
up_command += f" {EXTRA_ARGS}"
server.shell(
name="Authenticate Tailscale (Headless)",
commands=[up_command],
_sudo=True
)
@@ -0,0 +1,18 @@
import { Manifest } from '@bitsquare/nopy-cube';
import { z } from 'zod';
export default Manifest({
id: 'net:tailscale',
name: 'Install and authenticate Tailscale',
dependencies: () => ['apt:essentials'],
secrets: ['AUTH_KEY'],
schema: z.object({
AUTH_KEY: z.string().describe('Tailscale Auth Key for headless authentication').default(''),
LOGIN_SERVER: z
.string()
.describe('Custom login server (e.g., for Headscale)')
.default('https://controlplane.tailscale.com'),
EXTRA_ARGS: z.string().describe('Additional arguments for tailscale up').default(''),
FORCE_REAUTH: z.boolean().describe('Force re-authentication').default(false),
}),
});
@@ -0,0 +1,243 @@
# WiFi Access Point Cube (NetworkManager)
Configures a Linux device as a WiFi Access Point using NetworkManager's `nmcli` command. **This approach allows the device to simultaneously act as an AP AND remain connected to another WiFi network as a client.**
## Key Features
-**Dual WiFi Mode**: Acts as AP while staying connected to another WiFi
-**Modern Approach**: Uses NetworkManager (nmcli) instead of hostapd
-**Auto-start**: Configured to start on boot
-**IP Sharing**: Built-in internet sharing with `ipv4.method shared`
-**Simple Configuration**: Minimal parameters, maximum functionality
-**Supports 2.4GHz and 5GHz bands**
## Requirements
- Device with WiFi capability (e.g., Raspberry Pi 4/5)
- NetworkManager installed and running
- WiFi hardware that supports AP mode
## Configuration Parameters
> **This section is out of date** — it lists parameters the manifest does not
> declare (`NETWORK_DEVICE`, `CHANNEL`, `IP_ADDRESS`) and omits `AP_IP`. Read
> `manifest.mjs` for the real list. Tracked as §5 in the repository's
> `DOCS-AUDIT.md`.
### Prompted first
- **SSID**: WiFi network name (1-32 characters). Defaults to `PiPoint`.
- **PASSWORD**: WPA2 password (8-63 characters). Defaults to `1223334444` — a
placeholder that should not survive contact with a real network.
Declared in the manifest's `secrets`, so nopy keeps it out of session and
history files and masks it in printed commands, and re-prompts on replay. The
value is still on pyinfra's command line, so it is visible in `ps` during the run.
### Optional (with defaults)
- **NETWORK_DEVICE**: WiFi interface to use (`wlan0` default)
- **BAND**: Frequency band - `2.4GHz` or `5GHz` (auto-detected from current connection if not specified)
- **CHANNEL**: WiFi channel (auto-detected from current connection if not specified)
- 2.4GHz: 1-14
- 5GHz: 36-165
- **IP_ADDRESS**: AP gateway address (`192.168.50.1` default)
- **CONNECTION_NAME**: NetworkManager connection name (`net:wifi:ap` default)
## Example Usage
### Basic Configuration
```json
{
"SSID": "MyHomeWiFi",
"PASSWORD": "SecurePass123"
}
```
### Advanced Configuration (5GHz with specific channel)
```json
{
"SSID": "FastWiFi5G",
"PASSWORD": "SuperSecure456",
"NETWORK_DEVICE": "wlan1",
"BAND": "5GHz",
"CHANNEL": 36,
"IP_ADDRESS": "10.0.0.1",
"CONNECTION_NAME": "my-hotspot"
}
```
### Auto-Detection Configuration
When `BAND` and `CHANNEL` are not specified, the script will automatically detect the current AP's band and channel using `iw dev <interface> link` and use those values for the new hotspot. This is useful when you want the hotspot to operate on the same band/channel as your existing connection to avoid interference.
```json
{
"SSID": "MyAutoAP",
"PASSWORD": "SecurePass123"
}
```
## What This Cube Does
1. **Installs NetworkManager** (if not present)
2. **Enables NetworkManager service** and ensures it's running
3. **Auto-detects current AP settings** using `iw dev <interface> link` to find the band and channel
4. **Creates WiFi hotspot** using `nmcli` with auto-detected or specified settings
5. **Configures IP sharing** with `ipv4.method shared` (automatic DHCP + NAT)
6. **Enables IP forwarding** for internet routing
7. **Sets autoconnect** so the AP starts on boot
## How It Works (Dual WiFi Mode)
### Simultaneous AP + Client Mode
**Hardware Support Required:**
- Your WiFi hardware must support **simultaneous AP+STA (Station) mode**
- Most modern WiFi chips support this (e.g., Raspberry Pi 4/5, Intel WiFi cards)
- Older hardware may not support it (e.g., Raspberry Pi 3B and earlier have limitations)
**How NetworkManager Handles It:**
1. **Single Interface (e.g., wlan0):**
- If hardware supports AP+STA: ✅ **Both client and AP run on same interface**
- If hardware doesn't support it: ⚠️ **Client connection may be dropped**
2. **Multiple Interfaces (e.g., wlan0 + wlan1):**
- NetworkManager will use one for client, another for AP
- Always works regardless of hardware capabilities
**Example Scenario (Raspberry Pi 4):**
```
[Internet] <--WiFi--> [RPi4 wlan0 (Client+AP)] <--WiFi--> [Devices connect to AP]
```
**How to Check Hardware Support:**
```bash
iw list | grep -A 10 "valid interface combinations"
```
Look for: `* #{ managed } <= 1, #{ AP } <= 1` or `* #{ managed, AP } <= 2`
NetworkManager will:
- Share internet from **any** available source (WiFi client, Ethernet, cellular, etc.)
- Automatically handle routing and NAT
- Try to maintain client connection if hardware supports it
## Post-Installation
After deployment, the device will:
- ✅ Broadcast the WiFi network with your SSID
- ✅ Accept connections with your password
- ✅ Assign IP addresses to connected clients (via built-in DHCP)
- ✅ Share internet connection from any available interface
- ✅ Auto-start the AP on every boot
- ✅ Maintain client WiFi connection (if connected to another network)
## Managing the Hotspot
### View connection status
```bash
nmcli connection show
```
### Stop the hotspot
```bash
sudo nmcli connection down net:wifi:ap
```
### Start the hotspot
```bash
sudo nmcli connection up net:wifi:ap
```
### Disable autostart
```bash
sudo nmcli connection modify net:wifi:ap connection.autoconnect no
```
### Delete the hotspot
```bash
sudo nmcli connection delete net:wifi:ap
```
## Troubleshooting
### AP doesn't start
- Check NetworkManager status: `sudo systemctl status NetworkManager`
- Verify WiFi interface exists: `nmcli device status`
- Check if AP mode is supported: `iw list | grep -A 10 "Supported interface modes"`
### Can't connect to AP
- Verify password is correct (8+ characters)
- Check channel compatibility with your devices
- Try switching between 2.4GHz and 5GHz bands
### No internet on clients
- Verify host device has internet: `ping 8.8.8.8`
- Check IP forwarding: `sysctl net.ipv4.ip_forward`
- NetworkManager should handle NAT automatically with `ipv4.method shared`
### Hotspot conflicts with client WiFi
- This shouldn't happen with NetworkManager
- If it does, check if hardware supports simultaneous AP+STA mode:
```bash
iw list | grep "valid interface combinations"
```
## Advantages Over Traditional Approach
| Feature | Traditional (hostapd) | NetworkManager (nmcli) |
|---------|----------------------|------------------------|
| **Dual WiFi** | ❌ No (conflicts with wpa_supplicant) | ✅ Yes (AP + client simultaneously) |
| **Configuration** | Complex (multiple files) | Simple (one command) |
| **DHCP** | Manual (dnsmasq) | Automatic |
| **NAT** | Manual (iptables) | Automatic |
| **Management** | Multiple services | Single service |
| **Dependencies** | hostapd, dnsmasq, iptables | NetworkManager only |
## Security Notes
- Always use a strong password (minimum 8 characters)
- WPA2 encryption is automatically enabled
- NetworkManager handles firewall rules automatically
## Compatibility
Tested on:
- Raspberry Pi 4/5 with built-in WiFi
- Ubuntu/Debian-based systems with NetworkManager
- Devices with WiFi hardware supporting AP mode
**Note**: Older Raspberry Pi models (3B and earlier) may have limitations with simultaneous AP + client mode due to hardware constraints.
## Troubleshooting
```bash
sudo nmcli con add type wifi con-name MyHotspot ifname wlan0 mode ap ssid YourNewAP ipv4.method shared wifi-sec.key-mgmt wpa-psk wifi-sec.psk "YourNewPassword"
sudo nmcli con up MyHotspot
sudo nmcli con down MyHotspot
sudo nmcli con mod MyHotspot wifi.band <band> wifi.channel <channel_number>
# Find the currently used band and channel
iw dev wlan0 link
```
@@ -0,0 +1,70 @@
from pyinfra.operations import apt, server, systemd
from pyinfra import host
# Extract variables with proper defaults
SSID = host.data.get('SSID')
PASSWORD = host.data.get('PASSWORD')
BAND = host.data.get('BAND', '2.4GHz')
CHANNEL = host.data.get('CHANNEL') # Optional
AP_IP = host.data.get('AP_IP')
CONNECTION_NAME = host.data.get('CONNECTION_NAME')
# Determine band configuration
if BAND == '5GHz':
BAND = 'a'
default_channel = 36 if not CHANNEL else CHANNEL
else: # 2.4GHz
BAND = 'bg'
default_channel = 6 if not CHANNEL else CHANNEL
# Install NetworkManager if not present
apt.packages(
name='Install NetworkManager',
packages=['network-manager'],
update=True,
_sudo=True
)
# Ensure NetworkManager is running
systemd.service(
name='Ensure NetworkManager is running',
service='NetworkManager',
running=True,
enabled=True,
_sudo=True
)
# Check if connection already exists and delete it
server.shell(
name='Delete existing AP connection if present',
commands=[f'nmcli connection delete {CONNECTION_NAME} || true'],
_sudo=True
)
# Add Wifi AP connection
server.shell(
name='Detect WiFi interface',
commands=[
f'sudo nmcli con add type wifi con-name {CONNECTION_NAME} ifname wlan0 mode ap ssid {SSID} ipv4.method shared wifi-sec.key-mgmt wpa-psk wifi-sec.psk "{PASSWORD}"',
# CRITICAL: Force WPA2 (rsn) instead of WPA (wpa)
f'sudo nmcli connection modify {CONNECTION_NAME} wifi-sec.proto rsn',
f'sudo nmcli connection modify {CONNECTION_NAME} ipv4.addresses {AP_IP}/24',
# Use AES encryption (CCMP) instead of TKIP
f'sudo nmcli connection modify {CONNECTION_NAME} wifi-sec.pairwise ccmp',
f'sudo nmcli connection modify {CONNECTION_NAME} wifi-sec.group ccmp',
# Set band
f'sudo nmcli connection modify {CONNECTION_NAME} 802-11-wireless.band {BAND}',
# Enable autostart
f'sudo nmcli connection modify {CONNECTION_NAME} connection.autoconnect yes',
f'sudo nmcli connection modify {CONNECTION_NAME} connection.autoconnect-priority 10',
# Start the AP
f'sudo nmcli connection up {CONNECTION_NAME}'
],
_sudo=True
)
@@ -0,0 +1,21 @@
import { Manifest } from '@bitsquare/nopy-cube';
import { z } from 'zod';
export default Manifest({
id: 'net:wifi:access-point',
name: 'Configure WiFi Access Point (NetworkManager)',
dependencies: () => [],
secrets: ['PASSWORD'],
schema: z.object({
SSID: z.string().min(1).max(32).default('PiPoint').describe('WiFi network name (SSID)'),
PASSWORD: z
.string()
.min(8)
.max(63)
.default('1223334444')
.describe('WPA2 password (8-63 characters)'),
BAND: z.enum(['2.4GHz', '5GHz']).default('2.4GHz').describe('Frequency band'),
AP_IP: z.string().default('192.168.4.1').describe('AP IP address'),
CONNECTION_NAME: z.string().default('pi-point').describe('NetworkManager connection name'),
}),
});
@@ -0,0 +1,59 @@
# network:wifi:connection
**Configure a WiFi client connection using NetworkManager**
## Purpose
This cube allows you to connect your target host (e.g., a Raspberry Pi or a laptop) to an existing WiFi network using `nmcli`.
## What This Cube Does
1. Ensures `network-manager` is installed and running
2. Removes any existing connection with the same name to avoid conflicts
3. Connects to the specified `SSID` using the provided `PASSWORD`
4. Configures the connection to automatically connect on boot (optional)
## Configuration
| Variable | Type | Description | Required | Default |
| :--- | :--- | :--- | :--- | :--- |
| `SSID` | `string` | The WiFi network name | Yes | - |
| `PASSWORD` | `string` | The WiFi password | Yes | - |
| `AUTOCONNECT` | `boolean` | Automatically connect to this network | No | `true` |
| `CONNECTION_NAME` | `string` | Name for the connection in NetworkManager | No | `SSID` |
## Dependencies
- `apt/essentials`: Basic system utilities.
## Usage
### Simple Connection
```bash
nopy install network:wifi:connection --env SSID="MyHomeWiFi" --env PASSWORD="mysecurepassword"
```
### Connection with Custom Name and No Autoconnect
```bash
nopy install network:wifi:connection --env SSID="OfficeWiFi" --env PASSWORD="password123" --env CONNECTION_NAME="Work" --env AUTOCONNECT=false
```
## Security Notes
- `PASSWORD` is declared in the manifest's `secrets`: nopy keeps it out of session
and history files and masks it in every command it prints. It is prompted for
again on replay.
- That covers what nopy writes, not everything. The value is still on pyinfra's
command line, so it is visible in `ps` while the deployment runs.
- WiFi passwords will be stored in `/etc/NetworkManager/system-connections/` on the target host.
- Passing passwords via `--env` may leave them in your local shell history.
## Troubleshooting
You can check the status of your WiFi connections on the target host with:
```bash
nmcli connection show
nmcli device status
```
@@ -0,0 +1,43 @@
from pyinfra import host
from pyinfra.operations import apt, server, systemd
# [agnt://cogen/cogen/network-wifi-connection-2]{cartridge: "ansiblings/cubes", action: "generated", status: "generated"}
"""
Deployment script for network:wifi:connection.
Uses nmcli to configure a WiFi client connection.
"""
SSID = host.data.SSID
PASSWORD = host.data.PASSWORD
AUTOCONNECT = "yes" if host.data.get('AUTOCONNECT', True) else "no"
CONNECTION_NAME = host.data.get('CONNECTION_NAME', SSID)
# Install NetworkManager if not present
apt.packages(
name='Install NetworkManager',
packages=['network-manager'],
update=True,
_sudo=True
)
# Ensure NetworkManager is running
systemd.service(
name='Ensure NetworkManager is running',
service='NetworkManager',
running=True,
enabled=True,
_sudo=True
)
# Add/Update the WiFi connection
# We delete first to ensure a clean state with the new password/settings
server.shell(
name=f"Configure WiFi connection for {SSID}",
commands=[
f'nmcli connection delete "{CONNECTION_NAME}" || true',
f'nmcli device wifi connect "{SSID}" password "{PASSWORD}" name "{CONNECTION_NAME}"',
f'nmcli connection modify "{CONNECTION_NAME}" connection.autoconnect {AUTOCONNECT}',
],
_sudo=True
)
@@ -0,0 +1,26 @@
import { Manifest } from '@bitsquare/nopy-cube';
import { z } from 'zod';
// [agnt://cogen/cogen/network-wifi-connection-1]{cartridge: "ansiblings/cubes", action: "generated", status: "generated"}
/**
* Manifest for the network:wifi:connection cube.
* Configures a WiFi client connection using NetworkManager (nmcli).
*/
export default Manifest({
id: 'net:wifi:connection',
name: 'network:wifi:connection - Connect to a WiFi network',
secrets: ['PASSWORD'],
schema: z.object({
SSID: z.string().min(1).describe('The SSID of the WiFi network to connect to'),
PASSWORD: z.string().min(8).describe('The password for the WiFi network'),
AUTOCONNECT: z
.boolean()
.default(true)
.describe('Whether to automatically connect to this network'),
CONNECTION_NAME: z
.string()
.optional()
.describe('Optional name for the connection (defaults to SSID)'),
}),
});