a9c7235347
Unit Tests / test (push) Successful in 12m0s
Root-cause fix for ACME failures caused by clock drift breaking TOTP during DDNS registration: install and start chrony (all supported package managers) before the setup wizard runs, so the host clock is accurate from day one. Also enables and starts the pic systemd unit at the end of a cold install — previously the unit file was written but never activated, so the stack would not survive a reboot without a manual `systemctl enable --now pic`. Makefile uninstall hardened: `disable --now` instead of bare `disable` so the running unit is stopped before the unit file is removed; daemon-reload called afterwards to flush the stale unit; and all lingering cell-* containers (tor/sshuttle/redsocks/store services) are now force-removed so subsequent reinstalls start from a clean Docker state. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
380 lines
16 KiB
Makefile
380 lines
16 KiB
Makefile
# Personal Internet Cell - Makefile
|
|
# Provides easy commands for managing the cell
|
|
|
|
.PHONY: help start stop restart status logs clean setup check-deps init-peers \
|
|
update reinstall uninstall install \
|
|
build build-api build-webui \
|
|
start-core start-dns start-api start-wg start-webui \
|
|
backup restore \
|
|
test test-all test-unit test-coverage test-api test-cli \
|
|
test-phase1 test-phase2 test-phase3 test-phase4 test-all-phases \
|
|
test-integration test-integration-readonly \
|
|
test-e2e-deps test-e2e-api test-e2e-ui test-e2e-wg test-e2e \
|
|
reset-test-admin-pass \
|
|
show-admin-password reset-admin-password \
|
|
show-routes add-peer list-peers \
|
|
ddns-update ddns-register
|
|
|
|
# Detect docker compose command (v2 plugin preferred, fallback to v1 standalone)
|
|
DC := $(shell docker compose version >/dev/null 2>&1 && echo "docker compose" || echo "docker-compose")
|
|
|
|
# Full compose command: includes docker-compose.services.yml when it exists
|
|
DCF = $(DC) $(if $(wildcard docker-compose.services.yml),-f docker-compose.yml -f docker-compose.services.yml,-f docker-compose.yml)
|
|
|
|
# Default target
|
|
help:
|
|
@echo "Personal Internet Cell - Management Commands"
|
|
@echo ""
|
|
@echo "First install:"
|
|
@echo " check-deps - Install all required system packages (python3, docker, etc.)"
|
|
@echo " setup - Generate keys, write configs, create data dirs"
|
|
@echo " Env vars: CELL_NAME=mycell CELL_DOMAIN=cell VPN_ADDRESS=10.0.0.1/24 WG_PORT=51820"
|
|
@echo " init-peers - Reset peer list to empty"
|
|
@echo ""
|
|
@echo "Lifecycle:"
|
|
@echo " start - Start all services"
|
|
@echo " stop - Stop all services"
|
|
@echo " restart - Restart all services"
|
|
@echo " status - Show container status + API health"
|
|
@echo " logs - Follow logs from all services"
|
|
@echo " logs-<svc> - Follow logs for one service (e.g. make logs-api)"
|
|
@echo " shell-<svc> - Open shell in a container (e.g. make shell-api)"
|
|
@echo ""
|
|
@echo "Updates & reinstall:"
|
|
@echo " update - git pull + rebuild + restart (deploy latest code)"
|
|
@echo " reinstall - Full wipe and fresh install from current git checkout"
|
|
@echo " uninstall - Stop + remove containers; prompts whether to also delete data"
|
|
@echo ""
|
|
@echo "Build:"
|
|
@echo " build - Rebuild API image"
|
|
@echo " build-api - Rebuild API image (no cache)"
|
|
@echo " build-webui - Rebuild Web UI image (no cache)"
|
|
@echo ""
|
|
@echo "Individual services:"
|
|
@echo " start-dns - Start DNS only"
|
|
@echo " start-api - Start API only"
|
|
@echo " start-wg - Start WireGuard only"
|
|
@echo ""
|
|
@echo "Maintenance:"
|
|
@echo " backup - Backup config + data to backups/"
|
|
@echo " restore - List available backups"
|
|
@echo " clean - Remove containers and volumes (keeps config/data dirs)"
|
|
@echo " show-admin-password - Print the admin password (reads setup file or prompts to reset)"
|
|
@echo " reset-admin-password - Generate a new random admin password and print it"
|
|
@echo ""
|
|
@echo "Tests:"
|
|
@echo " test - Run all tests"
|
|
@echo " test-coverage - Run tests with HTML coverage report"
|
|
@echo " test-integration - Full integration tests (needs running stack)"
|
|
@echo " test-integration-readonly - Read-only integration tests (safe to run anytime)"
|
|
@echo ""
|
|
@echo "Peers:"
|
|
@echo " list-peers - List configured WireGuard peers"
|
|
@echo " show-routes - Show WireGuard routing table"
|
|
|
|
# ── Dependencies & setup ──────────────────────────────────────────────────────
|
|
|
|
check-deps:
|
|
@sudo sh scripts/check_deps.sh
|
|
|
|
setup: check-deps
|
|
@echo "Setting up Personal Internet Cell..."
|
|
@sudo chown -R $${SUDO_USER:-$$(id -un)}:$${SUDO_USER:-$$(id -un)} config/ data/ 2>/dev/null || true
|
|
CELL_NAME=$(or $(CELL_NAME),mycell) \
|
|
CELL_DOMAIN=$(or $(CELL_DOMAIN),cell) \
|
|
DOMAIN_MODE=$(or $(DOMAIN_MODE),lan) \
|
|
CELL_DOMAIN_NAME=$(or $(CELL_DOMAIN_NAME),) \
|
|
CLOUDFLARE_API_TOKEN=$(or $(CLOUDFLARE_API_TOKEN),) \
|
|
DUCKDNS_TOKEN=$(or $(DUCKDNS_TOKEN),) \
|
|
DUCKDNS_SUBDOMAIN=$(or $(DUCKDNS_SUBDOMAIN),) \
|
|
VPN_ADDRESS=$(or $(VPN_ADDRESS),10.0.0.1/24) \
|
|
WG_PORT=$(or $(WG_PORT),51820) \
|
|
WG_PRIVATE_KEY="$(WG_PRIVATE_KEY)" \
|
|
WG_PUBLIC_KEY="$(WG_PUBLIC_KEY)" \
|
|
python3 scripts/setup_cell.py
|
|
|
|
init-peers:
|
|
@echo "Initializing peer configuration..."
|
|
@echo '[]' > data/api/peers.json
|
|
@echo "Peer configuration initialized."
|
|
|
|
# ── Lifecycle ─────────────────────────────────────────────────────────────────
|
|
|
|
start:
|
|
@echo "Starting Personal Internet Cell..."
|
|
@docker network inspect cell-network >/dev/null 2>&1 || \
|
|
docker network create --driver bridge --subnet "$${CELL_NETWORK:-172.20.0.0/16}" cell-network
|
|
PUID=$$(id -u) PGID=$$(id -g) $(DCF) --profile core up -d --build
|
|
@echo "Services started. Check status with 'make status'"
|
|
|
|
stop:
|
|
@echo "Stopping Personal Internet Cell..."
|
|
PUID=$$(id -u) PGID=$$(id -g) $(DCF) --profile core down
|
|
@echo "Services stopped."
|
|
|
|
restart:
|
|
@echo "Restarting Personal Internet Cell..."
|
|
PUID=$$(id -u) PGID=$$(id -g) $(DC) restart
|
|
@echo "Services restarted."
|
|
|
|
status:
|
|
@echo "Personal Internet Cell Status:"
|
|
@echo "================================"
|
|
$(DCF) ps
|
|
@echo ""
|
|
@echo "API Status:"
|
|
@curl -s http://localhost:3000/health || echo "API not responding"
|
|
|
|
logs:
|
|
$(DCF) logs -f
|
|
|
|
logs-%:
|
|
$(DCF) logs -f $*
|
|
|
|
shell-%:
|
|
docker exec -it cell-$* /bin/bash 2>/dev/null || docker exec -it cell-$* /bin/sh
|
|
|
|
# ── Updates & reinstall ───────────────────────────────────────────────────────
|
|
|
|
update:
|
|
@echo "Pulling latest code..."
|
|
@git config --global --add safe.directory $$(pwd) 2>/dev/null || true
|
|
@git stash --include-untracked --quiet 2>/dev/null || true
|
|
git pull
|
|
@git stash pop --quiet 2>/dev/null || true
|
|
@echo "Rebuilding and restarting services..."
|
|
@docker network inspect cell-network >/dev/null 2>&1 || \
|
|
docker network create --driver bridge --subnet "$${CELL_NETWORK:-172.20.0.0/16}" cell-network
|
|
PUID=$$(id -u) PGID=$$(id -g) $(DCF) --profile core up -d --build
|
|
@echo "Update complete. Run 'make status' to verify."
|
|
|
|
reinstall:
|
|
@echo "Reinstalling Personal Internet Cell from scratch..."
|
|
PUID=$$(id -u) PGID=$$(id -g) $(DCF) --profile core down 2>/dev/null || true
|
|
docker network rm cell-network 2>/dev/null || true
|
|
@sudo rm -rf config/ data/
|
|
@$(MAKE) setup
|
|
@$(MAKE) start
|
|
@echo "Reinstall complete."
|
|
|
|
install:
|
|
@if [ -f /opt/pic/.installed ] && [ "$(FORCE)" != "1" ]; then \
|
|
echo "Already installed. Run 'make update' to update, or 'make install FORCE=1' to reinstall."; \
|
|
exit 0; \
|
|
fi
|
|
@echo "Running setup..."
|
|
@$(MAKE) setup
|
|
@echo "Installing systemd unit..."
|
|
@sudo cp scripts/pic.service /etc/systemd/system/pic.service
|
|
@-sudo systemctl daemon-reload && sudo systemctl enable pic
|
|
@sudo mkdir -p /opt/pic
|
|
@sudo touch /opt/pic/.installed
|
|
@echo "Installation complete. Run 'make start-core' to start core services."
|
|
|
|
uninstall:
|
|
@echo ""
|
|
@echo "This will stop and remove all containers."
|
|
@echo ""
|
|
@printf "Also delete config/ and data/? This cannot be undone. [y/N/cancel]: "; \
|
|
read ans; \
|
|
case "$$ans" in \
|
|
y|Y) \
|
|
echo "Stopping containers and removing images..."; \
|
|
for f in data/api/services/*/docker-compose.yml; do [ -f "$$f" ] && PUID=$$(id -u) PGID=$$(id -g) docker compose -f "$$f" down 2>/dev/null || true; done; \
|
|
PUID=$$(id -u) PGID=$$(id -g) $(DCF) --profile core down --rmi all 2>/dev/null || true; \
|
|
docker ps -aq --filter "name=cell-" | xargs -r docker rm -f 2>/dev/null || true; \
|
|
docker network rm cell-network 2>/dev/null || true; \
|
|
echo "Deleting config/ and data/..."; \
|
|
sudo rm -rf config/ data/; \
|
|
echo "Uninstall complete. Git repo and scripts remain."; \
|
|
;; \
|
|
n|N|"") \
|
|
echo "Stopping and removing containers (keeping images and data)..."; \
|
|
for f in data/api/services/*/docker-compose.yml; do [ -f "$$f" ] && PUID=$$(id -u) PGID=$$(id -g) docker compose -f "$$f" down 2>/dev/null || true; done; \
|
|
PUID=$$(id -u) PGID=$$(id -g) $(DCF) --profile core down 2>/dev/null || true; \
|
|
docker ps -aq --filter "name=cell-" | xargs -r docker rm -f 2>/dev/null || true; \
|
|
echo "Done. Images, config/ and data/ are untouched. Run 'make start' to bring it back up."; \
|
|
;; \
|
|
*) \
|
|
echo "Cancelled."; \
|
|
;; \
|
|
esac
|
|
@if command -v systemctl >/dev/null 2>&1; then \
|
|
sudo systemctl disable --now pic 2>/dev/null || true; \
|
|
sudo rm -f /etc/systemd/system/pic.service; \
|
|
sudo systemctl daemon-reload 2>/dev/null || true; \
|
|
fi
|
|
@-sudo rm -f /opt/pic/.installed
|
|
@echo "Note: Data volumes were not deleted. To remove all data, manually delete config/ and data/."
|
|
|
|
# ── Build ─────────────────────────────────────────────────────────────────────
|
|
|
|
build:
|
|
@echo "Building API service..."
|
|
$(DC) build api
|
|
|
|
build-api:
|
|
@echo "Rebuilding API (no cache)..."
|
|
$(DC) build --no-cache api
|
|
$(DC) up -d api
|
|
|
|
build-webui:
|
|
@echo "Rebuilding Web UI (no cache)..."
|
|
$(DC) build --no-cache webui
|
|
$(DC) up -d webui
|
|
|
|
# ── Individual services ───────────────────────────────────────────────────────
|
|
|
|
start-core:
|
|
@echo "Starting core services (caddy, dns, wireguard, api, webui)..."
|
|
@docker network inspect cell-network >/dev/null 2>&1 || \
|
|
docker network create --driver bridge --subnet "$${CELL_NETWORK:-172.20.0.0/16}" cell-network
|
|
PUID=$$(id -u) PGID=$$(id -g) $(DCF) --profile core up -d --build
|
|
@echo "Core services started. Run 'make start' to also bring up optional services."
|
|
|
|
start-dns:
|
|
$(DC) --profile core up -d dns
|
|
|
|
start-api:
|
|
$(DC) --profile core up -d api
|
|
|
|
start-wg:
|
|
$(DC) --profile core up -d wireguard
|
|
|
|
start-webui:
|
|
$(DC) --profile core up -d webui
|
|
|
|
# ── Maintenance ───────────────────────────────────────────────────────────────
|
|
|
|
clean:
|
|
@echo "Removing containers and volumes..."
|
|
$(DC) down -v
|
|
docker system prune -f
|
|
@echo "Done. config/ and data/ are untouched."
|
|
|
|
backup:
|
|
@echo "Creating backup..."
|
|
@mkdir -p backups
|
|
@sudo tar -czf backups/cell-backup-$(shell date +%Y%m%d-%H%M%S).tar.gz \
|
|
config/ data/ docker-compose.yml Makefile README.md
|
|
@sudo chown $$(id -u):$$(id -g) backups/cell-backup-*.tar.gz
|
|
@echo "Backup created in backups/."
|
|
@echo ""
|
|
@echo "WARNING: data volumes of installed store services (email, calendar,"
|
|
@echo "files, ...) are NOT included in this archive. They are only captured"
|
|
@echo "by API-driven backups (POST /api/config/backup), which dump each"
|
|
@echo "service's volumes via ConfigManager._backup_service_volumes."
|
|
|
|
restore:
|
|
@echo "Available backups:"
|
|
@ls -lh backups/cell-backup-*.tar.gz 2>/dev/null || echo "No backups found."
|
|
@echo ""
|
|
@echo "To restore: tar -xzf backups/cell-backup-YYYYMMDD-HHMMSS.tar.gz"
|
|
|
|
# ── Tests ─────────────────────────────────────────────────────────────────────
|
|
|
|
test:
|
|
@echo "Running unit tests..."
|
|
python3 -m pytest tests/ --ignore=tests/e2e --ignore=tests/integration -q
|
|
|
|
test-all: test test-integration test-e2e-api test-e2e-ui
|
|
@echo "All test suites complete."
|
|
|
|
test-unit:
|
|
python3 -m pytest tests/ --ignore=tests/e2e --ignore=tests/integration -q
|
|
|
|
test-coverage:
|
|
python3 -m pytest tests/ --ignore=tests/e2e --ignore=tests/integration \
|
|
--cov=api --cov-report=html --cov-report=term-missing --cov-fail-under=70 -v
|
|
|
|
test-integration:
|
|
@echo "Running full integration tests (requires running PIC stack)..."
|
|
PIC_HOST=$${PIC_HOST:-localhost} python3 -m pytest tests/integration/ -v
|
|
|
|
test-webui:
|
|
@echo "Running webui unit tests (requires node; builds a disposable container)..."
|
|
docker run --rm -v "$(PWD)/webui:/app" -w /app node:18-alpine \
|
|
sh -c "npm install --silent && npm test"
|
|
|
|
test-integration-readonly:
|
|
@echo "Running read-only integration tests (no peer creation)..."
|
|
PIC_HOST=$${PIC_HOST:-localhost} python3 -m pytest tests/integration/test_live_api.py tests/integration/test_webui.py -v
|
|
|
|
test-api:
|
|
python3 -m pytest tests/test_api_endpoints.py -v
|
|
|
|
test-cli:
|
|
python3 -m pytest tests/test_cli_tool.py -v
|
|
|
|
# ── E2E tests ─────────────────────────────────────────────────────────────────
|
|
# Run `make test-e2e-deps` once to install dependencies, then use the other targets.
|
|
# Admin password is read from data/api/.test_admin_pass (written by reset-test-admin-pass).
|
|
# Override: make test-e2e-api PIC_ADMIN_PASS=mypassword
|
|
|
|
test-e2e-deps:
|
|
sudo pip3 install --break-system-packages -r tests/e2e/requirements.txt
|
|
sudo python3 -m playwright install --with-deps chromium
|
|
|
|
test-e2e-api:
|
|
@PIC_HOST=$${PIC_HOST:-localhost} python3 -m pytest tests/e2e/api -v
|
|
|
|
test-e2e-ui:
|
|
@PIC_HOST=$${PIC_HOST:-localhost} python3 -m pytest tests/e2e/ui -v
|
|
|
|
test-e2e-wg:
|
|
@PIC_HOST=$${PIC_HOST:-localhost} sudo -E python3 -m pytest tests/e2e/wg -v -p no:xdist
|
|
|
|
test-e2e: test-e2e-api test-e2e-ui test-e2e-wg
|
|
|
|
reset-test-admin-pass:
|
|
ifndef PIC_TEST_ADMIN_PASS
|
|
$(error Usage: make reset-test-admin-pass PIC_TEST_ADMIN_PASS=newpassword)
|
|
endif
|
|
python3 scripts/reset_admin_password.py "$(PIC_TEST_ADMIN_PASS)"
|
|
|
|
# ── Admin password management ──────────────────────────────────────────────────
|
|
|
|
show-admin-password:
|
|
@sudo python3 scripts/reset_admin_password.py --show
|
|
|
|
reset-admin-password:
|
|
@docker exec cell-api python3 /app/scripts/reset_admin_password.py --generate
|
|
|
|
|
|
# ── Network / peers ───────────────────────────────────────────────────────────
|
|
|
|
show-routes:
|
|
@docker exec cell-wireguard wg show 2>/dev/null || echo "WireGuard not running"
|
|
|
|
list-peers:
|
|
@curl -s http://localhost:3000/api/peers | python3 -m json.tool || echo "API not responding"
|
|
|
|
add-peer:
|
|
@if [ -n "$(PEER_NAME)" ] && [ -n "$(PEER_IP)" ] && [ -n "$(PEER_KEY)" ]; then \
|
|
curl -X POST http://localhost:3000/api/peers \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"name":"$(PEER_NAME)","ip":"$(PEER_IP)","public_key":"$(PEER_KEY)"}'; \
|
|
else \
|
|
echo "Usage: make add-peer PEER_NAME=name PEER_IP=10.0.0.x PEER_KEY=<pubkey>"; \
|
|
fi
|
|
|
|
# ── DDNS ─────────────────────────────────────────────────────────────────────
|
|
|
|
ddns-update:
|
|
@python3 scripts/ddns_update.py
|
|
|
|
ddns-register:
|
|
@DDNS_TOTP_SECRET="$(DDNS_TOTP_SECRET)" python3 -c "\
|
|
import os, sys; sys.path.insert(0, 'scripts'); \
|
|
from setup_cell import register_with_ddns, _read_existing_ip_range; \
|
|
import json; \
|
|
cfg = json.load(open('config/api/cell_config.json')) if os.path.exists('config/api/cell_config.json') else {}; \
|
|
name = cfg.get('_identity', {}).get('cell_name', os.environ.get('CELL_NAME', 'mycell')); \
|
|
import os; os.remove('data/api/.ddns_token') if os.path.exists('data/api/.ddns_token') else None; \
|
|
register_with_ddns(name)"
|
|
|
|
# ── Dev ───────────────────────────────────────────────────────────────────────
|
|
|
|
dev:
|
|
$(DC) -f docker-compose.yml -f docker-compose.dev.yml up -d
|