Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions hunter/ansible/playbook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@
state: directory
mode: '0755'

- name: Create pickipedia backups directory (synced from maybelle)
file:
path: /opt/magenta/pickipedia-backups
state: directory
mode: '0755'

- name: Include Security role (SSH hardening, fail2ban, firewall)
include_role:
name: security
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ services:
# SSH keys mounted separately (read-only for security)
- {{ base_dir }}/{{ user.name }}/.ssh:/home/magent/.ssh:ro
- /var/run/docker.sock:/var/run/docker.sock
# PickiPedia backups (synced from maybelle daily)
- /opt/magenta/pickipedia-backups:/opt/magenta/pickipedia-backups:ro
networks:
- magenta-net
environment:
Expand Down
86 changes: 86 additions & 0 deletions hunter/container_startup.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,92 @@ def setup_workspace():
run_install=False
)

# Clone pickipedia (MediaWiki knowledge base)
ensure_repo_cloned(
"https://github.com/cryptograss/pickipedia.git",
workspace / "pickipedia",
user='magent',
run_install=False
)

# Create pickipedia config for local preview if it doesn't exist
pickipedia_dir = workspace / "pickipedia"
if pickipedia_dir.exists():
# Create .env with user-specific settings
pickipedia_env = pickipedia_dir / ".env"
dev_name = os.environ.get('DEVELOPER_NAME', 'dev')
if not pickipedia_env.exists():
pickipedia_env.write_text(f"""# PickiPedia local preview settings for {dev_name}
MEDIAWIKI_VERSION=1.43.0
WIKI_PORT=4005
COMPOSE_PROJECT_NAME=pickipedia-{dev_name}
DB_NAME=pickipedia
DB_USER=pickipedia
DB_PASSWORD=pickipedia_dev
DB_ROOT_PASSWORD=root_dev
""")
run_command(f"chown magent:magent {pickipedia_env}")
logger.info("✓ Created pickipedia .env for local preview")

# Create LocalSettings.local.php for docker-compose preview
local_settings = pickipedia_dir / "LocalSettings.local.php"
if not local_settings.exists():
local_settings.write_text("""<?php
// Local preview settings - connects to docker-compose MySQL
$wgSecretKey = "dev-secret-key-not-for-production-use-only";
$wgUpgradeKey = "dev-upgrade-key";
$wgDBtype = "mysql";
$wgDBserver = "db"; // docker-compose service name
$wgDBname = "pickipedia";
$wgDBuser = "pickipedia";
$wgDBpassword = "pickipedia_dev";
""")
run_command(f"chown magent:magent {local_settings}")
logger.info("✓ Created pickipedia LocalSettings.local.php for preview")

# Create helper script to load production backup into preview
load_backup_script = pickipedia_dir / "load-backup.sh"
if not load_backup_script.exists():
load_backup_script.write_text("""#!/bin/bash
# Load latest PickiPedia backup into preview MySQL
# Run this after 'docker-compose up -d' to populate with production data

set -e

BACKUP_DIR="/opt/magenta/pickipedia-backups" # Synced from maybelle daily
LATEST_BACKUP=$(ls -t ${BACKUP_DIR}/pickipedia_*.sql.gz 2>/dev/null | head -1)

if [ -z "$LATEST_BACKUP" ]; then
echo "No backup found in $BACKUP_DIR"
echo "Backups are created daily at 3:30am on maybelle"
exit 1
fi

echo "Loading backup: $LATEST_BACKUP"

# Get container name from compose project
CONTAINER=$(docker ps --filter "name=pickipedia.*db" --format "{{.Names}}" | head -1)
if [ -z "$CONTAINER" ]; then
echo "MySQL container not running. Start with: docker-compose up -d"
exit 1
fi

# Wait for MySQL to be ready
echo "Waiting for MySQL..."
until docker exec "$CONTAINER" mysqladmin ping -h localhost --silent 2>/dev/null; do
sleep 1
done

# Load the backup
echo "Loading data (this may take a moment)..."
gunzip -c "$LATEST_BACKUP" | docker exec -i "$CONTAINER" mysql -u pickipedia -ppickipedia_dev pickipedia

echo "Done! PickiPedia preview now has production data."
""")
run_command(f"chown magent:magent {load_backup_script}")
run_command(f"chmod +x {load_backup_script}")
logger.info("✓ Created pickipedia load-backup.sh script")


def setup_host_files():
"""Set up SSH keys and other host-provided config."""
Expand Down
3 changes: 3 additions & 0 deletions hunter/packages.txt
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,6 @@ tmux
screen
mosh
imagemagick
jp2a
caca-utils
libaa-bin
91 changes: 91 additions & 0 deletions maybelle/ansible/maybelle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,32 @@
owner: 1000
group: 1000

