streamline package naming

This commit is contained in:
Benjamin Diedrichsen
2026-07-29 13:07:34 +02:00
parent 1ba1c2a32a
commit 7e703c93b1
100 changed files with 141 additions and 139 deletions
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 bitsquare
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+59
View File
@@ -0,0 +1,59 @@
# @bitsquare/nopy-cubes-core
The core cube bundle for [nopy](https://www.npmjs.com/package/@bitsquare/nopy):
base packages, users, SSH, firewalling, networking, web serving and runtimes.
## Install
```sh
pnpm add -D @bitsquare/nopy-cubes-core
```
Then name it in `.nopyrc.json`:
```json
{
"hosts": ["web-1"],
"cubePackages": ["@bitsquare/nopy-cubes-core"]
}
```
`nopy` resolves the package from the directory of the config file that named it
and scans its `cubes/` directory exactly as it scans a `cubeDirs` entry. Nothing
has to be linked or copied.
## What is in it
| Area | Cube ids |
| ---------- | ------------------------------------------------------------------- |
| admin | `admin:cockpit`, `admin:hostname`, `admin:locale` |
| packages | `apt:essentials`, `apt:install` |
| hardening | `armor:fail2ban`, `armor:ssh`, `armor:ufw` |
| web | `caddy`, `caddy:spa` |
| source | `git:clone` |
| networking | `net:tailscale`, `net:wifi:access-point`, `net:wifi:connection` |
| runtimes | `runtime:docker`, `runtime:nodevm` |
| services | `service:autostart` |
| ssh | `ssh:authorize`, `ssh:keygen`, `ssh:keyman` |
| users | `user:add`, `user:edit` |
Run `nopy` and pick from the list, or `nopy -P` to print the pyinfra commands
without executing them. Each cube directory has its own `README.md`.
## Cube ids are global
An id such as `apt:essentials` is claimed repo-wide, not per bundle: two cubes
with the same id — whichever sources they came from — abort the run with an
error naming both. Prefix your own cubes distinctly if you also point
`cubeDirs` at a local tree.
## The bundle is read-only
Under pnpm the installed files are hardlinked into the global store, so a cube
that writes next to its own `deploy.py` corrupts that store for every project on
the machine. Cubes here write to `/tmp` or to the remote host, never to their
own directory.
## License
MIT
@@ -0,0 +1,76 @@
# cockpit
**Install Cockpit web-based server management interface**
## Purpose
This cube installs Cockpit, a powerful web-based interface for managing Linux servers, making system administration accessible through your browser.
## What is Cockpit?
Cockpit is a modern, interactive server admin interface that runs in your web browser. It provides:
- **Real-time monitoring**: CPU, memory, disk, and network usage graphs
- **Container management**: View and manage Docker containers
- **Service management**: Start, stop, and manage systemd services
- **Storage administration**: Manage disks, RAID, and filesystems
- **Network configuration**: Configure network interfaces and firewall
- **Terminal access**: Built-in terminal for command-line access
- **User management**: Create and manage user accounts
- **Software updates**: View and apply system updates
Think of it as a control panel for your Linux server - all accessible from any web browser.
## What This Cube Does
1. Installs the `cockpit` package
2. Installs `sscg` (Simple Signed Certificate Generator) for HTTPS support
3. Starts the Cockpit service
4. Makes Cockpit accessible on port 9090
## Configuration
This cube currently has no configurable parameters.
## Dependencies
None - this cube can run standalone.
## Post-Installation
Access Cockpit by navigating to:
```
https://your-server-ip:9090
```
Login with any valid system user account (e.g., root or a user created with the `user-add` cube).
### Security Notes
- Cockpit uses HTTPS by default (self-signed certificate)
- Your browser will show a security warning on first access (expected with self-signed certs)
- Consider using UFW to restrict access: `sudo ufw allow from YOUR_IP to any port 9090`
- Disable Cockpit when not in use: `sudo systemctl stop cockpit.socket`
## Common Use Cases
- Monitor server performance in real-time
- Manage Docker containers without command-line
- View system logs and troubleshoot issues
- Configure network settings
- Apply system updates
- Manage storage and filesystems
## Managing Cockpit
Start/stop Cockpit:
```bash
sudo systemctl start cockpit.socket
sudo systemctl stop cockpit.socket
sudo systemctl status cockpit.socket
```
Disable Cockpit from starting on boot:
```bash
sudo systemctl disable cockpit.socket
```
@@ -0,0 +1,13 @@
from pyinfra import host
from pyinfra.operations import server, apt
apt.packages(
packages=[ "sscg cockpit"],
present=True,
_sudo=True
)
server.service(
'cockpit',
running=True,
)
@@ -0,0 +1,7 @@
import { Manifest } from '@bitsquare/nopy-cubes';
export default Manifest({
id: 'admin:cockpit',
name: 'Install cockpit and utils',
dependencies: () => [],
});
@@ -0,0 +1,34 @@
from pyinfra import host
from pyinfra.operations import server, files
"""
Deployment script for the admin:hostname cube.
Uses pyinfra's server.hostname operation to set and persist the system hostname.
"""
HOSTNAME = host.data.HOSTNAME
if HOSTNAME:
server.hostname(
name=f"Set system hostname to {HOSTNAME}",
hostname=HOSTNAME,
_sudo=True,
)
# 2. Update /etc/hosts to prevent "unable to resolve host" errors
# This looks for the line starting with 127.0.1.1 and replaces it entirely
files.line(
name="Update /etc/hosts for local resolution",
path="/etc/hosts",
line=r"^127\.0\.1\.1\s+.*",
replace=f"127.0.1.1 {HOSTNAME}",
_sudo=True,
)
# 3. Restart Avahi (Network Broadcast)
# This pushes the name change out to the shared network
server.service(
name="Restart Avahi to broadcast new mDNS name",
service="avahi-daemon",
restarted=True,
_sudo=True,
)
@@ -0,0 +1,20 @@
import { Manifest, uniqid } from '@bitsquare/nopy-cubes';
import { z } from 'zod';
/**
* Manifest for the admin:hostname cube.
* This cube allows for setting and persistently changing the system's hostname.
*/
export default Manifest({
id: 'admin:hostname',
name: 'Permanently change the hostname',
dependencies: () => [],
schema: z.object({
HOSTNAME: z
.string()
.min(1)
.max(64)
.describe('The new hostname for the target host')
.default(`host-${uniqid()}`),
}),
});
@@ -0,0 +1,23 @@
# Cube: admin/locale
Configures system keyboard layout permanently by updating `/etc/default/keyboard` and using `localectl`.
## Configuration
- `LAYOUT` (string): Keyboard layout (e.g. "ch", "us", "de"). Default: "ch".
- `MODEL` (string): Keyboard model. Default: "pc105".
- `VARIANT` (string): Keyboard variant. Default: "".
- `OPTIONS` (string): Keyboard options (comma separated). Default: "".
## Usage
```javascript
import { Manifest } from '@bitsquare/nopy-cubes';
export default Manifest({
name: 'My Host Setup',
dependencies: () => [
['admin:locale', { LAYOUT: 'de' }]
]
});
```
@@ -0,0 +1,55 @@
from pyinfra import host
from pyinfra.operations import server, files
LAYOUT = host.data.LAYOUT
MODEL = host.data.MODEL
VARIANT = host.data.VARIANT
OPTIONS = host.data.OPTIONS
# Update /etc/default/keyboard
files.line(
name="Update XKBMODEL in /etc/default/keyboard",
path="/etc/default/keyboard",
line=r'^XKBMODEL=.*',
replace=f'XKBMODEL="{MODEL}"',
_sudo=True,
)
files.line(
name="Update XKBLAYOUT in /etc/default/keyboard",
path="/etc/default/keyboard",
line=r'^XKBLAYOUT=.*',
replace=f'XKBLAYOUT="{LAYOUT}"',
_sudo=True,
)
files.line(
name="Update XKBVARIANT in /etc/default/keyboard",
path="/etc/default/keyboard",
line=r'^XKBVARIANT=.*',
replace=f'XKBVARIANT="{VARIANT}"',
_sudo=True,
)
files.line(
name="Update XKBOPTIONS in /etc/default/keyboard",
path="/etc/default/keyboard",
line=r'^XKBOPTIONS=.*',
replace=f'XKBOPTIONS="{OPTIONS}"',
_sudo=True,
)
# Apply keyboard configuration
server.shell(
name="Apply keyboard setup",
commands=["setupcon", "service keyboard-setup restart"],
_sudo=True,
)
# Set X11 keyboard layout using localectl if available
server.shell(
name="Set X11 keyboard layout using localectl",
commands=[f"localectl set-x11-keymap {LAYOUT} {MODEL} '{VARIANT}' '{OPTIONS}'"],
_sudo=True,
_ignore_errors=True,
)
@@ -0,0 +1,14 @@
import { Manifest } from '@bitsquare/nopy-cubes';
import { z } from 'zod';
export default Manifest({
id: 'admin:locale',
name: 'Configure system locale and keyboard layout',
dependencies: () => [],
schema: z.object({
LAYOUT: z.string().describe('Keyboard layout (e.g. "ch", "us", "de")').default('ch'),
MODEL: z.string().describe('Keyboard model').default('pc105'),
VARIANT: z.string().describe('Keyboard variant').default(''),
OPTIONS: z.string().describe('Keyboard options (comma separated)').default(''),
}),
});
@@ -0,0 +1,39 @@
# apt:essentials
**Install essential packages**
## Purpose
This cube installs a curated collection of essential development tools and utilities that are commonly needed for server environments and development workflows.
## What This Cube Does
Installs the following packages via apt:
- **fish** - User-friendly command-line shell with autosuggestions and syntax highlighting
- **ranger** - Terminal-based file manager with vi key bindings
- **golang-go** - Go programming language compiler and tools
- **build-essential** - Essential compilation tools (gcc, g++, make, etc.)
- **python3** - Python 3 interpreter and standard library
- **pkg-config** - Helper tool for compiling applications and libraries
- **age** - Modern file encryption tool with small explicit keys
## Configuration
### Parameters
- **UPDATE** (boolean, default: `true`)
- If already installed, should packages be updated?
- Set to `false` to skip package cache updates and only install missing packages
## Dependencies
None - this cube can run standalone.
## Use Cases
This cube is ideal as a base dependency for other cubes that require:
- Basic development tools
- Modern shell environments
- File encryption capabilities
- Python or Go runtimes
@@ -0,0 +1,18 @@
from pyinfra.operations import apt
from pyinfra import host
UPDATE = host.data.UPDATE
apt.packages(
name='Install essentials',
packages=[
'fish',
'build-essential',
'python3',
'pkg-config',
'age'
],
update=UPDATE,
_sudo=True
)
@@ -0,0 +1,11 @@
import { Manifest } from '@bitsquare/nopy-cubes';
import { z } from 'zod';
export default Manifest({
id: 'apt:essentials',
name: 'Install essential packages',
dependencies: () => [],
schema: z.object({
UPDATE: z.boolean().describe('If already installed should packages be updated').default(true),
}),
});
@@ -0,0 +1,62 @@
# apt
**Install packages with apt**
## Purpose
This is a generic cube for installing custom packages via the apt package manager. It's useful when you need to install specific packages that aren't covered by other specialized cubes.
## What This Cube Does
1. Optionally updates the apt package cache
2. Installs the specified space-separated list of packages
## Configuration
### Parameters
- **UPDATE** (boolean, default: `true`)
- Update package cache before installing
- Set to `false` to skip updating and only install packages
- **PACKAGES** (string, default: `''`)
- Space-separated list of packages to install
- Example: `"vim git htop curl wget"`
## Dependencies
None - this cube can run standalone.
## Use Cases
Install development tools:
```
PACKAGES="vim git htop tmux"
```
Install database clients:
```
PACKAGES="postgresql-client mysql-client redis-tools"
```
Install system utilities:
```
PACKAGES="curl wget jq unzip zip"
```
## Example
When deploying this cube, you would typically configure it like:
```javascript
exec('apt', {
PACKAGES: 'nginx certbot python3-certbot-nginx',
UPDATE: true
})
```
## Notes
- Package names must match exact apt package names
- Invalid package names will cause the installation to fail
- Use `apt search <package>` to find package names
- Some packages may require additional configuration after installation
@@ -0,0 +1,31 @@
from pyinfra.operations import apt
from pyinfra import host
UPDATE = host.data.UPDATE
PACKAGES = str(host.data.PACKAGES).split(' ')
apt.packages(
name='Install essential packages',
packages=[
'htop',
'age',
'git',
'curl',
'nano',
'wget',
'ca-certificates',
'ufw',
"gnupg",
"lsb-release"
],
update=UPDATE,
_sudo=True
)
apt.packages(
name='Install custom packages',
packages=[p.strip() for p in PACKAGES if p],
update=UPDATE,
_sudo=True
)
@@ -0,0 +1,15 @@
import { Manifest } from '@bitsquare/nopy-cubes';
import { z } from 'zod';
export default Manifest({
id: 'apt:install',
name: 'Install packages with apt',
dependencies: () => [],
schema: z.object({
UPDATE: z.boolean().describe('Update package cache before installing').default(false),
PACKAGES: z
.string()
.describe('Space-separated list of packages to install')
.default('vim htop curl'),
}),
});
@@ -0,0 +1,41 @@
# armor-fail2ban
**Install and enable fail2ban**
## Purpose
This cube installs and configures Fail2ban, an intrusion prevention software that protects your server from brute-force attacks and unauthorized access attempts.
## What is Fail2ban?
Fail2ban monitors log files (e.g., `/var/log/auth.log`) for suspicious activity, such as repeated failed login attempts. When it detects malicious behavior patterns, it automatically:
- Bans the offending IP address by updating firewall rules
- Prevents the attacker from making further connection attempts
- Can send email notifications about bans (if configured)
Common use cases include:
- Protecting SSH from brute-force password attacks
- Blocking repeated failed login attempts on web applications
- Preventing DoS attacks from specific IP addresses
## What This Cube Does
1. Installs the `fail2ban` package via apt
2. Deploys a custom configuration file (`jail.local`) to `/etc/fail2ban/jail.local`
3. Configures fail2ban with sensible defaults for common services
## Configuration
This cube currently has no configurable parameters. The default configuration is applied from the included `jail.local` file.
## Dependencies
None - this cube can run standalone.
## Notes
After deployment, you can:
- Check fail2ban status: `sudo fail2ban-client status`
- View banned IPs: `sudo fail2ban-client status sshd`
- Unban an IP: `sudo fail2ban-client set sshd unbanip <IP_ADDRESS>`
@@ -0,0 +1,18 @@
from pyinfra.operations import apt, files, server
# Install Fail2ban
apt.packages(
name='Install Fail2ban',
packages=['fail2ban'],
update=True,
_sudo=True
)
# Configure Fail2ban
files.put(
name='Configure Fail2ban',
src='jail.local',
dest='/etc/fail2ban/jail.local',
mode='0644',
_sudo=True
)
@@ -0,0 +1,8 @@
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
findtime = 600
bantime = 3600
@@ -0,0 +1,7 @@
import { Manifest } from '@bitsquare/nopy-cubes';
export default Manifest({
id: 'armor:fail2ban',
name: 'Install and enable fail2ban',
dependencies: () => [],
});
@@ -0,0 +1,83 @@
# armor-ssh
**Secure SSH server by disabling password authentication**
## Purpose
This cube hardens your SSH server configuration by disabling less secure authentication methods, enforcing SSH key-based authentication only.
## Why Disable Password Authentication?
Password-based SSH authentication is vulnerable to:
- **Brute-force attacks**: Automated scripts trying millions of password combinations
- **Dictionary attacks**: Guessing common passwords
- **Credential stuffing**: Using leaked passwords from other breaches
- **Weak passwords**: Users choosing easily guessable passwords
**SSH key authentication is more secure** because:
- Keys are cryptographically strong (2048+ bit keys vs 8-12 character passwords)
- Private keys never travel over the network
- Immune to brute-force attacks
- Can be protected with passphrases for additional security
## What This Cube Does
1. **Disables challenge-response authentication**
- Prevents keyboard-interactive authentication prompts
2. **Optionally disables password authentication** (default: enabled)
- Forces users to authenticate with SSH keys only
- Prevents password-based login attempts
3. **Optionally disables PAM** (Pluggable Authentication Modules)
- Disables PAM-based authentication methods
- Reduces attack surface
4. **Restarts SSH service**
- Applies the new configuration immediately
## Configuration
### Parameters
- **DISABLE_PASSWORD** (boolean, default: `true`)
- Disable password authentication for SSH connections
- ⚠️ **WARNING**: Ensure you have SSH key access configured before enabling this!
- **DISABLE_PAM** (boolean, default: `true`)
- Disable PAM (Pluggable Authentication Modules) for SSH
- Recommended for key-only authentication setups
## Dependencies
None - this cube can run standalone.
## Security Best Practices
**Before deploying this cube**:
1. Ensure you have SSH key authentication set up and tested
2. Keep an alternative access method available (console access, VNC, etc.)
3. Test SSH key login before disabling passwords
4. Consider using the `user-add` or `ssh-keyman` cubes first
**After deployment**:
- Only SSH key authentication will work
- Password login attempts will be rejected
- Make sure to back up your private SSH key securely
## Post-Installation
The SSH service will restart automatically. Your current SSH session will remain active, but new connections must use SSH keys.
To verify the configuration:
```bash
sudo grep -E "PasswordAuthentication|ChallengeResponseAuthentication|UsePAM" /etc/ssh/sshd_config
```
## Recovery
If you get locked out:
1. Access the server via console (physical or cloud provider's web console)
2. Edit `/etc/ssh/sshd_config`
3. Set `PasswordAuthentication yes`
4. Restart SSH: `sudo systemctl restart ssh`
@@ -0,0 +1,43 @@
from pyinfra.operations import files, server
from pyinfra import host
from pyinfra import config
import logging
DISABLE_PASSWORD=host.data.DISABLE_PASSWORD
DISABLE_PAM=host.data.DISABLE_PAM
logger = logging.getLogger(__name__)
config.SUDO = True
files.line(
name='Disable challenge-response authentication in SSH',
path='/etc/ssh/sshd_config',
line='ChallengeResponseAuthentication yes',
replace='ChallengeResponseAuthentication no',
)
if DISABLE_PASSWORD:
files.line(
name='Disable password authentication in SSH',
path='/etc/ssh/sshd_config',
line='PasswordAuthentication yes',
replace='PasswordAuthentication no',
)
else:
logger.info('Password authentication allowed')
if DISABLE_PAM:
files.line(
name='Disable PAM in SSH',
path='/etc/ssh/sshd_config',
line='UsePAM yes',
replace='UsePAM no',
)
# Restart SSH service
server.service(
'ssh',
running=True,
restarted=True,
reloaded=True
)
@@ -0,0 +1,18 @@
import { Manifest } from '@bitsquare/nopy-cubes';
import { z } from 'zod';
export default Manifest({
id: 'armor:ssh',
name: 'Secure SSH server by disabling password authentication',
dependencies: () => [],
schema: z.object({
DISABLE_PASSWORD: z
.boolean()
.describe('Disable password authentication for SSH connections')
.default(true),
DISABLE_PAM: z
.boolean()
.describe('Disable PAM (Pluggable Authentication Modules) for SSH')
.default(true),
}),
});
@@ -0,0 +1,77 @@
# armor-ufw
**Activate UFW (Uncomplicated Firewall)**
## Purpose
This cube configures and enables UFW, a user-friendly firewall management tool for Linux systems, providing basic protection against unauthorized network access.
## What is UFW?
UFW (Uncomplicated Firewall) is a frontend for `iptables` designed to make firewall configuration simple and accessible. It provides:
- **Easy-to-understand syntax**: Commands like `ufw allow ssh` instead of complex iptables rules
- **Default deny policy**: Blocks all incoming connections except those explicitly allowed
- **Connection tracking**: Automatically handles related and established connections
- **Application profiles**: Pre-configured rules for common services
Think of UFW as a security gate for your server - it controls which network traffic is allowed in and out.
## What This Cube Does
1. Configures UFW to allow SSH connections (port 22)
- Ensures you don't lock yourself out when enabling the firewall
2. Optionally allows HTTP traffic (port 80) based on the `ALLOW_HTTP` parameter
3. Enables the firewall with the configured rules
## Configuration
### Parameters
- **ALLOW_HTTP** (boolean, default: `true`)
- Allow incoming HTTP traffic on port 80
- Set to `false` if you're only using HTTPS or don't need web traffic
## Dependencies
- **apt:essentials** - Required for basic system tools
## Security Notes
**Important**: This cube automatically allows SSH to prevent lockouts. If you need to allow additional services, you can run:
```bash
sudo ufw allow [port number]/[protocol]
sudo ufw allow [service-name]
```
Examples:
- `sudo ufw allow 443/tcp` - Allow HTTPS
- `sudo ufw allow 3000/tcp` - Allow custom application port
- `sudo ufw allow https` - Allow HTTPS by service name
## Post-Installation
Check firewall status:
```bash
sudo ufw status verbose
sudo ufw status numbered
```
Common UFW commands:
- Delete rule: `sudo ufw delete [rule number]`
- Disable firewall: `sudo ufw disable`
- Reset to defaults: `sudo ufw reset`
## UFW File Locations
UFW rules are stored in the `/etc/ufw` directory:
- `/etc/ufw/user.rules` - Custom rules added via the `ufw` command
- `/etc/ufw/before.rules` - Rules processed before user rules (high priority)
- `/etc/ufw/after.rules` - Rules processed after user rules (exceptions)
- `/etc/ufw/sysctl.conf` - Kernel network parameters (e.g., packet forwarding)
- `/etc/ufw/applications.d/` - Application profiles for common services
- `/etc/default/ufw` - Global UFW settings and default policies
Understanding these locations is helpful for troubleshooting, manual edits, or backing up your firewall configuration.
@@ -0,0 +1,13 @@
from pyinfra.operations import server
from pyinfra import host
ALLOW_HTTP=host.data.ALLOW_HTTP
server.shell(
commands=[
f"ufw allow ssh",
f"ufw allow http" if ALLOW_HTTP else "",
f"ufw enable",
],
_sudo=True
)
@@ -0,0 +1,11 @@
import { Manifest } from '@bitsquare/nopy-cubes';
import { z } from 'zod';
export default Manifest({
id: 'armor:ufw',
name: 'Activate ufw (uncomplicated firewall)',
dependencies: () => ['apt:essentials'],
schema: z.object({
ALLOW_HTTP: z.boolean().describe('Allow incoming HTTP traffic on port 80').default(true),
}),
});
@@ -0,0 +1,120 @@
# caddy
**Install Caddy webserver**
## Purpose
This cube installs Caddy, a modern, powerful web server with automatic HTTPS that's designed to be easy to use and configure.
## What is Caddy?
Caddy is a next-generation web server that stands out for its simplicity and built-in security:
- **Automatic HTTPS**: Automatically obtains and renews SSL/TLS certificates from Let's Encrypt
- **Modern HTTP features**: HTTP/2, HTTP/3 (QUIC) support out of the box
- **Simple configuration**: Human-readable Caddyfile format
- **Reverse proxy**: Easy proxying to backend applications
- **Static file serving**: Fast and efficient static site hosting
- **Zero-downtime reloads**: Update config without dropping connections
## What This Cube Does
1. **Adds Caddy's official repository**
- Installs required dependencies (debian-keyring, apt-transport-https, curl)
- Downloads and installs Caddy's GPG signing key
- Configures APT to use Caddy's official stable repository
2. **Installs Caddy**
- Installs the latest stable version of Caddy
- Sets up the Caddy service
3. **Configures TLS**
- Creates a Caddyfile with a reusable TLS snippet
- Configures TLS based on the `TLS` parameter
## Configuration
### Parameters
- **TLS** (string, default: `''`)
- TLS certificate configuration
- **Options**:
- `''` (empty string) - Automatic HTTPS with Let's Encrypt (recommended)
- `'internal'` - Use Caddy's internal CA for self-signed certs (testing only)
- `'/path/to/cert /path/to/key'` - Provide custom certificate paths
## Dependencies
None - this cube can run standalone.
## TLS Configuration Examples
**Automatic HTTPS (Production)**:
```javascript
exec('caddy', { TLS: '' })
```
Caddy will automatically obtain SSL certificates from Let's Encrypt for your domain.
**Self-Signed for Testing**:
```javascript
exec('caddy', { TLS: 'internal' })
```
Uses Caddy's internal CA. Browsers will show security warnings.
**Custom Certificates**:
```javascript
exec('caddy', { TLS: '/etc/ssl/certs/mycert.pem /etc/ssl/private/mykey.pem' })
```
Use your own certificate and private key files.
## Post-Installation
The Caddyfile is created at `/etc/caddy/Caddyfile` with a reusable TLS snippet:
```
(tls_cert) {
tls {TLS}
}
```
Other cubes (like `caddy-spa`) can import this snippet with `import tls_cert`.
## Managing Caddy
Start/stop/restart Caddy:
```bash
sudo systemctl start caddy
sudo systemctl stop caddy
sudo systemctl restart caddy
sudo systemctl status caddy
```
Reload configuration without downtime:
```bash
sudo systemctl reload caddy
```
Test configuration:
```bash
caddy validate --config /etc/caddy/Caddyfile
```
## Common Use Cases
- Reverse proxy for Node.js/Python/Go apps
- Static website hosting
- API gateway
- Load balancer
- SSL/TLS termination
## Notes
- Caddy runs on ports 80 (HTTP) and 443 (HTTPS) by default
- Ensure these ports are open in your firewall (UFW)
- For automatic HTTPS, your domain must point to your server's IP
- Caddy automatically redirects HTTP to HTTPS when using automatic HTTPS
## Additional Resources
- [Caddy Documentation](https://caddyserver.com/docs/)
- [Caddyfile Tutorial](https://caddyserver.com/docs/caddyfile-tutorial)
@@ -0,0 +1,34 @@
from pyinfra.operations import apt, server, files
from pyinfra import host
from io import StringIO
# 🔹 Variables
TLS = host.data.TLS
server.shell(
commands=[
f"sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl",
f"curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg",
f"curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list"
]
)
apt.packages(
name='Install caddy',
packages=['caddy'],
update=True,
_sudo=True
)
TLS_BLOCK = f"""
(tls_cert) {{
tls {TLS}
}}
"""
files.put(
src = StringIO(TLS_BLOCK), # local filename to upload,
dest = '/etc/caddy/Caddyfile', # the remote filename to upload to
_sudo=True
)
@@ -0,0 +1,19 @@
import { Manifest } from '@bitsquare/nopy-cubes';
import { z } from 'zod';
export default Manifest({
id: 'caddy',
name: 'Install Caddy webserver',
dependencies: () => [],
schema: z.object({
// tls /path/to/cert.pem /path/to/key.pem
TLS: z
.string()
.default('')
.describe(`
<empty-string> --> enabled and automatically managed
internal --> use Caddy custom root CA for self-signed certs (for testing purpose)
/path/to/cert /path/to/key --> Provide custom certificates
`),
}),
});
@@ -0,0 +1,137 @@
# caddy-spa
**Configure Caddy to serve a Single Page Application**
## Purpose
This cube adds a reverse proxy configuration to Caddy for serving a Single Page Application (SPA) from a local backend server, with automatic HTTPS support.
## What This Cube Does
1. **Adds domain configuration to Caddyfile**
- Creates a site block for the specified domain
- Configures reverse proxy to forward traffic to local application
- Imports TLS configuration from the `caddy` cube
2. **Configures reverse proxy**
- Proxies all requests to `localhost:{PORT}`
- Preserves headers and client information
- Handles WebSocket connections
3. **Restarts Caddy service**
- Applies the new configuration immediately
## Configuration
### Parameters
- **DOMAIN** (string, default: `''`)
- Domain name for the SPA application
- Example: `'myapp.example.com'`
- Must have DNS pointing to your server's IP
- **PORT** (number, default: `5432`)
- Port number where the SPA will be served
- Your application should be listening on this port locally
## Dependencies
- **caddy** cube (implicitly required) - Must be installed first to provide the `tls_cert` snippet
## What Gets Configured
This cube adds the following to `/etc/caddy/Caddyfile`:
```
# BEGIN DOMAIN myapp.example.com
myapp.example.com {
import tls_cert
reverse_proxy localhost:5432
}
# END myapp.example.com
```
## Use Cases
**Deploy a React/Vue/Angular app**:
```javascript
exec('caddy-spa', {
DOMAIN: 'app.example.com',
PORT: 3000
})
```
**Deploy multiple SPAs**:
```javascript
exec('caddy-spa', { DOMAIN: 'app1.example.com', PORT: 3000 })
exec('caddy-spa', { DOMAIN: 'app2.example.com', PORT: 3001 })
exec('caddy-spa', { DOMAIN: 'app3.example.com', PORT: 3002 })
```
## How It Works
1. User visits `https://myapp.example.com`
2. Caddy receives the request on port 443 (HTTPS)
3. Caddy automatically handles SSL/TLS encryption
4. Request is forwarded to `localhost:5432`
5. Your application receives the request and returns a response
6. Caddy sends the encrypted response back to the user
## Prerequisites
Before deploying this cube:
1. **Install the caddy cube first**
- Provides the base Caddy installation and TLS configuration
2. **Ensure your application is running**
- Your SPA backend should be listening on the specified PORT
- Example: `npm start` or `pm2 start app.js`
3. **Configure DNS**
- Point your domain's A record to your server's IP address
- Wait for DNS propagation (can take a few minutes to hours)
4. **Open firewall ports**
- Ensure ports 80 and 443 are open (for automatic HTTPS)
- `sudo ufw allow 80/tcp`
- `sudo ufw allow 443/tcp`
## Post-Installation
Verify the configuration:
```bash
sudo caddy validate --config /etc/caddy/Caddyfile
```
Check Caddy status:
```bash
sudo systemctl status caddy
```
View Caddy logs:
```bash
sudo journalctl -u caddy -f
```
## Common Issues
**502 Bad Gateway**:
- Your application isn't running on the specified PORT
- Check: `netstat -tlnp | grep {PORT}`
**Certificate errors**:
- DNS not pointing to your server
- Ports 80/443 blocked by firewall
- Check Caddy logs: `sudo journalctl -u caddy -f`
**Domain not resolving**:
- DNS propagation not complete yet
- Verify with: `dig +short myapp.example.com`
## Notes
- Caddy automatically obtains and renews Let's Encrypt certificates
- The reverse proxy preserves the original client IP and headers
- WebSocket connections are automatically supported
- You can add multiple domains by running this cube multiple times with different parameters
@@ -0,0 +1,32 @@
from pyinfra.operations import apt, server, files
from pyinfra import host
from io import StringIO
# 🔹 Variables (Modify as Needed)
DOMAIN = host.data.DOMAIN
PORT = host.data.PORT
site_block = f"""# BEGIN DOMAIN {DOMAIN}
{DOMAIN} {{
import tls_cert
reverse_proxy localhost:{PORT}
}}
# END {DOMAIN}
"""
files.block(
path = '/etc/caddy/Caddyfile',
content = site_block,
present = True,
before = False,
after = False,
_sudo = True
)
server.service(
service='caddy',
running=True,
restarted=True,
_sudo=True
)
@@ -0,0 +1,12 @@
import { Manifest } from '@bitsquare/nopy-cubes';
import { z } from 'zod';
export default Manifest({
id: 'caddy:spa',
name: 'Install single page application',
dependencies: () => [],
schema: z.object({
DOMAIN: z.string().describe('Domain name for the SPA application').default(''),
PORT: z.number().describe('Port number where the SPA will be served').default(5432),
}),
});
@@ -0,0 +1,18 @@
from pyinfra.operations import server
from pyinfra import host
USER = host.data.USER
REPO = host.data.REPO
APP = host.data.APP
server.shell(
name="Clone application",
commands=[
f"""
cd $HOME &&
git clone --recurse-submodules {REPO} {APP}
"""],
_sudo=True,
_sudo_user=USER,
_use_sudo_login=True
)
@@ -0,0 +1,13 @@
import { Manifest } from '@bitsquare/nopy-cubes';
import { z } from 'zod';
export default Manifest({
id: 'git:clone',
name: 'Clone a repository',
dependencies: () => [],
schema: z.object({
USER: z.string().describe('Username for which to clone the repository').default('vagrant'),
REPO: z.string().default(''),
APP: z.string().describe('The internal name used for this application').default(''),
}),
});
@@ -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-cubes';
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-cubes';
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-cubes';
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)'),
}),
});
@@ -0,0 +1,60 @@
# docker
**Install Docker and tools**
## Purpose
This cube installs Docker Engine, Docker Compose, and popular Docker management tools to enable containerized application deployment and management.
## What is Docker?
Docker is a platform for developing, shipping, and running applications in containers. Containers package an application with all its dependencies, ensuring it runs consistently across different environments.
Key benefits:
- **Isolation**: Each container runs independently with its own filesystem, network, and processes
- **Portability**: Containers run the same way on any system that supports Docker
- **Efficiency**: Containers share the host OS kernel, making them lighter than virtual machines
- **Scalability**: Easily deploy and scale containerized applications
## What This Cube Does
1. **Adds Docker's official repository**
- Downloads and installs Docker's GPG key
- Configures APT to use Docker's official package repository
2. **Installs Docker components**
- `docker-ce` - Docker Community Edition engine
- `docker-ce-cli` - Command-line interface for Docker
- `containerd.io` - Container runtime
- `docker-buildx-plugin` - Extended build capabilities with BuildKit
- `docker-compose-plugin` - Tool for defining multi-container applications
3. **Installs management tools**
- **lazydocker** - Terminal UI for Docker and Docker Compose management
- **docker-ctop** - Container metrics and monitoring (top-like interface for containers)
## Configuration
This cube currently has no configurable parameters.
## Dependencies
None - this cube can run standalone.
## Post-Installation
After deployment:
- Add users to the `docker` group to run Docker without sudo: `sudo usermod -aG docker username`
- Start using Docker: `docker run hello-world`
- Use lazydocker for easy management: `lazydocker`
- Monitor containers: `ctop`
## Notes
The Docker daemon starts automatically on boot. You can manage it with systemd:
- Check status: `sudo systemctl status docker`
- Restart: `sudo systemctl restart docker`
## Additional Resources
- [Installation tutorial Ubuntu 24.04](https://www.cherryservers.com/blog/install-docker-ubuntu)
@@ -0,0 +1,75 @@
from pyinfra import host
from pyinfra.operations import server, apt
DISTRO = host.data.DISTRO
server.shell(
commands=[
"install -m 0755 -d /etc/apt/keyrings",
f"curl -fsSL https://download.docker.com/linux/{DISTRO}/gpg | gpg --dearmor --yes -o /etc/apt/keyrings/docker.gpg",
"sudo chmod a+r /etc/apt/keyrings/docker.gpg",
f""" echo "deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/{DISTRO} "$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null """,
"apt update"
],
_sudo=True
)
apt.packages(
packages=[
"docker-ce",
"docker-ce-cli",
"containerd.io",
"docker-buildx-plugin",
"docker-compose-plugin",
],
present=True,
_sudo=True
)
server.shell(
name="Install Lazydocker system-wide",
commands=[
'export DIR=/usr/local/bin && curl -fsSL https://raw.githubusercontent.com/jesseduffield/lazydocker/master/scripts/install_update_linux.sh | bash -s --'
],
_sudo=True
)
# Install required packages
apt.packages(
name="Install required dependencies",
packages=["ca-certificates", "curl", "gnupg", "lsb-release"],
update=True,
_sudo=True
)
# Download and store the GPG key
server.shell(
name="Download and store Azlux repo GPG key",
commands=[
"curl -fsSL https://azlux.fr/repo.gpg.key | gpg --dearmor --yes -o /usr/share/keyrings/azlux-archive-keyring.gpg"
],
_sudo=True
)
# Add the repository to sources.list
server.shell(
name="Add Azlux repository to APT sources",
commands=[
""" echo "deb [arch="$(dpkg --print-architecture)" signed-by=/usr/share/keyrings/azlux-archive-keyring.gpg] http://packages.azlux.fr/debian bookworm main" | sudo tee /etc/apt/sources.list.d/azlux.list > /dev/null """,
],
_sudo=True
)
# Update APT and install docker-ctop
apt.update(
name="Update package lists",
_sudo=True
)
apt.packages(
name="Install docker-ctop",
packages=["docker-ctop"],
_sudo=True
)
@@ -0,0 +1,11 @@
import { Manifest } from '@bitsquare/nopy-cubes';
import { z } from 'zod';
export default Manifest({
id: 'runtime:docker',
name: 'Install docker and tools',
dependencies: () => [],
schema: z.object({
DISTRO: z.enum(['ubuntu', 'debian']).default('ubuntu').describe('Linux distribution to target'),
}),
});
@@ -0,0 +1,83 @@
# nodevm
**Install Node.js with essential global packages**
## Purpose
This cube installs the latest LTS (Long Term Support) version of Node.js along with essential global npm packages commonly needed for development and deployment.
## What is Node.js?
Node.js is a JavaScript runtime built on Chrome's V8 engine that allows you to run JavaScript on the server. It's widely used for:
- Building web servers and APIs
- Command-line tools
- Build tools and task runners
- Real-time applications (chat, notifications)
- Microservices
## What This Cube Does
1. **Installs Node.js LTS**
- Downloads and runs the official NodeSource setup script
- Installs the latest LTS version of Node.js
- Includes npm (Node Package Manager)
2. **Installs build dependencies**
- `libssl-dev` - SSL/TLS libraries
- `libtool` - Library building tools
- `cmake` - Cross-platform build system
- `libpng-dev`, `libjpeg-dev`, `libvips-dev` - Image processing libraries
3. **Installs global npm packages**
- **npm@11.1.0** - Latest npm version
- **pm2** - Production process manager for Node.js apps
- **yarn** - Alternative package manager
- **local-web-server** - Local development web server
- **node-gyp** - Node.js native addon build tool
- **inquirer** - Interactive command-line prompts
- **execa** - Better child process execution
- **@dotenvx/dotenvx** - Environment variable management
## Configuration
This cube currently has no configurable parameters.
## Dependencies
None - this cube can run standalone.
## Post-Installation
Verify installation:
```bash
node --version
npm --version
```
Common commands:
- Run a Node.js app: `node app.js`
- Start with PM2: `pm2 start app.js`
- Install packages: `npm install <package>`
- Use yarn: `yarn add <package>`
## PM2 - Process Manager
PM2 is included for production deployments. Common PM2 commands:
```bash
pm2 start app.js # Start application
pm2 list # List running apps
pm2 stop app # Stop application
pm2 restart app # Restart application
pm2 logs # View logs
pm2 startup # Enable PM2 on boot
pm2 save # Save current process list
```
## Notes
- Node.js is installed system-wide
- Global packages are accessible to all users
- npm cache is stored in `~/.npm`
- Use `nvm` if you need multiple Node.js versions
@@ -0,0 +1,58 @@
from pyinfra.operations import server, apt, npm, python
from pyinfra import host
from pyinfra.facts.files import Directory
from pyinfra.facts.server import Which
hasNode = host.get_fact(Which, 'node')
VERSION = host.data.VERSION
ALIAS = host.data.ALIAS
GLOBAL_PACKAGES = host.data.GLOBAL_PACKAGES
USER = host.data.USER
apt.packages(
name=f'Install nodejs tools',
no_recommends=True,
packages=[
'build-essential',
'libssl-dev',
'libtool',
'cmake',
'libcairo2-dev',
'libpango1.0-de',
'libpng-dev',
'libgif-dev',
'libjpeg-dev',
'libvips-dev',
'librsvg2-dev',
'libpixman-1-dev',
],
_sudo = True,
)
server.shell(
commands=[
"curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash",
"omf install nvm",
f"nvm install {VERSION}",
f"nvm alias {ALIAS} {VERSION}",
"set -gx NVM_DIR $HOME/.nvm",
],
_sudo=True,
_su_user=USER,
_use_su_login=True,
_shell_executable='/usr/bin/fish'
)
server.shell(
commands=[
"npm install -g pm2 yarn local-web-server node-gyp inquirer execa @dotenvx/dotenvx"
],
_sudo=True,
_su_user=USER,
_use_su_login=True,
_shell_executable='/usr/bin/fish'
)
@@ -0,0 +1,20 @@
import { Manifest } from '@bitsquare/nopy-cubes';
import { z } from 'zod';
export default Manifest({
id: 'runtime:nodevm',
name: 'Install nvm and nodejs with global packages',
dependencies: () => [],
schema: z.object({
VERSION: z
.nullable(z.string())
.describe('Node.js version to install. It is recommended to use semver notation')
.default('v22.20.0'),
USER: z.string().describe('Username for which to install nodejs').default('vagrant'),
ALIAS: z.string().describe('The alias for this node version').default('nodelts'),
GLOBAL_PACKAGES: z
.string()
.describe('Space-separated list of global npm packages to install')
.default('npm-check-updates'),
}),
});
@@ -0,0 +1,176 @@
# TypeStack Install Cube
Deploys a Node.js/TypeScript application from a Git repository as a systemd service with Docker Compose and PM2 support.
## Features
- Clones Git repository
- Installs dependencies with Yarn
- Builds the application
- Starts Docker Compose services
- Creates a systemd service for automatic startup
- Configures PM2 for process management
- Automatic restart on failure
## Requirements
- Git (for cloning repository)
- Yarn (for dependency management)
- Docker and Docker Compose
- PM2 (for process management)
- Node.js/NVM installed
- SSH key access to the repository (if using private repos)
## Configuration Parameters
### Required
- **USER**: System user to run the application (default: `teclabmin`)
- **REPO**: Git repository URL (default: `git@github.com:bennidi/teclab-flintstone.git`)
- **APP**: Application name/directory name (default: `flintstone`)
### Optional
- **ENV**: Application environment (default: `production`)
- **AUTOSTART**: Enable and start service immediately (default: `True`)
- **NODE_PATH**: Path to Node.js binaries (default: `/home/teclabmin/.nvm/versions/node/v21.7.3/bin`)
## Example Usage
### Basic Configuration
```json
{
"USER": "myuser",
"REPO": "git@github.com:myorg/myapp.git",
"APP": "myapp"
}
```
### Advanced Configuration
```json
{
"USER": "appuser",
"REPO": "git@github.com:myorg/myapp.git",
"APP": "myapp",
"ENV": "staging",
"AUTOSTART": false,
"NODE_PATH": "/home/appuser/.nvm/versions/node/v20.0.0/bin"
}
```
## What This Cube Does
1. **Clone Repository**: Clones the specified Git repository to `/home/<USER>/<APP>`
2. **Install Dependencies**: Runs `yarn install` to install all dependencies
3. **Build Application**: Runs `yarn build` to compile the application
4. **Start Docker Services**: Runs `docker compose up -d` to start containerized services
5. **Create Startup Script**: Creates `/home/<USER>/<APP>.service.sh` that:
- Starts Docker Compose services
- Starts PM2 with ecosystem.config.js
6. **Create Systemd Service**: Creates `/etc/systemd/system/<APP>.service` that:
- Runs after Docker service
- Uses the specified user
- Configures proper environment (HOME, PATH)
- Auto-restarts on failure
7. **Enable & Start Service**: Enables and starts the service (if AUTOSTART=True)
## Service Management
### Check service status
```bash
sudo systemctl status <APP>
```
### Start the service
```bash
sudo systemctl start <APP>
```
### Stop the service
```bash
sudo systemctl stop <APP>
```
### Restart the service
```bash
sudo systemctl restart <APP>
```
### View service logs
```bash
sudo journalctl -u <APP> -f
```
### Disable autostart
```bash
sudo systemctl disable <APP>
```
## File Structure
After deployment:
```
/home/<USER>/
├── <APP>/ # Application directory
│ ├── ecosystem.config.js # PM2 configuration
│ ├── docker-compose.yml # Docker services
│ └── ... # Application files
├── <APP>.service.sh # Startup script
/etc/systemd/system/
└── <APP>.service # Systemd service file
```
## Troubleshooting
### Service fails to start
1. Check service logs:
```bash
sudo journalctl -u <APP> -n 50
```
2. Verify Docker is running:
```bash
sudo systemctl status docker
```
3. Check if Node.js path is correct:
```bash
which node
which pm2
```
### Repository clone fails
- Ensure SSH keys are properly configured for the user
- Test SSH access: `ssh -T git@github.com`
- Check repository URL is correct
### Docker Compose fails
- Verify Docker is installed and running
- Check docker-compose.yml exists in the application directory
- Ensure user has Docker permissions: `sudo usermod -aG docker <USER>`
### PM2 not starting
- Verify PM2 is installed: `pm2 --version`
- Check ecosystem.config.js exists
- Verify NODE_PATH includes PM2 binary location
## Notes
- The service type is set to `forking` to support PM2's daemon mode
- Service will auto-restart on failure with a 5-second delay
- Maximum 5 restart attempts in the burst period
- The service waits for Docker to be ready before starting
- Environment variables can be configured in the ecosystem.config.js file
@@ -0,0 +1,27 @@
from pyinfra.operations import systemd
from pyinfra import host
APP = host.data.APP
# Enable and start the service based on AUTOSTART flag
if AUTOSTART:
systemd.service(
name=f'Enable {SERVICE_NAME} service',
service=APP,
enabled=True,
_sudo=True
)
systemd.service(
name=f'Start {SERVICE_NAME} service',
service=APP,
running=True,
_sudo=True
)
else:
server.shell(
name=f'Service {SERVICE_NAME} created but not enabled (AUTOSTART=False)',
commands=[f'echo "Service {SERVICE_NAME} is ready but not started. Enable with: sudo systemctl enable {APP} && sudo systemctl start {APP}"'],
_sudo=False
)
@@ -0,0 +1,17 @@
import { Manifest } from '@bitsquare/nopy-cubes';
import { z } from 'zod';
export default Manifest({
id: 'service:autostart',
name: 'Manage systemd service autostart',
dependencies: () => [],
schema: z.object({
APP: z.string().describe('The name of the systemd service (e.g., flintstone)'),
SERVICE_NAME: z
.string()
.optional()
.describe('Display name for the service')
.default('Application'),
AUTOSTART: z.boolean().describe('Should the service be enabled and started?').default(true),
}),
});
@@ -0,0 +1,32 @@
# ssh-authorize
**Authorize SSH public key for a user**
## Purpose
This cube adds a specific SSH public key to a user's `authorized_keys` file on the remote server, allowing them to log in via SSH using the corresponding private key.
## Configuration
### Parameters
- **USER** (string, default: `'vagrant'`)
- The username on the remote server to authorize.
- If the user does not exist, `pyinfra` will attempt to create it (though a full user creation with shell/groups is better handled by `user-add`).
- **PUBKEY** (string, required)
- The actual content of the public key (e.g., `ssh-ed25519 AAAA...`).
## Use Cases
Grant access to a developer:
```javascript
exec('ssh-authorize', {
USER: 'developer',
PUBKEY: 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA...'
})
```
## Dependencies
None.
@@ -0,0 +1,13 @@
from pyinfra import host
from pyinfra.operations import server
USER = host.data.USER
PUBKEY = host.data.PUBKEY
server.user(
name=f"Authorize public key for {USER}",
user=USER,
public_keys=[PUBKEY],
present=True,
_sudo=True
)
@@ -0,0 +1,12 @@
import { Manifest } from '@bitsquare/nopy-cubes';
import { z } from 'zod';
export default Manifest({
id: 'ssh:authorize',
name: 'Authorize SSH public key for a user',
dependencies: () => [],
schema: z.object({
USER: z.string().describe('Username to authorize').default('vagrant'),
PUBKEY: z.string().describe('SSH public key string').default(''),
}),
});
@@ -0,0 +1,93 @@
# ssh-keygen
**Generate SSH key for a given user**
## Purpose
This cube generates a new SSH key pair for a specified user, which can be used for secure, passwordless authentication to remote servers and services like GitHub, GitLab, or other SSH-accessible systems.
## What are SSH Keys?
SSH keys provide a more secure and convenient way to authenticate compared to passwords:
- **Public key**: Shared with servers/services you want to access (like GitHub)
- **Private key**: Kept secret on your local machine, never shared
- **Passphrase-free**: This cube generates keys without a passphrase for automation
- **Algorithm support**: RSA (traditional) or Ed25519 (modern, recommended)
## What This Cube Does
1. Creates the `.ssh` directory with proper permissions (700)
2. Generates an SSH key pair using the specified algorithm
3. Saves the keys as `id_{SUFFIX}` and `id_{SUFFIX}.pub`
4. Logs the public key to the console for easy copying
## Configuration
### Parameters
- **SUFFIX** (string, default: `'ed25519'`)
- Suffix for keyname (e.g., `github``id_github.pub`)
- Helps identify the purpose of the key
- **EMAIL** (string, default: `'undefined@bitsquare.dev'`)
- Email address to associate with the SSH key
- Used as a comment in the public key
- **ALGORITHM** (enum: `'rsa'` | `'ed25519'`, default: `'ed25519'`)
- SSH key algorithm type
- **Ed25519**: Modern, faster, more secure (recommended)
- **RSA**: Traditional, widely supported
- **USER** (string, default: `'vagrant'`)
- Username for which to generate the SSH key
- Inherited from `user-add` dependency
## Dependencies
- **user-add** - Creates the user account first
## Post-Installation
After the key is generated:
1. The public key will be logged to the console
2. Copy the public key and add it to the target service:
- **GitHub**: Settings → SSH and GPG keys → New SSH key
- **GitLab**: Preferences → SSH Keys
- **Remote server**: Add to `~/.ssh/authorized_keys`
3. SSH config is automatically set up for the key
## Key Locations
- Private key: `/home/{USER}/.ssh/id_{SUFFIX}`
- Public key: `/home/{USER}/.ssh/id_{SUFFIX}.pub`
## Algorithm Comparison
**Ed25519** (Recommended):
- Smaller keys (256-bit)
- Faster generation and verification
- More secure against certain attacks
- Not supported on very old systems
**RSA**:
- Larger keys (2048-4096 bit)
- Universally supported
- Slower than Ed25519
- Well-tested and trusted
## Example Usage
Generate a key for GitHub access:
```javascript
exec('ssh-keygen', {
SUFFIX: 'github',
EMAIL: 'myemail@example.com',
ALGORITHM: 'ed25519',
USER: 'myuser'
})
```
This creates `id_github` and `id_github.pub` in `/home/myuser/.ssh/`.
@@ -0,0 +1,46 @@
from pyinfra.operations import files, server, python
from pyinfra import host, logger
import logging
NAME=host.data.SUFFIX
EMAIL=host.data.EMAIL
ALGORITHM=host.data.ALGORITHM
USER=host.data.USER
# Ensure the .ssh directory exists
files.directory(
name="Ensure .ssh directory exists",
path=f"/home/{USER}/.ssh",
present=True,
mode=700,
user=USER,
group=USER,
)
# Generate the SSH keypair
server.shell(
name=f"Generate SSH key id_{NAME}",
commands=[
f"ssh-keygen -t {ALGORITHM} -f /home/{USER}/.ssh/id_{NAME} -C '{EMAIL}' -N ''"
]
)
# Print the public key
result = server.shell(
name="Print public key",
commands=[f"cat /home/{USER}/.ssh/id_{NAME}.pub"],
)
def callback():
# 🔹 Extract and log the key output
if result.stdout:
logger.info(f"Public Key for {USER}: {result.stdout.strip()}")
else:
logger.warning(f"No public key found for {USER} at /home/{USER}/.ssh/id_{NAME}.pub")
python.call(
name="Log public key",
function=callback,
)
@@ -0,0 +1,20 @@
import { Manifest } from '@bitsquare/nopy-cubes';
import { z } from 'zod';
export default Manifest({
id: 'ssh:keygen',
name: 'Generate SSH key for a given $USER',
dependencies: () => ['user:add'],
schema: z.object({
SUFFIX: z
.string()
.describe('Suffix for keyname, e.g. github => id_github.pub')
.default('ed25519'),
EMAIL: z
.string()
.describe('Email address to associate with the SSH key')
.default('undefined@bitsquare.dev'),
ALGORITHM: z.enum(['rsa', 'ed25519']).describe('SSH key algorithm type').default('ed25519'),
USER: z.string().describe('Username for which to generate the SSH key').default('vagrant'),
}),
});
@@ -0,0 +1,110 @@
# ssh-keyman
**Deploy existing SSH keys to host**
## Purpose
This cube deploys pre-existing SSH key pairs from your local machine to a remote server, configuring them for automatic use with specified hosts (like GitHub, GitLab, etc.).
## What This Cube Does
1. **Copies SSH keys to the server**
- Transfers both private and public keys from local directory to remote `.ssh` folder
- Sets correct file permissions (600 for private, 644 for public)
2. **Configures SSH client**
- Creates/updates `.ssh/config` to use the deployed key for specified hosts
- Disables strict host key checking for easier automation
- Maps each host to use the correct identity file
3. **Ensures security**
- Sets proper directory permissions (700 for `.ssh`)
- Ensures keys are owned by the specified user
## Configuration
### Parameters
- **KEY_NAME** (string, default: `'id_ed25519'`)
- Name of the SSH key file (without extension)
- Must exist in the KEY_DIR directory locally
- **USER** (string, default: `'vagrant'`)
- Username for which to deploy the SSH key
- Inherited from `user-add` dependency
- **HOSTS** (string, default: `'github.com'`)
- Space-separated list of hosts to add to known_hosts
- Example: `'github.com gitlab.com bitbucket.org'`
## Dependencies
- **user-add** - Creates the user account first
## Use Cases
Deploy GitHub SSH key:
```javascript
exec('ssh-keyman', {
KEY_NAME: 'id_github',
HOSTS: 'github.com'
})
```
Deploy key for multiple Git services:
```javascript
exec('ssh-keyman', {
KEY_NAME: 'id_git',
HOSTS: 'github.com gitlab.com bitbucket.org'
})
```
## What Gets Configured
After deployment, the `.ssh/config` file will contain entries like:
```
Host github.com
IdentityFile /home/{USER}/.ssh/id_github
StrictHostKeyChecking no
```
This means when you run `git clone git@github.com:user/repo.git`, it will automatically use the deployed key.
## Key File Requirements
The local KEY_DIR must contain:
- `{KEY_NAME}` - Private key file
- `{KEY_NAME}.pub` - Public key file
For example, if `KEY_NAME=id_github`, you need:
- `./vault/tmp/id_github`
- `./vault/tmp/id_github.pub`
## Security Considerations
- **Private keys are sensitive**: Ensure your local KEY_DIR is secure
- **StrictHostKeyChecking disabled**: Convenient but less secure
- Consider enabling it for production: Edit `/home/{USER}/.ssh/config`
- **Backup your keys**: Keep secure copies of private keys
- **Use different keys**: Consider separate keys for different services
## Post-Installation
Test SSH connection:
```bash
ssh -T git@github.com
# Should show: "Hi username! You've successfully authenticated..."
```
Clone a repository:
```bash
git clone git@github.com:user/repo.git
# Should work without prompting for credentials
```
@@ -0,0 +1,96 @@
from pyinfra.operations import server, files, apt, systemd
from pyinfra import host
import subprocess
import json
def get_keyman_config():
"""Get keyman configuration by calling keyman --print-config"""
try:
result = subprocess.run(
['keyman', '--print-config'],
capture_output=True,
text=True,
check=True
)
return json.loads(result.stdout)
except (subprocess.CalledProcessError, json.JSONDecodeError, FileNotFoundError):
return None
# Load keyman config for defaults
keyman_config = get_keyman_config()
USER = host.data.USER
KEY = host.data.KEY_NAME
# Use KEY_DIR from host data, or fall back to keyman config tmpDir
DIR = host.data.get('KEY_DIR')
if not DIR and keyman_config:
DIR = keyman_config.get('tmpDir')
if not DIR:
DIR = '../../vault/tmp' # Final fallback
# Support multiple hosts separated by space
HOSTS = map(str.lstrip, str(host.data.HOSTS).split(' '))
# 🔹 Define remote paths
SSH_DIR = f"/home/{USER}/.ssh" if USER != "root" else "/root/.ssh"
PRIVATE_KEY_PATH = f"{SSH_DIR}/{KEY}"
PUBLIC_KEY_PATH = f"{SSH_DIR}/{KEY}.pub"
# Ensure the .ssh directory exists
files.directory(
name="Ensure .ssh directory exists",
path=SSH_DIR,
present=True,
mode=700,
user=USER,
group=USER,
_sudo=True
)
# Copy private key to the remote server
files.put(
name="Copy private key",
src=f"{DIR}/{KEY}",
dest=PRIVATE_KEY_PATH,
mode="600",
user=USER,
group=USER,
_sudo=True,
)
# Copy public key to the remote server
files.put(
name="Copy public key",
src=f"{DIR}/{KEY}.pub",
dest=PUBLIC_KEY_PATH,
mode="644",
user=USER,
group=USER,
_sudo=True,
)
# Ensure correct permissions for the private key
server.shell(
name="Set correct permissions for private key",
commands=[f"chmod 600 {PRIVATE_KEY_PATH}"],
_sudo=True,
)
files.file(
name="Ensure .ssh/config directory exists",
path=f"/home/{USER}/.ssh/config",
present=True,
user=USER,
group=USER,
_sudo=True
)
for host in HOSTS:
files.line(
name=f"Configure SSH key for {host}",
path=f"{SSH_DIR}/config",
line=f"Host {host}\n IdentityFile {SSH_DIR}/{KEY}\n StrictHostKeyChecking no",
_sudo=True
)
@@ -0,0 +1,19 @@
import { Manifest } from '@bitsquare/nopy-cubes';
import { z } from 'zod';
export default Manifest({
id: 'ssh:keyman',
name: 'Deploy an ssh key managed by keyman',
dependencies: () => [],
schema: z.object({
KEY_NAME: z
.string()
.describe('Name of the SSH key file (without extension)')
.default('id_ed25519'),
USER: z.string().describe('Username for which to deploy the SSH key').default('vagrant'),
HOSTS: z
.string()
.describe('Space-separated list of hosts to add to known_hosts')
.default('github.com'),
}),
});
@@ -0,0 +1,143 @@
# user-add
**Add a user with Fish shell and tools**
## Purpose
This cube creates a new user account with a modern shell environment (Fish), SSH key authentication, and enhanced productivity tools pre-configured.
## What This Cube Does
1. **Creates a new user account**
- Sets up home directory with proper permissions
- Configures password authentication
- Adds user to specified groups (e.g., `docker`, `sudo`)
- Sets Fish as the default login shell
2. **Configures SSH access**
- Deploys the specified SSH public key for passwordless authentication
- Creates `.ssh` directory with proper permissions
- Sets up SSH config file
- Configures SSH agent auto-loading for Fish shell
3. **Installs Fish shell enhancements**
- Installs **Oh My Fish** (OMF) - Fish shell framework with themes and plugins
- Deploys custom Fish configuration (`config.fish`)
- Sets up Fish rc directory for modular configurations
4. **Creates workspace directories**
- Creates `/home/{USER}/tmp` directory for temporary files
## Configuration
### Parameters
- **USER** (string, auto-generated)
- Username for the new user account
- Default: `userXXXXX` (randomly generated 5-character suffix)
- **PASSWORD** (string, **secret**)
- Password for the new user account
- Default: the literal `changeme` — a placeholder, not a credential. Change it
on first login, or pass a real one.
- Declared in the manifest's `secrets`, so it is never written to a session or
history file and is masked in printed commands. A replay asks for it again.
- It used to default to a randomly generated password. That was removed: since
the value is not recorded, an unattended run created an account with a
credential nobody had seen, and replaying that run produced a different one.
- **GROUPS** (string, default: `''`)
- Comma-separated list of additional groups (e.g., `"docker,sudo"`)
- Common groups:
- `docker` - Run Docker without sudo
- `sudo` - Administrative privileges
- `www-data` - Web server file access
- **PUBKEY** (string, **required** — no default)
- SSH public key to authorize for the user
- Should be your public key for passwordless SSH access
- There is deliberately no default. It used to be a specific personal key, so
accepting the default authorized *someone else's* key on the new account.
No key would be a sensible guess, so the cube asks instead.
- Because it is required, `--use-defaults` refuses to run this cube unless
`PUBKEY` comes from `env` in `.nopyrc.json`, a dependency, or a hook.
- Submitting an empty value at the prompt authorizes no key at all (the account
is still created, with password login only).
## Dependencies
- **apt:essentials** - Provides Fish shell and basic tools
## What is Fish?
Fish (Friendly Interactive Shell) is a modern command-line shell that focuses on usability:
- **Smart autosuggestions**: Suggests commands as you type based on history
- **Syntax highlighting**: Color-codes commands in real-time
- **Tab completions**: Comprehensive, discoverable command completions
- **No configuration needed**: Works great out of the box
## Post-Installation
After deployment:
- SSH into the server as the new user: `ssh {USER}@server`
- Your SSH key will be pre-authorized (no password needed if using key)
- Fish shell will start automatically with OMF installed
- SSH agent auto-loads to manage your SSH keys
## Notes
- The user's home directory is created at `/home/{USER}`
- Fish configuration is stored in `/home/{USER}/.config/fish/`
- Oh My Fish provides package management: `omf install <package>`
- To switch shells: `chsh -s /bin/bash` (or back to fish: `chsh -s /usr/bin/fish`)
---
# 📌 Most Useful Fish Key Bindings (with Fisher Extensions)
## 🐟 Default Fish Key Bindings
- `Ctrl + C` → Cancel the current command
- `Ctrl + D` → Exit the shell (or logout if in SSH)
- `Ctrl + L` → Clear the terminal
- `Ctrl + R` → Search command history (enhanced by `fzf.fish`)
- `Ctrl + U` → Delete the entire command line
- `Ctrl + W` → Delete the last word
- `Alt + ← / →` → Move backward/forward by a word
## 🔍 Enhanced with `fzf.fish`
- `Ctrl + R`**Fuzzy search command history**
- `Ctrl + T`**Fuzzy search and insert file path**
- `Alt + C`**Fuzzy search directories (`cd` with `z`)**
## 📂 Directory Navigation (with `z`)
- `z <dir>` → Jump to a frequently used directory
- `z -l` → List most-used directories
- `z -c` → Remove a directory from `z`'s database
## 🔄 Process & Job Management
- `Ctrl + Z` → Suspend the current process
- `fg` → Bring a suspended process back to foreground
- `jobs` → List background jobs
## 🎨 Other Handy Shortcuts
- `fish_vi_key_bindings` → Enable Vi mode (press `Esc` for normal mode)
- `Ctrl + G` → Show Git status (if using `fzf.fish`)
- `Ctrl + E` → Edit command line in `$EDITOR`
## ⚙️ Useful Commands for Key Binding
```fish
# Set Fish default key bindings
fish_default_key_bindings
# Enable Vi mode
fish_vi_key_bindings
# Rebind a custom key (Example: Ctrl + G for git status)
bind \cg 'git status'
@@ -0,0 +1,10 @@
if status is-interactive
# Commands to run in interactive sessions can go here
# Execute all scripts in ~/.config/fish/rc/ on shell startup
for script in ~/.config/fish/rc/*.fish
if test -f $script
source $script
end
end
end
@@ -0,0 +1,93 @@
from pyinfra import host
from pyinfra.operations import server, files, apt
from io import StringIO
# Define the username, password, and public key for the new admin user
USER = host.data.USER
HOME_DIR = f"/home/{USER}"
TMP_DIR = f"{HOME_DIR}/tmp"
PASSWORD = host.data.PASSWORD
# An empty submission at the prompt must not become an empty authorized_keys
# line, so an absent key means no key rather than a blank one.
PUBKEY = host.data.PUBKEY
PUBKEYS = [PUBKEY] if PUBKEY and str(PUBKEY).strip() else []
GROUPS = list(filter(None, map(str.strip, str(host.data.GROUPS).split())))
FISH_PATH = "/usr/bin/fish"
FISH_CONFIG_DIR = f"{HOME_DIR}/.config/fish"
FISH_CONFIG_FILE = f"{FISH_CONFIG_DIR}/config.fish"
FISH_RC_DIR = f"{FISH_CONFIG_DIR}/rc"
SSH_AGENT_SCRIPT = f"{FISH_RC_DIR}/ssh-agent.fish"
apt.packages(
name='Ensure fish shell is installed',
packages=[ 'fish'],
_sudo=True
)
# Ensure the user exists with a login shell
server.user(
name=f"Create user {USER} [{GROUPS}]",
present=True,
user=USER,
password=PASSWORD,
create_home=True,
groups=GROUPS,
shell=FISH_PATH,
public_keys=PUBKEYS,
_sudo=True
)
for dir in [f"{HOME_DIR}/.ssh", FISH_RC_DIR, TMP_DIR]:
files.directory(
name=f"Ensure {dir} directory exists",
path=dir,
present=True,
mode=700,
user=USER,
group=USER,
_sudo=True,
_sudo_user=USER,
_use_sudo_login=True
)
files.file(
name="Ensure .ssh/config exists",
path=f"{HOME_DIR}/.ssh/config",
present=True,
user=USER,
group=USER,
_sudo=True
)
server.shell(
name=f"Install OMF(Oh My Fish) for {USER}",
commands=[
f"curl https://raw.githubusercontent.com/oh-my-fish/oh-my-fish/master/bin/install > install-omf",
f"fish install-omf --yes --noninteractive",
],
_sudo=True,
_sudo_user=USER,
_use_sudo_login=True
)
files.put(
name="Add SSH agent auto-load script to Fish rc directory",
src="ssh-agent.fish",
dest=SSH_AGENT_SCRIPT,
user=USER,
group=USER,
mode="755", # Make it executable
_sudo=True,
)
files.put(
name="Add custom config.fish",
src="config.fish",
dest=FISH_CONFIG_FILE,
user=USER,
group=USER,
mode="755", # Make it executable
_sudo=True,
)
@@ -0,0 +1,28 @@
import { Manifest, uniqid } from '@bitsquare/nopy-cubes';
import { z } from 'zod';
export default Manifest({
id: 'user:add',
name: 'Add a user with fish shell and tools',
dependencies: () => ['apt:essentials'],
secrets: ['PASSWORD'],
schema: z.object({
USER: z
.string()
.describe('Username for the new user account')
.default(() => `user${uniqid(5)}`),
// A fixed placeholder, not a generated one: the password is never recorded
// in a session, so a generated default meant every run produced credentials
// nobody had seen and a replay produced different ones again.
PASSWORD: z.string().describe('Password for the new user account').default('changeme'),
GROUPS: z
.string()
.describe('Comma-separated list of additional groups (e.g., "docker,sudo")')
.default(''),
// No default on purpose. This used to carry a specific personal key, which
// meant an unattended run authorised someone else's key on the new account.
// Leaving it required makes `--use-defaults` refuse by name instead of
// guessing, and there is no key that would be a sensible guess.
PUBKEY: z.string().describe('SSH public key to authorize for the user'),
}),
});
@@ -0,0 +1,14 @@
# Start SSH agent if not already running
if not set -q SSH_AUTH_SOCK
eval (ssh-agent -c)
end
# Add all private SSH keys in ~/.ssh to the agent
for key in ~/.ssh/id_*;
if test -f $key; and not string match -q "*pub" $key
ssh-add $key 2>/dev/null
end
end
# Export user and group ID for Docker
set -x UID (id -u)
set -x GID (id -g)
@@ -0,0 +1,52 @@
# user:edit
**Modify an existing user's password or group membership**
## Purpose
This cube allows you to update existing user accounts on the target system. It can be used to change passwords, add users to new groups (like `docker` or `sudo`), or revoke group memberships.
## What This Cube Does
1. Identifies the existing user on the target system
2. Updates the user's password if `PASSWORD` is provided
3. Adds the user to the groups specified in `GROUPS`
4. Removes the user from the groups specified in `GROUPS_ABSENT`
## Configuration
| Variable | Type | Description | Required |
| :--- | :--- | :--- | :--- |
| `USER` | `string` | The username of the account to modify | Yes |
| `PASSWORD` | `string` | New password for the user. **Secret**: never recorded in a session or history file, masked in printed commands, re-prompted on replay. | No |
| `GROUPS` | `string` | Comma-separated list of groups to ADD (e.g., `docker,sudo`) | No |
| `GROUPS_ABSENT` | `string` | Comma-separated list of groups to REMOVE | No |
## Dependencies
- `apt/essentials`: Standard system utilities.
## Usage
### Changing a Password
```bash
nopy install user:edit --env USER=myuser --env PASSWORD=newsecurepassword
```
### Adding a User to the Docker Group
```bash
nopy install user:edit --env USER=myuser --env GROUPS=docker
```
### Revoking Sudo Access
```bash
nopy install user:edit --env USER=myuser --env GROUPS_ABSENT=sudo
```
## Security Notes
- When setting passwords via the CLI, they may be visible in your shell history. Consider using a session file or interactive prompts for sensitive values.
- Changing your own user's groups or password may require a re-login to take full effect.
@@ -0,0 +1,24 @@
from pyinfra import host
from pyinfra.operations import server
# [agnt://cogen/cogen/user-edit-2]{cartridge: "ansiblings/cubes", action: "generated", status: "generated"}
"""
Deployment script for user:edit.
Updates password and group membership for an existing user.
"""
USER = host.data.USER
PASSWORD = host.data.get('PASSWORD')
GROUPS = [g.strip() for g in str(host.data.get('GROUPS', '')).split(',') if g.strip()]
GROUPS_ABSENT = [g.strip() for g in str(host.data.get('GROUPS_ABSENT', '')).split(',') if g.strip()]
# Update user details
server.user(
name=f"Update user {USER}",
user=USER,
password=PASSWORD,
groups=GROUPS,
groups_absent=GROUPS_ABSENT,
_sudo=True,
)
@@ -0,0 +1,27 @@
import { Manifest } from '@bitsquare/nopy-cubes';
import { z } from 'zod';
// [agnt://cogen/cogen/user-edit-1]{cartridge: "ansiblings/cubes", action: "generated", status: "generated"}
/**
* Manifest for the user:edit cube.
* Allows modifying existing user accounts (password, groups).
*/
export default Manifest({
id: 'user:edit',
name: 'user:edit - Modify an existing user account',
dependencies: () => [],
secrets: ['PASSWORD'],
schema: z.object({
USER: z.string().describe('The username of the account to modify'),
PASSWORD: z.string().optional().describe('New password for the user (optional)'),
GROUPS: z
.string()
.optional()
.describe('Comma-separated list of groups the user SHOULD be in (optional)'),
GROUPS_ABSENT: z
.string()
.optional()
.describe('Comma-separated list of groups to REMOVE from the user (optional)'),
}),
});
+45
View File
@@ -0,0 +1,45 @@
{
"name": "@bitsquare/nopy-cubes-core",
"version": "0.5.0",
"description": "The core nopy cube bundle: apt, users, ssh, networking, services and runtimes.",
"keywords": [
"nopy",
"nopy-cubess",
"pyinfra",
"deployment",
"infrastructure"
],
"license": "MIT",
"author": "bitsquare",
"type": "module",
"repository": {
"type": "git",
"url": "git+https://gitea.bitsquare.dev/BitSquare/ansiblings.git",
"directory": "packages/nopy-cubes-core"
},
"homepage": "https://gitea.bitsquare.dev/BitSquare/ansiblings/src/branch/main/packages/nopy-cubes-core",
"bugs": {
"url": "https://gitea.bitsquare.dev/BitSquare/ansiblings/issues"
},
"engines": {
"node": ">=22"
},
"nopy": {
"cubes": [
"./cubes"
]
},
"files": [
"cubes",
"!cubes/**/*.log",
"README.md",
"LICENSE"
],
"publishConfig": {
"access": "public"
},
"dependencies": {
"@bitsquare/nopy-cubes": "workspace:*",
"zod": "^4.4.3"
}
}