mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-07-30 18:56:16 +00:00
docs: runbook, FPGA programming guide, disaster recovery, API docs
- RUNBOOK.md: k3s/FPGA/Tailscale/GPU/DNS ops procedures - FPGA_PROGRAMMING_GUIDE.md: SUBLEQ format, memory map, 3 examples - DISASTER_RECOVERY.md: backup/restore for all components - API_DOCS.md: dashboard, credential, registry, jobs, blobs APIs
This commit is contained in:
parent
251ea9d9bf
commit
7a6df586b2
4 changed files with 1570 additions and 0 deletions
465
6-Documentation/API_DOCS.md
Normal file
465
6-Documentation/API_DOCS.md
Normal file
|
|
@ -0,0 +1,465 @@
|
|||
# API Documentation — Research Stack Services
|
||||
|
||||
**Last updated:** 2026-05-29
|
||||
**Base URL:** `https://researchstack.info`
|
||||
**Auth:** Authentik OIDC (SSO) or Bearer token (API endpoints)
|
||||
|
||||
---
|
||||
|
||||
## Authentication
|
||||
|
||||
### OIDC (SSO) — Web Endpoints
|
||||
|
||||
Web-facing endpoints use Authentik OIDC. Access via browser redirects to `auth.researchstack.info`.
|
||||
|
||||
### Token Auth — API Endpoints
|
||||
|
||||
API endpoints (`/api/*`) use Bearer token authentication:
|
||||
|
||||
```
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
Tokens are managed by the Credential Server.
|
||||
|
||||
---
|
||||
|
||||
## Cluster Dashboard
|
||||
|
||||
**Namespace:** `monitoring`
|
||||
**Internal URL:** `http://cluster-dashboard:8787`
|
||||
**External URL:** `https://researchstack.info/server/dash/` (via Homarr)
|
||||
**Tech:** FastAPI + Vite
|
||||
|
||||
### Endpoints
|
||||
|
||||
#### `GET /`
|
||||
|
||||
Dashboard UI (Vite SPA).
|
||||
|
||||
#### `GET /api/status`
|
||||
|
||||
Cluster health summary.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"name": "nixos-laptop",
|
||||
"status": "Ready",
|
||||
"ip": "100.102.173.61",
|
||||
"roles": "control-plane",
|
||||
"version": "v1.35.4+k3s1"
|
||||
}
|
||||
],
|
||||
"pods": {
|
||||
"total": 24,
|
||||
"running": 23,
|
||||
"pending": 0,
|
||||
"failed": 1
|
||||
},
|
||||
"namespaces": ["services", "media", "monitoring", "ai-models", "edge", "research", "mail"]
|
||||
}
|
||||
```
|
||||
|
||||
#### `GET /api/metrics`
|
||||
|
||||
Prometheus-compatible metrics.
|
||||
|
||||
#### `WebSocket /ws/live`
|
||||
|
||||
Real-time cluster events stream.
|
||||
|
||||
**Message format:**
|
||||
```json
|
||||
{
|
||||
"type": "pod_event",
|
||||
"namespace": "services",
|
||||
"pod": "homer-abc123",
|
||||
"status": "Running",
|
||||
"timestamp": "2026-05-29T12:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Credential Server
|
||||
|
||||
**Namespace:** `services`
|
||||
**Internal URL:** `http://credential-server:8080`
|
||||
**External URL:** `https://researchstack.info/api/cred/`
|
||||
**Auth:** Bearer token
|
||||
|
||||
### Endpoints
|
||||
|
||||
#### `GET /api/cred/health`
|
||||
|
||||
Health check.
|
||||
|
||||
**Response:** `200 OK`
|
||||
```json
|
||||
{"status": "ok"}
|
||||
```
|
||||
|
||||
#### `GET /api/cred/tokens`
|
||||
|
||||
List available tokens (requires admin token).
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"tokens": [
|
||||
{"name": "registry", "scope": "read,write", "expires": "2026-12-31"},
|
||||
{"name": "jobs", "scope": "read,write,execute", "expires": "2026-12-31"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### `POST /api/cred/issue`
|
||||
|
||||
Issue a new token.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"name": "service-name",
|
||||
"scope": "read,write",
|
||||
"ttl": "30d"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"token": "rs_tk_...",
|
||||
"expires": "2026-06-29T12:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
#### `POST /api/cred/validate`
|
||||
|
||||
Validate a token.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{"token": "rs_tk_..."}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"valid": true,
|
||||
"name": "registry",
|
||||
"scope": "read,write"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Registry API
|
||||
|
||||
**Namespace:** `services`
|
||||
**Internal URL:** `http://registry-api:8080`
|
||||
**External URL:** `https://researchstack.info/api/registry/`
|
||||
**Auth:** Bearer token
|
||||
|
||||
### Endpoints
|
||||
|
||||
#### `GET /api/registry/health`
|
||||
|
||||
Health check.
|
||||
|
||||
#### `GET /api/registry/artifacts`
|
||||
|
||||
List all registered artifacts.
|
||||
|
||||
**Query Parameters:**
|
||||
- `type` — Filter by artifact type (e.g., `bitstream`, `lean-build`, `receipt`)
|
||||
- `since` — ISO 8601 timestamp
|
||||
- `limit` — Max results (default: 50)
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"artifacts": [
|
||||
{
|
||||
"id": "art_abc123",
|
||||
"name": "research_stack_top.fs",
|
||||
"type": "bitstream",
|
||||
"size": 184320,
|
||||
"sha256": "...",
|
||||
"created": "2026-05-29T10:00:00Z",
|
||||
"tags": ["fpga", "production"]
|
||||
}
|
||||
],
|
||||
"total": 42
|
||||
}
|
||||
```
|
||||
|
||||
#### `POST /api/registry/artifacts`
|
||||
|
||||
Register a new artifact.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"name": "research_stack_top.fs",
|
||||
"type": "bitstream",
|
||||
"sha256": "...",
|
||||
"blob_ref": "blob_xyz",
|
||||
"tags": ["fpga"]
|
||||
}
|
||||
```
|
||||
|
||||
#### `GET /api/registry/artifacts/{id}`
|
||||
|
||||
Get artifact by ID.
|
||||
|
||||
#### `PUT /api/registry/artifacts/{id}`
|
||||
|
||||
Update artifact metadata.
|
||||
|
||||
#### `DELETE /api/registry/artifacts/{id}`
|
||||
|
||||
Remove artifact registration (does not delete blob).
|
||||
|
||||
---
|
||||
|
||||
## Jobs API
|
||||
|
||||
**Namespace:** `services`
|
||||
**Internal URL:** `http://jobs-api:8080`
|
||||
**External URL:** `https://researchstack.info/api/jobs/`
|
||||
**Auth:** Bearer token
|
||||
|
||||
### Endpoints
|
||||
|
||||
#### `GET /api/jobs/health`
|
||||
|
||||
Health check.
|
||||
|
||||
#### `GET /api/jobs`
|
||||
|
||||
List jobs.
|
||||
|
||||
**Query Parameters:**
|
||||
- `status` — Filter: `pending`, `running`, `completed`, `failed`
|
||||
- `type` — Filter by job type
|
||||
- `limit` — Max results (default: 20)
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"jobs": [
|
||||
{
|
||||
"id": "job_abc123",
|
||||
"type": "lean-build",
|
||||
"status": "completed",
|
||||
"created": "2026-05-29T10:00:00Z",
|
||||
"started": "2026-05-29T10:00:05Z",
|
||||
"finished": "2026-05-29T10:05:30Z",
|
||||
"result": {
|
||||
"jobs": 3572,
|
||||
"errors": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
"total": 156
|
||||
}
|
||||
```
|
||||
|
||||
#### `POST /api/jobs`
|
||||
|
||||
Create a new job.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"type": "lean-build",
|
||||
"params": {
|
||||
"target": "Semantics",
|
||||
"branch": "main"
|
||||
},
|
||||
"priority": "normal"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"id": "job_def456",
|
||||
"status": "pending",
|
||||
"created": "2026-05-29T12:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
#### `GET /api/jobs/{id}`
|
||||
|
||||
Get job status and result.
|
||||
|
||||
#### `POST /api/jobs/{id}/cancel`
|
||||
|
||||
Cancel a pending or running job.
|
||||
|
||||
#### `GET /api/jobs/{id}/logs`
|
||||
|
||||
Stream job logs (Server-Sent Events).
|
||||
|
||||
**Response:** `text/event-stream`
|
||||
```
|
||||
data: {"line": "Building Semantics...", "ts": "2026-05-29T12:00:01Z"}
|
||||
data: {"line": "3572 jobs, 0 errors", "ts": "2026-05-29T12:05:30Z"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Blobs API
|
||||
|
||||
**Namespace:** `services`
|
||||
**Internal URL:** `http://blobs-api:8080`
|
||||
**External URL:** `https://researchstack.info/api/blobs/`
|
||||
**Auth:** Bearer token
|
||||
|
||||
### Endpoints
|
||||
|
||||
#### `GET /api/blobs/health`
|
||||
|
||||
Health check.
|
||||
|
||||
#### `POST /api/blobs`
|
||||
|
||||
Upload a blob.
|
||||
|
||||
**Request:** `multipart/form-data`
|
||||
- `file` — Binary file content
|
||||
- `sha256` — Expected SHA-256 hash (verification)
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"id": "blob_xyz789",
|
||||
"size": 184320,
|
||||
"sha256": "...",
|
||||
"created": "2026-05-29T12:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
#### `GET /api/blobs/{id}`
|
||||
|
||||
Download a blob.
|
||||
|
||||
**Response:** `application/octet-stream` with blob content.
|
||||
|
||||
#### `HEAD /api/blobs/{id}`
|
||||
|
||||
Check blob existence.
|
||||
|
||||
**Response:** `200 OK` (exists) or `404 Not Found`
|
||||
|
||||
#### `DELETE /api/blobs/{id}`
|
||||
|
||||
Delete a blob.
|
||||
|
||||
#### `GET /api/blobs`
|
||||
|
||||
List blobs.
|
||||
|
||||
**Query Parameters:**
|
||||
- `limit` — Max results
|
||||
- `offset` — Pagination offset
|
||||
|
||||
---
|
||||
|
||||
## Authentik — OIDC Configuration
|
||||
|
||||
**URL:** `https://auth.researchstack.info`
|
||||
**Protocol:** OpenID Connect
|
||||
|
||||
### Provider Configuration
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Authorization URL | `https://auth.researchstack.info/application/o/authorize/` |
|
||||
| Token URL | `https://auth.researchstack.info/application/o/token/` |
|
||||
| UserInfo URL | `https://auth.researchstack.info/application/o/userinfo/` |
|
||||
| JWKS URL | `https://auth.researchstack.info/application/o/research-stack/jwks/` |
|
||||
| Issuer | `https://auth.researchstack.info/application/o/research-stack/` |
|
||||
|
||||
### Scopes
|
||||
|
||||
| Scope | Description |
|
||||
|-------|-------------|
|
||||
| `openid` | Standard OIDC |
|
||||
| `profile` | User profile (name, email) |
|
||||
| `email` | Email address |
|
||||
| `groups` | Group membership |
|
||||
|
||||
### Redirect URIs
|
||||
|
||||
```
|
||||
https://researchstack.info/oidc/callback
|
||||
https://researchstack.info/apps/chat/oidc/callback
|
||||
https://researchstack.info/apps/budget/oidc/callback
|
||||
```
|
||||
|
||||
### Application Setup
|
||||
|
||||
In Authentik admin:
|
||||
|
||||
1. **Applications** → Create → Name: `Research Stack`
|
||||
2. **Providers** → Create → OAuth2/OpenID Provider
|
||||
3. Set client type: `Confidential`
|
||||
4. Set redirect URIs above
|
||||
5. Assign to application
|
||||
|
||||
### Service Account Tokens
|
||||
|
||||
For API-to-API auth (no user interaction):
|
||||
|
||||
```bash
|
||||
# Create service account in Authentik
|
||||
# Admin → Directory → Users → Create
|
||||
# Type: Service Account
|
||||
|
||||
# Get token
|
||||
curl -X POST https://auth.researchstack.info/application/o/token/ \
|
||||
-d "grant_type=client_credentials" \
|
||||
-d "client_id=<service-account-id>" \
|
||||
-d "client_secret=<service-account-secret>" \
|
||||
-d "scope=openid"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Response Codes
|
||||
|
||||
| Code | Meaning |
|
||||
|------|---------|
|
||||
| 200 | Success |
|
||||
| 201 | Created |
|
||||
| 400 | Bad request (invalid parameters) |
|
||||
| 401 | Unauthorized (missing or invalid token) |
|
||||
| 403 | Forbidden (insufficient scope) |
|
||||
| 404 | Not found |
|
||||
| 409 | Conflict (duplicate resource) |
|
||||
| 422 | Unprocessable entity (validation error) |
|
||||
| 500 | Internal server error |
|
||||
|
||||
---
|
||||
|
||||
## Rate Limits
|
||||
|
||||
| Endpoint | Limit | Window |
|
||||
|----------|-------|--------|
|
||||
| `/api/cred/*` | 100 req | 1 minute |
|
||||
| `/api/registry/*` | 200 req | 1 minute |
|
||||
| `/api/jobs/*` | 50 req | 1 minute |
|
||||
| `/api/blobs/*` | 100 req | 1 minute |
|
||||
|
||||
Rate limit headers:
|
||||
```
|
||||
X-RateLimit-Limit: 100
|
||||
X-RateLimit-Remaining: 95
|
||||
X-RateLimit-Reset: 1685366400
|
||||
```
|
||||
343
6-Documentation/DISASTER_RECOVERY.md
Normal file
343
6-Documentation/DISASTER_RECOVERY.md
Normal file
|
|
@ -0,0 +1,343 @@
|
|||
# Disaster Recovery — Research Stack
|
||||
|
||||
**Last updated:** 2026-05-29
|
||||
|
||||
Procedures for backing up and restoring all Research Stack components.
|
||||
|
||||
---
|
||||
|
||||
## k3s Cluster
|
||||
|
||||
### Backup etcd
|
||||
|
||||
```bash
|
||||
# Snapshot etcd (on control plane node — nixos)
|
||||
sudo k3s etcd-snapshot save \
|
||||
--name researchstack-backup \
|
||||
--dir /var/lib/rancher/k3s/server/db/snapshots
|
||||
|
||||
# List snapshots
|
||||
sudo ls -la /var/lib/rancher/k3s/server/db/snapshots/
|
||||
|
||||
# Copy snapshot off-node
|
||||
scp /var/lib/rancher/k3s/server/db/snapshots/researchstack-backup-* \
|
||||
user@backup-host:/backups/k3s/
|
||||
```
|
||||
|
||||
### Restore from Snapshot
|
||||
|
||||
```bash
|
||||
# On control plane (nixos):
|
||||
sudo systemctl stop k3s
|
||||
|
||||
# Restore
|
||||
sudo k3s server \
|
||||
--cluster-reset \
|
||||
--cluster-reset-restore-path=/var/lib/rancher/k3s/server/db/snapshots/researchstack-backup-<timestamp>
|
||||
|
||||
# Restart
|
||||
sudo systemctl start k3s
|
||||
|
||||
# Verify
|
||||
export KUBECONFIG=/tmp/researchstack-kubeconfig.yaml
|
||||
kubectl get nodes
|
||||
kubectl get pods -A
|
||||
```
|
||||
|
||||
### Backup Manifests
|
||||
|
||||
All Kubernetes manifests are in the git repo. The source of truth is:
|
||||
|
||||
```bash
|
||||
cd ~/Research\ Stack
|
||||
git push origin main
|
||||
```
|
||||
|
||||
### Automated Backup Schedule
|
||||
|
||||
Create a cron job on nixos:
|
||||
|
||||
```bash
|
||||
# /etc/cron.d/k3s-backup
|
||||
0 3 * * * root k3s etcd-snapshot save --name researchstack-backup --dir /var/lib/rancher/k3s/server/db/snapshots && \
|
||||
find /var/lib/rancher/k3s/server/db/snapshots/ -mtime +7 -delete
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## FPGA — Tang Nano 9K
|
||||
|
||||
### Backup Bitstream
|
||||
|
||||
The bitstream source and compiled output are in the git repo:
|
||||
|
||||
```bash
|
||||
cd ~/Research\ Stack
|
||||
|
||||
# Source
|
||||
ls 4-Infrastructure/hardware/*.v
|
||||
|
||||
# Compiled bitstream
|
||||
ls 4-Infrastructure/hardware/research_stack_top.fs
|
||||
```
|
||||
|
||||
### Re-flash from Git
|
||||
|
||||
```bash
|
||||
cd ~/Research\ Stack
|
||||
|
||||
# Rebuild if needed
|
||||
cd 4-Infrastructure/hardware && bash build_research_stack.sh
|
||||
|
||||
# Flash
|
||||
openFPGALoader -b tangnano9k research_stack_top.fs
|
||||
|
||||
# Verify
|
||||
openFPGALoader -b tangnano9k --verify research_stack_top.fs
|
||||
```
|
||||
|
||||
### Backup SRAM Contents
|
||||
|
||||
SRAM contents are volatile (lost on power cycle). To preserve runtime state:
|
||||
|
||||
```python
|
||||
# Dump memory via UART before power-off
|
||||
import serial, struct
|
||||
|
||||
ser = serial.Serial('/dev/ttyUSB0', 115384, timeout=10)
|
||||
|
||||
# Trigger memory dump (send HALT with dump flag)
|
||||
# ... protocol-specific ...
|
||||
|
||||
# Read and save
|
||||
with open('fpga_memory_dump.bin', 'wb') as f:
|
||||
data = ser.read(8192) # 4K words = 8K bytes
|
||||
f.write(data)
|
||||
|
||||
ser.close()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tailscale
|
||||
|
||||
### Re-authenticate Node
|
||||
|
||||
```bash
|
||||
# On the node that lost auth:
|
||||
sudo systemctl restart tailscaled
|
||||
tailscale up --authkey=<TS_AUTH_KEY>
|
||||
|
||||
# Or interactive
|
||||
tailscale up
|
||||
```
|
||||
|
||||
### Re-join Tailnet
|
||||
|
||||
```bash
|
||||
# Check current status
|
||||
tailscale status
|
||||
|
||||
# If node is missing from tailnet:
|
||||
# 1. Generate new auth key at https://login.tailscale.com/admin/settings/keys
|
||||
# 2. On the node:
|
||||
sudo tailscale up --authkey=tskey-auth-<key>
|
||||
|
||||
# Verify connectivity
|
||||
tailscale ping nixos-laptop
|
||||
tailscale ping qfox-1
|
||||
```
|
||||
|
||||
### Backup Tailscale State
|
||||
|
||||
Tailscale state is stored at `/var/lib/tailscale/`:
|
||||
|
||||
```bash
|
||||
# Backup state directory
|
||||
sudo tar czf /backups/tailscale-state-$(date +%Y%m%d).tar.gz \
|
||||
/var/lib/tailscale/
|
||||
|
||||
# Restore
|
||||
sudo systemctl stop tailscaled
|
||||
sudo tar xzf /backups/tailscale-state-*.tar.gz -C /
|
||||
sudo systemctl start tailscaled
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Git Repository
|
||||
|
||||
### Backup
|
||||
|
||||
The canonical backup is the remote origin. Ensure all changes are pushed:
|
||||
|
||||
```bash
|
||||
cd ~/Research\ Stack
|
||||
|
||||
# Check for unpushed commits
|
||||
git log --oneline origin/main..HEAD
|
||||
|
||||
# Push everything
|
||||
git push origin main --tags
|
||||
|
||||
# Verify clean state
|
||||
git status --branch --short --untracked-files=all
|
||||
```
|
||||
|
||||
### Restore from Remote
|
||||
|
||||
```bash
|
||||
# Clone fresh
|
||||
cd ~
|
||||
git clone <remote-url> "Research Stack"
|
||||
|
||||
# Or reset to remote state
|
||||
cd ~/Research\ Stack
|
||||
git fetch origin
|
||||
git reset --hard origin/main
|
||||
```
|
||||
|
||||
### Backup Remotes
|
||||
|
||||
```bash
|
||||
# List remotes
|
||||
cd ~/Research\ Stack
|
||||
git remote -v
|
||||
|
||||
# Add backup remote
|
||||
git remote add backup <backup-url>
|
||||
git push backup main --tags
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Secrets (sops-nix / age)
|
||||
|
||||
### Backup age Keys
|
||||
|
||||
```bash
|
||||
# age key location (NixOS)
|
||||
cat /etc/age/keys.txt
|
||||
|
||||
# Backup
|
||||
cp /etc/age/keys.txt /backups/age-keys-$(date +%Y%m%d).txt
|
||||
chmod 600 /backups/age-keys-*.txt
|
||||
|
||||
# Or from home directory
|
||||
cat ~/.config/sops/age/keys.txt
|
||||
```
|
||||
|
||||
### Restore age Keys
|
||||
|
||||
```bash
|
||||
# Restore key file
|
||||
cp /backups/age-keys-*.txt /etc/age/keys.txt
|
||||
chmod 600 /etc/age/keys.txt
|
||||
|
||||
# Verify sops can decrypt
|
||||
sops -d secrets.enc.yaml
|
||||
```
|
||||
|
||||
### Backup sops Configuration
|
||||
|
||||
```bash
|
||||
# The .sops.yaml config is in the repo
|
||||
cat ~/Research\ Stack/.sops.yaml
|
||||
|
||||
# Encrypted secrets are also in the repo
|
||||
find ~/Research\ Stack -name '*.enc.yaml' -o -name '*.enc.json'
|
||||
```
|
||||
|
||||
### Regenerate age Key (Last Resort)
|
||||
|
||||
```bash
|
||||
# Generate new key pair
|
||||
age-keygen -o /etc/age/keys.txt
|
||||
|
||||
# Get public key
|
||||
age-keygen -y /etc/age/keys.txt
|
||||
|
||||
# Re-encrypt all secrets with new key
|
||||
# (requires old key or plaintext backup)
|
||||
sops updatekeys secrets.enc.yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## DNS — Porkbun
|
||||
|
||||
### Backup API Key
|
||||
|
||||
```bash
|
||||
# From k8s secret
|
||||
kubectl get secret porkbun-credentials -n services \
|
||||
-o jsonpath='{.data.api-key}' | base64 -d
|
||||
|
||||
# Back up securely
|
||||
echo "<api-key>" | age -r <backup-age-pubkey> > /backups/porkbun-api.age
|
||||
|
||||
# Or store in password manager
|
||||
```
|
||||
|
||||
### Recover API Key
|
||||
|
||||
1. Log in to [Porkbun](https://porkbun.com)
|
||||
2. Navigate to **Account → API Access**
|
||||
3. Regenerate or copy existing API key
|
||||
4. Update k8s secret:
|
||||
|
||||
```bash
|
||||
kubectl create secret generic porkbun-credentials \
|
||||
-n services \
|
||||
--from-literal=api-key=<new-key> \
|
||||
--dry-run=client -o yaml | kubectl apply -f -
|
||||
|
||||
# Restart Caddy to pick up new key
|
||||
kubectl rollout restart deployment/caddy -n services
|
||||
```
|
||||
|
||||
### Verify DNS Records
|
||||
|
||||
```bash
|
||||
dig researchstack.info +short
|
||||
dig auth.researchstack.info +short
|
||||
dig registry.researchstack.info +short
|
||||
|
||||
# Test Porkbun API
|
||||
curl -X POST https://api.porkbun.com/api/json/v3/ping \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"apikey": "<key>", "secretapikey": "<secret>"}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Recovery Priority Order
|
||||
|
||||
When restoring from a total failure, follow this order:
|
||||
|
||||
| Step | Component | Depends On |
|
||||
|------|-----------|------------|
|
||||
| 1 | **Tailscale** | Auth key (from password manager or backup) |
|
||||
| 2 | **Git repo** | Network connectivity (Tailscale or direct) |
|
||||
| 3 | **age keys** | Backup location |
|
||||
| 4 | **k3s cluster** | etcd snapshot + age keys for secrets |
|
||||
| 5 | **DNS/Porkbun** | API key (from backup or Porkbun account) |
|
||||
| 6 | **FPGA** | Git repo (source + bitstream) |
|
||||
| 7 | **Services** | k3s cluster + secrets + DNS |
|
||||
|
||||
---
|
||||
|
||||
## Full System Checklist
|
||||
|
||||
After recovery, verify each component:
|
||||
|
||||
- [ ] Tailscale: `tailscale status` shows all 5 nodes
|
||||
- [ ] k3s: `kubectl get nodes` shows all nodes Ready
|
||||
- [ ] Pods: `kubectl get pods -A` — all Running/Succeeded
|
||||
- [ ] DNS: `dig researchstack.info` resolves correctly
|
||||
- [ ] TLS: cert valid (`openssl s_client`)
|
||||
- [ ] Auth: `auth.researchstack.info` loads Authentik
|
||||
- [ ] Funnel: `361395-1.tail4e7094.ts.net` responds
|
||||
- [ ] Ollama: `curl http://100.88.57.96:31434/api/tags` returns models
|
||||
- [ ] FPGA: `openFPGALoader --detect` sees Tang Nano 9K
|
||||
- [ ] Dashboard: `https://researchstack.info` shows Homer
|
||||
363
6-Documentation/FPGA_PROGRAMMING_GUIDE.md
Normal file
363
6-Documentation/FPGA_PROGRAMMING_GUIDE.md
Normal file
|
|
@ -0,0 +1,363 @@
|
|||
# FPGA Programming Guide — SUBLEQ on the Blitter
|
||||
|
||||
**Last updated:** 2026-05-29
|
||||
**Board:** Sipeed Tang Nano 9K (Gowin GW1NR-LV9)
|
||||
**CPU:** Blitter6502OISC (One Instruction Set Computer — SUBLEQ)
|
||||
|
||||
---
|
||||
|
||||
## SUBLEQ Instruction Format
|
||||
|
||||
The Blitter implements a **SUBLEQ** (Subtract and Branch if Less-than-or-Equal to Zero) CPU.
|
||||
|
||||
Each instruction is **3 words** (3 × 16-bit = 6 bytes):
|
||||
|
||||
```
|
||||
[src] [dst] [next]
|
||||
```
|
||||
|
||||
**Semantics:**
|
||||
```
|
||||
mem[dst] = mem[dst] - mem[src]
|
||||
if mem[dst] <= 0:
|
||||
PC = next
|
||||
else:
|
||||
PC = PC + 3
|
||||
```
|
||||
|
||||
**Special addresses:**
|
||||
- If `next == 0` (or `next == PC`): **HALT**
|
||||
- If `src == 0`: reads zero (constant source)
|
||||
- If `dst == IO_ADDR`: writes to I/O
|
||||
|
||||
---
|
||||
|
||||
## Memory Map ($0000–$FFFF)
|
||||
|
||||
The SUBLEQ address space is 16-bit (64K words). The memory map is divided into regions:
|
||||
|
||||
| Address Range | Size | Function |
|
||||
|---------------|------|----------|
|
||||
| `$0000–$0FFF` | 4K words | Program + data (BRAM) |
|
||||
| `$1000–$7FFF` | 28K words | Extended data (if available) |
|
||||
| `$8000–$800F` | 16 words | Q16 LUT result registers |
|
||||
| `$8010` | 1 word | Voltage controller mode |
|
||||
| `$8011` | 1 word | Scale space parameter |
|
||||
| `$8020–$8025` | 6 words | HiGHS pivot registers |
|
||||
| `$FF00` | 1 word | UART TX data register |
|
||||
| `$FF01` | 1 word | UART TX status (bit 0 = busy) |
|
||||
| `$FF02` | 1 word | UART RX data register |
|
||||
| `$FF03` | 1 word | UART RX status (bit 0 = data available) |
|
||||
| `$FFF0` | 1 word | LED output (bits 0-5 = led[0:5]) |
|
||||
| `$FFF1` | 1 word | Button input (bit 0 = user_btn) |
|
||||
|
||||
---
|
||||
|
||||
## Q16 LUT ($8000–$8025)
|
||||
|
||||
The Q16 LUT is a hardware-accelerated fixed-point arithmetic unit. It operates on Q16.16 values (16-bit integer, 16-bit fraction; total 1.0 = 65536).
|
||||
|
||||
### Q16 Operations
|
||||
|
||||
Write operands to the LUT registers, then read the result:
|
||||
|
||||
| Address | Register | Function |
|
||||
|---------|----------|----------|
|
||||
| `$8000` | OP_A (lo) | Operand A, low word |
|
||||
| `$8001` | OP_A (hi) | Operand A, high word |
|
||||
| `$8002` | OP_B (lo) | Operand B, low word |
|
||||
| `$8003` | OP_B (hi) | Operand B, high word |
|
||||
| `$8004` | OPCODE | Operation selector (0-7) |
|
||||
| `$8008` | RESULT (lo) | Result, low word |
|
||||
| `$8009` | RESULT (hi) | Result, high word |
|
||||
|
||||
### Opcodes
|
||||
|
||||
| Code | Operation | Latency |
|
||||
|------|-----------|---------|
|
||||
| 0 | A + B | 2 cycles (74ns @ 27MHz) |
|
||||
| 1 | A - B | 2 cycles |
|
||||
| 2 | A × B | 2 cycles |
|
||||
| 3 | A ÷ B | 2 cycles |
|
||||
| 4 | √A | 2 cycles |
|
||||
| 5 | \|A\| | 2 cycles |
|
||||
| 6 | min(A, B) | 2 cycles |
|
||||
| 7 | max(A, B) | 2 cycles |
|
||||
|
||||
### Q16.16 Encoding
|
||||
|
||||
```
|
||||
value = integer_part × 65536 + fraction_part
|
||||
|
||||
Examples:
|
||||
1.0 = 65536 (0x00010000)
|
||||
0.5 = 32768 (0x00008000)
|
||||
3.14 = 205887 (0x000323D7)
|
||||
-1.0 = -65536 (0xFFFF0000)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Voltage Controller ($8010)
|
||||
|
||||
The voltage controller manages BRAM access modes:
|
||||
|
||||
| Mode | Value | Description |
|
||||
|------|-------|-------------|
|
||||
| STORE | 0 | Direct memory read/write |
|
||||
| COMPUTE | 1 | Q16 LUT computation mode |
|
||||
| APPROX | 2 | Approximate computation (fast) |
|
||||
| MORPHIC | 3 | Morphic field mode |
|
||||
|
||||
```subleq
|
||||
; Set voltage controller to COMPUTE mode
|
||||
; Write 1 to address $8010
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Scale Space ($8011)
|
||||
|
||||
The scale space parameter controls Gaussian kernel selection:
|
||||
|
||||
| Value | σ (sigma) | Kernel Bank |
|
||||
|-------|-----------|-------------|
|
||||
| 0 | 0.25 | Bank 0 |
|
||||
| 1 | 0.50 | Bank 1 |
|
||||
| 2 | 0.75 | Bank 2 |
|
||||
| 3 | 1.00 | Bank 3 |
|
||||
|
||||
---
|
||||
|
||||
## HiGHS Pivot Registers ($8020–$8025)
|
||||
|
||||
3-stage simplex pipeline interface for hardware-accelerated LP solving:
|
||||
|
||||
| Address | Register | Function |
|
||||
|---------|----------|----------|
|
||||
| `$8020` | PIVOT_ROW | Row index |
|
||||
| `$8021` | PIVOT_COL | Column index |
|
||||
| `$8022` | PIVOT_VAL (lo) | Pivot value, low |
|
||||
| `$8023` | PIVOT_VAL (hi) | Pivot value, high |
|
||||
| `$8024` | PIVOT_CTRL | Control/status |
|
||||
| `$8025` | PIVOT_RESULT | Result/iteration count |
|
||||
|
||||
---
|
||||
|
||||
## Example Programs
|
||||
|
||||
### 1. Blink LED
|
||||
|
||||
Blink LED 0 in a loop:
|
||||
|
||||
```subleq
|
||||
; Program at address 0
|
||||
; Toggle LED 0 by XOR with 1
|
||||
|
||||
; mem[100] = 1 (constant)
|
||||
; mem[101] = LED address ($FFF0)
|
||||
; mem[102] = current LED state
|
||||
; mem[103] = 0 (zero constant)
|
||||
|
||||
; Instruction 0: sub 103 from 102, store in 102 (clear 102)
|
||||
addr 0: 103 102 3 ; mem[102] = mem[102] - mem[103] = 0
|
||||
|
||||
; Instruction 3: sub 103 from LED, store in LED (clear LED)
|
||||
addr 3: 103 2545 6 ; mem[$FFF0] = mem[$FFF0] - 0
|
||||
|
||||
; Instruction 6: sub 100 from LED, store in LED (set bit 0)
|
||||
addr 6: 100 2545 9 ; mem[$FFF0] = mem[$FFF0] - 1
|
||||
|
||||
; Instruction 9: delay loop
|
||||
addr 9: 104 104 12 ; mem[104] = mem[104] - 1
|
||||
addr 12: 104 104 0 ; if mem[104] <= 0, jump to 0 (restart)
|
||||
|
||||
; Data
|
||||
addr 100: 1 ; toggle mask
|
||||
addr 101: 0 ; unused
|
||||
addr 102: 0 ; LED state
|
||||
addr 103: 0 ; zero
|
||||
addr 104: 50000 ; delay counter
|
||||
```
|
||||
|
||||
**Assembled binary:**
|
||||
```
|
||||
0064 0066 0003
|
||||
0067 09F1 0006
|
||||
0064 09F1 0009
|
||||
0068 0068 000C
|
||||
0068 0068 0000
|
||||
0001 0000 0000 0000 C350
|
||||
```
|
||||
|
||||
### 2. Q16 Addition
|
||||
|
||||
Add two Q16.16 values using the hardware LUT:
|
||||
|
||||
```subleq
|
||||
; Write operands to Q16 LUT, read result
|
||||
|
||||
; mem[200] = operand A = 3.14 (Q16: 205887 = 0x000323D7)
|
||||
; mem[201] = operand B = 2.72 (Q16: 178258 = 0x0002B8F2)
|
||||
|
||||
; Write A low word to $8000
|
||||
addr 0: 200 32768 3 ; mem[$8000] = mem[200] (A low)
|
||||
|
||||
; Write A high word to $8001
|
||||
addr 3: 201 32769 6 ; mem[$8001] = 0 (A high)
|
||||
|
||||
; Write B low word to $8002
|
||||
addr 6: 202 32770 9 ; mem[$8002] = mem[202] (B low)
|
||||
|
||||
; Write B high word to $8003
|
||||
addr 9: 203 32771 12 ; mem[$8003] = 0 (B high)
|
||||
|
||||
; Set opcode to 0 (add)
|
||||
addr 12: 204 32772 15 ; mem[$8004] = 0
|
||||
|
||||
; Read result low from $8008
|
||||
addr 15: 204 32776 18 ; mem[$8008] -> read
|
||||
|
||||
; Store result to mem[210]
|
||||
addr 18: 32776 210 21 ; mem[210] = result low
|
||||
|
||||
; HALT
|
||||
addr 21: 0 0 0
|
||||
|
||||
; Data
|
||||
addr 200: 23D7 ; A low (3.14)
|
||||
addr 201: 0003 ; A high
|
||||
addr 202: B8F2 ; B low (2.72)
|
||||
addr 203: 0002 ; B high
|
||||
addr 204: 0000 ; opcode 0 (add)
|
||||
```
|
||||
|
||||
### 3. UART Send Character
|
||||
|
||||
Send 'A' (0x41) over UART:
|
||||
|
||||
```subleq
|
||||
; Wait for TX to be not busy, then send character
|
||||
|
||||
; mem[300] = 0x41 ('A')
|
||||
; mem[301] = 0 (zero)
|
||||
; mem[302] = UART TX status address ($FF01)
|
||||
; mem[303] = UART TX data address ($FF00)
|
||||
|
||||
; Check TX status (poll loop)
|
||||
addr 0: 302 304 3 ; mem[304] = mem[$FF01]
|
||||
addr 3: 304 304 6 ; mem[304] -= mem[304] (test if zero)
|
||||
addr 6: 304 304 9 ; if <= 0 (not busy), continue
|
||||
addr 9: 301 304 0 ; else, reset and retry
|
||||
|
||||
; Send character
|
||||
addr 12: 300 303 15 ; mem[$FF00] = 0x41
|
||||
|
||||
; HALT
|
||||
addr 15: 0 0 0
|
||||
|
||||
; Data
|
||||
addr 300: 0041 ; 'A'
|
||||
addr 301: 0000 ; zero
|
||||
addr 302: FF01 ; UART TX status
|
||||
addr 303: FF00 ; UART TX data
|
||||
addr 304: 0000 ; temp
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Loading Programs via UART
|
||||
|
||||
### Using Python
|
||||
|
||||
```python
|
||||
import serial
|
||||
import struct
|
||||
|
||||
# Load assembled program (array of 16-bit words)
|
||||
program = [0x0064, 0x0066, 0x0003, ...] # assembled instructions
|
||||
|
||||
# Connect to FPGA UART
|
||||
ser = serial.Serial('/dev/ttyUSB0', 115384, timeout=1)
|
||||
|
||||
# Send program: each word as 2 bytes, big-endian
|
||||
for word in program:
|
||||
ser.write(struct.pack('>H', word))
|
||||
|
||||
ser.close()
|
||||
```
|
||||
|
||||
### Using openFPGALoader (SRAM load)
|
||||
|
||||
For quick iteration (non-persistent):
|
||||
|
||||
```bash
|
||||
# Load bitstream to SRAM (lost on power cycle)
|
||||
openFPGALoader -b tangnano9k --sram research_stack_top.fs
|
||||
|
||||
# Load to flash (persistent)
|
||||
openFPGALoader -b tangnano9k research_stack_top.fs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Reading Results
|
||||
|
||||
### Via UART
|
||||
|
||||
```python
|
||||
import serial
|
||||
|
||||
ser = serial.Serial('/dev/ttyUSB0', 115384, timeout=5)
|
||||
|
||||
# Read result bytes
|
||||
data = ser.read(2) # 1 word = 2 bytes
|
||||
result = struct.unpack('>H', data)[0]
|
||||
print(f"Result: {result} (0x{result:04X})")
|
||||
|
||||
ser.close()
|
||||
```
|
||||
|
||||
### Via LEDs
|
||||
|
||||
Read the 6 LEDs (pins 10-16) as a 6-bit value from `led[0:5]`.
|
||||
- LED 0 = bit 0 (rightmost)
|
||||
|
||||
### Via Memory Dump
|
||||
|
||||
After HALT, the UART TX beacon outputs the full memory contents.
|
||||
Connect a serial terminal and observe the dump.
|
||||
|
||||
---
|
||||
|
||||
## Build Toolchain
|
||||
|
||||
```bash
|
||||
# Synthesis
|
||||
cd 4-Infrastructure/hardware && bash build_research_stack.sh
|
||||
|
||||
# Simulation
|
||||
cd /tmp/fpga_sim_full && ./obj_dir/sim_top
|
||||
|
||||
# Flash
|
||||
openFPGALoader -b tangnano9k research_stack_top.fs
|
||||
```
|
||||
|
||||
### Tool Versions
|
||||
|
||||
| Tool | Version |
|
||||
|------|---------|
|
||||
| Yosys | 0.64 |
|
||||
| nextpnr-himbaechel | 0.10-75 |
|
||||
| gowin_pack | latest |
|
||||
| Verilator | 5.048 |
|
||||
| openFPGALoader | latest |
|
||||
|
||||
---
|
||||
|
||||
## Timing
|
||||
|
||||
- **Clock:** 27 MHz (37.04 ns period)
|
||||
- **Achieved Fmax:** 195.92 MHz (7.2× margin)
|
||||
- **Q16 LUT latency:** 2 cycles (74 ns)
|
||||
- **MAX_CYCLES:** 1,000,000
|
||||
399
6-Documentation/RUNBOOK.md
Normal file
399
6-Documentation/RUNBOOK.md
Normal file
|
|
@ -0,0 +1,399 @@
|
|||
# Ops Runbook — Research Stack
|
||||
|
||||
**Last updated:** 2026-05-29
|
||||
|
||||
Quick-reference operational procedures for k3s, FPGA, Tailscale, GPU, and DNS.
|
||||
|
||||
---
|
||||
|
||||
## k3s Cluster
|
||||
|
||||
### Start / Stop
|
||||
|
||||
```bash
|
||||
# Set kubeconfig
|
||||
export KUBECONFIG=/tmp/researchstack-kubeconfig.yaml
|
||||
|
||||
# Check cluster status
|
||||
kubectl get nodes
|
||||
kubectl get pods -A
|
||||
```
|
||||
|
||||
**Control plane (nixos):**
|
||||
```bash
|
||||
# Restart k3s server
|
||||
sudo systemctl restart k3s
|
||||
|
||||
# Check k3s server health
|
||||
sudo systemctl status k3s
|
||||
sudo journalctl -u k3s -f --lines=50
|
||||
```
|
||||
|
||||
### Check Health
|
||||
|
||||
```bash
|
||||
# All nodes ready
|
||||
kubectl get nodes -o wide
|
||||
|
||||
# All pods running (any namespace)
|
||||
kubectl get pods -A --field-selector='status.phase!=Running,status.phase!=Succeeded'
|
||||
|
||||
# Specific namespace
|
||||
kubectl get pods -n services
|
||||
kubectl get pods -n media
|
||||
kubectl get pods -n monitoring
|
||||
kubectl get pods -n ai-models
|
||||
```
|
||||
|
||||
### Restart Pods
|
||||
|
||||
```bash
|
||||
# Restart a deployment (rolling restart)
|
||||
kubectl rollout restart deployment/<name> -n <namespace>
|
||||
|
||||
# Examples
|
||||
kubectl rollout restart deployment/homer -n services
|
||||
kubectl rollout restart deployment/ollama -n ai-models
|
||||
kubectl rollout restart deployment/cluster-dashboard -n monitoring
|
||||
|
||||
# Force delete stuck pod
|
||||
kubectl delete pod <pod-name> -n <namespace> --grace-period=0 --force
|
||||
```
|
||||
|
||||
### View Logs
|
||||
|
||||
```bash
|
||||
# Pod logs
|
||||
kubectl logs <pod-name> -n <namespace> --tail=100 -f
|
||||
|
||||
# Previous container (if crashed)
|
||||
kubectl logs <pod-name> -n <namespace> --previous
|
||||
|
||||
# All pods matching label
|
||||
kubectl logs -l app=<label> -n <namespace> --tail=50
|
||||
```
|
||||
|
||||
### Common Pods
|
||||
|
||||
| Namespace | Service | Deployment Name |
|
||||
|-----------|---------|-----------------|
|
||||
| `services` | Homer | `homer` |
|
||||
| `services` | Hermes | `hermes` |
|
||||
| `services` | Actual Budget | `actual-budget` |
|
||||
| `services` | Uptime Kuma | `uptime-kuma` |
|
||||
| `services` | Vaultwarden | `vaultwarden` |
|
||||
| `services` | Authentik | `authentik` |
|
||||
| `services` | Credential Server | `credential-server` |
|
||||
| `services` | Registry API | `registry-api` |
|
||||
| `services` | Jobs API | `jobs-api` |
|
||||
| `services` | Blobs API | `blobs-api` |
|
||||
| `ai-models` | Ollama | `ollama` |
|
||||
| `monitoring` | Cluster Dashboard | `cluster-dashboard` |
|
||||
| `media` | Jellyfin | `jellyfin` |
|
||||
| `research` | AlphaProof | `alphaproof` |
|
||||
|
||||
---
|
||||
|
||||
## FPGA — Tang Nano 9K
|
||||
|
||||
### Flash Bitstream
|
||||
|
||||
```bash
|
||||
# Build
|
||||
cd 4-Infrastructure/hardware && bash build_research_stack.sh
|
||||
|
||||
# Flash via USB-JTAG
|
||||
openFPGALoader -b tangnano9k research_stack_top.fs
|
||||
|
||||
# Verify flash
|
||||
openFPGALoader -b tangnano9k --verify research_stack_top.fs
|
||||
```
|
||||
|
||||
### Verify LEDs
|
||||
|
||||
After power-on, `led[0:5]` (pins 10-16) should show SUBLEQ state.
|
||||
- **All off:** CPU halted or not clocked
|
||||
- **Blinking:** CPU running (heartbeat)
|
||||
- **LED 0 solid on:** CPU halted (trap)
|
||||
|
||||
### Debug UART
|
||||
|
||||
```bash
|
||||
# Connect to UART TX (pin 17) via USB-serial adapter
|
||||
# Baud rate: 115384 (27MHz / 234)
|
||||
picocom -b 115384 /dev/ttyUSB0
|
||||
|
||||
# Or with screen
|
||||
screen /dev/ttyUSB0 115384
|
||||
```
|
||||
|
||||
### Run Simulation
|
||||
|
||||
```bash
|
||||
cd /tmp/fpga_sim_full && ./obj_dir/sim_top
|
||||
```
|
||||
|
||||
### Pin Reference
|
||||
|
||||
| Pin | Signal | Direction | Notes |
|
||||
|-----|--------|-----------|-------|
|
||||
| 52 | clk | input | 27 MHz oscillator |
|
||||
| 4 | rst_n | input | Active-low reset (pull-up) |
|
||||
| 3 | user_btn | input | Active-low (pull-up) |
|
||||
| 10-16 | led[0:5] | output | LVCMOS18 |
|
||||
| 17 | uart_tx | output | 115384 baud |
|
||||
| 18 | uart_rx | input | Pull-up |
|
||||
|
||||
### FPGA Not Responding
|
||||
|
||||
1. Check USB connection to Tang Nano 9K
|
||||
2. Verify `openFPGALoader` sees the device: `openFPGALoader --detect`
|
||||
3. Re-flash bitstream: `openFPGALoader -b tangnano9k research_stack_top.fs`
|
||||
4. Check clock: pin 52 should have 27 MHz (oscilloscope)
|
||||
5. Assert reset: pull pin 4 low momentarily, then release
|
||||
6. Check power: board should draw ~100mA from USB
|
||||
|
||||
---
|
||||
|
||||
## Tailscale
|
||||
|
||||
### Check Status
|
||||
|
||||
```bash
|
||||
# Show tailnet status
|
||||
tailscale status
|
||||
|
||||
# Show current node IP
|
||||
tailscale ip
|
||||
|
||||
# Check connectivity to other nodes
|
||||
tailscale ping qfox-1
|
||||
tailscale ping 361395-1
|
||||
tailscale ping racknerd-510bd9c
|
||||
```
|
||||
|
||||
### Restart Funnel
|
||||
|
||||
The Funnel runs on `361395-1` and routes to Traefik NodePort 30080:
|
||||
|
||||
```bash
|
||||
# On 361395-1 (edge node):
|
||||
tailscale funnel 8080 off
|
||||
tailscale funnel 8080
|
||||
|
||||
# Verify funnel URL
|
||||
tailscale funnel status
|
||||
```
|
||||
|
||||
### Debug Relay
|
||||
|
||||
```bash
|
||||
# Check if relay is in use
|
||||
tailscale status | grep relay
|
||||
|
||||
# Force direct connections (disable relay)
|
||||
tailscale set --direct-peers-only=true
|
||||
|
||||
# Reset to default
|
||||
tailscale set --direct-peers-only=false
|
||||
```
|
||||
|
||||
### Re-authenticate
|
||||
|
||||
```bash
|
||||
# On any node:
|
||||
tailscale up --authkey=<TS_AUTH_KEY>
|
||||
|
||||
# Or interactive login
|
||||
tailscale up
|
||||
```
|
||||
|
||||
### Node IPs
|
||||
|
||||
| Node | Tailscale IP | Role |
|
||||
|------|-------------|------|
|
||||
| nixos | 100.102.173.61 | Control plane |
|
||||
| qfox-1 | 100.88.57.96 | GPU worker |
|
||||
| 361395-1 | 100.110.163.82 | Edge/Funnel |
|
||||
| racknerd | 100.80.39.40 | Edge worker |
|
||||
| steamdeck | 100.85.244.73 | Worker |
|
||||
|
||||
---
|
||||
|
||||
## GPU — QFox (RTX 4070)
|
||||
|
||||
### Check nvidia-smi
|
||||
|
||||
```bash
|
||||
# GPU status
|
||||
nvidia-smi
|
||||
|
||||
# Watch GPU utilization
|
||||
nvidia-smi -l 1
|
||||
|
||||
# Check driver version
|
||||
nvidia-smi --query-gpu=driver_version --format=csv,noheader
|
||||
# Expected: 610.43
|
||||
```
|
||||
|
||||
### Restart Ollama
|
||||
|
||||
```bash
|
||||
# If running as k3s pod
|
||||
kubectl rollout restart deployment/ollama -n ai-models
|
||||
kubectl logs -l app=ollama -n ai-models --tail=20 -f
|
||||
|
||||
# If running as systemd service on qfox-1
|
||||
sudo systemctl restart ollama
|
||||
sudo journalctl -u ollama -f --tail=20
|
||||
```
|
||||
|
||||
### Pull / Manage Models
|
||||
|
||||
```bash
|
||||
# List loaded models
|
||||
curl http://100.88.57.96:11434/api/tags
|
||||
|
||||
# Pull a model
|
||||
curl http://100.88.57.96:11434/api/pull -d '{"name": "deepseek-coder-v2:16b"}'
|
||||
|
||||
# Test inference
|
||||
curl http://100.88.57.96:11434/api/generate \
|
||||
-d '{"model": "deepseek-coder-v2:16b", "prompt": "Hello", "stream": false}'
|
||||
```
|
||||
|
||||
### Ollama via NodePort
|
||||
|
||||
```bash
|
||||
# Access from cluster
|
||||
curl http://<any-node-ip>:31434/api/tags
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## DNS & TLS
|
||||
|
||||
### Check LE Certificates
|
||||
|
||||
```bash
|
||||
# Check cert expiry
|
||||
echo | openssl s_client -connect researchstack.info:443 -servername researchstack.info 2>/dev/null | openssl x509 -noout -dates
|
||||
|
||||
# Wildcard cert
|
||||
echo | openssl s_client -connect registry.researchstack.info:443 -servername registry.researchstack.info 2>/dev/null | openssl x509 -noout -dates
|
||||
```
|
||||
|
||||
**Current cert valid until:** 2026-08-18
|
||||
|
||||
### Renew Certificates
|
||||
|
||||
Caddy auto-renews via Porkbun DNS-01. If manual renewal is needed:
|
||||
|
||||
```bash
|
||||
# Restart Caddy to trigger renewal check
|
||||
kubectl rollout restart deployment/caddy -n services
|
||||
|
||||
# Check Caddy logs for renewal
|
||||
kubectl logs -l app=caddy -n services --tail=50 | grep -i renew
|
||||
```
|
||||
|
||||
### Check Porkbun DNS
|
||||
|
||||
```bash
|
||||
# Verify A record
|
||||
dig researchstack.info +short
|
||||
dig auth.researchstack.info +short
|
||||
dig registry.researchstack.info +short
|
||||
|
||||
# Check API key (set PORKBUN_API_KEY env var)
|
||||
curl -X POST https://api.porkbun.com/api/json/v3/ping \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"apikey": "'$PORKBUN_API_KEY'"}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Failures
|
||||
|
||||
### Node Down
|
||||
|
||||
```bash
|
||||
# Identify down node
|
||||
kubectl get nodes
|
||||
|
||||
# Check node conditions
|
||||
kubectl describe node <node-name>
|
||||
|
||||
# On the node itself:
|
||||
sudo systemctl status k3s-agent # worker nodes
|
||||
sudo systemctl status k3s # control plane
|
||||
|
||||
# Rejoin cluster if needed (worker):
|
||||
sudo k3s agent --server https://100.102.173.61:6443 --token <TOKEN>
|
||||
```
|
||||
|
||||
### Pod CrashLoopBackOff
|
||||
|
||||
```bash
|
||||
# Check events
|
||||
kubectl describe pod <pod-name> -n <namespace>
|
||||
|
||||
# Check logs (including previous crash)
|
||||
kubectl logs <pod-name> -n <namespace> --previous --tail=100
|
||||
|
||||
# Common fixes:
|
||||
# - ConfigMap/Secret missing: kubectl get configmap -n <ns>; kubectl get secret -n <ns>
|
||||
# - Image pull error: check image tag and registry access
|
||||
# - Resource limits: kubectl top pod -n <namespace>
|
||||
# - OOMKill: increase memory limit in deployment spec
|
||||
```
|
||||
|
||||
### FPGA Not Responding
|
||||
|
||||
1. Unplug and replug USB cable
|
||||
2. Check `ls /dev/ttyUSB*` — device should appear
|
||||
3. Re-flash: `openFPGALoader -b tangnano9k research_stack_top.fs`
|
||||
4. If JTAG fails: try holding reset (pin 4 low) while plugging in
|
||||
5. Check power LED on Tang Nano 9K board
|
||||
6. Try different USB port / cable
|
||||
|
||||
### Tailscale Tunnel Down
|
||||
|
||||
```bash
|
||||
# Check daemon
|
||||
sudo systemctl status tailscaled
|
||||
|
||||
# Restart
|
||||
sudo systemctl restart tailscaled
|
||||
tailscale up
|
||||
|
||||
# Check if key expired
|
||||
tailscale status | grep -i expir
|
||||
```
|
||||
|
||||
### DNS Resolution Failing
|
||||
|
||||
```bash
|
||||
# Check Caddy pod
|
||||
kubectl logs -l app=caddy -n services --tail=50
|
||||
|
||||
# Test DNS from inside cluster
|
||||
kubectl run -it --rm debug --image=busybox --restart=Never -- nslookup researchstack.info
|
||||
|
||||
# Check Porkbun API key
|
||||
kubectl get secret porkbun-credentials -n services -o jsonpath='{.data.api-key}' | base64 -d
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Escalation Matrix
|
||||
|
||||
| Issue | First Response | Escalation |
|
||||
|-------|---------------|------------|
|
||||
| Pod down | `kubectl rollout restart` | Check node, check resource limits |
|
||||
| Node down | SSH to node, check `systemctl` | Reboot, check hardware |
|
||||
| FPGA unresponsive | Re-flash bitstream | Check USB, try different board |
|
||||
| Tailscale tunnel | `tailscale up` | Check auth key, restart daemon |
|
||||
| DNS/cert | Restart Caddy | Check Porkbun API, check ingress |
|
||||
| GPU errors | Check `nvidia-smi` | Restart driver, check PCIe seating |
|
||||
| OOM on Ollama | Restart pod | Reduce context length, switch model |
|
||||
Loading…
Add table
Reference in a new issue