- name: Create PickiPedia secrets directory
file:
path: "{{ jenkins_home }}/secrets/pickipedia"
state: directory
mode: '0755'
owner: 1000
group: 1000

- name: Deploy PickiPedia LocalSettings.local.php from vault
copy:
content: |
<?php
// Secrets deployed from vault - do not edit manually
// DB settings point to production NFS MySQL
$wgSecretKey = "{{ pickipedia_secret_key }}";
$wgUpgradeKey = "{{ pickipedia_upgrade_key }}";
$wgDBtype = "mysql";
$wgDBserver = "{{ pickipedia_db_server }}";
$wgDBname = "{{ pickipedia_db_name }}";
$wgDBuser = "{{ pickipedia_db_user }}";
$wgDBpassword = "{{ pickipedia_db_password }}";
dest: "{{ jenkins_home }}/secrets/pickipedia/LocalSettings.local.php"
mode: '0600'
owner: 1000
group: 1000

- name: Create PR builds directory
file:
path: "/var/www/builds"
Expand Down Expand Up @@ -666,6 +692,71 @@
minute: "0"
job: "/usr/local/bin/backup-to-nfs.sh"

# PickiPedia backup - pull MySQL dump from NFS for hunter preview
- name: Create pickipedia backup directory
file:
path: /mnt/persist/pickipedia/backups
state: directory
mode: '0755'

- name: Create pickipedia backup script
copy:
dest: /usr/local/bin/backup-pickipedia.sh
mode: '0750'
content: |
#!/bin/bash
# Daily PickiPedia MySQL backup from NFS
# Pulls dump via SSH for use in hunter preview environments
LOG_FILE="/var/log/pickipedia-backup.log"
BACKUP_DIR="/mnt/persist/pickipedia/backups"
SSH_KEY="/root/.ssh/id_ed25519_nfs"
NFS_HOST="ssh.nyc1.nearlyfreespeech.net"
NFS_USER="jmyles_pickipedia"

echo "$(date): Starting PickiPedia backup" >> "$LOG_FILE"

# Run mysqldump on NFS and pipe back
BACKUP_FILE="$BACKUP_DIR/pickipedia_$(date +%Y%m%d).sql.gz"
if ssh -i "$SSH_KEY" "${NFS_USER}@${NFS_HOST}" \
"mysqldump -h pickipedia.db pickipedia" 2>> "$LOG_FILE" \
| gzip > "$BACKUP_FILE"; then
echo "$(date): Backup successful: $BACKUP_FILE ($(stat -c%s "$BACKUP_FILE") bytes)" >> "$LOG_FILE"
# Keep last 7 days of backups
find "$BACKUP_DIR" -name "pickipedia_*.sql.gz" -mtime +7 -delete

# Sync to hunter for preview environments
echo "$(date): Syncing to hunter..." >> "$LOG_FILE"
rsync -avz "$BACKUP_DIR"/ root@hunter.cryptograss.live:/opt/magenta/pickipedia-backups/ >> "$LOG_FILE" 2>&1 || \
echo "$(date): WARNING - sync to hunter failed" >> "$LOG_FILE"
else
echo "$(date): Backup FAILED" >> "$LOG_FILE"
rm -f "$BACKUP_FILE" # Remove partial file
exit 1
fi

- name: Create pickipedia backup log file
file:
path: /var/log/pickipedia-backup.log
state: touch
mode: '0644'

- name: Schedule daily pickipedia backup
ansible.builtin.cron:
name: "pickipedia backup from nfs"
hour: "3"
minute: "30"
job: "/usr/local/bin/backup-pickipedia.sh"

- name: Add NFS pickipedia site to SSH config
blockinfile:
path: /root/.ssh/config
marker: "# {mark} PICKIPEDIA NFS"
block: |
Host nfs-pickipedia
HostName ssh.nyc1.nearlyfreespeech.net
IdentityFile ~/.ssh/id_ed25519_nfs
User jmyles_pickipedia

# Security: remove vault password after deploy completes
# The encrypted vault.yml is in a public repo, so vault password on disk is a risk
# Re-run chapter-1.sh to restore it if needed for future deploys
Expand Down
20 changes: 20 additions & 0 deletions maybelle/jobs/pickipedia.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
pipelineJob('pickipedia') {
definition {
cpsScm {
scm {
git {
remote {
url('https://github.com/cryptograss/pickipedia.git')
credentials('github-token')
}
branch('*/main')
}
}
scriptPath('Jenkinsfile')
}
}
triggers {
// Poll every 5 minutes for changes
cron('*/5 * * * *')
}
}