From 28326cb0495dbaf81b6d40f3a655e0495bfae3cc Mon Sep 17 00:00:00 2001 From: "Rodrigo Rodriguez (Pragmatismo)" Date: Tue, 24 Mar 2026 13:43:33 -0300 Subject: [PATCH] Update submodules --- AGENTS-PROD.md | 479 ++++++++++++++-- botapp | 2 +- botbook | 2 +- botmodels | 2 +- botserver | 2 +- botui | 2 +- default-vault.tar | 0 migrations.tar.gz | Bin 0 -> 71693 bytes prompts/c1.md | 85 +++ prompts/container.md | 1088 +++++++++++++++++++++++++++++++++++++ prompts/fail2ban-start.sh | 11 + prompts/go.md | 15 + 12 files changed, 1633 insertions(+), 55 deletions(-) create mode 100644 default-vault.tar create mode 100644 migrations.tar.gz create mode 100644 prompts/c1.md create mode 100644 prompts/container.md create mode 100644 prompts/fail2ban-start.sh create mode 100644 prompts/go.md diff --git a/AGENTS-PROD.md b/AGENTS-PROD.md index b61980e..709462d 100644 --- a/AGENTS-PROD.md +++ b/AGENTS-PROD.md @@ -1,35 +1,391 @@ # General Bots Cloud — Production Operations Guide ## Infrastructure Overview -- **Host OS:** Ubuntu 24.04 LTS, LXD (snap) -- **SSH:** Key auth only, sudoer user in `lxd` group -- **Container engine:** LXD with ZFS storage pool +- **Host OS:** Ubuntu 24.04 LTS, Incus +- **SSH:** Key auth only +- **Container engine:** Incus with ZFS storage pool +- **Tenant:** pragmatismo (migrated from LXD 82.29.59.188 to Incus 63.141.255.9) -## LXC Container Architecture +--- + +## Container Migration: pragmatismo (COMPLETED) + +### Summary +| Item | Detail | +|------|--------| +| Source | LXD 5.21 on Ubuntu 22.04 @ 82.29.59.188 | +| Destination | Incus 6.x on Ubuntu 24.04 @ 63.141.255.9 | +| Migration method | `incus copy --instance-only lxd-source:` | +| Data transfer | rsync via SSH (pull from destination → source:/opt/gbo) | +| Total downtime | ~4 hours | +| Containers migrated | 10 | +| Data transferred | ~44 GB | + +### Migrated Containers (destination names) +``` +proxy → proxy (Caddy reverse proxy) +tables → tables (PostgreSQL) +system → system (botserver + botui, privileged) +drive → drive (MinIO S3) +dns → dns (CoreDNS) +email → email (Stalwart mail) +webmail → webmail (Roundcube) +alm → alm (Forgejo ALM) +alm-ci → alm-ci (Forgejo CI runner) +table-editor → table-editor (NocoDB) +``` + +### Data Paths +- **Source data:** `root@82.29.59.188:/opt/gbo/` (44 GB, tenant data + binaries) +- **Destination data:** `/home/administrator/gbo/tenants/pragmatismo/` (rsync in progress) +- **Final path:** `/opt/gbo/tenants/pragmatismo/` (symlink or mount) + +### Key Decisions Made +1. **No `pragmatismo-` prefix** on destination (unlike source) +2. **iptables NAT** instead of Incus proxy devices (proxy devices conflicted with NAT rules) +3. **Incus proxy devices removed** from all containers after NAT configured +4. **Disk devices removed** from source containers before migration (Incus can't resolve LXD paths) + +### Port Forwarding (iptables NAT) +| Port | Service | +|------|---------| +| 80, 443 | Caddy (HTTP/HTTPS) | +| 25, 465, 587 | SMTP | +| 993, 995, 143, 110, 4190 | IMAP/POP/Sieve | +| 53 | DNS | + +### Remaining Post-Migration Tasks +- [x] **rsync transfer:** Source /opt/gbo → destination ~/gbo ✓ +- [x] **Merge data:** rsync to /opt/gbo/tenants/pragmatismo/ ✓ +- [x] **Configure NAT:** iptables PREROUTING rules ✓ +- [x] **Update Caddy:** Replace old IPs with new 10.107.115.x IPs ✓ +- [x] **Copy data to containers:** tar.gz method for proxy, tables, email, webmail, alm-ci, table-editor ✓ +- [x] **Fix directory structure:** system, dns, alm ✓ +- [x] **Caddy installed and running** ✓ +- [ ] **SSL certificates:** Let's Encrypt rate limited - need to wait or use existing certs +- [ ] **botserver binary missing** in system container +- [ ] **DNS cutover:** Update NS/A records to point to 63.141.255.9 +- [ ] **Source cleanup:** Delete /opt/gbo/ on source after verification + +### Current Container Status (2026-03-22 17:50 UTC) +| Container | /opt/gbo/ contents | Status | +|-----------|---------------------|--------| +| proxy | conf, data, logs, Caddy running | ✓ OK (SSL pending) | +| tables | conf, data, logs, pgconf, pgdata | ✓ OK | +| email | conf, data, logs | ✓ OK | +| webmail | conf, data, logs | ✓ OK | +| alm-ci | conf, data, logs | ✓ OK | +| table-editor | conf, data, logs | ✓ OK | +| system | bin, botserver-stack, conf, data, logs | ✓ OK | +| drive | data, logs | ✓ OK | +| dns | bin, conf, data, logs | ✓ OK | +| alm | alm/, conf, data, logs | ✓ OK | + +### Known Issues +1. **Let's Encrypt rate limiting** - Too many cert requests from old server. Certificates will auto-renew after rate limit clears (~1 hour) +2. **botserver database connection** - PostgreSQL is in tables container (10.107.115.33), need to update DATABASE_URL in system container +3. **SSL certificates** - Caddy will retry obtaining certs after rate limit clears + +### Final Status (2026-03-22 18:30 UTC) + +#### Container Services Status +| Container | Service | Port | Status | +|-----------|---------|------|--------| +| system | Vault | 8200 | ✓ Running | +| system | Valkey | 6379 | ✓ Running | +| system | MinIO | 9100 | ✓ Running | +| system | Qdrant | 6333 | ✓ Running | +| system | botserver | - | ⚠️ Not listening | +| tables | PostgreSQL | 5432 | ✓ Running | +| proxy | Caddy | 80, 443 | ✓ Running | +| dns | CoreDNS | 53 | ❌ Not running | +| email | Stalwart | 25,143,465,993,995 | ❌ Not running | +| webmail | Roundcube | - | ❌ Not running | +| alm | Forgejo | 3000 | ❌ Not running | +| alm-ci | Forgejo-runner | - | ❌ Not running | +| table-editor | NocoDB | - | ❌ Not running | +| drive | MinIO | - | ❌ (in system container) | + +#### Issues Found +1. **botserver not listening** - needs DATABASE_URL pointing to tables container +2. **dns, email, webmail, alm, alm-ci, table-editor** - services not started +3. **SSL certificates** - Let's Encrypt rate limited + +### Data Structure + +**Host path:** `/opt/gbo/tenants/pragmatismo//` +**Container path:** `/opt/gbo/` (conf, data, logs, bin, etc.) + +| Container | Host Path | Container /opt/gbo/ | +|-----------|-----------|---------------------| +| system | `.../system/` | bin, botserver-stack, conf, data, logs | +| proxy | `.../proxy/` | conf, data, logs | +| tables | `.../tables/` | conf, data, logs | +| drive | `.../drive/` | data, logs | +| dns | `.../dns/` | bin, conf, data, logs | +| email | `.../email/` | conf, data, logs | +| webmail | `.../webmail/` | conf, data, logs | +| alm | `.../alm/` | conf, data, logs | +| alm-ci | `.../alm-ci/` | conf, data, logs | +| table-editor | `.../table-editor/` | conf, data, logs | + +### Attach Data Devices (after moving data) +```bash +# Move data to final location +ssh administrator@63.141.255.9 "sudo mv /home/administrator/gbo /opt/gbo/tenants/pragmatismo" + +# Attach per-container disk device +for container in system proxy tables drive dns email webmail alm alm-ci table-editor; do + incus config device add $container gbo disk \ + source=/opt/gbo/tenants/pragmatismo/$container \ + path=/opt/gbo +done + +# Fix permissions (each container) +for container in system proxy tables drive dns email webmail alm alm-ci table-editor; do + incus exec $container -- chown -R gbuser:gbuser /opt/gbo/ 2>/dev/null || \ + incus exec $container -- chown -R root:root /opt/gbo/ +done +``` + +### Container IPs (for Caddy configuration) +``` +system: 10.107.115.229 +proxy: 10.107.115.189 +tables: 10.107.115.33 +drive: 10.107.115.114 +dns: 10.107.115.155 +email: 10.107.115.200 +webmail: 10.107.115.208 +alm: 10.107.115.4 +alm-ci: 10.107.115.190 +table-editor: (no IP - start container) +``` + +--- + +## LXC Container Architecture (destination) | Container | Purpose | Exposed Ports | |---|---|---| -| `-proxy` | Caddy reverse proxy | 80, 443 | -| `-system` | botserver + botui (privileged!) | internal only | -| `-alm` | Forgejo (ALM/Git) | internal only | -| `-alm-ci` | Forgejo CI runner | none | -| `-email` | Stalwart mail server | 25,465,587,993,995,143,110 | -| `-dns` | CoreDNS | 53 | -| `-drive` | MinIO S3 | internal only | -| `-tables` | PostgreSQL | internal only | -| `-table-editor` | NocoDB | internal only | -| `-webmail` | Roundcube | internal only | +| `proxy` | Caddy reverse proxy | 80, 443 | +| `system` | botserver + botui (privileged!) | internal only | +| `alm` | Forgejo (ALM/Git) | internal only | +| `alm-ci` | Forgejo CI runner | none | +| `email` | Stalwart mail server | 25,465,587,993,995,143,110 | +| `dns` | CoreDNS | 53 | +| `drive` | MinIO S3 | internal only | +| `tables` | PostgreSQL | internal only | +| `table-editor` | NocoDB | internal only | +| `webmail` | Roundcube | internal only | ## Key Rules -- `-system` must be **privileged** (`security.privileged: true`) — required for botserver to own `/opt/gbo/` mounts -- All containers use LXD **proxy devices** for port forwarding (network forwards don't work when external IP is on host NIC, not bridge) -- Never remove proxy devices for ports: 80, 443, 25, 465, 587, 993, 995, 143, 110, 4190, 53 -- CI runner (`alm-ci`) must NOT have cross-container disk device mounts — deploy via SSH instead +- `system` must be **privileged** (`security.privileged: true`) — required for botserver to own `/opt/gbo/` mounts +- All containers use **iptables NAT** for port forwarding — NEVER use Incus proxy devices (they conflict with NAT) +- **Data copied into each container** at `/opt/gbo/` — NOT disk devices. Each container has its own copy of data. +- CI runner (`alm-ci`) must NOT have cross-container disk device mounts — deploy via SSH only +- Caddy config must have correct upstream IPs for each backend container + +## Container Migration (LXD to Incus) — COMPLETED + +### Migration Workflow (for future tenants) + +**Best Method:** `incus copy --instance-only` — transfers containers directly between LXD and Incus. + +#### Prerequisites +```bash +# 1. Open port 8443 on both servers +ssh root@ "iptables -I INPUT -p tcp --dport 8443 -j ACCEPT" +ssh administrator@ "sudo iptables -I INPUT -p tcp --dport 8443 -j ACCEPT" + +# 2. Exchange SSH keys (for rsync data transfer) +ssh administrator@ "cat ~/.ssh/id_rsa.pub" +ssh root@ "echo '' >> /root/.ssh/authorized_keys" + +# 3. Add source LXD as Incus remote +ssh administrator@ "incus remote add lxd-source --protocol=incus --accept-certificate" + +# 4. Add destination cert to source LXD trust +ssh @ "cat ~/.config/incus/client.crt" +ssh root@ "lxc config trust add -" +``` + +#### Migration Steps +```bash +# 1. On SOURCE: Remove disk devices (Incus won't have source paths) +for c in $(lxc list --format csv -c n); do + lxc stop $c + for d in $(lxc config device list $c); do + lxc config device remove $c $d + done +done + +# 2. On DESTINATION: Copy each container +incus copy --instance-only lxd-source: +incus start + +# 3. On DESTINATION: Add eth0 network to each container +incus config device add eth0 nic name=eth0 network=incusbr0 + +# 4. On DESTINATION: Configure iptables NAT (not proxy devices!) +# See iptables NAT Setup above + +# 5. On DESTINATION: Pull data via rsync (from destination to source) +ssh administrator@ "rsync -avz --progress root@:/opt/gbo/ /home/administrator/gbo/" + +# 6. On DESTINATION: Organize data per container +# Data is structured as: /home/administrator/gbo// +# Each container gets its own folder with {conf,data,logs,bin}/ + +# 7. On DESTINATION: Move to final location +ssh administrator@ "sudo mkdir -p /opt/gbo/tenants/" +ssh administrator@ "sudo mv /home/administrator/gbo /opt/gbo/tenants//" + +# 8. On DESTINATION: Copy data into each container +for container in system proxy tables drive dns email webmail alm alm-ci table-editor; do + incus exec $container -- mkdir -p /opt/gbo + incus file push --recursive /opt/gbo/tenants//$container/. $container/opt/gbo/ +done + +# 9. On DESTINATION: Fix permissions +for container in system proxy tables drive dns email webmail alm alm-ci table-editor; do + incus exec $container -- chown -R gbuser:gbuser /opt/gbo/ 2>/dev/null || \ + incus exec $container -- chown -R root:root /opt/gbo/ +done + +# 10. On DESTINATION: Update Caddy config with new container IPs +# sed -i 's/10.16.164.x/10.107.115.x/g' /opt/gbo/conf/config +incus file push /tmp/new_caddy_config proxy/opt/gbo/conf/config + +# 11. Reload Caddy +incus exec proxy -- /opt/gbo/bin/caddy reload --config /opt/gbo/conf/config --adapter caddyfile +``` + +#### iptables NAT Setup (on destination host) +```bash +# Enable IP forwarding +sudo sysctl -w net.ipv4.ip_forward=1 + +# NAT rules — proxy container (ports 80, 443) +sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j DNAT --to-destination 10.107.115.189:80 +sudo iptables -t nat -A PREROUTING -p tcp --dport 443 -j DNAT --to-destination 10.107.115.189:443 + +# NAT rules — email container (SMTP/IMAP) +sudo iptables -t nat -A PREROUTING -p tcp --dport 25 -j DNAT --to-destination 10.107.115.200:25 +sudo iptables -t nat -A PREROUTING -p tcp --dport 465 -j DNAT --to-destination 10.107.115.200:465 +sudo iptables -t nat -A PREROUTING -p tcp --dport 587 -j DNAT --to-destination 10.107.115.200:587 +sudo iptables -t nat -A PREROUTING -p tcp --dport 993 -j DNAT --to-destination 10.107.115.200:993 +sudo iptables -t nat -A PREROUTING -p tcp --dport 995 -j DNAT --to-destination 10.107.115.200:995 +sudo iptables -t nat -A PREROUTING -p tcp --dport 143 -j DNAT --to-destination 10.107.115.200:143 +sudo iptables -t nat -A PREROUTING -p tcp --dport 110 -j DNAT --to-destination 10.107.115.200:110 +sudo iptables -t nat -A PREROUTING -p tcp --dport 4190 -j DNAT --to-destination 10.107.115.200:4190 + +# NAT rules — dns container (DNS) +sudo iptables -t nat -A PREROUTING -p udp --dport 53 -j DNAT --to-destination 10.107.115.155:53 +sudo iptables -t nat -A PREROUTING -p tcp --dport 53 -j DNAT --to-destination 10.107.115.155:53 + +# Masquerade outgoing traffic +sudo iptables -t nat -A POSTROUTING -s 10.107.115.0/24 -j MASQUERADE + +# Save rules +sudo netfilter-persistent save +``` + +#### Remove Incus Proxy Devices (after NAT is working) +```bash +for c in $(incus list --format csv -c n); do + for d in $(incus config device list $c | grep proxy); do + incus config device remove $c $d + done +done +``` + +#### pragmatismo Migration Notes +- Source server: `root@82.29.59.188` (LXD 5.21, Ubuntu 22.04) +- Destination: `administrator@63.141.255.9` (Incus 6.x, Ubuntu 24.04) +- Container naming: No prefix on destination (`proxy` not `pragmatismo-proxy`) +- Data: rsync pull from destination (not push from source) ## Firewall (host) + +### ⚠️ CRITICAL: NEVER Block SSH Port 22 +**When installing ANY firewall (UFW, iptables, etc.), ALWAYS allow SSH (port 22) FIRST, before enabling the firewall.** + +**Wrong order (will lock you out!):** +```bash +ufw enable # BLOCKS SSH! +``` + +**Correct order:** +```bash +ufw allow 22/tcp # FIRST: Allow SSH +ufw allow 80/tcp # Allow HTTP +ufw allow 443/tcp # Allow HTTPS +ufw enable # THEN enable firewall +``` + +### Firewall Setup Steps +1. **Always allow SSH before enabling firewall:** + ```bash + sudo ufw allow 22/tcp + ``` + +2. **Install UFW:** + ```bash + sudo apt-get install -y ufw + ``` + +3. **Configure UFW with SSH allowed:** + ```bash + sudo ufw default forward ACCEPT + sudo ufw allow 22/tcp + sudo ufw allow 80/tcp + sudo ufw allow 443/tcp + sudo ufw enable + ``` + +4. **Persist iptables rules for NAT (containers):** + Create `/etc/systemd/system/iptables-restore.service`: + ```ini + [Unit] + Description=Restore iptables rules on boot + After=network-pre.target + Before=network.target + DefaultDependencies=no + + [Service] + Type=oneshot + ExecStart=/bin/bash -c "/sbin/iptables-restore < /etc/iptables/rules.v4" + RemainAfterExit=yes + + [Install] + WantedBy=multi-user.target + ``` + + Save rules and enable: + ```bash + sudo iptables-save > /etc/iptables/rules.v4 + sudo systemctl enable iptables-restore.service + ``` + +5. **Install fail2ban:** + ```bash + # Download fail2ban deb from http://ftp.us.debian.org/debian/pool/main/f/fail2ban/ + sudo dpkg -i fail2ban_*.deb + sudo touch /var/log/auth.log + sudo systemctl enable fail2ban + sudo systemctl start fail2ban + ``` + +6. **Configure fail2ban SSH jail:** + ```bash + sudo fail2ban-client status # Should show sshd jail + ``` + +### Requirements - **ufw** with `DEFAULT_FORWARD_POLICY=ACCEPT` (needed for container internet) -- LXD forward rule must persist via systemd service - **fail2ban** on host (SSH jail) and in email container (mail jail) +- iptables NAT rules must persist via systemd service --- @@ -44,17 +400,17 @@ **Fix:** ```bash # Insert loopback ACCEPT at top of INPUT chain -lxc exec -system -- iptables -I INPUT 1 -i lo -j ACCEPT +incus exec system -- iptables -I INPUT 1 -i lo -j ACCEPT # Persist the rule -lxc exec -system -- bash -c 'iptables-save > /etc/iptables/rules.v4' +incus exec system -- bash -c 'iptables-save > /etc/iptables/rules.v4' # Verify Valkey responds -lxc exec -system -- /opt/gbo/bin/botserver-stack/bin/cache/bin/valkey-cli ping +incus exec system -- /opt/gbo/bin/botserver-stack/bin/cache/bin/valkey-cli ping # Should return: PONG # Restart botserver to pick up working cache -lxc exec -system -- systemctl restart system.service ui.service +incus exec system -- systemctl restart system.service ui.service ``` **Prevention:** Always ensure loopback ACCEPT rule is at the top of iptables INPUT chain before any DROP rules. @@ -66,13 +422,13 @@ lxc exec -system -- systemctl restart system.service ui.service **Diagnosis:** ```bash # Get bot ID -lxc exec -system -- /opt/gbo/bin/botserver-stack/bin/tables/bin/psql -h localhost -U gbuser -d botserver -t -c "SELECT id, name FROM bots WHERE name = 'botname';" +incus exec system -- /opt/gbo/bin/botserver-stack/bin/tables/bin/psql -h localhost -U gbuser -d botserver -t -c "SELECT id, name FROM bots WHERE name = 'botname';" # Check if suggestions exist in cache with correct bot_id -lxc exec -system -- /opt/gbo/bin/botserver-stack/bin/cache/bin/valkey-cli --scan --pattern "suggestions::*" +incus exec system -- /opt/gbo/bin/botserver-stack/bin/cache/bin/valkey-cli --scan --pattern "suggestions::*" # If no keys found, check logs for wrong bot_id being used -lxc exec -system -- grep "Adding suggestion to Redis key" /opt/gbo/logs/error.log | tail -5 +incus exec system -- grep "Adding suggestion to Redis key" /opt/gbo/logs/error.log | tail -5 ``` **Fix:** This was a code bug where suggestions were stored with `user_id` instead of `bot_id`. After deploying the fix: @@ -110,6 +466,15 @@ Navigate to: https://chat.pragmatismo.com.br/ # - No errors in browser console ``` +**On destination (Incus):** +```bash +# Verify botserver binary +incus exec system -- stat /opt/gbo/bin/botserver | grep Modify + +# Restart services +incus exec system -- systemctl restart system.service ui.service +``` + --- ## ⚠️ Caddy Config — CRITICAL RULES @@ -148,9 +513,20 @@ The full config has ~25 vhosts. If you only see 1-2 vhosts, you are looking at a ### Caddy in Proxy Container - Binary: `/usr/bin/caddy` (system container) or `caddy` in PATH - Config: `/opt/gbo/conf/config` -- Reload: `lxc exec -proxy -- caddy reload --config /opt/gbo/conf/config --adapter caddyfile` +- Reload: `incus exec proxy -- caddy reload --config /opt/gbo/conf/config --adapter caddyfile` - Storage: `/opt/gbo/data/caddy` +**Upstream IPs (after migration):** +| Backend | IP | +|---------|-----| +| system (botserver) | 10.107.115.229:5858 | +| system (botui) | 10.107.115.229:5859 | +| tables (PostgreSQL) | 10.107.115.33:5432 | +| drive (MinIO S3) | 10.107.115.114:9000 | +| webmail | 10.107.115.208 | +| alm | 10.107.115.4 | +| table-editor | 10.107.115.x (assign IP first) | + ### Log Locations **botserver/botui logs:** @@ -175,13 +551,13 @@ vector_db/ # Qdrant vector DB logs **Checking component logs:** ```bash # Valkey -lxc exec pragmatismo-system -- tail -f /opt/gbo/bin/botserver-stack/logs/cache/valkey.log +incus exec system -- tail -f /opt/gbo/bin/botserver-stack/logs/cache/valkey.log # PostgreSQL -lxc exec pragmatismo-system -- tail -f /opt/gbo/bin/botserver-stack/logs/tables/postgres.log +incus exec system -- tail -f /opt/gbo/bin/botserver-stack/logs/tables/postgres.log # Qdrant -lxc exec pragmatismo-system -- tail -f /opt/gbo/bin/botserver-stack/logs/vector_db/qdrant.log +incus exec system -- tail -f /opt/gbo/bin/botserver-stack/logs/vector_db/qdrant.log ``` ### iptables loopback rule (required) @@ -254,9 +630,9 @@ curl -X POST "http://alm.pragmatismo.com.br/api/v1/repos/GeneralBots/BotServer/a ``` ### SSH Hostname Setup (CI Runner) -The CI runner must resolve `pragmatismo-system` hostname. Add to `/etc/hosts` if missing: +The CI runner must resolve `system` hostname. Add to `/etc/hosts` **once** (manual step on host): ```bash -lxc exec pragmatismo-alm-ci -- bash -c 'echo "10.16.164.33 pragmatismo-system" >> /etc/hosts' +incus exec alm-ci -- bash -c 'echo "10.16.164.33 system" >> /etc/hosts' ``` ### Deploy Step — CRITICAL @@ -275,7 +651,7 @@ The deploy step must **kill the running botserver process before `scp`**, otherw ### Binary Ownership The binary at `/opt/gbo/bin/botserver` must be owned by `gbuser`, not `root`: ```bash -lxc exec pragmatismo-system -- chown gbuser:gbuser /opt/gbo/bin/botserver +incus exec system -- chown gbuser:gbuser /opt/gbo/bin/botserver ``` If owned by root, `scp` as `gbuser` will fail even after killing the process. @@ -309,32 +685,35 @@ Failure to push the root `gb` repo will not trigger CI/CD pipelines. ## Useful Commands ```bash -# Check all containers -lxc list +# Check all containers (Incus) +incus list # Check disk device mounts per container -for c in $(lxc list --format csv -c n); do - devices=$(lxc config device show $c | grep 'type: disk' | grep -v 'pool:' | wc -l) - [ $devices -gt 0 ] && echo "=== $c ===" && lxc config device show $c | grep -E 'source:|path:' | grep -v pool +for c in $(incus list --format csv -c n); do + devices=$(incus config device show $c | grep 'type: disk' | grep -v 'pool:' | wc -l) + [ $devices -gt 0 ] && echo "=== $c ===" && incus config device show $c | grep -E 'source:|path:' | grep -v pool done # Tail Caddy errors -lxc exec -proxy -- tail -f /opt/gbo/logs/access.log +incus exec proxy -- tail -f /opt/gbo/logs/access.log # Restart botserver + botui -lxc exec -system -- systemctl restart system.service ui.service +incus exec system -- systemctl restart system.service ui.service # Check iptables in system container -lxc exec -system -- iptables -L -n | grep -E 'DROP|ACCEPT.*lo' +incus exec system -- iptables -L -n | grep -E 'DROP|ACCEPT.*lo' # ZFS snapshot usage zfs list -t snapshot -o name,used | sort -k2 -rh | head -20 # Unseal Vault (use actual unseal key from init.json) -lxc exec -system -- bash -c " +incus exec system -- bash -c " export VAULT_ADDR=https://127.0.0.1:8200 VAULT_SKIP_VERIFY=true /opt/gbo/bin/botserver-stack/bin/vault/vault operator unseal \$UNSEAL_KEY " + +# Check rsync transfer progress (on destination) +du -sh /home/administrator/gbo ``` --- @@ -344,13 +723,13 @@ lxc exec -system -- bash -c " ### Check CI Runner Container ```bash # From production host, SSH to CI runner -ssh root@-alm-ci +ssh root@alm-ci # Check CI workspace for cloned repos ls /root/workspace/ # Test SSH to system container -ssh -o ConnectTimeout=5 pragmatismo-system 'hostname' +ssh -o ConnectTimeout=5 system 'hostname' ``` ### Query CI Runs via Forgejo API @@ -365,24 +744,24 @@ curl -X POST "http://alm.pragmatismo.com.br/api/v1/repos/GeneralBots//acti ### Check Binary Deployed ```bash # From production host -lxc exec -system -- stat /opt/gbo/bin/ | grep Modify -lxc exec -system -- strings /opt/gbo/bin/ | grep '' +incus exec system -- stat /opt/gbo/bin/ | grep Modify +incus exec system -- strings /opt/gbo/bin/ | grep '' ``` ### CI Build Logs Location ```bash -# On CI runner (pragmatismo-alm-ci) +# On CI runner (alm-ci) # Logs saved via: sudo cp /tmp/build.log /opt/gbo/logs/ # Access from production host -ssh root@-alm-ci -- cat /opt/gbo/logs/*.log 2>/dev/null +ssh root@alm-ci -- cat /opt/gbo/logs/*.log 2>/dev/null ``` ### Common CI Issues **SSH Connection Refused:** -- CI runner must have `pragmatismo-system` in `/root/.ssh/config` with IP `10.16.164.33` -- Check: `ssh -o ConnectTimeout=5 pragmatismo-system 'hostname'` +- CI runner must have `system` in `/root/.ssh/config` with correct IP +- Check: `ssh -o ConnectTimeout=5 system 'hostname'` **Binary Not Updated After Deploy:** - Verify binary modification time matches CI run time diff --git a/botapp b/botapp index 9e38944..ea625fa 160000 --- a/botapp +++ b/botapp @@ -1 +1 @@ -Subproject commit 9e3894411131f9919fbcbb0c38fe975970675ca6 +Subproject commit ea625fa6b1e6617a71b7856337976b5d96600bee diff --git a/botbook b/botbook index 7b2b7ab..94ca51a 160000 --- a/botbook +++ b/botbook @@ -1 +1 @@ -Subproject commit 7b2b7ab3c53c65a68930a8cb2e7ca359d8e22bcf +Subproject commit 94ca51a670cfa664ba7abde24991bf831dac4fbd diff --git a/botmodels b/botmodels index 5e74489..09c59dd 160000 --- a/botmodels +++ b/botmodels @@ -1 +1 @@ -Subproject commit 5e74489076c00e13e5660228ebb159fae9c9e791 +Subproject commit 09c59ddc6bbbff6613b9dc3bcdd4d6e59704e05a diff --git a/botserver b/botserver index 43f2eb7..adb2633 160000 --- a/botserver +++ b/botserver @@ -1 +1 @@ -Subproject commit 43f2eb7e5cdc81d8c3690cb899ecd1207cc2ac04 +Subproject commit adb26330d27049ec4d3a335c4adb1b6afd5a05ae diff --git a/botui b/botui index 138cc59..222e327 160000 --- a/botui +++ b/botui @@ -1 +1 @@ -Subproject commit 138cc59be33942eba28d8a2c30bafb8ab04f0f12 +Subproject commit 222e32725991f189b4d6b03b1e4d2fcfd3435eda diff --git a/default-vault.tar b/default-vault.tar new file mode 100644 index 0000000..e69de29 diff --git a/migrations.tar.gz b/migrations.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..3cb6e489b7f0549cec1160bd3978e799f804468f GIT binary patch literal 71693 zcmV()K;OR~iwFP!000001MEFbZyQN=KI>OhCId(UD4Uc-$sT(g&{9igR-!Z{H8VSd z1ySrSk~MC2bNWNJyjURDAcr8EJtTX{4ssX_kUdQn2<905ll+0?C*-}V>h9_+0g1PdyrN*YO`cxT~uP*ZzJR|F-tqP5p0lJ!rLC?e_lOUTbIj zL9?~nY_}hf_MK6rU7;UPkB|qRYk7PcvbQC0RqcPK*F2H`-oxg@_ExjCWi#qIx2O&^ z<-gn7ZSU;uw?O`T&7Iv3@_!#!x%@5nwe!&b)_(UfP{4b8yJhmIkKjxI;X42|{7S^(;W{PKfC-Dew)C z`3nYPIa4xmJ;tXFdBv`X39}45%ALU9RFmzzI59K|5)nmnAqSnoLFdqTTGgQ*BuWjY z6tMXssMo_}Uce{Z1ZLFiAP65(--mh6nNxF|&<7DOJa-Drt3Q{wHHlI>w!#Ez!U9{S zuE%xo`*ukmuJ?GA$Nz9~JJG)?{_Z}CEW2c4mDeAe$81L@6feT;*?0YfCNvUfHt zb(b4!1TJooi;M0dIq!E*JN;jhUl_k6hsIIo;$%ps%o%x*4!QGD7;&iL{6WDh>JlrC+o`^*eI9$aDjfK5YCxa;pJ zWZ*DEL_)8>zy5a@ke(c$A4+7u`>jC6CcALK$L$!I>x^CMSzzc0(REnM{{HU-!d7$q{o(ZwCbYPMi2wQhzy9xk{i8s#x3jl5!RhQJ({U%kRx+mnd?f?%RY+|f;s}CY zYC``ZIc@Gfe)0r9?#Gy3Guw6`r^EIaY`(=r6k_DFkV=*ygFc5(64CenB00P}eoS#* zkChhRs`o<=dv?(u+@pI{nd^;xtLsgv!{3Uy6*)!O67axY!R;!Oze@LrhmRFoqTmx) zV1OGb1s}61cOW`O9YPseM|19kxGt%3y1^`*j~&YGQRvxvbnr)W-~tVh)E8%GCq}22 zM{?9T85q&XY-G`^c*GdZA{^V?pE0WznkWd2uQV2~6d7a}Z4q3ji|BK3)*B4_9hd;Q zL}M9g>_2#E9Q=Z8Xm`3ju(pK#WF1zo%$qZd!@&5RT9@c{t@X|2(Mm1bLBn$E^^J_$ zEeTZ$ORQcwR8dPcjzsV|B)VEuBT7VHgeY#UF)OJ61(QlN!G-F$M$Bjr($5 z(GvZ}&Px>@qMor4ZI-wL7l^aUk6OKsok*tep;)jJb!9PBr zkXev$Pt*+fC9CjAQAtv2L#`aT5vxsXV1boQXFBpt$l%Kjaz9EvAYmWbe9nWc5zF>DeUlVqQ!U?ykkw*k z>E~;;XP{X-taEo{lNYcQ z1{*j#?n7+7FUw3<)^BW|0!`MmZr|>1!41G#FbMs!SnRmL2v>`s{OggS@waJn*6wF* zvwV$#hVCygVAYM*ii}mvR+K?lzLR1e^@9;LP2kbHnzmx1vT5~A1mP3|G2+I`g})Pyc$TC#XypK)=%Az9g22Od6T8?BqCDSL*fcj*0G_KyxgP2Sh8Lz zji-ihO3_n+PLb6htKd~KD?uq+YoJN#R2jCM;%V!Y3x`7})1H2cYPX!pmd!4iO%m_) zdd5JWi4)*HaYEbPjL5v`bw9hP$IMYE1{RuEpRWS^ivsgzi37V4DY!OHnm;#pf?e%I^!~TVl z!GCMf#bXfIU;#7oaRpqb&Xwws!MV&=YjVuE>%5?=eYTD*>t_q_)$p-8Mv&N2bdf~s zVT1eupC8nja3duy-36q?82hdoqq~>Pt398J0Y!|C)j8*j@?JD>!Eb{5gpXFZu*H@W zqK#bPOj9}|k=K%PgCq)cCr;An54%TQeE52Q?or>w^K)MTaNiGCb6dqraYb1&^-Nt1 zbX_)Jfz5QSZ1qm+nS^C;793)4&MlS{vtZ10TAN8-Yp;QI!DT(6)>Q2SQ;2d-P)Ak6 z{iTrUY%D>U<8)2bQWHv$CTg-Y*DUlZI-&}m5k$hG|xWSH;l<38`bj2-h+fdS7Ubst8r5#@y zRi)?4qgqN@Y{dzUck{5V)9-bA$K<>-e3>$0a9p7ogdS*%9c>%?20V(xuKatjXSuid z4t5l*wvTJc6Z}7eNAFAPQevxq>bbbMRsX5D#8B`UTH?t(guZiaj(5Rqe4}>U%U$#L zq`dFGD@I_cjzx;w@8|K9!d34vGH8Iuz@K@`Y$T~1+}ajJo&DlVS~cT&Dh4mt@MN1r zCVNexD#%{Tif=m3Y+_eL-rXm517I|0@?mG#AxCF@VDzb6THuQ+w?3d4kz({GNnt73 zW~pXdw4XCFDCCKFPMX=#8EwL8Xj{7)xy=lXR7tB5@2zU2oO)&(XiHsN1^-2tN`}Zf z?%5=`` zCQ`?RNfR^aVkTXFz%J1QJWlP0#P9=rDJx<1^$%%oFQ`Xh-G*Ed+|xG9)?h(2@djz_ zW>~PJuC0RqqAMPtd^d0m63LkQWX|jv5(taU>+f*RVq3DG!ol?&#bS~RR3Usf%@W#^ zh?+q=11= zPzs8-P~U};P=c-iIMTpEi7S;Icz})F|}vZQ~CisA{dAlOcQB( zQ*=hEyM>c{sypv!;~Xv?gvTp&-0>DWt@x?@x7usfNNs0J?_0`y@EiGNp<$kDg_3EY zIC5*fV#l-D_VY8``+{zPinhOb_6Tl1TRiVnJKzn3aQ^!z(%N~r-+c0DyV-iYDN)&= zI=-!BHTI~_ZN@=JIF<*7Y70Y#30@`w1_1^%-f=IjbP@UZXO9-opUrljpN+pkg^>n~ z1OvT0&!2_%^Jg}HejeF4wnrXcS9Qc}^D*| z6Z|4X3-}{L(=s|^BuB0o5h%fMoqC;sf!MGAbIwhC)gfS}7RStG5G>H6GKZ40IsIPtYq}!dFovakJ-RJFhcaqiVLvX zM89K0{s$w9k{!n8F!_G{-6gYQk;-8P%<-@Pc*&B1AwTNutIpovJ} zQ||iU=-dT&xbP0Z(m!HkXpjl`5JjvH-VImybaBoL=EaPnHmonfoOrPT$LR@>fhl&XF$+JhQ9b|w7K_4S{=quLX?4+ULH#8GZbp$m7+e{2C z8OHOZgVIo&a1)dxius`HvI6H~;_I+CG?3B-69t`>5_0pnJC5t{IW$Idh$Wy;!?n4| z(d5At(j8y~hT9UIB+^G3=sOIjb^RSAWBGy4T`!2Epf(n8#_=c2N9@TY75409tP}Dout>$-3CJ*_ z3St~OnK@15c1j(gsmQYlx;u1N>K!TSYf0&!G!5>v1&Y~2^%O3WW%Ah=64PWZf}2R> zEUsP)6M2jyJ=!7_sI5ewiv+YQ1eD5fM7_2;y@|IV=hK_i=F>*Orrbud-S&oAO$VR- zDIMO5UKiYNqSJ%cujM4+`gQyXuaCI((I@1iZ(AQV()W=Y*dlWwd~PkGmQ0AUz!B{t zYr|eRS(}Z_nafR!St`kJT9PaVGW%ypa8-QH2q=7|37} zVUDvMatMAAZs#yhsO?vw)^fWds^0}QM_ekl;L5BHGbG*=+FxbFpnyl}=?;pcnT26* z7B+Q6(k*B~{nvEiM%nPgtUfGUJdyeK_QNWM%>JBK9@^V?zyWg>k$l_?{+ud zalq#dOrsPZu_RTP;a>kh!ntIAv~rjhtQ>B~Pwp-c>_AJ=+8q-uEUUf8c;AsJrJG*x z#U$Op#jLX4TwSvUBGYFgvBj0@yHMI8m!goHtDW4E=C&#{S6j5BLZtbYEX5ab*{lJ~ zG}%IZHf>CACs8317U{4wk5+mOwMfM-Is&m?n{!_-+AR)?EItmoYE@O)Xn9XPNUkrF zZ>f@iZe?=$e=C;LUXo$h;-LXhN#CkcdQv*~#M;XeYo2gUV`myKFLUnqz1UY*7Ig*F z^2tZFjv8}91DJKI4Z@-~FqjCzPO}(6eG;B5#cE~-{M%GhS@#$CVYF*l~tW??E4Q4EQrKRWuoa(~+P3rCSp6HQvQIjC2($pb= z{aikK!KT7mZe6_5lfiTCxrc^y`({W%^0Fp=2npIxSgp`-rmHvgp69{Re9k}>Ah^j@ zAkR*{y)woDb7m9^#8?mUGm~B&-T93BGu-IKh~Z2Tw+AcDfr|-nX7)Eg9_7V?tI1aGeL7{ZzM=LNBwc>$ z_-F+($CWhzLDq*`AB#Z5vfxcH;?6fr?4d41^m;}EpDq^a4VFR}@j?S64SpGtu^Yg` zmO2izOA!<{1g=}$!xu-}j%XOTb6QHgzQ4T?z=Z!^m606XPqy{qJmzdJPcnVQu9gGf z7jC7H>+Mf-$M2UNPp?AtnEKot36^Berq^0Z&$w4eUX3Bevu!2u44!mj4OEtEhU$IT zETXtcJKDbP`S=Mkq$iv^8V-Eh`wjDT&L{na3^X>$W4fN!1IB7oP0*BRi-0q1u0WlE zu7f?{ykrP6t8NciSI+9fCUn*95mBG{L4)Em?h7NT^Pf=Eq|!g9;x1#VNbR5eN(G{brQ_e6?F zo#JHtE62oHEtwMTUveF+VcG69KE<+r2XSP|3izY1wYtb-Zfb#6h!`TWBlUbFcG66% z{a3!r%3~~N{oJ*f9f7K|xa)#fQlEmx^5io2i#ddZ%tqc)z~xg5m_wbKNYk+L+frxY zU5ZnJ>dY(+Ea<3ZvQurQXhbEJnpW;gAnt%d+xAl9&nc0rBA3DIa;^YP=A-~i;8Frv z4w~X`44V8>5}}sGEFv=vpYRF(rq2~&Z#ep{vvjpCLAw!`?gz$+aWEt;a@0RNjkdc9 z?)c`6&FMphI_@BkG!e2D_DVh_>!~DqU7X<{yTuS{%G_EPh&IXarO}IVc20(nq{+Fb za1p_jgR_&1(;kU&$oirIqjZnt=ewOA=F{&uD@0LeGWhr7k98vwaB)z|2zY56jhI+% z@mS=1sjgC-CY>)RpNjXSaq*XVF`D5{pP~jWyT^@<;34LV;BEYxrsKZe*wnza<4WZ4 zBW>W37zO=Y^xou#*4mIn$d^9#$6N(d%6DsMly0sm#a#|2MUhz#E?Fw-8kwi5Z}d8+ zAd@o^qe@gEzi8EeuuJ?dTXo8s!yy9RE9i(n(n05i6c&uE3@H|r<}a&;Q17xvY+8X# zG}V>J6Ly}C4kD#pWnh^Rz00r?gC@ybl+pybmho8`z;eFlV6~JWW_je(AeM871nw*5 zD8En~{PYry=rfXiH1{aZzF2r{!aU}ftTsBWOIppP&m{Pe5gKt(R>b|3e_bnQOWF@h z=Rr-=4Tb9;O@KR%wPLx+1T@!2o4fRQ6R==B{iJ&05|x6_;juhA3F@Pf;8{sXREpZ# z!br_=_1Y3{Q!}wrD8gR0oTCQH09Dn8TrjNJh%V%jNCoAl$bIJE3Q9&8wH;S&gNT2x z66hd2Sh<`8!7BbOy*j_UXfZrA58rDj%f+=qr#z#y;_jqqZoobvUqE8zI_voJP$HWnT1nGAO}EpO;1Zph$F&*@J^5LD0dGa(Q{`>F1X+o;k_bUQMjN8x8#o8 z^MlAwpsCvX>E?kz0M!kjo=Swfnc7QLO-)rz6>jt4+>i2Ujs64#n6f5GJt}9#kW`rr z3&4sg`70YTnA0hl?h5YG7WZQIxi*Dqe>5!#lj2~v$Cfi=*Y{~=T(Fb{=br~vdRMp_ zDX&1!?_~sfeU)Z-c2h!HdZ8cN2+s=>tjx~AnhmVW)V4C;-P`2##@5DWas(HE$lMcB z{gA=!%M6$A7aA$%Em_CBPVVxe;8+3~aWh%znpVPv{2vgv&MuGM(mq|_Sp^A){C#$M z^2Y>lFS*ULJGfBAI8YA|jPodP>V%!Ngo%boS(9N}4QrarY*5VL`c%b7Lrzg|>4mAA z^dBdI-}%|e$@~3-pWi}`z~m?1 zDk9P+)^M_lyaCo=XYmnt1VRtWF%7dzm52vtpCJA(V4*EWXoemo9;v4e-7LYsm52xA zKyr)iv{s|rexIJdy4K>$7TddEdB!CbKD?1n2rtVUmZ`7LF)OYzIfxQ=G3*nrQTP7> zi3}nuveu|v-aa&k#H|IsmQs+uqKXjYJ*tx$b%cgemL>m*DhGdsw__UzEzyu8Tf)$$ zWkeJQXjy@%f!=E6nP+u;1%_+kK{|;>mDCW=nRX&_n2a`9H`?|3dij`f>NTPU>LPP{ zoPs{%hRBPHV+c-Xd&iY}Ey{GW0+}61VpMgg;;>lzW{20Y5IuHYbS~FEJfnQu^clrt zMy6LW&--!<<|1;7Y^||Ir~biGXev-#+`KCgi>r49U<~1L1&#wD`t0+wuM{-4A3@OY zE*~dRv`i9dbX*JdOP4y?_$nJhRH1IwKt}FXqs6}DsKH)&=)tZK)VQ(p39p6cllBR7 zS8HVhzG~`t;_K;leBa4St0PZdZ8lao=`c)25^7^vj%pFMWvfmE-*?oSqjSQ+pzB0< z8L?e{2~39_z%C&L;0HK`DQstJO)9jV`pnz*eIuKoL9_R z4uc)H)ETAV5KZ$af-!SON`*~%dczt-T9WNXYUvwZO-#nx3u=?+Gq} zBRk^aFuTqtd8mKh@~xeWX=!z>iy_!K>=rL#XPtKE9KG|`c~%Ttn{w8nKe{xg&XnwQ zbh~QxP90V&dTvG}tJKk83>jhwF+h5}27_5<`OPHxidk8l=j8yJn?+sI7*$k5&VV^~ zE7_klCqhL9Fuvln%!WynHXYWz!9xy5Kew)FdCNJ>nzVRDx@ zBL=aTT+JFtqK08PX#UqCg+S(O66i3)gq0P~f!OD+l>Wu^)ac7}Z6Q?f)edSwe6@ab z3?zO4ylI(wBc|isI&60|r=dq=_#lPDyBc9F<;H8BLvDXU%;-oegso4+F|pqh#FrS+&YpL*&t*HrTAv z5MC12Nm^BclVO(Dm^W!^m<{qejM93!c9pJCzI{YSh2VJfvgT;endvqy+D0SF#j!!7 zPTUO0@{uzjE40smD65(;zh#3No;n~lR8U~j3Pv&*vsUXYm=)Tm!K@LS2cxhu5wO={ zYk@hCW#VXCDL<|9687b|(yh~^mSy&u;3O&KgYzTHDO1CDWH_Vxn@~#uGr!J@BIu-` zRfxKerqu`=x0RBb0QZ&~x2v+q*00iaGGWLoDX^es#&rf&Q@q3+;G3=(lg_JEc62KGX`B7wiOFrWXxdSC!@CuD8na z;((r9l|)S9!hI(%=Q|yJS%!y6|FFu zRcB0MF|eG-Lm@u;b}zpn9%LvZM{T$ik_9GWpL_r=$f#`^TY?vBu98t(FuhOPb@AAJ zXy@Wlh+P*CYN3CEZ~5qs?fVMZ$nMBt2M@CTQloN`vB-|1(4J%m&r6P?H`8rNf(j3= zvd$HE738z6!R0S498tqrc19IF7N$?Qm+%*~OE7rUpT*oQy2#d#OVjGmnew2oc3oQ_ z`n{K^1=a%aBGAFrKHOc0fZ9jLA5UpD`e7|OKl(uSfzyK{H2^>jP+>=5p?=uieCLj& zHHS-ccIw0eT|iRD8{UibNKo^zH6^gCm%Ygr=3#R#boF2{X0%W^6=78ZT~D(+aOa26 z4b+kN?OP~CW`#1hd&qvEz@qsR`{HT}H;)eFZZzo% zhN6>`M)pc*t9ID%3vMmOL8e3OvG_{Hc4vlw?#xE)7}8EoVzslNtY3~kxx4T#_%18g5SXvl}t2!@a3GW|79O@LOEz; zLFWxx%tz(UDw@=-O<~-LK(F$sX9B&cWsX|`Dc#RmL?3zg_YK9XQ zbeLc+UY%E0*-I|eGl~%eR^xc2jyANOP#>lY?^o$zjBnF|$n%a53a7g~{&aM4x&P_& z<-f_Md3P~2pA`~0whEQ*h1!aAlByP+Y9S+y45oQeUayLyTDUgMgjp3D{?TyYJHQ!8 zetwU4u`bczAU*xGdmNYY3Wwo`HYS*Ig8^R=%KQC`;{&RE$BTtIy^V=17=ch2?-P-` zgY0`v3X(JwAs-IK2Y`1V^MEAatv3Yc9mPF{d*&Do(55r3KcV2+N=}l+Vle$8l8v|! z@tSlL)A+eQf+EKEZ9JILz-|Bi;_URj(vta-&qf!-m8awPXJ;oz`=@qkI4M6NEs05` z6onBNK_Z0pcPzW`CsURkNyp0ZW29H|kX;xOF17(jcDNHLX|Gg)iS=P)|Gx|$Ykqc2#C7%vHBmX^{W|RG6{H_St z^$LjMc9=+|u?@kUpw&-!`a7V?0Z8rKlw?dM*cDgkNfoB#ZUB__Z5@U}Mh~pthNtd$ z3HVa(yoEg5_&F{3-p9k2%iRy-$O^QAbRo4f|B+hVPNbv&q%n-uq*Xm5&s7g$sq2+i z5Wf~&$nQCeg;>=vjBUh(FAz49jA37Gv%@wi#trkPb$s=1ln+LU(vRDe?B!6A^dRLm z*Fu}Be_F&Ai@Tb##ZpGE9D*F%UT`zTye_z@*x%UxA)#A)NDEhX41;w7Sf&r z87@AaU=aE3a7}G^FRuaV=*uuyE95`d$OKdd6k)`y8(7$tUP9)B{@ufitvA&2P!?G_ z=_Nv=h+tLMApk3+M5%c63X0chQ7=hSj3Z+s`4%NKd%ad=kr?td^MAF0MO*&k#QL~A zI^8F`XqbX%IIMNdlxLFAcvdv|I+h^Zpq@>qWz{T|fLy`4+6YKwhjJaleAKmYvU;AB zq3wZUqI$}-L=lfGD;^-aYq?}I?c`%+=JNv8i1IOr=lxk5<&YEMdOghs5COou`G6^& zd0D|(e5u4^=OAjK9KY`3g=}oqLr0p?(^GQYoQ~DeN`}%T5*yZ|Q^=>Y!=n?OMlLKO zIXU@+Cz>OEnUDq#)aj*OCRMnC7q$@wtz#cIxJ(8z)Q2i3a&vKyNyFYI-wm^DT4&kU z^{tJ)tM7WrcQ?~!y{zl??aiz26sR^;Ah?yH7_UH*krlj$NduLMOH+>WB@x@xoXB%4 zH&yv7{9BZRw17Xz@iHB!}QD`AD-?LS5hqE=*sQNY~D097%n>a8*@g7qEOq0&rQ8M`B}y zft?;vom&ep8_dXQcq~r6O8CxN+3^p)TwI=g>i?I79XpqM;`D|TebC;|K{^yJ3DrQM zP72kCZ<7yX;gj#6+SUf@zP>Zz_;}v5teYD$)xfmI_w}#Hy~Z{FqnzB7!z-1KjYtI- z4}*gW=OxA`zItWdrl2-vP)huS|pBq-GN6xcRQ+&bEur9|kXZKRH!RlnY)xR+FQpwv>RI@#ybnOYPX-_CYdxl=Kr|2?! zio&sF0cjQpP~A?O^<93%=&4HGQPWJ{e*Zn{bV2L!_J_?Mw!XjJT2X5Ku=V@xUb4IS z``2vIdok_pZ0xN!v+Amxt6jA=ZFzfh_lF+UZM1jb>Lyz+AY=`S>8R;eHP_D8;lum= z{p0UN<@Ic`e!I1?N51TBktx{P+}?HKyYPJpy}kK~2bRuh0}|+b zPx6O8f4U+n#KVGYetdX zq=S3h>ZR}Qdyo>r6?RhI33pr;4w|MYnW4YrRY?jqRXR;*fNi`BJ|?X;8A~Kiqc+35 zASY#qHqvWHBr=!0?#SrusHq6F`l4c^f7m}i_-X$fqm(eWa(W$zUaKXYR{I9d!q)^v zvx+eJ^6PBcdac3{c#Uh8yj@;pOjFLe?Z?9Xp$XatyfSDo@3HO^i`Z3SM^iHQX>wDP zS80*Zyv0fwU>LpPW04kwZ6;|=(d~+~PnZ{Vkdr<>wFCKoPm!N7h|n@yh<^;x*oU(L z;=TodI|65ky5Q)(Pbh^uCXs3z6z|145&}REX&4PMtP}$YLO|~!E^>BFYUZ%HktqSf z*$}v@_d?<&p@&Y0`O1r!E>5c9W;&7R$qFw7GbyZB!H|Fom+LgnP#i-53DE_q;g2QI zer3kGf)c$1i!kDV<;cZlC*Wk0>`p=8Ij2B&C2A_Hp4k-e<}Kt`X3=5k!JQnca@Yjm zE+mb@|LZJ=sF7Fq$ss{FA^=@w5S}}^BWO!jobdOgBx9_U3k@n#z|bjVBmi<|o076* zCT0V09|>*sY={IxU&PF1dp$We9`Bzxq+owUUHI$_s%t1Y+v4h?szKt7?A@Hrt#R(V1?;?XFtiGXq6cZ83W*28f+WnStbP*@C&W7_5Z-~WEc6fnn43_L7m z!`u7ejnDZa08GTV^oBdZAz;QCWHMT4Csa(>Wg{Zo8t2NGI3@8!uT?RlmFcMEi=7Zr zbJ4K{;v2O&l?Xnja4EO0-8~31#F;^Vu0Utr_-RH9R&?Oj1{77T9hZ@@9ZJB%D!u0X z#>q@*GpyLcxd`jN=WA2;iD+0sE+7X<4F<+n@#V_@sh(Xy6evA-qf)aZa=cL`ZJKm2 zqUSLq^x~iX>%Z` z&(Na4DuXpilO4>ZnVSj)s~AjzU?g6IB=QM!1ZBn70#wW)&PT>!TBWdEyS=Lz>YQ{M z)QMq&S*luM1yLT+VCLD@fuM%OIr4F2Is%JF0BHr!Ko^YxRxG zq9TsO?PMg(>XdM@$tY!tHBGW+k#ZT;F-R_9UbCqCRR(mU)u&bx4dQ`u4A)KgW<^}t zSH{XZo0xBzFOF2T20xLILu+=Ok%>XYq*pvZ@M_V8iwF`pVwWdDk5Q6d{(2TWvpGXu zh^^v-5KNEXtc4l#X~ur?Emu$oNlwltmEy zn$N;_rC3$<=z5~ zf6rgaaeGfw=}2bfPU{H$)6zt>bW~5{M*HXx3|Im!2hMAaiE z>X{@&!&)=KzrIhC)X>!id`hSCW3u{hIXMVr#ahCOkr|v-%!U!6Qa78K+r?P|B9&F6 zrSl#K>opasjt_OcVz9QNXKgR1psg%0HyYS(1X0mUxhTnf0jipL=BWKwHYv|HU^~Mx zT;X$(mM8<=!&!9~gUQEj2oM;oqM#Vg2N@Uf>yT@X=Gx$w2ivQ>a+tDN*h&K-+o;MY zZWEYMK+;-Az%t~Z2XO2qHxn?$Wj|c1dhHBWqenk z`D4ZCYvj-Ej7I_gXrX8$A`GiCYa1MM?gAj`$e5oPr)4#-g;bXUEOs-hyqnlAW>fwP53 z1)hF(Vk0=)pMh7#bV>?y$>}C1g2}zkmS_MjZr*Ok^D-rH`wH9#;QM22XVa<#-M_hO zvvXFFXA|q2$9s;JtuwNDsA&#t7gM~eZ-gzYGCITkS(Vd!gn#<8gINCkKUR1I3^Aeh zhUq^oE^RkMq_J?Sl_G3xv3E4PO!TJa056v~*n%h~LiiFQYQ!A@?a1MBg>(2X7zuvq z81&l}a^-Y)2I{HL2x;Yd_f0z3+Pqrd`(ZFx-`#n$y`Jv9ezU%rZEjy}4|ayTZ+^e> z_AO8n4Fi*d7Zp+wlC~TJg~u;yoGBe-XeY01;7`TJlX)p85qCI@xdSPTy9c@ElU>s} zJUTepKR?38Zu<%=_>sL*cEcZ)OFiJ>n>AkOT>54nA`C9BlW0Zt3(;4No`~`O-a9!t zWSFAm2}Fm&=E>IP?Du?PuN57Mf}as zQ~X9W6~C0Ii}IzKoLzrQT@mFwGvm7b#jGKZ+S)s+m{6)q0ut|xK*($OUyY*FrRXVd z%sDWqK_i|vtXAvrt1R-(zki30bRa3&?x5P843@y57MfE6d@B+~#n3dzf| z_AiB^#ZNj-I%UI-O6Zb?k7fiUsOKHwxd$g-4v!8u%qde;B=3x-U2(ZRXiX1-c+6YH zzyBM*#N@+l_cdhuxvawF!z}w zveCT|CVzxeHEwiVPf{wJIUr*Q#fNdK?rD01o^sM(Q)INjYG^CNvRpmKT-v-~Q zR7Vw4?@UIACA6h6bpwt?()1zk?7k^Fji_l>xyF-Ev_VdD&$C8`3z=r4XU~&m@5^b~1#mbZ66!rU9>~K%OIQRw8p8CZ z&)VQo4{k(A3_1m-W>)rM#fWv_PGaJ(FDhEeOae%Uh^gc@>6X_7YV$}5iqyVzr*c z#Wb;U&I#>7I+zOc&M12SQF@yx3|Z>_l@`fyZJbl{II^Trn+`oG=8mz{vPN78uX5Iz zq((XCZzxAGXz6$|geEe^6Y?5N>+0#1pJ>yGevl2cW31kWYM+BPFToPWo5Qy`-l5fP za>72STA6!&X1cMsb*?y{>oCqyx*(z`4;JIEV@X+*Z()icC}Ub6j8CJgN*JMS>s1H!~6 z#YC7*vxc#FUkwKluXT@Cw)ccMcr>XY*fe*)J3j$7BQ%JrlMiYPdJxb9-OAn9F%~voOFGL%;fD27t~%kg(bb( zD^oh60*^;R3QT}D<2;2HFK;Jk)3*}7g|M6Qa9^Lhp@WezALnpFl&`Ina91|Zijoi_xK9aGW{B`I^Xh50uvcXf}+&kQTFn`^WFBj;vw z;NjlJybCU_CWljI!_U$6dk+q-ZmzJ}FN?J+`QlvcT@N!D<^1G5z`kLZQ>yf64Z@qO-1zwHYu4zRLcfWv657Sr}8Et z^CqjNRbFSwhYU@1cF2@ZUoI}!f7<`UQF6_S(CE|SY1XjV5TEivRo3NolkB|S!vp2V zvb-T1z%OO>wVtK}8t&)p^5`w)))yF966b;ZeRg{C#{@#t;CZF)2`x|$?9g1hRU`Xh z{x1;0dNNF_Vf{E^79lQU+E72KbeqGi5T{r3r zujf!bdV7i-Z!=Ak_3}lzA8zMjVW&5jliG zT4X?H{d$xF~@37$epp4X~qAL~$aR*Q1z%xKrm%`!vvA#2^Zm9PsNi z5oKBShPJZE9dGjrOsV}Va`3pR2$m1qs*SS@LVouL=`@BB@ZxuAj{jC=IgYGCq_4V5 z6H~H{$CkwiYUQlK{=`-yG*ni@2olA09rl4aUzOkbV~8x<%AWN~t!Oa>2j2XOaWe9w ztpeREiu5YOEY3t6kHILVR$S3)RxCU%z9m>{|oQ8ikUkr*)BOXvaPcH-4RGMJs*kRb(Riwt9C&{Jbv8JHi#S zsoW%^t97pd4Mi7@A4oa{S3@$*aWa7@9@8CBxXO?_wUz%0OhaB$j3^Fqz9O~{WZs0Y zlo300HqGmDNKhiOJV8&`LQplTJXPs8)NL?H@vT~wi;te# z;`72+?Q!zyTwEp>Up{|6JHKSO@F@MhB(yHB5#o7PvBu&dfk*}6G&|}!Vs51>KZ#+! zFW23xZg)sUC_9W?5V&Td#rAXG-)z&kiXa8f)KT#kHAPuS_V@ugt0+@TC+6U%ql2Gi zj2L`(e44DT)Nn|_OK0qGkAGj4jl7t)7Ma~gSx%AhRC}v6&y0)e{7`G^%+z#K^&a22I^ZL2&)fH?o`un z-qDYF zXOw6|m;_yo(66orY^~=OOHj6tp+Y`^{BWOK$Sh@=@^gPUWLkaZk_OHdD0AHRM64Di z>Pru`pZnYUpr~xhL0PcO3&2YsW48c*UOEU6f-ceifF=OQi!Fl5&_QCH?J1n3>qZWz zCL9<_CuvbgvkMFZuLdtMLx3&-dbvX27BKw;7MTF4)3{0wBO9G2|3)M!RjzoLVnbO5 z=12!?hIHX`#ey5TV}ZJ(8rigFK$JIfwGY1VEp71le&PhyQ>Xrs(q{0$FQ1hVO@drl zI_fFp^!eah`K%f_9|wV9Q~5-t37h7*iww)Qf z4%@Y8QUw|ttE`BifGC^O#31i^HVw!ORloiI`*b^9=WyV1#`mb4Y8HQb3Sc(rLthM` zWhV$DE(B(0VZj1O3sHKlGCj2OBSNA}XH}-d0i@31J!88D{%AYDOw;><)OY)TR9vIt z8H8|PiY6BkaMdGXHcbI{|4UGgd-K@rFrUFuIc-gO!-*eV6GJK5KkT1e92upZ=O=MjyhI^@DJIx<{do}lDA%&(ELH$rqK#JyBm82nV2 zP?YguwD3W{|Czxd4Zc>ClbiJ>tHx}wURhXki)M#QwH(z)jL12PW*lSyU_hV0+31+6 zmK-Y|CZ0Q3)-r`}CDNtEs`S`)=E9l633q8$=`U6?(ZTc7VU9*Sh2c>kDlF3vRqcD{ z7Wh#h1YSQ6bc3(NjC3_&M~GBb_V_~Jz~k_wpC=`F=aUWXW0g)v;DJSLBZ4t~AS8JM z-_~iKGsb~{EukUhUW*aARHmfba^_}6bPzgcacJ#WF->ph2(nJo5@C<*{FPjDe3cDh z1MJ`8?VjZQA1{ygd)$dLVQ*{dfeaH&9dMrJV6V)vk8db5*i+{zih-!`8TU|LMRN@m z3@2zCj@H+1KzUX>6{tGa<&fvOX6e*ul!so|jiFarbC+e4g!g&LJVGGNCbPh%krC1lYBTi+m{EWt&PpOWec5{wG}ZbTA$~hHc>r$ zq_G4}Dk`+C^`HxZ0~pL}=7OVlJsMt9I-#ipkB24sj%$pr34El31`~0uLx##HRF49g zK$Y(6%)eln%)<7dn44P!RPpA?gTAEcYF#ouMCxzKX+E&%^-q6h^g7H2Iki#vN~F~9 zow@`23URefoL++*Q>&&QR>N?)Wf)a%;pw!+f-=gt7C4&pMNPDJIYC8R0Yp{hTw#Oj za+(meVU|M!RlpVy{3oQ*u1KjMc znT?@hSQfe*D#h4~)rn*jpq!)~5Tj{Y0mpca16nMzQGjuRc0hYK(zYFkP-UsvEnz)?dtWe zZD=Wr$URp{cGgAwewf7Wm{8v_oKII2T?6No5EubOCC$l~ zU(1x^^_tkR6x*SCVSAxni?DGkv0j&Wa~r<2kp#P~$YQ>F&zaLIFHyVHhWEDTp;hwo zoVz9+z?{@*F6x>G*&Pjtuz*ij-}&_HrO&crvX_Tqoagxq+ww0@%o7OQAe$ry|mFPUwor< zS9Xt1DZ7YKJp$hVZ^?RzI2b3O6m5Zyj6^^ zvI(mHcb!8Akg$dtbZjA;TXynE4Thguhv3i@=VQ1anbDiYZH1?0onv0(Y1+Wm*!CJk z`WZwOtkOGnwYbiFg>~gN*+&5({;EU+^yby+uhe|P~sPZ`6)gvAm-=2dYWHoS7SBH?M}Rpb#zzN&WwP{BIKcqxmyTR zb>0kN-n72MJx!de$;OXFSEw&_Ik-Bfy}O!N^V^JpYY;`JHeBjs(S%?P_8qS6{&bYq z*$(|@*Sfa#8D1wlXj!3u?`n#yYbDqf4}A3&ek6%v&1Ifft8UBf8ZR$>M$m1Gpt{RX zOBDZUHclt&`DDErW$Q&*PQ?xUnim1nPAA5$c=ocJV8VPl@~StIF)eatK*DEHKPvMhY{JE{5p9#%x-o14SLC=|Jfk5DBA? z`-=YicPQ*Yf~8*a9fmZ4ButZRMi8ZdG)$^^2m8Cm0p-`N60J022n-8<1ScjBy%qcZdx1jh%p^UD7-S~%xcrJ(7l>MfruCc2t z+2p3k>(NS&v&^+Q#9#EUBs6hybolWo`FOtn`6nLhgr-`+lUDS=P=uWDSH{{56u5z09 zfs{-Ozs(IZP0iaz_qkQ~i?6zb+0n(DvEs+@@_t0`?vB9vMR`X(TU3xIXTKbsYk<2! z=CrJXRk$Q85bc($xB_b{^AI0h1?Gaibrr}9^xIXT`aF1jp1ZE?ke#+*d0Tfx3vJkr zXljL9_dx@v=GhB>`toW2bp7~r{qm=y^^>!+&&mGh&*x`g|9rr~Y@Zz_6CTbjc+J|1 z{q{&&Ta0s;syXdV*CW=EHiv#APLC%%2fgshW%p4!nPf#k^Fz3QKF!tZ-;SF`EVXa@ zSwX$AY`Th&(;P(TY`=N^nkR~3>5TgHqv&u&vC< z;h)@vgvsuk4u=&aA+tyfl?_y%>Ld^5alHx>Yz?8hv!uu;U&E?Fxe>71OI{jm*1Z8~ zVFp{5a25=Lp%9D}KhVjEy7SV-s;pA6s(2u8jR0ZJxgze`>k%vO+E8_ElMT63pbs0m z2s-D0%#%=chN>*36S8taeGcequn$kCdkQwKDSlaOxbLI@6%lSl=cWohsvQulc2ySW zFgRLW4jmbeMynvARh2IrOuQDN7^-V{tHrF z1dtdLjgUNUZmR4C^~3@DGPoE&BGTljEQT?-w(*17*BWHa4k*fc z+OZmK3oMV+PQorg6&`)QkE46+fH6IFrh ziYNsGXp;xkFz>36B?Caj?&~}dM9aZGZD`7)fW=ratSIGo4smJA8_?PC09b}7w^+@9 z?x+^09#Ca1sTfe+Mymu=NV+mWh1Nw4phDD+oGL);tl0r5-JJ}8_%hi=2EAjj`UuG2r z1Z=->kEhZ&M^JVijVp|4=h#)ONZi&F$W#wONMKs@`fICop^KJ9pd!v~M|?CB@wFhO zO!E?jl@f3WDX|Z2{0s|W26YR4=0?g#!?P#E99v#?$%uS~BE>@&n^D9ZCl`tpbR5Xi zS9zZbU%@A($b4-$y03tLNbjYU$^|*rPUR!fzD9#;BSyhhwcwm`62Egx^TV@SzVnGo zA9&=2mh1P=`DbyNdZE|Dog6_QnyZL!j4j~Y zgLPGU+)+!#pfOhY!8bx@Rrk1L(T9%4AiI>?V-jXhF${|?niF*#CsKv(ZcD<9>>wo= zGAw9(Q6rYf(r@2s`a5{H(xWt+tS2HY&B_*zWQ7MMIX=BOI=_VPXMXb$6>xfqswM6j zR-c~BuG2FuH$4GQ+Gu*36{hE~v-Dh+6bVYTne-H^NYAi#^b9LT&#+ENh`RNmXIK?_ zhTWiNTL#q5X%>KIAMI!P{a79;)n4`Hwh~r_y6#5Rj#n| z;q%e?!O`jE@ySvD;Ear5b7OA}3H10h`FMWz<#V!nc78|(^!|@YB!J&P9K!FrLHM-) zukO)(IzDxOfLxzf{qhQYF8hSTtukz;K~C{1rMZ+;NvMQJiU!A)YGLqjZCjzytpj2| zNv>xQB|IX(2W^271Y=?lyiH?V9+m}<&bD++>?JO?q|wdUA7C(__s=hnaoZtlt#xfJ zNo#d^bTAfObb;p@jf0xHb+K*)JX{22HxeIMkT#*Y{vY2-<|j#4i`Vwg${zV&?R}6k zV6uu=_!p%f(NBCwI!?&!!^!c%WuhBOlfyGEgj^g+EHT_6-VqVx;N;8U(cy-%Wr`Xu zmk6brYiAX8P2UKmUc~0B7FV`pqa<^Xu*$M6+Zeg#B4LeQqYtB|?kZtbjoAO~O77C= zHDQ%S(i0^OUkRnOAy-JbLHCkSY8aA-B5CaTYDCEvEw_Gpf zbjBr&a|&S^nhU`@uFi$WviHAp*!E>X#iY)JCoDiYrUmPtHCWxJiW%~wCyN{YA3R;U z^Mp2G4nK!?-FzBUD8c!3H1*H1O)WBpBb5Kc@zF2y>6iYHXLqQTsj>l#lU<`%GJDXV zhBe++zb+li42>tZeF{K^8Q8ygrASWM20yK28$6!@`>7sfvJE974}U}N>hqs6Xo0N4 z;*0PXGW-C4qs9B+FQutWys=_;IKL3Idxi3eJHSHrN=V*nBWTC{QJ;0=N;InDb0R@G zwF_*bzuus()SO{;8mWMIczkhrOh_;RD?(bcU5{(JoF&zp&`?GB%_^+YCY7@rOoVlX zykb;RvE?NM#8+DBi+;bJLlPI1vsZ>JX3Uflc zxPn1&K0Nx@%k%w%%hjXLX9qvAlf%K;{s~#?qg8o9k7;2*4WopaT0@8+NJxz3mn!LO zSa%=H8R@Z(8Z#raa`2I^f(V>!@1oQyFE8111Pm9e_P2LOfeBJJWOaVH!@4q0_Blf! zfGU`|P7k7>l*)`#IiEpOAOUCx`xi%mho@Yna0!2JB}XUZ{boXVFyY5Ez~cA%B6BIY z4{_~X0J#nXA#PHLTLFmF;t$YXStlFoMB`A|@r7Hl5r9cL^XwZ3;7vfFeLf>6o8zO4 zWVOm5c--8PNDdIWlaz;P)+0xQXC;^dW8yoLvVF}b3yp}ff8^)Ay!$ItJ-0{|7-9{K zg4h`#6X9e3HOr=4#Vho;HRhGgDykV!gF~x#N6Z!cXR(52hcQ+-{G|p8XL?w@Z`5bU z*(~!;6xW@&qtGInO|4s~b{`yfPvd9cza%@6-Qj`8Q~fHYzdP*sXNBkfXQvLFw7(J( z#30ecEGPYdaL@UGZd5XaAB!AQ^We_}!QUpI(@Ru9X;3~yYo^&p~Oelx+hc?#1> z-a#C^bU4geuoet&3Y$J*E7NINHAqVomH0)!$E{3=1eon`f;% zOAlEgZ1)0#yb#uwji&{YKtnTY00}qjrS!D`UYSpBf|pWg+?akNAPR>Gj4t55LGB1V zV7g~Hya~AH3~A=3G=t*FVl-1uA(C2Tw`A)S8DZ$#>%71KxFct_`$b2@H0P)j?8@wAPi zOiANwUD|<0c$i1h)ai_jfJh?ObgxpHydzE0*1CYd&a1k?k@WK^ws8D8;jsotEBKwY z0MNU@My0hM^cd8a{Mc?Z&{Ytm_(%BfFXZS-MBUGy&(1GtVpoh*yV*zX0PzLy%4#^D za45D;#9kJAi~az)n@=9)t4ej?YrH!Ns(k8khI06uGAgVGA7|ch<4x1wL6Rn3nX>_7wQ7g@v zoXMU!Uu&{M9|(pJNeIvqFb1=AQMt=#`ub~MM+8z-+OE#dkB&c{;?i@UIPp>O_{5}7 z>Nm^PQ`}wBaripV;3%cw9&RuyW(OTv^{ykd2cWM>3BGLH&`c{X(^75mvuLRzo7@oT zOp8=bzf3D0Qc3%mV}w)-DV1wxq{L)a6gb^(+@*3Tjb=p-9kWc47_00$t02@Pomp+1 zYWE1oBM3fcG6R>>y+unDLj*0E%+5ne3O)z|a;$DoSxZ0hSPa%dLu|E;CftPN=!lc5 zybO9SQiI$v70Ax8F)XtLq@Bgv# z_AR=F#BEEDwH=}$$CkmW;G;GnlZ4G!-s1jgVT>#;n_Aoq&AWCk#E4inC&E4Pina79yjae8=(J9)Q}Re@u+C0D z9DfAG+2tQUlU^PFE7;hCR%MH(r_R60u=67_-FM^+A(R{`%cyoWYm$0Y&WL1wm7$oo z@DB6LTuhcAA}zZW_v-LW#}9eMgS!K4QBw@Kh*V?dKQS0(<8*_kK}ht68H}WU<6W|1 zHN0Zh0>4fQP*lo?&Qc(B(qkkk0}+o`*3eih%Bm6Y?GbC>uq0E|fz1Ne5(RA){E>20z}I(Z_zb+x^$>2;2K@zcva zw3SxsWlgWE?PX1`bKHxcUPan_nmP7%U|AFGpMl4iZ59WmpHGOiRTRsfYiIMzn`!sx zA9J43jb!Q53#~4HR)x#VVkh%P3Mu{iell3rbeiohYeKE@E_O;6Dmhb8|CG%6(P6TG zfR2BvpudfR{@MPl8F5AjqWu8v(oC&|7F=*-${};a;)VNgB1^8C-@(1U-ydLzoP@*1` z)2!Bmmw-#Mr&Cl7&ExA3?Cy66@r32`Ndvx-MMM&o%}qrOjg?&^kYAOqo1e@OE!<+> z2Q51tY+Qg+ud7s0w?4mjm5t(h&Pd~9v~te@S5I$g0*=mY>3~dW#0?j}%>`8um>V$v zJQr*k)WreWjOoIR79E!`G%4Kt!6zr^qsT6vaGsLZbvek>r~psh!@qZ7mb^zH7>>aQ zAkM{Xe;rozq*<_h@~%R*i7mj$(xI8YsD&-i3RiFes4QfquxL7TH_fb;U7FU$8_cG|3et@alTNn~O?j#ZfXbO4x2Tdh1wL4Ik{ z!HD{f2V7woy-&-WPNb4V0lh%MK~$A;JmKPgGa`bGWh&*`UpvvO131qb>3}SdZ7#rw z8v(%ZeJ;Ra-96AvyohEgjURvqtZYT&=^d-h=)hecC@h5y4^c)xl5uryq~RyGOC)yf2&Nr#!2`m;U}OdZV*= zX{Yx!hE=A}s?v-EY5-gNH*gTN<^f_M5x{ixi27?+d0 zDJx(D)Wr`b!ou^=e$F~a-Q5|u1-rh*VWBM)JMskDK5%#kiQWuST;=SfOcCy8)HQ6B z4ZivhECodXuoJ8(p=Lcn9xddVti1BqZ#uVf9W>j%ItI;Ef>8GZxLDa6T zBauz``uBLJ9UZJVWV)g1-R=rpO6_jwQbcD8T8UWi6zcpyg+Cv88hcK%`tcCFJ;{DF z$=F|qRXM#Xzu^}41%mysefI75-?Ix9ZLn;+IFxYR*uYy)$nIF%=LOoP8np^<)??^!-aFXyR;dMfDzFq>|;ka zh6t&!Z?e9g&&m|x$+Uqig0{N0~BT%ma^ zKFSu&WTYI4dn&3#F;s&bZH{Ubh-RrKU{&wv0UfV-G$2~F7m-lYG5bo` zEf;XI2p*WDZ?aoJGA<<;-|GCYnU+6koR2eo9P+LLM69qo(_>Ed2_3-q(0iVQitRRF zlTs`{4podOK}19ECwnh*s!Ez6oL zZdb>ax$|X9yGke@^?-y|vf*NQCpxA=AFsPiLN>)sKOOThTFQw;r!`){O>RWUi8)_2 zB`>A!$+|jKlSqWJlun(CMmp#{m11V=za?U&tLmjyC%SbaR;>XSt-7070I_N<3uz(^*rFnRk@TV(>F5m1L9I_-(TJ?gHXU%?b$m zyY&vQ4t_z&u9s}npT`p-zBj#OhyJ9YZFyI-=aQCWWNC429XZJe|2^^(``s5o$d z%;uF9CW4L9?j!~YxWLzm`Wi#{DZO7#$C={l3xp0VtDAI^|CK7P#-JyzXq-5-heN=v zNph7zs3nZ1F)NyUN{+6iTKEnnn+i0lt8F}hajLE3#9pBOk=w^wN;FU+be zbP-A%ROqCxP*JM9o8aFo>2RD+P-xR- zkZhWwjwT!k^|4S0-d_KBKMM&Ix)+4Wy4nFGpWT?Q_LSvN4ka9fs_HqQVP$GZTp2sG zh@$q`(ADF>E2N1+(gW~WwO9_EhWr@``2%U!;wKd() zFx=Hm(yB`DA(#nQs&hrVs$Abl&RJVE*~f|aoE8O^YH!-D;l@}h*Hr1imEc_QPgOBf zIV;c;h;a=HHN5X{HmfmZL#GWbSz2>ljqlW+hng_;WzdGL`*SwZK?fMo1O3Pe-8d<4 zU@?ber$7(KYxP)*JL^@Z?DtpqOv@B8rKVu4G$-Y48XIZ8W*mD6#y$d0BX=wae54n` z%@z1ELRZ7wyrq}Cl6|rjU#9ifgl^3&DjN@E_LfE*;3%KT;&g*g)^RVNl;d&+SF_0n zk)nz=4p9tq^eI3u7Ha66#|)~ z6J-dv+cR+nF>3$f=j3D`3~oGx5vekS_#eEYq(a6wf<7+HX`&BZdps3*o9^2Yqs1S6 z1$;|7hH`P|7erTZeuzu;BNy`khwub}rpP{dofk#;U`9(gPi5?r8P8?0!60Hp92E^z z)vNM4TH&g}Qs&WwCqH$BDQDdTDZV_CFUJ0tOW+b0KVLjLuP~S|yQ51FEQ}LVjv#qR z*qM#M>r+TiM&_o!s!|oVLH6C}w87Lgc~fK|Jr3KGTuyP_vyRHNjBq8drD{DR8`o`4 zShWc7nG><7QFw^6-UN095frpYjl{lJX`K&fTKaUXvI$q3Uc*#R!&uWy(w-tb-+e*@ zNQ=laHava zwX){xkb#G+W5!$Tpky2XT182698V`R(i3kh0$&+?@f#r*+zKq^x6gUcB(P4FQqE_LF>jNsR$W*Wj95H=5B>uV+ECua#XqGi# z(HjyCZUIp(KA-HLKK{-}4-;^%66X!zi8Ymo8F$Cfz;zxLZY6n808h5@!AiMyJ!Kg~ zKH%OBjefDi7Ai9A5aI&*S$pRVk!~e{bnM!GfKKP%nzjgOz8$X4$1{4YWE+sWrBKG| z*(JnBA|WZa{g~YJbQ=rq?oqo79|7HI{(_nf_?sa$E#%rz;@Gf38wxKrzH|sUr9T}_-E&y@NRu#Y1+7@C2jyWJOW9&dZAZ1G9Otb}=WyQQn2bjhp zC&MkWTU8gL8fH*fo0rLRbWlO-U;38Dgl8Sn7_Ll3RRg<74gzherp;7ewTK z{QUq;nv(sqU4d}^;fy(RtD67zoj`*6t?vXl5gzVaPN|_{!E*3C@_1Iz+oZHh4hO*n zvTTSGS1kPfV=Fwe*}n&9j7I&1DJveZ0P zLk&g9A(`;#e8_yKf?*dBh>}U{d2p*3CZ(&x=aW};7+nR^2JPKG-e)p3xjKEeW-UtO z;He*lxfFafYfN#Zd?KVz&chyG?+1H9dbkyDlyNEklS*O*5j81mx+==SSFRYYxV9Mk z6Um`Wj8%A)`6-%9SuhnoWdV)mR2Eb|DK4n&+Z<4Jji?3q(eIC_psM$wAPsaUPQ$rZ1VD`2g6Mo`Qc`|hk z&0D8>P^aJs)WUaY0vq9l-&VRTi*{DV++C~Il4Ke5YaG&n{i*UtxNm+zM{(86yI&YD zK8BO&xTo+j{X;YtGvHz8Y-n-$4GdVhdZo(%sD`k447vy5noRw{$^ON~@rUDs{mZ2s z2nPk(UGwW4v+Xs)-BTtD1&OQ3=q48AF^m{Fz`;XZq@RWRL0RQDIfQh_^IW7&#+F1Z z+l(bg2_ucjOWt;PoZZ~u(cPEu<}QUS9aRP1WsH;X&fv?;_L^dDN?A6%N(WyTBQdEH z#H1$m6-rKIN9|M00x{|G?C=c!Wr0uN&wY%u4}V<{7>8d@u;9nD{S#W@4FCOd`ZMXt zX{3bZ##eG0^{_~R+WQAM}lC2%x)y_IKw z1#TwN6Yn3By=_>^49KDMb*xgKI~M=8IT^ps9%fW6W(fN(tivNq@Jtqi%mrwY-Gx3s zNS~xFo5pz5SIBrSF8@eb$xoM`kcAxX6I9s0I8xaPXF14;K9Tm1KhsLx_>GBA3a2qY z#pi-DVd^w*!CfA;b5HAKQ9aWUidz6FbI77wFLT3=qj&CO`nN&%+;v8YJ7G#N^y-a= z_UK;~eO#A)g=0uWIjqvgP5k&T^de014z9je_vIwROx|otSNT=8 zt8A3s=4C~b!eb2xtX5}acFUritk)SNbi^3xWM;<@Jtq0Lyh(>ykqmPXkwJdIwqB`A z=vXaBhA%ZvTA-9KY8qzbrsLFN#Za2_R=cb6CiB(%F>)9T$^0YW1)iq!gD&?Hy;HxT zbP{hgJMe;j6|VT0N*>)3@zNlr(A5l4QX=D;^S+?JAL^(hJAzd(`|Sh!K^Jn~sle80 z9^Z$wKK}%?w68PgpC{#Akqybw;5{VRy+Ea59l9sSaf@)+8)_Wx5&2>^`AUBGkZGLn ze^isopO#JbHu(c`4jI3e@g0y;K*^H)3x>SyC6lsAK9_ZKQ)L(ba)Rg$_ae!TOO7rg zvKsBmpvcIu!Pu9*Iy^u7EF~C0a&-pxY5VLSe*9qq#gFbFfG~>VR*a zOeHfBY?$$=Rn*0nlj1&EPi~Rf))sq>WG)QbO0g z)y}L0;j{0Z+bwM=wB6oiSK?we-aCA5*zKNR-|BlNS=nwpC89SSUkgf7AYT*F5o>&H z=#1RgjLptj^edQ7MJ^yTWpCtE_GCGorS!?NEZcjF0?)O#h@o`T1wEiV2lAoEa}>i!$IvN&oQnDB^E;hxR<~M*>Cb^mC}eSa0a$z zeE)9jYR|`upxK|;-{o~em;)ZQjG@p~p3yNgJxv4=r#-RRV9`Cez_{59aP$ zFCoHzFBzxR*I{`_4t%3#ToB${-}aJkMNPgF@!xs2b+pNRSRnusjS~Z>I{Nl4*I4&> z3>o~J$-ljX-tOv=S;AK!_s2D=2#V0~f%y479Ej>^I-r{di!XycqE%ZOpjfb>i60Pn zh#=G$&*(H#GX28C%wQz~_;)YQZ|oU!%}M?J>y6Egt?l*At@X4a63CDc^!E#CW0O2@ z-t57@TW|I@)qnZ(yRE&g-L2ido!4)+ez&>x`pui2-z9qsX}#r{LC}IE`CU~G$=)@~ z{^}{$_Wd{dw5&fc$Bp{e;z4b|0>6H}>s$Yw?VVkH{kJ!FU+?`c*<8>VgU{dj`lApg zghMq-0mlImh~chEr$o5nzsZgqQJGg7l*0_8BH~{^&R3NP&T*|aDGkAhPE%2hi5k&u z4AKcivaCmC)eL4$R4c4bx3Lh(s=m&uy4%F3H#CY2NFnT>s4i;tNJryZBj>_dM(ft} zpQy&PYWO3rIgXM{&nfgyQH@%aJIyP%#`me+v`dR~b(2&gEUFxLZHmoimn!k=CaQEm zEK#Qu_TwWSMK+g zd&sRJC7M}_8ae&e2XDdtIG@a#ESTh;`P9K>P4J_>nfNnMU!H$q=AOH<0+)gQsGL>7 ziCYkR zH;h_=l9RH@)U4ZF_KUQhb+?Lz*?^Gu=4S_{!?QD*hsA!vF4U0h+(@Qn=USzuG^%0m zd^{jkMgc%V#1YGB-CTZc{BR=e(T+FjcgqB)82h6|BIztLCvXe7u zORRA`V=Z87C+opN=LRnioY3H9z)VPH*L|@)_-k*$6J9diGoH<{etBsBN*l zzAo}frUg3_JCAECWcquon47Sh45o$Tddedm3aL+bxOaph zf|~$U;)qP?I`co?b{m>BP4THH)2wO5{}=#`x!FLV90xuKXVzw?IRhm z4LPA{(Od$MOxu(WOdvv+&KV{$4@CD(fslR914zhZaiDO1JfQey8VQQ4!936uS<#kwRO!DEC4YlglC>w8Lds<|$AT|$&(mz;%k%55!81;^*{E3f@)mb6 z<&Het?{u4_eC=~flO;S(EqqrrjGb9J*53?g8C4z6yR1JNM4(#$fUVpp>|pJa7S3-vKLPlP4#nY%iB#N^+O&qQd~ z@w-#F%z(d@mEs4V=S7pA8w#k|a9M|~;eam(`GnQf=NT%QWgM zXWQR||H<6LmD+mG=^xV@8T)d(^~SAxaKZ9hmVGJ@7ZhJs`;fN91xJxNV?YTR)&)zI zjAMW)CQ}oX*O=^wCQZmDFzo;nuo3w+ZXuj7q1!!m>;5pAp#Ei5h>WfQdz6kYI4x#o zTiK}qQ6;Mg#CND!eZUl)^^eXns(OTg_^vt3sXmxNTdG&jtw9t!Mu+|xx^_1_v0H^Y zXIgoc*YUPemxUn=Oc-~cT5IyR^O*O4cR&F+NDHtXR|~QL&td=Fd;JpsXDLs3{TE{a zZn6Jvy&-FE*nhWQZ@uV$m+^RwRvZV&hL(bpFzR;#MlQ2FM4;?Yoc|7)Dn*^_7PhD= z8$-~%s^+;nVwy9n;_nWPQn#9{%Y;}JyEO^YFumV22pKZXO08B&vjSd;tswbHhf0J$ z#4R%`FPAyKiFP|VuYzZpaax6}x3X+(-&+l~1}=Ndp*|FhD=yF{s(4!|X^ai?|HW7U z+w6avZ+1=l-9a5_?hRHm`FC-5sYNN7fN{v19paSA)4M`R)^MJY?Y#m>aTfh44&Q zzen4Yi;`y8bi~WRYrnXw1~RIVPPY|Fi=sbFoe$u9ZVrbNjWE!Q~#^knbp#u_5vTRtdQx^k{8)ebvnNelGu1^U?=YxC-q0U*w z`gUp&FvkotG|U5SCq4o4nXDP_PfSFVN!f_x#5^Dljmp)(%C5^Q^VpwZql7t);e|Ct z><7k72rL7Y_UE7d#cga`=e@yfmg+HO}eXkv1|&hx2nSc4bryAxl@-n8Oml5R+G$9E28NOCokWldq!62|G|3RsIOk2U+2?-k%}f7_ zy8QLKR{PJ^-W%)w*XHia{ja4wgbGfwn{;rWd?KsP)1n(W00#s;$|6UNBgA-GJPv+4&{rTVw6vEU(7p9wU~O>lC|e8l|aQ=UP2UsdI+k1qHhz0 zyRQ}$<0;;z1?aTy8Q%6F=*d}xU?kyvMGgLCnuhCf0APSCC%$PbN+l(wn z7qd>%;V_36X~7#_ud;$BV5BtKjL3pl8M8iM8pcU>XHX?h!F#szAtu)m1ZeD)^OK`V zgf0!IhXyqTXPfsvk|^_`c599m)qUNEq`~wajcw2w&ge>*X(B@Ox(Eu5p(K4-I1MPc zZ^Lie5yDKI7-@AbR30vR&57%w^d1yvGlIbpMxs+K)kYaol)%giq_Q3c9yoxh}7S z&^#73XVcqK&5`4{GB&C2GSy|ph2U65v4HYQ^9@YYeoP)~;erd@#K+Y+WEKVVY$B$y z4Z74@iz@HvT4{^neU1^{phoO4zt@FOfr+}GRQka zLcN~GCmoLSrkNYfdF=%Y`+TMS!oC;FGiv#R@s;NhK{lixocb-Ns&%&W@<8y zEtMQZ3>9?v8sY(k%&hRW8xwj=$J0JJTf9I12qO*+Z{O4XA2}b%CDK8e~vjS&mr)J>I=;|Ad7w4x5E!`ZZe+Xh*}Jx3MvGY<~4N&f3B<36SeqVfnP z!mV2IKPOEePlnmIFk`)^yicP zgCi;AwTpJI&W|p?oS$9@adZD7`Nw~J_5SGN@hOt;>CrD6N~7;6r}`1U(YjFN$5%(E zhd;jh$A3&t_D?^4+5dQyOpECak#c@~EOC}^hYW5jH0<&FqYr0fhQEA1#3}Q`$I_&1 zai z+jKI@Yx0t?+`@Z|An2krb8vR@<&%G)8(d3$A<(TSc8gvjJW_g`9=bsn1h;YlS2Qw4 zfnElX+~z5h7m9rHwOeB-MnGHV@CVtYXly*PN?IG>a00?a5A6Yy9=*Lx2G|TXRu*@7 zG5r5FLHti=O*zJwi%|k~*Z;iP+I;cW_##TYy~K z8pR`6fc0gGnx%kkw;_%r#pv(ux_+Lze+>eYHJtSVPz2z-$2JZkM_@wTNG zRYJ-;7fyw2*nWI6(tdNr8Ubksz>kU`qATs_rS1kd%l2UFlj*GK$AyI{XH9%z+^U%i zXe4gyxp6GN<}L_1D96(xYk?3S7RN$GoZF80Xb3NDNGVFjVWk8dbkTu6wDB`6l%saX z?Yuq*Qa&1K>8rd? zg|Fa~Qe?h19NkyIKcx3k>*|8+j-C;KY>;SQqd~P1qu{Dq5OBb!zGJE{=OXJmJonMW z(BH*@b1U&&l`I9GS9vF)_XlM$8&6gj<342Ae;>pBXTJOY+b{Z`Wjx;XU*7$Ha@w=v zzwYe4od1^cfd3C%V?Tetf52}R=92^rr&SG3K#)PGycQQC15uc(t8~x@A4ejGmIMy| zHxOUhDvy7+RY2!SEICJomZ0RaJnay(dUdD~Gi0TBOn;zcU&d#vmA^Yzjp?3NKBHMH zqgJu*i)(6;WU=_G`N`M4SEM2W|-S;n51=*Cf8L4ArUaGE8bXc%H-_&Iv-@o&g;G8 z`{ZL;-V_0HE_cJco)+o7ef=s*0HHu$zt;`X z3vo2D!RWguc%19%;!*-xlV$&iTNz(>=4!zlRAIdd&K2bi`k&BUFIg?}uUWEx$g2uG zq`!mK4Q2Oy=6J!9w^4z`aV=08NpKzi_AutN!cz=(K{mk2tRR~}knaR_&ZZdl%+Z94 z_4=(oM_d&eBt4K~+L?LSp*|*~&8oCmnTLJNA7@${XOE3WpakxC5-*a!%qDcW^ny#w zKCzfJM@S)~gWQ)wwg%akc3?`sgSYBQZ|5d$UU`mnqZ888ha?v63u$^Cz322LOIC;E zxISp8r#^M71Bb?T<}S}L3pt2MGLw6vjt^y)8j+bmlC(&uH>hjwx4p%^sUyr zXwE%X5BM@1&L1$s4m=e@W@1(NAawc4O=3t}`DBz;IrW8YMx^k~hzVMeg49EaZDG^? z8>Kn6f_xfm!Xo<`5n;x-LOHdLwpry2l^Aqa89d9p8xKRukUAjoJ`$Ru{u7Ycx_0Q3 zrg1dk7-DlbkoGt^)^!}HOYtgTsvEW@*31E12yHIl{9}i;wp7g8itAlxXgViwB~6L9 z30-E0YsgT^3*hp-611TsKy5-4fZe9Two15QD%W&6fK`kmI#7nla|~TI&^TevLu;y; zNGCQn{09KlR!M~^mkHSFy9GX|J7cW<;*S@XN1u{&avHiwRxk2#4hPaU)fv@qtu4G` zuZ~YIj?OQ^8|h3_iUGszX@}3Adc^F>lieCQ5uSWGy5JWxKNjUxS|s~$mxXHW@XPAT z4Swk3B^L<3q87~IE@x9>P?irfavC2{m1I)hX7qTj-7c!L24g1m=$_ldt4N$#)*Fd? zTGa+ONK2sDV7u(<%2i%~^QY|Id-}reAHO4A&#uILkS;I)v@g^29#evivu0EdYpXT% zh00;imJJ?=cm6hIz2uslm1ktkq`}!RZ$J&kCIa2NZeZpnP||9=hO19`b1$2|U~dA0 zWXlMN_RKhDNoBHALN-jbdDi#6ks>aagtOdLCzuig(a;%9q`Dx zq_wefVV(4e%tzLc#>tWJuB?XS4KF~sku?{LeMzLG=ZvT}0WI0Ngejg?B(<6vWyO?` z&{x^mPwb7|+_^Cht=)Pyot9OTH2L5wlmZRbq@1iHTsjk0;~bJ~{4;yw7;8GA6REV% z+n84QZC+$I+0bf;FYiTGL72Vui)l81+;0w6kbN7BAk~xX%_q2G7r_efDxSna_<2=+ z%NXY4S9A%4q?q-6$kcx}^sv?uam!i(nYA_NVwx$Hup~CE1=`dqBk8hH5 z@dgm_HqY(|szIy-Rt4av#XZ4(!SCwPRhd>pCpMC^>yUh-Tt}UNX?aI@4PY^n1N_25 z@N0i~3#^kOISuKp#Ob!w$KnPB_6I_saa*g6f~-HHVXcFRkStCb3Mv4;%5G3=oe|z& z-b3zA+9AqlG5SF{oWTJE7&K_GZ8nE-F?o|8;g@Ldglv`&Z9Myiro4g!#{1_QhGDZ1 zUwh(WF|bk;;vd9cu$JrPq`2om(?M0jKZBaFMj*`zu=bw%*E{@MmFOv6vO!hy2CVkll@65$ZUKC)mfB5P6 zlrRG(7pgsgVwQZS>7iStG^bv?CzSaCF;0sWBR6Zd)?}mGpa~-BYHP*COQ+1nNCb5j zymV>Q=Ab!e((nx0z9X_oCi#-p{HJo5U*Ees;A&97^R_t5z;~U}C;VkK$zE$sf+Xf@ zP6G3`$HYlxjPK#=K;urE#!mD6QrD#C4_yiH5%#0Tfu0CyrT8gA#CR<{^C%iqvcgoqMskIhl^*|c} zCyqe877Z}evInE$muM-BShOJvwM1L&w|^ilh(zHF*$;&}Azmh20<7gLIx6+OXv&5X zXmWg=_9+r72>j{8K29gzp8K9~Ez~n(p!oIc6z~9c(h)&8kuX?kUk4dzJ0BAsS(A;L zPRz0VDJS*<;xG`Q6dYM5C26ywL&xASM5G5dm{D2s7|Eg&2JApDvTq=);CS&b**5!) z?cq_)PYDu8c)YC8@mO!^{jB(!(9g_a0#lY8T>Rntlf(Vb=E&SliE}+FW2~KkovSpm zr-vx!Pg010%sIYwNctOCWoW9 z7p8jFoJIDsMfSoqlopC-uqmPRS^rcTlyCNj!|y8^0g?_)DJ|QxAw{%{5lqx(&%}#; z8Uuy4D9c$5%1<7AM4Qz=DefLNZlW$rM@>8ch+imTE4OqI6M2asN>b1kMhfuk_h3ej zj^jkaW_e}J1$g$Gsv61p zh4LmKD1{;L!V-)J5e|u{{7=atIf3O492iav3?RkTzgfB{CXY}c>P|kKv?BwwVM?T| z86dJTT`#3E1#ozwXou>J+pHoWu;2I>8*Z(HkJ!Rv_X_oZ|vc?kv(G@LSZeA(|D_xN$;b7NMMcRv0Gks*GMpN7SLQRwBk!Wex43 z!Tb4ZeORKj)>@?&ZXcO!(j1@Gs$9Q_RwG855h;2AiqZQGs$fBcKjbf&gSSGY%D1L0 z3(EFJWkGq81cwwPD!V!$)xFd*U>Fzlspo~n3t>;tX(%l+MnbhxgM(7uISm@t+^fyX zskdwyW_tpRuR&`1FgBJ;`cnL6_K{)6%p*+kF@P)?v{)l+v<*?VSWn9d7Conb!Un1| zeVLMzI`ig4r3V|UzhMC|FuepXYimW}rQ8xeA17b4dwvuaAC*5NmJ!==tRe@YaEYpH z;7@l`-W810F(#k(k54pZ-0BIaK+a|jL=6QE2u41ALLJ@e3iLoM3TRpru%d_`80?+e zN7!KnLk=?`n-=7?*>7z*5PY3c+_)hB3h?*?qatd+cv&wnOW@=H*-sm!tllv(Q(Wn`P-R{8&{DMl7E(#Vh%n5em+#QcU2I^{Ug(aj<`K zbb7dd9@sq&h?oO5KdDIPZ=QBT8L(a$XCU@gynu~`7sZ_gPXts@RLTmWS-#T`J-jvR z$fE$E6xmCNWZJ+m=|16XPm0TJ4I*bkbb~*%_UIciI<$C7uB|pFfV4KJ@_4cI`i8L& z31V48uu)JYSfaM4{;fi6Z65Av_OE8O6uv)@H{ZWk->{36c>$tgdF>gp2C21OOiTXtDnI&M{-5{_OvW< z2wl+m;fGvRApi+*ClOKk3{Ud{i_b>bAk}bMF+yZSJXZLEW*;)_7SeBkGN5 zfI>4yW(4LQ=ZO8S%o=|~8!XC!)tU+*G`joA0Q*n=Ro1-*yF~r9DW?gPNCrjDtod>{ ztY!c>g!Tc+j7I6_`eQVf{G`0X9e^@mi11H+-dJ6Iy&M4BR?f{8k+fCC6fz>ll!oG? zzIGB+d!vFXet|#iW*Hl#>jygjwx(mqj1@KaAA8JlA5z=Z4G%{90%)N9rb_FP((on! zZ7ngS>|sL2G}fCFFaquTIxDGT3snVC@5tm|w71M>lKSpxEr{BI7F_O`@Dn{kQ|X-u zALaQPJaQc?=$^Xh&Fk#o!m)S#0=~0nlOmsF%8peI>L=c?NDmB!KJ(C2wE5~AsGx0A z^tWM#8kp8d4XUD+%g`j_YCsZ@uZ00kOM(#TpP<6jT7slAtQb=@&Tej45_PpP1(dfr zW8>1=p|8X^tPPAcBlj;~BbFsgYlgiesQdZwLl1F^CH_zvks!RSG~gQw4(fdKxKv*- z7AstJwbHy5*ULadwtJ7(q(hU^(yRh7fFRr*HDen}u?~5flLOUHmEsQsw;z?2vW6J# zj@8g{MT4rg9?K&xemXk3bTEmdZ-cC`xWvVXoPC~{Pt?!{L#023pZGyJtQarxFO^^L z7lxxfKsOE}1|*Ydcw3Une1B60Et_r8w`vO}MT^!1q(EyHQJd{ax<(A>fzOpBY|)m4 z6KKrx?>b~&rR*oQe}i^bcyKGHTG5sS6KHFU{@F-dGjOKhzm6~puQr2;EGycuUpd+s zl#>a&D1jS1bpJqq$4~=C2GMkk>M<#OsY6ybl3ePKqk0E?y{?VLiNdr8o$urwRlma6={VPQ;a*j6$OB|5&dqZ z(}*n4!evL!^eYYihMamx6$`NgxJ?Lg46e-OgM6AA15;tC4Mv+9f7Z;m);e{$ju;02R1mKE|pZcpPAn=73u(WXaAltqx z3K&I+?t(7sdckMRSrtVe6*Ru6X)FO7GOy-^d#!8ZKX7&D0Ry0_&B<%Rph3ker3BuP zfa7fmZgfwwqV8M!%>nzOyuyVTDDQbY4k+y{TB-=OMfQpn)>N!kqY{vRpP zJXZiVfpf=xP4G$bUYcF+Nttg0T`=5M;g$FWEDRlSYZmIc;h)7Hyf%NK`>A3BrbmZp z!#u;;&_&e%c#9Vc`0Te0R#pkXwQj5kxa>C;(;fLl3$k*cg=tm(B^xvjCi$6tQC>ln zgxC0nrVi2NYh+t*&HOR0p35Uzw0 zeY9#?ld%lx{n=&mPs#rAGYKNFx^h*TUgPgeWhKaG5IE2)UrHQTrS2`B=H}ERSc`x- zXyqy>2gUzYF5S6->KZ5xk(r92yDBMgsPYDv8>q#tI9&GA?4@$K&JA!nyDD;Oqy0?( z6d?I;C6|p#crMho!ZShHy;lxb}x`N~JY{G7AI~!ErsV$0|XU7TfSBQ)PCzgY9 zninO%znA74e77d$xS$0lF7gw1_>|Wb@a6t)jgNq4c05g@U0mfpn?<=Fr(Y#!M(+t)de&GDR^h z`|=k93wP|;g>~|YOu7WFRCR-9@pR}8!(!{~ALCDUdr-a=k&G1hN_`g!$;d^J*n+kD z^-5wC1t!?H3E5CZfnR6YjLs^5g+D~KP=m@vG}+?0^ST>0GqhJ(YgK_Kn~fEv z_D}au{&;zO05{9ek3OHBUy{$yMy?Xlt8puvzZP$3+%#R-mC@1!7*mC7NVxB!2}`k}HiP#4 z?+MSDeh*H#VC+|$Q2&;2+zrY|#|Cpy{C;SB8y;Lj1q+~t__)eWK{z6+6+K`Yd zxmr)OWvV_&v-IQ&-rwqpLo_E~1=@4y0%Vi?4I31Zob)q-)|vvz*&cXqz|}|#=kTq< z4+s|Uvhc^O5P=3nNe)7J=Wfdmu%tkt9y4llL@=yGBn??mLTPPqm}MAcPm6rS33;B@q>QWAIVb*TrjX-J<={&c2*O_L6@B z+C2Jrj^R!(o(=svIo*Z9=eQOzW5Lf=|3{nMsquL&Ue21c5Kn0mOmGt(J;vRXtRB;t z;U_D>FwLXVkV9drG%Enbv})-km4?r0T)4mpW$jQ~ciGjbEKQTsFYK*&!HPKscc&DM zSwGRBsW4F6G+D9Qi1v5Pbf(i>JuH7dwvWkVhrGl9DdcpD(Z}hLV4CaX+tvz%SK)2^ zoZTz2&Wu4Zza1WuMQM!x3^k>Nuh!P>Sg*yy+~n189b8=Q69cT-OwN$P!=?>63TIdY5NLCt0cZ$o`K03baUj- zPspdA4V#8jAsDU9IB`v>+n43(5lv!7>_rYm#TCskrqcv((_+?IyNW?bZ{2EAu@AL3 zsxxh^MaH&`v8oDLwN1dYqyM2u`jhZm{G@n94EU2pf@?2nC$`knQ>1G zwP6KTM9WTvgx0orYgkv!b~R0ex8b|8`UNY#$zP%EiGww+0pX%AbK@Mc?2&x;XxLdUXD5 zxaE*0Qm<6Ofo>@*#X6qI&ZZeSe%FMU5?%v07fQvlpoF5l)o;)71}#=e8#Ec^mt)qV z302{dt!-&OjY>O=J!ap?;TJSho|bs^)^;3cV5K$winXHV%izKIWt$49)@HSP3baAz zod?CUp4K%kLMqMsK6E?K9bJO8;u#&`cn%mAJ00=&Gy6NaP_{)L{pybT2O0qy<=-kG z<-MVmW38I$546k{uGW1?BSHxzT6>|!mlWx?9IcChjoMkrY+i-d+VrGsAP1=S{o(Q_H~Abf%o{AK3?fhLqFnUpQ#*7loR6gC=33aq7WS zBPF5`no*2ggtWd`aw_EV5EMu5UrB*Et{_Y|20oG1>Cs({ozMFNM_ZaxkiDiP0bx@L z^v0H=SJNMg;!yecGC9;=$n-k|!eBHhi}L0kj33w663(bH;QVxM!1)R7|H7tV0wB^O zra080e4uX)Q2Z@RAqc5X@V1T)u}&f4B2F1lD*VFhF7j8E$vcL(>@F{ZCuUB8M^ouZ zh%B`phe=rx>_qGXhD+vY`;5+w)9NeU0}|jq@izjXd_xnsDZX>o^bNDytSF}f1Jqv^ z<#lt1Au8x=7S};VC4SMMkGJkHzY4=berv(=o|Ffzeab^@sPzSO%qWKSK8n8t1pLS; z<0&{3q*q;+@=vmV0VjY@N2iw;kCMqtOL}?(;RW_jPSoTK?nJA)1jZ83_LO><==XNjuZ>7vfoE-mrMB~+O{O37kdnM9lvZ{n* zem>*3(zn|)K%IL8}(9mEH%784d+aLdNDjG9d3@gBjUV# zjQQ=8e?iUo9F~inv@n2kR(V#d{eWgl^K)-~5bX@b_vbJY@aCbcnyEksSGoraG;<&;#TwEowLLKX|h>f7Zc647m5H33?YXM!2 z0IWukR5OaH8EDiD2I>slsRh<#eb8e?nsFV?xQljBL@Ros9bS+jxEeCg2y>@Jqcg(B zIYPWS1F%_v(v09=j4sqzR(b#`BWRQs@d(Wb`=g!+O z=?D5LFKka%w_b55IDXKE%fbBo-q>CF2fRrLH$2%E_^3Orz)H4dEMpsjIsANV?WA)X z2+-|;ZbgbWg_4@$S?opVoe|!MPV#Zyu(Yn>4auxAh}?LTO~nL3b0Mv=%t+;=xL@;c zAdW4;wG9c_0q!pNxit#`hNUwlYc`^Jk-}k2AdQ=m%tJ5&|K~+%9B z5Lqi7S(z8=O;5vJhNm|IPjv@PMAu59t7&QMS=v)$mF-U2wjJR-2osW^!jY;Gp~{m? zg5uq%&Va5C?L1K7#Eaw9HKtECROx1Hj8s1rb{?$DlM}~H{V$(vyiU(kW9W(y(s|s} z&@P#`Y(Tex3#ymyS2Nw^ZL!!?>4vg_O{;qF4M7Uu{Wtpf)*lW~8}+ZnBHDlje*JpayZ*bIyTHiUy;7tE~p<11&WY?{~QFdu-oUkL%0Jmm;F zp^7S4;A%D`TU($23ys$wm9wh2Z!hR6p*qSDL4<76f8QIAkN#)VMNxkn{cr8Qd1Lbb z*RS8a(Em~%P$->zN>i5} zVi*dF91gDz&X4vlk0cEeQO{3@Od~_2(Jx<)50lU5$Dj7k|Cs!I^ha@{^d_71D{?j} z$NkwXAFi@%XDp>#pI3!uPQRR-(68wK?*0UP&i!Q!F?>K)A0~f*D2Dszt6Q6!ti60P zoi+WY{F+T_8XoQFT+|nx-K2%fS<|63%mit6NZuci@t=@Zd&%oHZ+X!F69gVllgr~z zM;Dj-pFUsyn_c?!?3dNGS8L=f!ItFs^zi6kjU~y4-{5F=OP8b1W;G!qyuDbfa?T0V zc%XRVc0*+A)Cn9!6L&+SOWO$;hUaxd6Vtp#k7GbGpXfdlLULt0@?$q$$SV64kB4q_ z3}bi;8Vd~GCKzlK;LSqt22pO$DyCul`{R!UjUy4hr7-wH6p>}YJT)2I_s8u}o9jjv zW0aLbyr1O#+1bg_{;6H{IxXsq&Gw9j=(UpJykChnmR|PGG zRix9(+i1E{At~5vKN zeTAz<*PHHb&R6gEdvn(N{ap3N>9>A7*Brfnh z7KZ(_nU9NqIX(WDFGq~U_K@hKgf^SgxS2QmI2R9RaFG$Yq2edTbvd_ukEi-JE=kY+ z^KeHtWwgMM@TL2H0p{@t6eZ$3myssTs6#~}MRD?lYr=hS`Kd=`F^r*$jUUv$)>BA7 z8&6KGryZ-&w(tsEe~7`|U@@%H2Vc(5k4`W9@KtZGwJN;rU!H$CJ-}Q94Ma*)!L7Z0 zE1}Zv(7D45wlN|Z?@GsNv*~b7ihQMD@g*v++OU|axZ1FInpULq9FZAcf|{P49}O!3 z0Uq7BCNLlBIn^kH$+O#J{2>b}W(v-jlz<+NA|()lo#FC~d>X<(u4WCHv{u$i6EHY? zFH4jCV_KCrm2@yO#q;?&bSxjHSkMZ?SpXfH@*!T@Wk4m~=rF~5 zhX1r4gQA;tgnRjKeUOW5K=g9@p9^|24ZOGTaoTc8X^LVgB@Ih9|>pkW=>| zUkU1l)z6!3>@UQUF866%sWx4YTt2@BM<=MZaxV0KkUB^#C{wgp3mh#=h?-mjWP^%U z0Ew!KipmDpoNCKFbZ5TO_bmifsftQe9_Q0DI-j(Si+ z0{D}&eV3@cvB~v6b$&A)%znN$KaZs9{sYRNQ3X_;{1r;Xk94s9hQS#+8 z%H0|;{7$|mNBajqCFf_qkp3JUe7TI8E@|^L?0-FZA>Ho2_yZd7QUB=_~>;McPJbjRn-Feu@#7`6fs5@^^{f4pOTsuQeCf#)o0M5oGP$pHqQwc|?WS zv;9#sE?ft0{+;lieBvrVPsQZ8Cbhn6vN6(O%uh`xgdAFN8xqQJ+e5)~Saq16T*`8H zNoALvP4Zu78J-Q<;z@L%#+gJM2r>Rk0m! z8->taC70(-C1g>CEUbr#`-=VN`49*dDFu?_jB)x9Dn*S@qV%%{?nv#biPG`PRTCti z<^WXQbA@7fEtO<=X0{>U7*^r3IQn%}K@u@VvFAb}^Xw=@KqU+UY*bn8#QcHroi*!u zU(c=}D3c4)RXM!(5*U7z)Fmu(>0pGidjBtV$q3KZuFCcBs6e@L8bWevC7jkgZG!{b zHiqG68tkUJH|!~fHWqDseWt~2vGui5E8C33_*hTbR>XecMkM+PH?C>06Y3a6?~P9e zSph%db|@3U`wTiDk@;;^oqbMmtY`1!{IwFg)h6d1R)Z#$Gs__Eec}|=zQu{6Pbbc4 zmds`7Lj?VibPY7fB`Mtm&86 ze@0F!l*jW{Tp3$T8vTyHswIT^&n zXJ630@jW0(_d7whDY&75S3O8@4?LYNpBm#gT|^h-$cgV95@QE|Vv)i?dtx;;ft1=2 zzGr)Lo5Px7dia~cdxrz)pe)Kt6nLW?xBg-0>WA&?*W4pV^G;QJpw3jNCnDQ#2=V7S znyK6x`>`;`O%J9UYXCK!5It$pv{I*EOvou z*XLY3r4g}u<}iAqvvE$h=7iEljD=JSSS?04&RUJoF$5YBgJ7Yy6x(Tnw$l)<*p}9( zJ2PX-riZb&wr!aSLq(p}j_u8Tv}P7<$3EUiIT>88YZ`SDkRFFw$|__cskT&ctuY_LG6u_8F~&a4~w|y zQog7+7^onp__x@&zF32W&43NJ5Xve|JDoOdar#SR$vGOE?W?hjnC{fc(U)tJ*fUX_VxVMp_FNY2EahJwQd!Cr5g=l#x4)UWYFv%%`@? z$QACXbKZf9%qCRCXW!hIFRltuT+b#W@c+O-^QwS2m{p)&WrV^YVF*2tvy=Qfq4%?r z>|2iU_Q;zFIY<%4nB)yiPBxyF;1{>{>hMgz-=>d=(g-{(*7Ft(@tjXce`s1Z^#2&A z8??zA)jeAQNnW@Vg`-YuZV!@4KSEiC%Dj{J<83%?PkcX zF&So{>cbR!V2013AHU8r)dJ@|p4%AQR}m48>UNP;rAUMdz|n>blH>v(t8aBd)^`vW zbZsA*XMRd)=WtFA`#v9ZPADGW{BvB?5my78vOEp2&-1}X3SkbB!L{vi0`Dcph?Lvv zc$Wnnt|(oo}s7b4A@qx^eVr|M-N@Z-mjA-_Ct*mKet|h}u`^ zX-RQtup`n&7)o%zq&jTZ#*CjqoMe#(t<4x*s`_|&P3^Gul?xkoffy@||7aCk#eEZN zQDz+-dzAfB|IpF7R=_h1^#YvU(XLQUA3Fiouf=n@)v?R(G7d}cjztb15(U#iXpf*=^G-uO(Il2i7-WInUeM~Y z#EE%Y19Hy2Er=qV9A%w%bY-6Wa{S-Hquu}C+}PTL003kqhOc(n{zdi{0AVw*%2 zXD|4f(^V&*z)nzB5MZ0d4D7QVHPDi)mT0U~t+GNLP^gR3Dj#I%DMv$pqB8<{6`Tp9 zN_*$5D)47qx5Q2BDDcv39aT7Lg6O(SfBSx2%x)quMX5pE&b!r#=o?$Bh(U{z{7PR; z+4$L^WupivlFp(Ql9h}Ylh#jVOlA}#lgd58jvU11DQRMUYcYQb6GObA$msHg;XHj{ zrX@Y${eLk*pjz_3ZSU-Dnfw3d_SVb(zl=xIY4CNQUvUBaqid2X!tfk7(&ux<4qS2P z93tW_@N{%v=YzE1x1xaiBqAf*UT@ocU%Ri${#M^s`pc%X4l|&c_gd z*_#DG24U}zG2kwPh}DUbM~^SXFef{b678MgXIc%)Vdg-brPxq4I^=uQa_Mm46vu`U zLvb{R3|l&uYeqwVg_N%IZ)9p6L^&U&$-Ec&1W}9O_8fTJ1wWUMRkn!qTfi^>l~j8? z!3ZvJEOFhq8KY{vDpYlKSv5oyhG+n7%&`-G%pEwMFQAZue+w(ul@J{v-9ipGw`l=R zTVS4($%W|48QIth=KUp>`Tii6jO)} zgML`mgpzP)bVG~>8t2JTpcv{oK}i9{Il_P?3cxu&N32`GDCPqi9&x}n=L4v4X6{Do z#dXt6s3JT6*19IPfzkNO%8_H(T2~Fa96P zc-q!~5gs6I`JZ;p{2$w!ueaX3*ngJscum3*2m0A_+P0|zd7O!i{+Jm0N(X)i~+`1c?*wDJE%cmTB6|KIHHTKs=! z^Ckb^QXWmkWl_L#b&-OP1oVgC=3kYCdR4(0@06C(bYZUBMlosS^^6oWVe;vhPebbD?=jBz&1XFL~I zlQ%^W*GwasrTGSMsbNDK&N5G6coB!SDHT+SLgBo6bZdo!A*mq)VXF<7Ix{KmaZ{Ks68Oq8H7Vcf{I8krN;A&K zjvjW#o3emR^BuQUQE8DDL1m4qy#5N|p9!Trff2WLS!vH47fQwKkIG?5c;~$vd*nL& zI)j_*DHs6Qf|O*hOpAg}K?mt5YOBI{V={tn)p^~?3T1XBCM{*(Af5_b7p0r5^cZxYN(Y)%#7ix9ydlvl?paSsTx1^d2@M-V)@it8&g~2(&DV-`taVf8 z1=~)$)2akPp>k~58jiBksdi^RjLA5_b(#)lHgZ(r03b=(~F23#AO4n1xi46yuY69%mufVzQqqPYQF;QLcs z=^yVg{XcfV|6`nG&4OZo!PDLUbMMXW-i!a|G9Lf>FDCXEJZ=7;+pqUb|KHu+m-rt` zc_1}uRZf!&BJmDq1rg>yk;R8>hAfGZJBSW1-5;~kEXyf?VgQnZ5s~{p(U_S*Xbc9C z(9?zo*;E1kGRm9ms!Xe4a`X+;eEpO|PL2Bz&^uA7pJ62*#DZPBjD%J2ct+NECD5qR zN}w)amsL6uc9ST0jaI_}@*D+?wx#9ZDpA@2>T_B(`5>RBEfdFqO_lPlo|WY|u2WF( zZ+Saj`s3&Si;4XNPniF2Z@$^uweEjxy?){U%XkPWx~MP^>RO}4H{B%CZyF~HMM07wA_OCiGNq9UoRMF})-miO+fMt)U79zF7b{Cy95SJ? zJb5AK3+8?_v6It2rDa9K&^|*na?sP>vm>wT(5Tm!QSbHo{-d&9Um~pqLVRk6B9E&& z2)}|%udiUX1w(#|heM4wZa8Vf@@fRcg$M?tk=opR2In~B_o9y>GVL=rk&7O-rVh7L zTJioV?lW%ERhp3z6F{XC$xz~hD?q$`P#u?1pfNiCY?#ND{!5upy0L;HyEfBxJy^ktoAK#_CF z5PSr>UyYmckQP?LPO#|cO4Dj^4<{B-t|m=QhDJcSsLfh*a(>rGazLI=J==0P*+q*17m4d7V#wTtsUm3r zuu`l8bOQzb@b!WnT5!wc26V4*ZmTULIE|vbTo9XywY4ZK zZL1a-{}0#rFk-r?@?k%l!0ZJ)W+g#kdG&^jD6`EJ2VBn_@{ZIW zbVs38t&oeRT#&g}jtiiQMGks$84DxHhMEkY8`b#tJKEtsVHxAJah@(ao?@fUsk+m> zFH2&T{W{C)g<`SqVqZJL(uEDYaaHxmaWd2Dn*avmF92KNYAPBXhd zhePAS4{czeaGcpp^A5{`8le|zvTu#JkL*42RoS$-hfM8(L1>d790fVB)%S#o#)86P zw60HibkoM>lG^maN62kDME}W32ONK}(&4+$RXS|zSmkx32~eW#PV)qI)dwBK^jpBu z_Eyr_bV7C>-~BZ?F{nOqVx(D;5PP5;=vMw(PTKO3e7QW3x7BDKgREZ>_)6!~rUAwh zzVfCUQRlF_wr|2>=%UBt92p)aqK~@2nHF#zucDV-bQ{;xh?q7-C_AhDF|f0`AxW(B z!V^Uzp_;hr25gjyg)L+O7s!WrDVzD;{Ushr)~Y0 z5dU?D{GS#7_4W3P|JO1eZ@^B01AQWLP7A`&x%?T3^69D+k$%)wEh4+BtVS=CLogQ_ z2CL?8I#sXxvk8xR7S&r(uQh?Kqs~=P2E(~Jo4EG8tAJ509pd)25Qp2 zMptF(>$*Rgjjy`2DJphrkd$XS!|0|+mDyF14?0xf=-j1^cvPYlIxg`zgZYKySP|DP zULhJ4c!h2qQ&wx)8iW5eNYTXC#lU%THIvP8u%;DUM|cdT+WNqdGf;Dr_M6V&|^VV ztF&l88|HDES$;@qbuEZ+>w!j}(y7NqMMA)RKsHSP6Gcevgur^k4x!ehM^Z~Peh)_J zgmk{1jmK$qAAJvPjzwdvj?Goi7-;)4DF>J5u~LqJ>c%F*+7umc*1lcJe8Ct)+jVJl=cK>V|I4hr^3 zQwh6a1#ULFRncxRxKV}&XDGpSICMLTADF6wcR`YQoY@UadHf0oaP%1lT#YCv8Ra6U zsKKvtwhx0IOWZBcj|zNNIvzSdnY2uJPtH+K9A_fdR5$F{t%&bjtZH9b)5sJ7)RdI{ zaZFoGVUD(xA|1wHTIX5-#{`rOwP{-t^#;ZM9#3SNInx6zYCd#Rss|DmIQa|+0W_9i z4I{6&!-f!Ra0o?e>fErjN{45wC*)7wxTyDzZmUsiZClZu&B|hmEY(69mU&-?EIxPH zAswy;KB%F9s(yb5A2cD$C|s1);9T^nwfpUlY5(~F>^}r;va}kE78C=ryZ_hg*IO_7 zznAia*MBiFAY1&uws*I;?EJ4U{$ERZyp9$e2aNkD0oM$x#Glu5xJ zKOr~ywHD^&(^in9b6YvYnzll05!;IK8Tu`VYzuwD`~PBMfVTMmZ0+p5HvRwKync!Q zxs*p!b6e!_TTpUGWe1~S&|FywP2(=%+X(zme-3#YboUNgp4G?XeGf3$QZxvfdMum? zCRTT(ytacrwB<3>udk~XJ=Th4Pw$j5aF}1^Mc&-^9Z3;$DRM+vFw)`h8F_~3Tj%q` z3y0~~MCJ~+Q1=w0mDlJCGvUWai6CSqAkFbgCoVC{Z$ zg_dOYj4&tK8OO2qY&bE?pHFI7E_vsl#b2{C_Hq_91qm9Ub1oc_7qh@s60^LR#F%Od z?PrTw;&3oOY8gBvh*@rCq9GRV5!Da0i5Jt9M+wCMEHEV;fD7}k@+-m-=1oBwnn(3S z_Ke8%Va~ra#nYoW|H_euTz}1j@iUrP7Wk({q%z_HISPd00)+zD#$@ANINRwK{>Q5a zfjPA&yWRHWQN?(`vC~EzAbHc@hUAa+?$$p!p(I}}j!!=(ACFI0p-gzf3>BCLkn>Q9 z(ZQan+BYX0Zj#!10fkBQ?3^7h&4ToIkjHV}N7Mgq zZES=3cbH+W?`lygAm_0E@9k{8*#DRD1lNB-DIi7kfy9Lg@! zZ0ArHtZ1e|2Mnhc*Qy_p>*_i!>g=g5hv@SQQmi@0Tq|L|_hg%k04Q@tSukb3sP16d zOgP~xv&7uxpBl?w9^Yg5f4l+yKh4T%ku5F({2cQC_V$bXzmzAi{tHV0-zNX>ys`4X zzj?|3yPU@<{|~Bh(7O)4q&o60M-KNn5!hONXKF(|np0`6L4+k3Y^C?G|1a z4HW@dBK>>x^>aM|`d?TA@Rt1F+i!NP{NFooUg&=*PeA@xX=SmFfU2>RMcQV{ZWhPe z8$H6|JE#I9--E`|>%6KRsd|+lIi7F6?7h>G5lf!=o0V6&D;f#dEz`-p1JGZ}tG*pX zU-fKoT*mM2`lvcCIh@}JF_7iE&a-03-B0XN>2O$Ob=?Psyyft=QonDzys`4eH3Kjh z*XXAu`BGSE-}!_RX73DeK&@9wssYXEQ^6UqSA^Zlo+gTbN`7FlhVH5F0x|Eks_aCm zRrQ;;wW<{B&C2+;wXU#;>wOLw!?o(tnxoaXY;-=x#pQs%5ysW#8hw{t)p_Iceat6A zLe)+Zft#%5eFhh2)eswuiYJp9A-f7vLd=wOqIbi-RSLJ@iJu zn^xsjN*&}|WAfw>HewRxC{KzOgks~;)o5v1->6wxcdmZezJ5Lb)r*6((~HaVeKIf? z67|6piKO*uhz5E4dHuD75tD;&4S}3Z9OeSQ1Rc z*XVi4o#&N_h{M>0)p*ZH*|%vnK!0q)U$Q>zipe2C+y`&Uz^)($O09*K66Z)O=vrl6_@aej(Mk%NqLsqTiB@97rB{>$8LrHzO0O&_Fv4<9R{bM@FvcoCaH+YQIR1wQ^OIX>ps`2g`^ zc^sY0J)B#e-xOi=!R-IHz~KE{Tw@I?H`hM2aCIGs!rgTsVJ@!&0TOmYVT2zC#^C!p zq;~$V1Jm{a9aL~XFy>g@7mQhM*&lR3cQodr4dzaZtLRsZal75$$c``{KYq}rqUU56 zi*WCQp|4XYboE^OlK}$P_rC2$SUtN!&wE>Rb#Fey8Dpn*Tx-u|EH*u}h7@pV=$54r z3u(7FE$I3!{`#Hw!u!-=NMv*kM?=*JaQy0x0Y}5J2rvY9 z98fq$#SBZ==E(uY>+>EM@?fDX?}cSq)8VMN}?0{BQIRUNN z!USkF2mRC9aimXy7}663ph-=yj!!R+&M!fRKXYr#U8>QZs$TA?hk+hb75BIvxJQ+| zYp*WI${t)M<%R&b0smb&1uapsMgF(hW4dAf&&ogiemC8@`eEhOhx4;fI%Os$8Ti@7 zMe^Uzj!#jvNUmO;9Dh2#OtxN;@gAI=emFTM4IZAss{eF+`tdo8_Fw2(3R1i$+vI>$s5N)U+?`s+r0W4 z8uc$Y+C3m;vdC~TTN&;4_F!)>`x_eV2`NN|`?{A%jHY8=`G@R!mpls_Y>fYx8UJq& z8~_LDZ zs~_ZDKvd}`1`d}ELjCD+2_k?5PF;oqRgV)HGgNorNhi2a*h#lSM*$_$zQ%OQkww-E zCO&_xTd$=JcN~u+rb5jHM>1!Q%?i5?N1t`;`CN@GEHA2G9~qpZn4G9jeFlHZN7Mgo zZ)~k^Zmw6^HJPN*`m`j>^8VpwpzijcH?Lp6ZLV-`shz|1RYr^uIqGdI-NM-DFQH zU5@ga7psW?p4G@eKnh>S^hh_eDid&%oSdtx+q6hlvyGdLUh>`6@H_bL9sYNW|Goci z?Nx}Q>>nN~3`KG8H9D_o?TMgG`uYV=xk>gduOTB7DL*KyVGY9@rG)dP$uPT4XGJ5r z%OI_8CF63^jMiR#`3$#+%pQTisnPhmWMyl-@`{U^+BXnzQMi4@W}S(vkewwNmV+7m zKw^A#aQ5lb(dlJ^u01$pz3B}b^vaiq&3&?-)T6R$vbvE~$Rd*^9VU09Y?2IT(*o{u zk+sITTzTQ&zxl_s|Nb5WK&SQSs!R!uEW`(F4*7p;XZuC|U&hn6{)_MdYu*1}Z<+D` zUvIz2|4Vti4mAP?`qz0uc7s~J3Ao|NT99s4+_wVS@wf^pHvb9YPKvj9yr*1gp%@3R8%}qLm zkFWnjWap&^E^#z6^MXI3yH{ko>E*l8Si8;qcq2iDoDL+0hHU(_Gi}~qcDP;B>4w?! z`|WO1qG)?ulNQIt-8iqs9pSJ*j{EKYSGdXlI?JkiHPJXtI~eV}^%QueN+-Vsw^SvI zX#n-evQ4sq%=;di{dzFU#%X_;UFVZLt_tT>P<1#fTtRd&*mZqB8Q6X#-J?oBC4w+< z_<1m%Hk`qDK;Z%~nl|)d>dHP12&E8oIi*Pen!WXRrR`G1(Zk{G{Htx zJP-Ct0Oam^HY`P6ZRwm#HKxPVI8p~((?Q#P)!??|YWDz@G5vjD{Ta9e>PQ_R`)sa{ z!=cQ{EHyV)XNef6RySy^#~$EXw;2^_EVac;&fT)`oOCxTeJ5i9Dum{NX%pR3t2$mD z$7A?^?5+>e@ifhECW{OI5NrS2+k5l+H5mRjw_fkQ-hHwEE#nES|I+gRk#*PCe;Y91 zMgO~u2mJraqJXQf$pK&bWIg!-3AfZe$*^*jiOK$fr48f{LxeCDm?Si!UuGOV)Vm)eqADlJfs; zZEbJw8T|jvYw-Vgq5sE!2>rk7@POx<$|gRM<8(e4rISfkB%e5{C!Yx&U6<7uiZf2~ zma&qq4MK&j4F$GnUe|rN=nNDeL}b<;&~L=+K$r$ZudQkum?Feq09As@bDLIVzA8KU z4m%xL1-0Ms=75uxGa3wEXb(u{jWCnW4GzG7{o(#=P;PX{-!595Nz{uEp zf@SxTd;;;$s&w2-uG2wwRhD0S$rzUYmAEr05Bh(p%gL3cl@{HE6F~yjmvKg*j;GCE z@iTy+UlPY>Eyl3@d7L#>K9G~=?!wg!j@fdChvn?5$dW1S6@a4HkUX}S{Sfdb#`)=Yw7{yeGi( z0jY~BJAzE#_?(izXxWQ@Ibr&7GM413D%0U0t(%Rjv?d7Oj9$sWx-@=if_|(wc+nt8y*mku^!x%ITG_(oY6To*;DYM_c=4XuP7b^?lvJuG4f z*pzBdpNgHUY#=?QBWrvTchyNem%`gB%9qJ7EAm^=!1g8ftA{+j$CnPaqieFS;9k_i?L`$g zx2IraedBXhRb|yDuWGOlFb?txmH$&B=L!~vn?xsuz>#%NMb=AxZ1AC@JXidtn0PdX zutWlOIcjUZDu?%KgM2y0*)UHrYLZ@$Cn8Am{zT=t$SqX~ z+bnyC%u;0OM3lQGRYIV{8|%HKl z^34DTTO>PVG%66*ocfZml73Tu%_hpgsa;flW;4~#CV&+~Yo(iNmhsY=GLv46pas3` zWl-c8BB>P)E9k-n_?%DsEaj~}_+1)<26ggeawd(NJUTP2Hyg<>Akptnr^$Pvgh3Oi zj)AtS=HW;fWJ}7DVoqTeD?uejmbsVrsIO!lFp7{bP(WafId6B_)rg#5FjxbIR1YlR zf+_!p4ZQWmw{LmL&UtnP7mY;zCdF`1U(>G0xoRr+ z3y-0P3z)tQF>)es^!KZhLq`Bn=gfek&(>2b#r}>S!~cI9!@rhQwm9qGJoo?hUPJtc z7yIAiKi>6UnDwtc{_oDtmU;hkXYa-Sx0FY<|B(Y0Il2>xh#Z}UGqMlS^pz+Jupl9L z_?o~4aVi>kZ?-WIeO96F-xTFliX4BKi4g1#$gE9O$T49h?${#E7UqC6$(sHhy5G-L z{iZ63{P_R?ctK#Unxpv}EwahYoULN4bQ~KW1Ni}azeXH@2OdUPGc9i+H}rS4`OHBl-2#*0AO0c zV^SO4kh}ziD1nj>K%dlQJ<}rVL%nXxgVkW;bTgY^wQSOD@bxI4PVx9q*4>&TpRe-< z&P6x*M4Pg7c$-cJWXkf%EkO*jB&eFN%AyPWyR^zi<*d&7ML9@2qv=)J3`U`i%8w45 z31I%rZiJOiX4jA-yUOOMoKMKAmxHezoBTFMtt!K8Tm5O7Lp~T~@#s-@TBL&vxmdQ2 zadaSAxL*6K~6|;u4a|Go!h-lo9+!lrOqwYS#_Ha<{YA0nYQfOI)*vfATf4r z7uYS5^4t<~cgc7TiFgi4*t^k?(-YV+P$zBC6Y!|(ydFR}ts&WM^6Q-L7IUzxxljNr z6=_{}sztlpy|t1YlgU2OxnAt?Ei_8VZ_8SK>Vx%fge#!r| zl!vgYBe&WIU6vHt3fLGwNGS&>svf$Wo6mWpI&xO0TwY)EFoZA+;C$+3>aIW(M);7v zD11?bbr61OU4c26rR>}8T=OJ&rw#~K_s|v-l2!C*V`sC3OweFf;A*dRY+DKuWI7Ix zKkc88+3oFg9)l`XJPZ;`;3Pq8cLp)Cy3rj56+|8eLz7G_wA=bo5O0Lg?`=ev`vv-R zpLqQgAV$77$H0wKfsDEE5J(U16+bgPpga5o@t}T~CyEF3$2?yc{Q~^@G~a>U+a#XZ zwU{PdJB?@f2SLyi1kSOmz!6^$Rorn1?8N_|QBVzl9Wi;oqkhcKN-Y)HgZitg%pNlN zh6XKx;LhC_K-Z+r2ZU?X9xf6HH5&)j6@aF3lRz{9AgE#v@^z|Zmb=+pY)hz})$km1 zK_Q6$AQ-Ar`eB38wA2pn`MIm{B7pV6e(@kGblEc&1CZM{SU@860uP;x7|WG^(TdrP z7JR8Qi3wxY!@-#0!@7&?A|{~bt%t|9Np{D#R?R6(2jR4af^!ysxyoOf&RqdGu9Jxh z((%0mbh`Dc@bFCs2#5qK0C>@%zT7%AD8`P`3817(9in(Ic4dP2`8Y4qil5P&Qk>ek z$vkXNU+9T40&J(#T}(H-o$?|e#-8>fKt-SWB49G$%-Tgmb@4n=JSk4n%{{{Jg zw#I*<@gLOq-&@-+{+~;E;BZ%!)8u0X+L_N;m6yZhf?m6z$)221O&EssZ=hO#iq)9U z6@q(DzbB`qVg~qJW!G7iO(5oH1nh}Ym#}?N*U`*=k&!duPkD{1iU`;#ROpXr1qZ+{ zcy|=RM)h@u_t+5XZ!1Sakp35B18nvG-rcqGe{R0S|6j^O5I=zP+Py$S73aYGg6&R+VtN7^LZS;!ZWEmtKr8PG7mGF{la2xJ8{H8@7nhWg4f}>DN2W_ zk(7${Edp=^0rR`oRXL&~Ot(;XgGl6Ldh}6$u+wy*dxCDPKCk}#COAXU|mwHYg3bMd{Nd{Bp!^}Si z|NbA(NjC7&lY?*gib@s&r0zdDM^LwRfPQZBGoV?wbwV=;nM+R<)>(ovC^j?-u-J_o@D^4P@iGxU*kZgu1f;ZkX>I5*6<%gur7vy#wUC5{gbq5;zDMkzIy^STUU;&X(I6R``C&>te9`d-A0z(nCa8bXSyPVjGQeZBKS|I2td z{b%3u8pD}7aEfw)B)?Abh8BeDA5}R{$^zVnX@<(RXdIRK58(Lp3+FsQITuQ39EHuwiN-RQec0ckRPJ1tp&n&9@iV>19?rhNg;0!myfaySBSK*>qtLx&M{aZEU zvTNo=QAcV($0G7YK){|Apx-cyn3!7(B*}Sxgsr2yCK9qBfFs&E1Qn!=JOQ?DMRT?I zeQ0`ER<$npNw>~URoJHcHM_TGlYOIkMQt>t1Bu2xb}H5S*i+gUbE&$n5EGt@|J!`M_j3MU#^YcA#RY(DJO6K+=YR5b z@8$fzl*cKv0|J2E4W(Ogn0J-}b5fsdMUe9|RBo6-S_lFyT<*8!Joh*7`00Of0YF;l z|Mi<~EB@>D?&b^qFXJK7_epk>4#;5uR-OD8Q)^`3AT&2*_{DSnfZ#`2SpRrrVmNinj7-(fcgGrNo7{NJ9Wi{dJcqT@7 zfaJr;+5Y7Z+#jf!rG?`I>E{imvj8t8E#sE=h#hbJbd=O`!dcUBaCUledA<*Q8H}<4 z*{NU~V%Tx;)6v1t2|FB;{wAvoGm@1YEd6vK@UzNp^Xv}(x++RY=K+6~lRkKI!k@!( zlEJ^CGx!5euSG$AtgVH5BT$g_NBqVfk<%Uk<9L{?jPjci{5z{4EeL^aTXpUGQb>J$ zj=z9p_-0kX==RHs6bMIS2}D8cSRf3WB@Bt)u^{A$pF(h-@qM)g<%s(HO)LZ@`8fq( zd4B;f)kv+4KWg&P19&BX3MqnUf4vELIDe@7;I_9QU}RADx(}e1JnTB3=?F(-!jsSF^taom>j;<>N0%wPWrvPcC5cQ<-loW!AnL-d; zJGwy@9>FE#ZPn(4G@P;P=i_`bgIG?Q2Vh>KCk@(hxI)~@cN9le(YS{Trs15rgdK4X ztHR8>fw4vDDYK|z8a>N`d8?Wg5!_gweIJNvYGS$hQBHQJKTR753lQ)shxj1XEs@Zu z7T=2b#n6TD)_!^2zss_(y5$7F!KZOKX+}C4;VbxdpOHh8_6eNe+yZGi5WUyJkg;nf zfa`Y4n04yU8D`oysR@2n51@j9jkTbu$oQva*ue}BF6=EeVO zDNkVi7p4Gi(f@4iZf)u7zrDG+xA&s|S;nK}|9P7j7y#SnyABWTw_6+3p`%Gga~Y>D7a&)q`o(m5jt%8TH&RvZU|rS?QcxQ#l_`YPd^1%&yZ}(Ij&4$k1-C zHmdR}0iU?FSI4IpN9UJtxn^{&NX7r(|Mh?Wza>9s_pctBWLjOl))jOV;NAJ({?Ext zKKY8#^$w`K@%=uPC6d+Cv*hs0=ab_DGNS)s4OXt?NqH5ZMy_R6M)=?<>8rhUSQ$}A z>D?KKAm3kR={Q;a@H2E1bxzQmbgYfgf-IC%IAH)sDIY9cWa&_a?dmg>u{!*J`hOya zc*RY5@-g$ht3>4bt`ZQcSjeFm-cwD7e1_!=6kSueIt;8xy6O(@ZP;38|JVOR@;RSo zMLx+A&?-sCXc*s=eWK!Gjq8tp1}4!_kIxYPa*TI}^a=Yj$?kf|ud}p3M%x2XW?I&1 zK?pUwDVsc{Qq3Lwrzq=&I=)_|S9t+uBzQ1fED?n?{rMf6K>n~4=;}oLAO9c80c1`P z7hr{hs@>l`-ru87`^P6eB2f}@N`5(de{p6oX#ty+ncy{sZt{BLbEmxW+Qj@{P>KeYJ}K_G zV|p*)firH(4EdRUs(?{Uu$p{0QE|xs%m3^D`~Uu5$nJhqzhcH^qwt^ptaar7o*bY8 zEcrk~l&;q00OFK`9W#BKKw5k97eUx-BBv9?``SyIyBzdvWTTk?NmS{Wyn|ci&~-8f zvJT9YGDMDx@cS$TYLej9a?Og(H%f9Mf}pAV$YqV6o<;Z^30W8qC4sY;64#Q0_F*z& zusB+=4U(UYE$3S6Obn_(393C;y>(F50fZCCTPJ6|vfQ-7l_^)#s&XFL((2e~aNORO z@I6)Q6jL3h>yU6hUf5vL|Gs&N|Fx7ybIs5x zbh*)pg)f5R8|BHa+p~I)gV$*Fyq$ZS*2M|;wH#(H_D4yk1$ZQ5GMU054kbMB^+~HIB?d0CEte7`3AU1gLam*zTXN<| z-N0Qq#V?oLMW7~lz$m0LC@0tX5O0K8xJQEkRJB@s-aIC9t)vXT+||NEc7M^_i|y8n z1}i2hh5+l?6e1JUb8T{S5G)IIXisR*H60!_5XZIUVu5APiP})O&PYrAZf=KsM4{9m z7UPi95J;*b?uJrTS5!tbnoLQ%gX-OSI1XqDkDiE=vxELq%QSOQtz$!2_&xw$Z4j{5 z5hI8ZADcp77x)Z^+Fwp059NBYrU7PR7H;bNo-$T|XH{FP;|tls{^_By0`Qf5A;UZm z)c4nAkk_!XZ#z>IST9KHOD@MJsYN7rbKjC|GcroADeP!se+nk{b_TqFbZl-_C(Mm8 z_3eJFE+wlA5IZPEfS{bf0YfMk_?>#GZBP(Cq5-NXa5R7)dYYo3r|1VFIxiz08L&m@ zBR2?#s+|K-F*t5LoVtD|I91e%0j9352B>9uZS#f;ia!Lj&#K~rB7#IQED>cjByqQ4 zT6#Eq{yuU69400UlE%7P0r7w;8VJU;S}QAIu(bLLCpkR2IPfB;ZNB56GL2^ zR}sl#Y0C1qa07ezS~z?`oJ}ocJv1j3XEQDQ&vc4$!mxSZQyY3bj}ia(4VeF?DG^Sd zIR5+YOZ@kxJpT1xUi|mX-Pcz9_nnvcKTCO>o*IY%{b4znF~^Xn?&Zt#SdX9nmlpqh z^Ue02<^Qqua{p^7PaqPe!3Cry5B01@?kZ=TcBy9iGQrQo$%)Gk<}#Ty&T+|s~k&q|*5)Bm#MKl0}r zEB`yB1bdM}(aSW9{o*(^xw$hZw1gyN%43AAs@QdNv-X)xBcP z&FemMqkL!(f6Lo|C8JQ+No1m6v)(i2I7&3u79M}WsI|8oe!Ebt1LITSg(vM7yeJQn zE>M`W?N;1|FM3niDWiI)K1>{ighyO|%V)GI8#61c|2&4@krD zu}JHtPYXVn;H+dDI01b*jUw@xIkA{6x86f4Na?7O3hSYZKxu%U(^b8$hPuy=BjC_Pqj|Y<2WE`@B}^7>6B$|^(bc`3*FU`3{QcJUrs(6ar3x!~msS(J z5jq0d9(?i%?;2zd;rjQgbWMnGFe~QP(cm27z)XzwR)N{mYcSux7c0QFc3n8B>7%c?&u>m0Li8eFm_ zI~k@Rg3ggiGd>T>qO1f1f3qfZD{PuVXGV5lV?uvtY$7v5F-lA_M-gStdVGeW5XM?J zcmoq>eNfwAnDc>VZej){7(3<=Ngz-=0YN6{^y3lP{PUX|xQDy?5%t!+^;d4t9z*kvXX5W69VPsU+xiMW;bV+;UAqBM z0ua^_zDAWkEBW^O?-P_MR+6Lp{{WydINI8`e2Gg7RsSqTh` zmQS$!_tTXBN%c3k{Qk1@#QLZ2sI>H5FZm8l3HR_9m4d(fbNHf`M(iegi%@(TRor_B zTQq$W=?Nt6si+bsD(Q6CXXFHejs9iT?X2DoQ4p8|CjZtQYAPJ!O_%~h{V7^Ix24jw zqe-oR&PEwBQWJiwM#y3IpPw_b?ghD{HF`9-@yZ+ue>ifmpmEw(xisozBr(&`M3?E@rD!~uMdX(Z|L@I8d8 zYlEs~UdYMm@RZaalLEl7x@p41Ty-FL+Izn63Q}vxw<>WcnnW37IVV2YqJ+3`BdQ2< zN~b23`ykE+mX+QH}L-$l+_~L|K{-j+k3tD=EeVS8IO1U7w7)h9{>N%8`J-1 z=OzF1a-MMhkA`eGcsL6LICUioiOm$Ez<Wju?> z|G_NK@t_3Rvy*}2Li2-?h{QmWec({U8x&*DK7BeOhhI=R(IG&Y4ZZ#2i)7^#{wS}L zW|SqL%euL#vWtH?5wnGQs|J#j!)%S-wu!a9` z@0s!+IrYAN@&8`R;}`2F67-w&Tfd4;`oJqy8F<{nUz6-xD44!op*6n0U8O_Ez*v>_ z^ClbD^zZ0OHS>Co=ou@2n-=-7Kbtgp5nqYsw(ZhP^n|hU9cF8q-cz^K=%I@m(=-Q7 zN0%zm0}(ay$!(brLir}FDji18pfhX{ZADk?Fm0kndtMT&N<=CUyP0?_6Lq4ON7QJ^ zbzyNB&)q4D;9JTmG#*p3tq@0@mSP&V#Fc#x7r{dN|Duk7E%(3nc6N8|_#ZF%AC~fH z<{%S4mm9p3QxLns<@fpJ+>668-nPXv76pA*07L=qHLUn?bWPxp*R>i+-t+K2b`aE z2rwb98iP@_c-RbQgGOf-YE<1oE{$JjDFiOLmzc8Y4js=Xa*~5@6X{Vf-@tsd)Q!M{ z1jn$etQsPt4IQ^-ebLY#h#_2*dYXO3wWwF6fyT*c?xfPoHZj)HX>QA zY(`mSRIIY=tkQB?C|BNMHnl-Hp5ko-3D~vQsvmv(tH~f8{4dC_$XI3*AOE8tog2sWb=0r7)x?n>iw(u|6G zzFABNFCu$FTMnPCN0YfBf^ol*=mg zVO%IZER?+pZSfN($=b3RrJpf{5U0v+R0b?lXytW(m|dq32|XsWm3=M3Dh-kQAIQsO z9CV+mAAKoITx}0G@3A#Fq4~3Uc&!{xI0{dU0;c8$v4Dx>AW_|te!_OId;tL1m5Tsa zb}R_3fUaSUSy|oqI0bb{gE zA{W>&!Um^IS5X9)LbnF1P?&hf8la&S`9{vasZse{}1iIFaCdz{{+{6K@Naz{@=Sho2LKY_V(tB{eKyc z(@*6q@M!8xT#xdp&z*%7%RoEi51!01sn7mlkz#$(pEDW)?(0=Qt?P1-(;j$AV?=_u zqNLQIELa4FK%u|Gj3+_*UyuV(i~rB|>)mbB|L4u-%l)sVJnQSpIa}`J`{d_Ic~@k^ zn=E+`cNe}-PEJ0tkkELm-1yF;yaYoBsdTm2I2+ukz$u$5GJTPqJBNKXbzU+6v=d5I zH~s1Cs>th6H^mq#|3u{I4i)psF#F~)wHS$i)u4!P-$Lk#tK}&2Xcc<667eF8CF{ur ziV7N`Vw4gwLVX<_E<%GP`(?p1y4(>kVvPw>;8RCn%CegvG| z2GM#cRS7lY`26dzN--yWZ1eSvUaKfh1*6)`J>J1()TEp-$RzvWm=HekmW(uNO<^16SQUDY{cXKq=2R<1v* z3QyPz8@#&w_E)yjBElbEa@OCdRp*n)Cx)z$YhHb+O8o#O{y5>qViB**(Z>ml4>%ec zUo#&-xvUH~FX(}x3wnCa81wTu4zMkHT%Q$u)Y!8DnjoJR!Ccd!?RXVre6+&VeZMJ7 zmBb=URx(Z(qbWJp-N;ER2RQaGz{waAsBf@2qtbQtQd9t7bj22Exo|aAamY~S5 zGe}yH6va5XCJ<*8k@&A>O)^afUrD8WB6^Srbp+mTuk-P=$Oqtm1i!LrirxwT69E5Q zq?4ph?`w&JkijI)2{VT-V${fb!;peMR_Sz<95Jm`PzILDtz`b;pM*l4XAeJEClcW8EpOW*l zUr4`>4!)3rAHJL(Tpok;Oq-BUL8zOJC#!4XE^c5{>iN(bm!?Pej?4|hJ2s{GBacpD z55~|`TZU&`&dQ!a$+8a}(c|iW&G=6k0BV|*(_(QU0Ozp(Zok=mvHvdP39bK<;=k-{ zS?f=*^!{c#2!eRVv3s+HhJWJ7hoAj=Yr$)c5x zT@qTl+hX8Fk9N?YeP}O5)kf{*#_1%zi2*2)ti5dHNLoVG7_7(RLE>T@;i55R6p{)T z6xbaurs8LN$#9XsA3vGD)h8tXEh+xb?$+xaQ~ulCeUbl`@@Seby6(%pSfy(3Racs} zONr<;;|V|DyQerEl@l#}1oM?CuOKm}_5LsA6=C^J;c7y1WyeRhleup4(J2nz-y<6g zxWhwMAMJXsgg?qF>k8pjUNxg(dat}nZ&QNXhH3jMFV*|{xB=mdKmFNp^HHeq&@@!^ zA^R-X^94%N+Zz4rJS&C~#bIClt8-~EwUMfRl=tJ}FTBwdg&D!w=W$b9-Cm#Y7Ndix z+-3zTm(GdJEHgby9bO1@E`o6M6lvQ65eC`?_Ic3bNfCfcV)NsHHnT2;e_)FBH~~~{ zZ3h)J8i((UXJmPrs0tmrH|-nPzcLo|i0nqC(#W zCkpU{>|s;5C>|G2A-lfLkU;xrGM0Om{2-&gEQh8U28pYbe3^PiXZ;AJoYZ~`Y#sDQ zHtS^k+Rv($Q3f##luUCX=b(hVdl^J~_^tAGCRjZyG~hZ(2Y+^B$a2C8X!E8iPv)P# zg)mv{M9kJod)pF&hY0mKgc@mMs4RHNeU?@=BuLMy9OJg};1avNEjGwo@(J$@@Feox zs7zE}kW|bVsKPk;1O`MPrcwJ1Zo$G1;m=7(u_z`uj!0h4tHefdF}{t;8NnJ1u2X|| zmyAflk%p|Ro_jc*LK@7uu}D;RVUZb1Y?-XGU>yf6|ruJ%jusuo$P$TMyF$}8Fd zSxLSO)MTmyrJ7VG1gc4gPQGNqf74%yI~TGlmM@_d62Y_(IHr=$4D%VKg^n-8Y6VtD zGgC;w?AQ{$QmSplf})>ATcOZ%uLVUMheOC_S%C~N{M%|=XACQp512glw9tXDf+>*1 z`}o;5O>jQ~;_%Z1iYtgQy3hkf9@ifvD^O#iZzVqI6k> zTLuh8IH_XsI7-zG)=C>p42qsl!&GiV;$K4R>@w3UkhITHsOWGO@WK-UPKXm2{t7DX zvQHh@o9->)f)z7H-ATg*MT#IjA#lV1lrdu*5NX7ir#5-KJj!F(|6YUn4|1ZH3rqk! zhyUN^?)HoS-!dNm`Y$d4aEt%n)-J&fGylu(Oa9NLJYL5bL;@m3jLG?9C~gg9-?G6h zoS@2GksJ~sFto>;#91fgt3)eg->5Ly>YJz+kLe35+unzCHq6U@Q>BBiagfyt?cfWD zq+iqGtpj9*V|qc0wzoeh;NAo(_1o7R%SNqqfl-Cp05VV_WM=;=^!gWHzCRR`Tyb)K)3k+Y!S|H^8f9Z_&>{e*4L9iz;Y*_8CHkx!s+<`Af2rsH6tdu zAav!W@2;dTrgB{tGX#B?51UcqjOnxW`|VAu{3y$BMvbS!<_|m$(6n|J*-?`-^oKJ_ zSp9L`7Fh_+NJT!$`Vg4M?=JHlvd!<_CV%=f_ncuIKUnWie`bZXttzg{lr(@Ui1tC$ z8NX58G78X_lR_(1+z5*(qH5rTgwj``o-Ts|Ga7tO#978=U(Ul><;7F3Qr+Z8>aloE>uzma{$QO&Rr14A7J>S zy$ITNjQ$2}JU-6D?x%7Dv@JeW6G1UuI7F3?`+0Q9au(D6EnG6p+UN}yM0#}u+D#|S z1teG5vGShuYF=AVJV|LnsKicuy(+OI7UdQB*AJ|xOp=E4C&-?Y)ywQ#(@QQ!=`^c* z$?-V7$?COsS}0ic=S>XQOKJsme8ugC#w-isPb|@ZGBQwVk4TiX;5QFqZ*J@{3wv7u zDn7InVlsoT)HZjEYbbB`m1CVQrzwWJXrW1Y=e}jL<=ZRSJ;td`obt6!n?;F{IbvM( z9*K8YJ`Raxt~6L5Gpqx0b-??>3{;n?cIk*D4ys>u#Fof+MAcF4c*pcs&jsbK%W779Jl$pd>DO6W z0N?Wz91$KXePaS*zV1gudU%5g3PWp7Fk?!PLqAMpRaI8~F{w+o>W4Y7h{TyjrFir) z^p+BH*oQf|p{_Z#A&PT>0p}GM>LR~W?JN)Rm8?R9rWwJX@2hNBMo2S;diS}YsC@y6 zchtx8_;<(F%dm%O$rmo$x|QmRV5%3GN8a@){A1eaX#-~=!=75f_3{KCP5-;Sf!99= zWwp5AFTb0|{=4`3_1261cNvd&{TFBcZO#9&_2!MK|KE9u|FWD1V|b3s+bn5D86?jp z^3te3EsK0`pU@~&G^!&Hlsu@?yP~Gy6<_V2TppdXEnhD{4QK~vCtp6DY6(Hq23~fD zCx5*3zc}+>tN+KQP5+ya|LKMPAO9irzdsy$2)`-aWDh|SiTKBhfgG6C$Us1tCipYC zCR0U@1stx(+?%Xs8#f!h-Z zydlCVkys)3a+MJbAJ#CuQA#*pn($n!yt@q2>Q;gYh}T}R$bLqT;3kC8_`76fYrOJ` zI@KEAz`d5b*0Na#Q5w96;UvZ5J0dZ@Iyn3E>4+R!iPX)8tT(-3gI@Xau(?mxlX^rB ze_7qgDrAw#k`9x*Q8r12Gl<5E9-}yyD=+-}H~%R7e|vNL_4?+{`qth$Ufft8RO9tQ zI-aKa4f#BO8i!Nw#uu z$#SAvM@m^v_K-@kAQB=GfdCf|$;z+a>3P3rKv9(A5QoGfW*WducTe|p&(~31>Sa`G z3LHPVEz)@$p3+Eueh>CZ*GEGF7-Ih@Zf2(#; zOF$g7X9>qF3#iE>{Ys-<(A0#3N%M&TAHg9x;3oJTGYnMl{=?NDWV>`V@`;#$}y7EEubi%u0qKnLY)bAvM#nyqKXo(f$IYcZ34q{^F zX$|sax|$&R_mWHs{=j!3cF2J(*sFf~?GcrhW$Ph;?^+3rzd03O=Ro{hrA_mIocamk z!1{3o0V^SKsNbY%3PVZ+c8Lje5{GYM;ZZ*C#8e2+IyEWQo^wLQ7Yn5v>O(rg??54! z#MfzxF>V0t@-k5#!Ni9CI*@NFgGem10g~XNdf4w)-Y~Ol%)RJvlGmMdD;=Z}-p#-Z z(b5Eb%DgWR5*s)?X4F=G(LtjqjivNyG<7I%mU+``YGmFMPlB+PZ)jAP;~4;I=mR|1 zF-$)ur|NM{B!_n^i z|5mQh{BI%+B((o~dOA|)e{^s%I@s<1Y~ymCjz-HF*2TPDDEZrSp4#X^6q~n4b(d$X zV-u<;qBp0tYkTzIWj>reifj&NH+f~g{ymzaoA+oA-hiv@h&G~*9c1kz9j&zIvK1vH z|C>ky37r2vGU`8!4o0WD{Ew|%is_{B$aX$>mss*`>I-0f2+SFopT8@WVkKI4_ zF=?w`$_n%XHw@W)Sny+2R-Mp>PheS2%ID{30^)Oce5@4Y*XbfH=h>v`mKZYMJb$wO zM43aWtsgI4{)JVfw}v6|l@Z`M9?RZYI%h-52DvL+Y`#cf=Oe!7M%LFdn?mfvSDZlF z2d16UGA$lksDd7KOp^9O^LdxyEVMqoR&{op)ev3j_q5xt#C~a7^y(JHySU{XsW82) zO5ijU>9~fbU|k>bb}_5dhYgf-u?d4>a;+~6jF3;AXp-ksC&B#%#3J{3)ipwngId4L z9(uIyFM3a#38HJolJ|m&$z@G!XsOUwG0!ZM;pitfy(f+?$vl24eeckCSl~N8&-69ZMKrMHT03a1>I#-g1D)vwG2SJ9BPUnCJZ@WqZ|I?Ee-}7>$~1F1ExH*p z7YY87TbKX#kF+TM@%!FWxDn3#JDJVLinn=@X{W65Pfh-3#$x`|r~14i?v91ZMh<4% zqdbnSAJE#mG7sZ5+jfP*l!)IGLk}x23q8ftjUgl|2N)|BFCjoN!iCfNJ+_C#gUJ0d z{)_n_-ew@JGvPNEq}i$&M_D+Pw%2!T7y@nGSPS!jZF|#3i4|U;!_1V4g2o;sPTeLO zec@5DM@T+azo%e(-}Vf{A)v>+MYancB`|zrSsEKT$L+-@TplZbWGD5>R zzs;~&O(i1Tj8-EuLB>QTHKTrqkBljgGAUIOO?q|@QfJjKogkZT@NwY}n%#~5dTR{a4j39gi zn}X#$y2IB|Lgz1idF6jYHef*hk53Ma`adVfr#t!I#)ZtR57|7OJ|<^$){{>d2>{ts z07(s-;n)d;?eMC|aiA7kXCJ8$CsgW@rIeA(rn;(@%`kzi#T71h?K_}jDuwk!x3&PS zoRfj>dlNCZA`7I6hR#yk3lkO9G*yy#L|WT4rMUk}927mJ%mv6l&J(m$^|?>;BE880 zKYK#zzyG189ncAy)LmhxC31Y6=b+oV%e;|RORV_p>hk*2$F~5!JzZqeyNNo!XFr~w z{U;fyKVDoWgFRrNy@?7)>1n7za{Sn9h2K(?| ztjx-r4J2k6O2a+mqZ*bH^VvKC?tU(3A9~W0FH2yH;$^0}%U8*Mk`{o`@ksrCvPi)e z*5Q$C!@R~cBIJyix=6TC`!(c?_CgMmB@-x`_bCoq*xT9Ted0KuHa3_a4f#Yol`+5Z9#{-+>kRQm*F54C0KWuU7G^nK5z^T* zFF_8_lNh%|>TNRH6FglCLy{WyIU76M#j z|7Uc1u-pIH#^sy;O@#nM{@>}?IRAZmvcrGh%H<5@5gwQ<(q_@7Xz5;6tFB0W$8+@_ z=s!JL^Mkg*E?^^QuPfWZ!sbMMoIv0|_> z;wX}%9r*#GJ;e&|(U?yQm^QgLhaV`zPijM2EOCNY!N^!Z@Y zTxQ$YKCuEwTHohQPIhvl=|>JI{a`e#7+NU$lOrdXw2qf#{vwlQw!DGDjCEB+Xb!Vq zoE(|`q@jrJoimpq{(;biVr@kV7VFiA7vz2c{0ZA9sne#1ZM2oz5!}%iTYodUuK61& z%S3=4SM(9>sD2N(3^d`I*58cjve5IIP!ofR+1^h>qmwMS5 zd$($pvEzF7JwgWA0fIfjs0ADd*PzC#KqHkA*)(qe122c^jY0y*#i|w6zRB7a2PdMT zMxGZ{B(0|l>@3foN#vKYq+wGE)-2=x@LK)Wv#wIai&vSd~YdwW!wE zkW}KH%o?0f5cjYqBz9q>2FIa;$hHtDiEO$Zbqo<3$FXpHPOhJ#ei}*for2 z1zBf%k9kr&r9{rgbQ3;v%iJK{p25WlmR-^2t0E3bkaS6BGq!gU*Q>0`$7R*UwQj36 zrF%F*b(57dJ3Pn}F{doML{FK_!jGCR^Ncqe3K?XQ{FB|oznb25LZ2s3&zH2B|uB-6(4#oRMxJAlV=@;Z4sfm0R-u0L^ED9Mg{7XPfVklj7 z@kzT`j%!hcQSEwJ{Sp%EU0FRK&v_p0Ux+RwJ2Ggn;iNkU3eraO-x_w9Thhsf{&rap z*WB_5VO#m`-lS+MLdKDaGzA=kea)CnlQoP)C@Q_C9&yeNkIFnt?Td|BE$b^yZl}?A zaSsLGd)O+cq9^*Wt$Xa`;!QQE$=wS&b?t%G@yXDKIN;OVfkl{Z%T`zzv&IqKbkK7_ zr+H(m4qBo?Bt^bkZl@>hr%PyRyc5GyiyBmxvuYbsk+vG7WeJ5_sPb0IMs38Ps1bTE zWe?DhH(en5b%%JD+&3sn{(?l8sH|vCm6Ik6D%?;C60l7Op`lCyWh7+_+7IVuNgXjB z!MC9qeX%dygSb_~$OEEeOU|D0j>1rwaFS%vjv=`+^LmVDXf>`%Sw|byz#_VQ@>CTCxq|xBcqLqL?&Wbta+~zHRXQ%s$)ySbyDSC<&b%i(UiGE z?xM{a#8cGuyGk!Yv%BX^&779!jNIw5s!J)`7CN#Q8bPJ4EHs3t8bQIbicm5=^p%dB^>94 zF254U_3skQW3T!?6gs@Cs})XcX-LxZuNUY4m9d4OfO3}3uzkEr*@*~CD^Xw>c2ihu z{s?$?jB?r7ldIFVg(JPlMwuYlm|y?|S0c977AK10Z0uJ8c2me2*ysdm1-<1gON&N< z(&N86Kr2Mh7g7fH|+BhPQd$0Bn7;f&vheWnOlWzW`-++H8(oB{npNV-^%{s-nZOdeJzs zbQm48^p$3s7HDcGsLKs(tSo2`ypmG6r3(|SZRr@FJRaOr) zdWbmhNa4J5yukJ5JF+f1xUMg^N##mhtiyTzk+f2p$U^Lpk1)s)4iqaaV@8W-U8an; zJl1+#xplRqVuSfY%%9PKq6OI)*;QCdXNe#y9KT5}kPy@@(u}uhSG1TV*qFV#xV%39 z_z6OmS8lzdyUQTtLngOFE|)`x;0-PE_4?H{nx?Z)$veb)7nkTC>>+5<(}q84?;U;X z{p6Q=PH@EV6eN2c`+Gc0P7YqZ|9JHihKHjYO;o?hS&E1`&aN)sf4G3>Ucv7_UR?fw z#_2CK3P0xag&2bc`3vbWP)&cAF}S4pLTnUR{!nssFieiW`P+=chw4ELLV^D{K_hT@ z{6tnjYGce-kb+=K5-*FmZy3oNyK4VZT(M-poS*$BMef&|<%|PIUVTI={O1pE&(4WQ zr+a(J+iR@g=pf+iIc<<3(G0Di_}}RiZ`Z-UUkL?@zUS?S^Xs$o0nS=u^Rl=18b*-* zCLJ*xB9&WoFmqiEwRs;EVo0yf33wVOi+=)+%N__#`opTOZYY4`Fd=wo3aRY#Dgz+S z$+V~%QfPe0PZ182qQXo9wI-EruvrN&*R*Ikz0I>?27mG;U?^pNq2GZt{1a2a^Do>` z_=y${gEuD7TqObH`d^n8Iifz>gM@9?;F^Y+d=qGM#3LpW=3Spfd3LQcN~V&|A)Qh3 z+a@LyP2j3RO#Ad%Mc>dt08;wt0bY#H_L>tMJ*)gvFrUt=HpjaI@aF^hAF$gVwGU3X zpbLs#7jle*oha3gvf7c3Vq9L5$QE ` +2. Add eth0 network: `incus config device add eth0 nic name=eth0 network=PROD-GBO` +3. Sync data: `incus file push --recursive /opt/gbo/tenants/pragmatismo// /opt/gbo/` +4. Copy binaries: from source via `lxc file pull` → scp to dest → `incus file push` +5. Create service file: `cat > /tmp/.service && incus file push .service /etc/systemd/system/` +6. Enable/start: `incus exec -- systemctl enable && systemctl start ` diff --git a/prompts/container.md b/prompts/container.md new file mode 100644 index 0000000..bdea1b8 --- /dev/null +++ b/prompts/container.md @@ -0,0 +1,1088 @@ +# Container Bootstrap Plan — Automating GB Container Deployment + +## Overview + +This document describes how to improve `installer.rs` to automate the deployment of General Bots containers on Incus. The goal is to replicate what was done manually during the pragmatismo migration from LXD to Incus. + +--- + +## What Was Done Manually (Reference Implementation) + +### Migration Summary (pragmatismo tenant) +| Item | Detail | +|------|--------| +| Source | LXD 5.21 @ 82.29.59.188 | +| Destination | Incus 6.x @ 63.141.255.9 | +| Method | `incus copy --instance-only lxd-source:` | +| Data transfer | tar.gz → push to containers | +| Containers | 10 (dns, email, webmail, alm, drive, tables, system, proxy, alm-ci, table-editor) | +| Total data | ~44 GB | + +--- + +## Container Architecture (Reference) + +### Container Types & Services + +| Container | Purpose | Ports | Service Binary | Service User | +|-----------|---------|-------|---------------|-------------| +| **dns** | CoreDNS | 53 | `/opt/gbo/bin/coredns` | root | +| **email** | Stalwart mail | 25,143,465,587,993,995,110,4190 | `/opt/gbo/bin/stalwart` | root | +| **webmail** | Roundcube/PHP | 80,443 | Apache (`/usr/sbin/apache2`) | www-data | +| **alm** | Forgejo ALM | 4747 | `/opt/gbo/bin/forgejo` | gbuser | +| **drive** | MinIO S3 | 9000,9001 | `/opt/gbo/bin/minio` | root | +| **tables** | PostgreSQL | 5432 | system-installed | root | +| **system** | botserver + stack | 5858, 8200, 6379, 6333, 9100 | `/opt/gbo/bin/botserver` | gbuser | +| **proxy** | Caddy | 80, 443 | `/usr/bin/caddy` | gbuser | +| **alm-ci** | Forgejo runner | none | `/opt/gbo/bin/forgejo-runner` | root | +| **table-editor** | NocoDB | 8080 | system-installed | root | + +**RULE: ALL services run as gbuser where possible, ALL data under /opt/gbo, Service name = container name (e.g., proxy-caddy.service)** + +### Network Layout +``` +Host (63.141.255.9) +├── Incus bridge (10.107.115.x) +│ ├── dns (10.107.115.155) +│ ├── email (10.107.115.200) +│ ├── webmail (10.107.115.87) +│ ├── alm (10.107.115.4) +│ ├── drive (10.107.115.114) +│ ├── tables (10.107.115.33) +│ ├── system (10.107.115.229) +│ ├── proxy (10.107.115.189) +│ ├── alm-ci (10.107.115.190) +│ └── table-editor (10.107.115.73) +└── iptables NAT → external ports +``` + +--- + +## Key Paths (Must Match Production) + +Inside each container: +``` +/opt/gbo/ +├── bin/ # binaries (coredns, stalwart, forgejo, caddy, minio, postgres) +├── conf/ # service configs (Corefile, config.toml, app.ini) +├── data/ # app data (zone files, databases, repos) +└── logs/ # service logs +``` + +On host: +``` +/opt/gbo/tenants// +├── dns/ +│ ├── bin/ +│ ├── conf/ +│ ├── data/ +│ └── logs/ +├── email/ +├── webmail/ +├── alm/ +├── drive/ +├── tables/ +├── system/ +├── proxy/ +├── alm-ci/ +└── table-editor/ +``` + +--- + +## Service Files (Templates) + +**RULE: ALL services run as gbuser where possible, Service name = container name (e.g., dns.service, proxy-caddy.service)** + +### dns.service (CoreDNS) +```ini +[Unit] +Description=CoreDNS +After=network.target + +[Service] +User=root +WorkingDirectory=/opt/gbo +ExecStart=/opt/gbo/bin/coredns -conf /opt/gbo/conf/Corefile +Restart=always +RestartSec=5 + +[Install] +WantedBy=multi-user.target +``` + +### email.service (Stalwart) +```ini +[Unit] +Description=Stalwart Mail Server +After=network.target + +[Service] +Type=simple +User=root +WorkingDirectory=/opt/gbo +ExecStart=/opt/gbo/bin/stalwart --config /opt/gbo/conf/config.toml +Restart=always +RestartSec=5 + +[Install] +WantedBy=multi-user.target +``` + +### proxy-caddy.service +```ini +[Unit] +Description=Caddy Reverse Proxy +After=network.target + +[Service] +User=gbuser +Group=gbuser +WorkingDirectory=/opt/gbo +ExecStart=/usr/bin/caddy run --config /opt/gbo/conf/config --adapter caddyfile +Restart=always +RestartSec=5 + +[Install] +WantedBy=multi-user.target +``` + +### alm.service (Forgejo) +```ini +[Unit] +Description=Forgejo Git Server +After=network.target + +[Service] +User=gbuser +Group=gbuser +WorkingDirectory=/opt/gbo +ExecStart=/opt/gbo/bin/forgejo web --config /opt/gbo/conf/app.ini +Restart=always +RestartSec=5 + +[Install] +WantedBy=multi-user.target +``` + +### drive-minio.service +```ini +[Unit] +Description=MinIO Object Storage +After=network-online.target +Wants=network-online.target + +[Service] +User=gbuser +Group=gbuser +WorkingDirectory=/opt/gbo +ExecStart=/opt/gbo/bin/minio server --console-address :4646 /opt/gbo/data +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-user.target +``` + +### tables-postgresql.service +```ini +[Unit] +Description=PostgreSQL +After=network.target + +[Service] +User=gbuser +Group=gbuser +WorkingDirectory=/opt/gbo +ExecStart=/opt/gbo/bin/postgres -D /opt/gbo/data -c config_file=/opt/gbo/conf/postgresql.conf +Restart=always +RestartSec=5 + +[Install] +WantedBy=multi-user.target +``` + +### webmail-apache.service +```ini +[Unit] +Description=Apache Webmail +After=network.target + +[Service] +User=www-data +Group=www-data +WorkingDirectory=/var/www/html +ExecStart=/usr/sbin/apache2 -D FOREGROUND +Restart=always +RestartSec=5 + +[Install] +WantedBy=multi-user.target +``` + +--- + +## iptables NAT Rules (CRITICAL - Use ONLY iptables, NEVER socat or Incus proxy devices) + +### Prerequisites +```bash +# Enable IP forwarding (persistent) +echo "net.ipv4.ip_forward = 1" | sudo tee /etc/sysctl.d/99-ipforward.conf +sudo sysctl -w net.ipv4.ip_forward=1 + +# Enable route_localnet for NAT to work with localhost +echo "net.ipv4.conf.all.route_localnet = 1" | sudo tee /etc/sysctl.d/99-localnet.conf +sudo sysctl -w net.ipv4.conf.all.route_localnet=1 +``` + +### Required NAT Rules (Complete Set) +```bash +# ================== +# DNS (dns container) +# ================== +sudo iptables -t nat -A PREROUTING -p udp --dport 53 -j DNAT --to-destination 10.107.115.155:53 +sudo iptables -t nat -A PREROUTING -p tcp --dport 53 -j DNAT --to-destination 10.107.115.155:53 +sudo iptables -t nat -A OUTPUT -p udp --dport 53 -j DNAT --to-destination 10.107.115.155:53 +sudo iptables -t nat -A OUTPUT -p tcp --dport 53 -j DNAT --to-destination 10.107.115.155:53 + +# ================== +# Tables (PostgreSQL) - External port 4445 +# ================== +sudo iptables -t nat -A PREROUTING -p tcp --dport 4445 -j DNAT --to-destination 10.107.115.33:5432 +sudo iptables -t nat -A OUTPUT -p tcp --dport 4445 -j DNAT --to-destination 10.107.115.33:5432 + +# ================== +# Proxy (Caddy) - 80, 443 +# ================== +sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j DNAT --to-destination 10.107.115.189:80 +sudo iptables -t nat -A PREROUTING -p tcp --dport 443 -j DNAT --to-destination 10.107.115.189:443 + +# ================== +# Email (email container) - Stalwart +# ================== +sudo iptables -t nat -A PREROUTING -p tcp --dport 25 -j DNAT --to-destination 10.107.115.200:25 +sudo iptables -t nat -A PREROUTING -p tcp --dport 465 -j DNAT --to-destination 10.107.115.200:465 +sudo iptables -t nat -A PREROUTING -p tcp --dport 587 -j DNAT --to-destination 10.107.115.200:587 +sudo iptables -t nat -A PREROUTING -p tcp --dport 993 -j DNAT --to-destination 10.107.115.200:993 +sudo iptables -t nat -A PREROUTING -p tcp --dport 995 -j DNAT --to-destination 10.107.115.200:995 +sudo iptables -t nat -A PREROUTING -p tcp --dport 143 -j DNAT --to-destination 10.107.115.200:143 +sudo iptables -t nat -A PREROUTING -p tcp --dport 110 -j DNAT --to-destination 10.107.115.200:110 +sudo iptables -t nat -A PREROUTING -p tcp --dport 4190 -j DNAT --to-destination 10.107.115.200:4190 + +# ================== +# FORWARD rules (required for containers to receive traffic) +# ================== +sudo iptables -A FORWARD -p tcp -d 10.107.115.155 --dport 53 -j ACCEPT +sudo iptables -A FORWARD -p udp -d 10.107.115.155 --dport 53 -j ACCEPT +sudo iptables -A FORWARD -p tcp -d 10.107.115.33 --dport 5432 -j ACCEPT +sudo iptables -A FORWARD -p tcp -s 10.107.115.33 --sport 5432 -j ACCEPT +sudo iptables -A FORWARD -p tcp -d 10.107.115.189 --dport 80 -j ACCEPT +sudo iptables -A FORWARD -p tcp -d 10.107.115.189 --dport 443 -j ACCEPT +sudo iptables -A FORWARD -p tcp -s 10.107.115.189 -j ACCEPT +sudo iptables -A FORWARD -p tcp -d 10.107.115.200 -j ACCEPT +sudo iptables -A FORWARD -p tcp -s 10.107.115.200 -j ACCEPT + +# ================== +# POSTROUTING MASQUERADE (for return traffic) +# ================== +sudo iptables -t nat -A POSTROUTING -p tcp -d 10.107.115.155 -j MASQUERADE +sudo iptables -t nat -A POSTROUTING -p udp -d 10.107.115.155 -j MASQUERADE +sudo iptables -t nat -A POSTROUTING -p tcp -d 10.107.115.33 -j MASQUERADE +sudo iptables -t nat -A POSTROUTING -p tcp -d 10.107.115.189 -j MASQUERADE +sudo iptables -t nat -A POSTROUTING -p tcp -d 10.107.115.200 -j MASQUERADE + +# ================== +# INPUT rules (allow incoming) +# ================== +sudo iptables -A INPUT -p tcp --dport 80 -j ACCEPT +sudo iptables -A INPUT -p tcp --dport 443 -j ACCEPT +sudo iptables -A INPUT -p tcp --dport 4445 -j ACCEPT +sudo iptables -A INPUT -p tcp --dport 53 -j ACCEPT +sudo iptables -A INPUT -p udp --dport 53 -j ACCEPT + +# ================== +# Save rules persistently +# ================== +sudo sh -c 'iptables-save > /etc/iptables/rules.v4' +``` + +### IMPORTANT RULES + +1. **NEVER use socat** - It causes port conflicts and doesn't integrate with iptables NAT +2. **NEVER use Incus proxy devices** - They conflict with iptables NAT rules +3. **ALWAYS add OUTPUT rules** - PREROUTING only handles external traffic; local traffic needs OUTPUT +4. **ALWAYS add FORWARD rules** - Without them, traffic won't reach containers +5. **ALWAYS add POSTROUTING MASQUERADE** - Without it, return traffic won't work +6. **ALWAYS set route_localnet=1** - Required for localhost NAT to work + +### Testing NAT +```bash +# Test from host +nc -zv 127.0.0.1 4445 +# Should connect to PostgreSQL at 10.107.115.33:5432 + +# Test from external +nc -zv 63.141.255.9 4445 +# Should connect to PostgreSQL at 10.107.115.33:5432 + +# Test DNS +dig @127.0.0.1 webmail.pragmatismo.com.br +# Should return 63.141.255.9 +``` + +--- + +## CoreDNS Setup + +### Corefile Template +```corefile +ddsites.com.br:53 { + file /opt/gbo/data/ddsites.com.br.zone + bind 0.0.0.0 + reload 6h + acl { + allow type ANY net 10.0.0.0/8 127.0.0.0/8 + allow type ANY net /32 + allow type A net 0.0.0.0/0 + allow type AAAA net 0.0.0.0/0 + allow type MX net 0.0.0.0/0 + allow type TXT net 0.0.0.0/0 + allow type NS net 0.0.0.0/0 + allow type SOA net 0.0.0.0/0 + allow type SRV net 0.0.0.0/0 + allow type CNAME net 0.0.0.0/0 + allow type HTTPS net 0.0.0.0/0 + allow type CAA net 0.0.0.0/0 + block + } + cache + errors +} + +pragmatismo.com.br:53 { + file /opt/gbo/data/pragmatismo.com.br.zone + bind 0.0.0.0 + reload 6h + acl { + allow type ANY net 10.0.0.0/8 127.0.0.0/8 + allow type ANY net /32 + allow type A net 0.0.0.0/0 + allow type AAAA net 0.0.0.0/0 + allow type MX net 0.0.0.0/0 + allow type TXT net 0.0.0.0/0 + allow type NS net 0.0.0.0/0 + allow type SOA net 0.0.0.0/0 + allow type SRV net 0.0.0.0/0 + allow type CNAME net 0.0.0.0/0 + allow type HTTPS net 0.0.0.0/0 + allow type CAA net 0.0.0.0/0 + block + } + cache + errors +} + +. { + forward . 8.8.8.8 1.1.1.1 + cache + errors + log +} +``` + +### Zone File Template (pragmatismo.com.br) +``` +$ORIGIN pragmatismo.com.br. +$TTL 3600 + +@ IN SOA ns1.ddsites.com.br. hostmaster.dmeans.info. ( + 2026032301 ; Serial (YYYYMMDDNN) + 86400 ; Refresh + 900 ; Retry + 1209600 ; Expire + 3600 ; Minimum TTL +) + +@ IN CAA 0 issue "letsencrypt.org" +@ IN CAA 0 issuewild ";" +@ IN CAA 0 iodef "mailto:security@pragmatismo.com.br" + +@ IN HTTPS 1 . alpn="h2,h3" + +@ IN NS ns1.ddsites.com.br. +@ IN NS ns2.ddsites.com.br. + +@ IN A + +ns1 IN A +ns2 IN A + +@ IN MX 10 mail.pragmatismo.com.br. + +mail IN A +www IN A +webmail IN A +drive IN A +drive-api IN A +alm IN A +tables IN A +gb IN A +gb6 IN A +``` + +### Starting CoreDNS in Container +```bash +# CoreDNS won't start via systemd in Incus containers by default +# Use nohup to start it +incus exec dns -- bash -c 'mkdir -p /opt/gbo/logs && nohup /opt/gbo/bin/coredns -conf /opt/gbo/conf/Corefile > /opt/gbo/logs/coredns.log 2>&1 &' +``` + +### DNS Zone Records (CRITICAL - Use A records, NOT CNAMEs for internal services) +``` +# WRONG - CNAME causes resolution issues +webmail IN CNAME mail + +# CORRECT - Direct A record +webmail IN A +mail IN A +``` + +--- + +## Container Cleanup (BEFORE Setting Up NAT) + +**ALWAYS remove socat and Incus proxy devices before configuring iptables NAT:** + +```bash +# Remove socat +pkill -9 -f socat 2>/dev/null +rm -f /usr/bin/socat /usr/sbin/socat 2>/dev/null + +# Remove all proxy devices from all containers +for c in $(incus list --format csv -c n); do + for d in $(incus config device list $c 2>/dev/null | grep -E 'proxy|port'); do + echo "Removing $d from $c" + incus config device remove $c $d 2>/dev/null + done +done +``` + +--- + +## installer.rs Improvements Required + +### 1. New Module Structure + +``` +botserver/src/core/package_manager/ +├── mod.rs +├── component.rs # ComponentConfig (existing) +├── installer.rs # PackageManager (existing) +├── container.rs # NEW: Container deployment logic +└── templates/ # NEW: Service file templates + ├── dns.service + ├── email.service + ├── alm.service + ├── minio.service + └── webmail.service +``` + +### 2. Container Settings in ComponentConfig + +```rust +// Add to component.rs + +#[derive(Debug, Clone)] +pub struct NatRule { + pub port: u16, + pub protocol: String, // "tcp" or "udp" +} + +#[derive(Debug, Clone)] +pub struct ContainerSettings { + pub container_name: String, + pub ip: String, + pub user: String, + pub group: Option, + pub working_dir: Option, + pub service_template: String, + pub nat_rules: Vec, + pub binary_path: String, // "/opt/gbo/bin/coredns" + pub config_path: String, // "/opt/gbo/conf/Corefile" + pub data_path: Option, // "/opt/gbo/data" + pub exec_cmd_args: Vec, // ["--config", "/opt/gbo/conf/Corefile"] + pub internal_ports: Vec, // Ports container listens on internally + pub external_port: Option, // External port (if different from internal) +} +``` + +### 3. Component Registration with Container Settings + +```rust +fn register_dns(&mut self) { + self.components.insert( + "dns".to_string(), + ComponentConfig { + name: "dns".to_string(), + // ... existing fields ... + + // NEW: Container settings + container: Some(ContainerSettings { + container_name: "dns".to_string(), + ip: "10.107.115.155".to_string(), + user: "root".to_string(), + group: None, + working_dir: None, + service_template: include_str!("templates/dns.service").to_string(), + nat_rules: vec![ + NatRule { port: 53, protocol: "tcp".to_string() }, + NatRule { port: 53, protocol: "udp".to_string() }, + ], + binary_path: "/opt/gbo/bin/coredns".to_string(), + config_path: "/opt/gbo/conf/Corefile".to_string(), + data_path: Some("/opt/gbo/data".to_string()), + exec_cmd_args: vec!["-conf".to_string(), "/opt/gbo/conf/Corefile".to_string()], + internal_ports: vec![53], + external_port: Some(53), + }), + }, + ); +} + +fn register_tables(&mut self) { + // PostgreSQL with external port 4445 + self.components.insert( + "tables".to_string(), + ComponentConfig { + name: "tables".to_string(), + container: Some(ContainerSettings { + container_name: "tables".to_string(), + ip: "10.107.115.33".to_string(), + user: "root".to_string(), + nat_rules: vec![ + NatRule { port: 4445, protocol: "tcp".to_string() }, + ], + internal_ports: vec![5432], + external_port: Some(4445), + // ... + }), + }, + ); +} +``` + +### 4. Container Deployment Methods + +```rust +// Add to installer.rs + +impl PackageManager { + + /// Bootstrap a container with all its services and NAT rules + pub async fn bootstrap_container( + &self, + container_name: &str, + source_lxd: Option<&str>, + ) -> Result<()> { + info!("Bootstrapping container: {}", container_name); + + // 0. CLEANUP - Remove any existing socat or proxy devices + self.cleanup_existing(container_name).await?; + + // 1. Copy from source LXD if migrating + if let Some(source_remote) = source_lxd { + self.copy_container(source_remote, container_name).await?; + } + + // 2. Ensure network is configured + self.ensure_network(container_name).await?; + + // 3. Sync data from host to container + self.sync_data_to_container(container_name).await?; + + // 4. Fix permissions + self.fix_permissions(container_name).await?; + + // 5. Install and start service + self.install_systemd_service(container_name).await?; + + // 6. Configure NAT rules on host (ONLY iptables, never socat) + self.configure_iptables_nat(container_name).await?; + + // 7. Reload DNS if dns container + if container_name == "dns" { + self.reload_dns_zones().await?; + } + + info!("Container {} bootstrapped successfully", container_name); + Ok(()) + } + + /// Cleanup existing socat and proxy devices + async fn cleanup_existing(&self, container: &str) -> Result<()> { + // Remove socat processes + SafeCommand::new("pkill") + .and_then(|c| c.arg("-9")) + .and_then(|c| c.arg("-f")) + .and_then(|c| c.arg("socat")) + .execute()?; + + // Remove proxy devices from container + let output = SafeCommand::new("incus") + .and_then(|c| c.arg("config")) + .and_then(|c| c.arg("device")) + .and_then(|c| c.arg("list")) + .and_then(|c| c.arg(container)) + .and_then(|cmd| cmd.execute_with_output())?; + + let output_str = String::from_utf8_lossy(&output.stdout); + for line in output_str.lines() { + if line.contains("proxy") || line.contains("port") { + let parts: Vec<&str> = line.split_whitespace().collect(); + if let Some(name) = parts.first() { + SafeCommand::new("incus") + .and_then(|c| c.arg("config")) + .and_then(|c| c.arg("device")) + .and_then(|c| c.arg("remove")) + .and_then(|c| c.arg(container)) + .and_then(|c| c.arg(name)) + .execute()?; + } + } + } + + Ok(()) + } + + /// Copy container from LXD source + async fn copy_container(&self, source_remote: &str, name: &str) -> Result<()> { + info!("Copying container {} from {}", name, source_remote); + + SafeCommand::new("incus") + .and_then(|c| c.arg("copy")) + .and_then(|c| c.arg("--instance-only")) + .and_then(|c| c.arg(format!("{}:{}", source_remote, name))) + .and_then(|c| c.arg(name)) + .and_then(|cmd| cmd.execute()) + .context("Failed to copy container")?; + + SafeCommand::new("incus") + .and_then(|c| c.arg("start")) + .and_then(|c| c.arg(name)) + .and_then(|cmd| cmd.execute()) + .context("Failed to start container")?; + + Ok(()) + } + + /// Add eth0 network to container + async fn ensure_network(&self, container: &str) -> Result<()> { + let output = SafeCommand::new("incus") + .and_then(|c| c.arg("config")) + .and_then(|c| c.arg("device")) + .and_then(|c| c.arg("list")) + .and_then(|c| c.arg(container)) + .and_then(|cmd| cmd.execute_with_output())?; + + let output_str = String::from_utf8_lossy(&output.stdout); + if !output_str.contains("eth0") { + SafeCommand::new("incus") + .and_then(|c| c.arg("config")) + .and_then(|c| c.arg("device")) + .and_then(|c| c.arg("add")) + .and_then(|c| c.arg(container)) + .and_then(|c| c.arg("eth0")) + .and_then(|c| c.arg("nic")) + .and_then(|c| c.arg("name=eth0")) + .and_then(|c| c.arg("network=PROD-GBO")) + .and_then(|cmd| cmd.execute())?; + } + Ok(()) + } + + /// Sync data from host to container + async fn sync_data_to_container(&self, container: &str) -> Result<()> { + let source_path = format!( + "/opt/gbo/tenants/{}/{}/", + self.tenant, container + ); + + if Path::new(&source_path).exists() { + info!("Syncing data for {}", container); + + SafeCommand::new("incus") + .and_then(|c| c.arg("exec")) + .and_then(|c| c.arg(container)) + .and_then(|c| c.arg("--")) + .and_then(|c| c.arg("mkdir")) + .and_then(|c| c.arg("-p")) + .and_then(|c| c.arg("/opt/gbo")) + .and_then(|cmd| cmd.execute())?; + + SafeCommand::new("incus") + .and_then(|c| c.arg("file")) + .and_then(|c| c.arg("push")) + .and_then(|c| c.arg("--recursive")) + .and_then(|c| c.arg(format!("{}.", source_path))) + .and_then(|c| c.arg(format!("{}:/opt/gbo/", container))) + .and_then(|cmd| cmd.execute())?; + } + Ok(()) + } + + /// Fix file permissions based on container user + async fn fix_permissions(&self, container: &str) -> Result<()> { + let settings = self.get_container_settings(container)?; + + if let Some(user) = &settings.user { + let chown_cmd = if let Some(group) = &settings.group { + format!("chown -R {}:{} /opt/gbo/", user, group) + } else { + format!("chown -R {}:{} /opt/gbo/", user, user) + }; + + SafeCommand::new("incus") + .and_then(|c| c.arg("exec")) + .and_then(|c| c.arg(container)) + .and_then(|c| c.arg("--")) + .and_then(|c| c.arg("sh")) + .and_then(|c| c.arg("-c")) + .and_then(|c| c.arg(&chown_cmd)) + .and_then(|cmd| cmd.execute())?; + } + + // Make binaries executable + SafeCommand::new("incus") + .and_then(|c| c.arg("exec")) + .and_then(|c| c.arg(container)) + .and_then(|c| c.arg("--")) + .and_then(|c| c.arg("chmod")) + .and_then(|c| c.arg("+x")) + .and_then(|c| c.arg(format!("{}/bin/*", self.base_path.display()))) + .and_then(|cmd| cmd.execute())?; + + Ok(()) + } + + /// Install systemd service file and start + async fn install_systemd_service(&self, container: &str) -> Result<()> { + let settings = self.get_container_settings(container)?; + + let service_name = format!("{}.service", container); + let temp_path = format!("/tmp/{}", service_name); + + std::fs::write(&temp_path, &settings.service_template) + .context("Failed to write service template")?; + + SafeCommand::new("incus") + .and_then(|c| c.arg("file")) + .and_then(|c| c.arg("push")) + .and_then(|c| c.arg(&temp_path)) + .and_then(|c| c.arg(format!("{}:/etc/systemd/system/{}", container, service_name))) + .and_then(|cmd| cmd.execute())?; + + for cmd_args in [ + ["daemon-reload"], + &["enable", &service_name], + &["start", &service_name], + ] { + let mut cmd = SafeCommand::new("incus") + .and_then(|c| c.arg("exec")) + .and_then(|c| c.arg(container)) + .and_then(|c| c.arg("--")) + .and_then(|c| c.arg("systemctl")); + + for arg in cmd_args { + cmd = cmd.and_then(|c| c.arg(arg)); + } + cmd.execute()?; + } + + std::fs::remove_file(&temp_path).ok(); + Ok(()) + } + + /// Configure iptables NAT rules on host - ONLY method allowed, NEVER socat + async fn configure_iptables_nat(&self, container: &str) -> Result<()> { + let settings = self.get_container_settings(container)?; + + // Set route_localnet if not already set + SafeCommand::new("sudo") + .and_then(|c| c.arg("sysctl")) + .and_then(|c| c.arg("-w")) + .and_then(|c| c.arg("net.ipv4.conf.all.route_localnet=1")) + .execute()?; + + for rule in &settings.nat_rules { + // PREROUTING rule - for external traffic + SafeCommand::new("sudo") + .and_then(|c| c.arg("iptables")) + .and_then(|c| c.arg("-t")) + .and_then(|c| c.arg("nat")) + .and_then(|c| c.arg("-A")) + .and_then(|c| c.arg("PREROUTING")) + .and_then(|c| c.arg("-p")) + .and_then(|c| c.arg(&rule.protocol)) + .and_then(|c| c.arg("--dport")) + .and_then(|c| c.arg(rule.port.to_string())) + .and_then(|c| c.arg("-j")) + .and_then(|c| c.arg("DNAT")) + .and_then(|c| c.arg("--to-destination")) + .and_then(|c| c.arg(format!("{}:{}", settings.ip, rule.port))) + .and_then(|cmd| cmd.execute())?; + + // OUTPUT rule - for local traffic (CRITICAL for NAT to work) + SafeCommand::new("sudo") + .and_then(|c| c.arg("iptables")) + .and_then(|c| c.arg("-t")) + .and_then(|c| c.arg("nat")) + .and_then(|c| c.arg("-A")) + .and_then(|c| c.arg("OUTPUT")) + .and_then(|c| c.arg("-p")) + .and_then(|c| c.arg(&rule.protocol)) + .and_then(|c| c.arg("--dport")) + .and_then(|c| c.arg(rule.port.to_string())) + .and_then(|c| c.arg("-j")) + .and_then(|c| c.arg("DNAT")) + .and_then(|c| c.arg("--to-destination")) + .and_then(|c| c.arg(format!("{}:{}", settings.ip, rule.port))) + .and_then(|cmd| cmd.execute())?; + + // FORWARD rules + SafeCommand::new("sudo") + .and_then(|c| c.arg("iptables")) + .and_then(|c| c.arg("-A")) + .and_then(|c| c.arg("FORWARD")) + .and_then(|c| c.arg("-p")) + .and_then(|c| c.arg(&rule.protocol)) + .and_then(|c| c.arg("-d")) + .and_then(|c| c.arg(&settings.ip)) + .and_then(|c| c.arg("--dport")) + .and_then(|c| c.arg(rule.port.to_string())) + .and_then(|c| c.arg("-j")) + .and_then(|c| c.arg("ACCEPT")) + .and_then(|cmd| cmd.execute())?; + } + + // POSTROUTING MASQUERADE for return traffic + SafeCommand::new("sudo") + .and_then(|c| c.arg("iptables")) + .and_then(|c| c.arg("-t")) + .and_then(|c| c.arg("nat")) + .and_then(|c| c.arg("-A")) + .and_then(|c| c.arg("POSTROUTING")) + .and_then(|c| c.arg("-p")) + .and_then(|c| c.arg("tcp")) + .and_then(|c| c.arg("-d")) + .and_then(|c| c.arg(&settings.ip)) + .and_then(|c| c.arg("-j")) + .and_then(|c| c.arg("MASQUERADE")) + .and_then(|cmd| cmd.execute())?; + + // Save rules + SafeCommand::new("sudo") + .and_then(|c| c.arg("sh")) + .and_then(|c| c.arg("-c")) + .and_then(|c| c.arg("iptables-save > /etc/iptables/rules.v4")) + .and_then(|cmd| cmd.execute())?; + + Ok(()) + } + + /// Start CoreDNS (special case - doesn't work well with systemd in Incus) + async fn start_coredns(&self, container: &str) -> Result<()> { + SafeCommand::new("incus") + .and_then(|c| c.arg("exec")) + .and_then(|c| c.arg(container)) + .and_then(|c| c.arg("--")) + .and_then(|c| c.arg("bash")) + .and_then(|c| c.arg("-c")) + .and_then(|c| c.arg("mkdir -p /opt/gbo/logs && nohup /opt/gbo/bin/coredns -conf /opt/gbo/conf/Corefile > /opt/gbo/logs/coredns.log 2>&1 &")) + .and_then(|cmd| cmd.execute())?; + + Ok(()) + } + + /// Reload DNS zones with new IPs + async fn reload_dns_zones(&self) -> Result<()> { + // Update zone files to point to new IP + SafeCommand::new("incus") + .and_then(|c| c.arg("exec")) + .and_then(|c| c.arg("dns")) + .and_then(|c| c.arg("--")) + .and_then(|c| c.arg("sh")) + .and_then(|c| c.arg("-c")) + .and_then(|c| c.arg("sed -i 's/OLD_IP/NEW_IP/g' /opt/gbo/data/*.zone")) + .and_then(|cmd| cmd.execute())?; + + // Restart coredns + self.start_coredns("dns").await?; + + Ok(()) + } + + /// Get container settings for a component + fn get_container_settings(&self, container: &str) -> Result<&ContainerSettings> { + self.components + .get(container) + .and_then(|c| c.container.as_ref()) + .context("Container settings not found") + } +} +``` + +### 5. Binary Installation (For Fresh Containers) + +```rust +/// Install binary to container from URL or fallback +async fn install_binary_to_container( + &self, + container: &str, + component: &str, +) -> Result<()> { + let config = self.components.get(component) + .context("Component not found")?; + + let binary_name = config.binary_name.as_ref() + .context("No binary name")?; + + let settings = config.container.as_ref() + .context("No container settings")?; + + // Check if already exists + let check = SafeCommand::new("incus") + .and_then(|c| c.arg("exec")) + .and_then(|c| c.arg(container)) + .and_then(|c| c.arg("--")) + .and_then(|c| c.arg("test")) + .and_then(|c| c.arg("-f")) + .and_then(|c| c.arg(&settings.binary_path)) + .and_then(|cmd| cmd.execute()); + + if check.is_ok() { + info!("Binary {} already exists in {}", binary_name, container); + return Ok(()); + } + + // Download if URL available + if let Some(url) = &config.download_url { + self.download_and_push_binary(container, url, binary_name).await?; + } + + // Make executable + SafeCommand::new("incus") + .and_then(|c| c.arg("exec")) + .and_then(|c| c.arg(container)) + .and_then(|c| c.arg("--")) + .and_then(|c| c.arg("chmod")) + .and_then(|c| c.arg("+x")) + .and_then(|c| c.arg(&settings.binary_path)) + .and_then(|cmd| cmd.execute())?; + + Ok(()) +} +``` + +--- + +## Full Bootstrap API + +```rust +/// Bootstrap an entire tenant +pub async fn bootstrap_tenant( + state: &AppState, + tenant: &str, + containers: &[&str], + source_remote: Option<&str>, +) -> Result<()> { + let pm = PackageManager::new(InstallMode::Container, Some(tenant.to_string()))?; + + for container in containers { + pm.bootstrap_container(container, source_remote).await?; + } + + info!("Tenant {} bootstrapped successfully", tenant); + Ok(()) +} + +/// Bootstrap all pragmatismo containers +pub async fn bootstrap_pragmatismo(state: &AppState) -> Result<()> { + let containers = [ + "dns", "email", "webmail", "alm", "drive", + "tables", "system", "proxy", "alm-ci", "table-editor" + ]; + + bootstrap_tenant(state, "pragmatismo", &containers, Some("lxd-source")).await +} +``` + +--- + +## Command Line Usage + +```bash +# Bootstrap single container +cargo run --bin bootstrap -- container dns --tenant pragmatismo + +# Bootstrap all containers for a tenant +cargo run --bin bootstrap -- tenant pragmatismo --source lxd-source + +# Only sync data (no copy from LXD) +cargo run --bin bootstrap -- sync-data dns --tenant pragmatismo + +# Only configure NAT +cargo run --bin bootstrap -- configure-nat --container dns + +# Only install service +cargo run --bin bootstrap -- install-service dns + +# Clean up socat and proxy devices +cargo run --bin bootstrap -- cleanup --container dns +``` + +--- + +## Files to Create/Modify + +### New Files +1. `botserver/src/core/package_manager/container.rs` - Container deployment logic +2. `botserver/src/core/package_manager/templates/dns.service` +3. `botserver/src/core/package_manager/templates/email.service` +4. `botserver/src/core/package_manager/templates/alm.service` +5. `botserver/src/core/package_manager/templates/minio.service` +6. `botserver/src/core/package_manager/templates/webmail.service` +7. `botserver/src/core/package_manager/templates/tables-postgresql.service` + +### Modified Files +1. `botserver/src/core/package_manager/component.rs` - Add ContainerSettings +2. `botserver/src/core/package_manager/installer.rs` - Add container methods, update registrations + +--- + +## Testing Checklist + +After implementation, verify: +- [ ] `incus list` shows all containers running +- [ ] `nc -zv 127.0.0.1 4445` - PostgreSQL accessible +- [ ] `dig @127.0.0.1 webmail.pragmatismo.com.br` - Returns correct IP +- [ ] `curl https://webmail.pragmatismo.com.br` - Webmail accessible +- [ ] NAT rules work from external IP +- [ ] Zone files have correct A records (not CNAMEs) +- [ ] Services survive container restart +- [ ] `which socat` returns nothing on host +- [ ] No proxy devices in any container + +--- + +## Known Issues Fixed + +1. **socat conflicts with iptables** - NEVER use socat, use ONLY iptables NAT +2. **Incus proxy devices conflict with NAT** - Remove all proxy devices before setting up NAT +3. **PREROUTING doesn't handle local traffic** - Must add OUTPUT rules +4. **CoreDNS won't start via systemd in Incus** - Use nohup instead +5. **DNS CNAME resolution issues** - Use A records for internal services +6. **route_localnet needed for localhost NAT** - Set sysctl before NAT rules +7. **FORWARD chain blocks container traffic** - Must add FORWARD ACCEPT rules +8. **Return traffic fails without MASQUERADE** - Add POSTROUTING MASQUERADE rules +9. **Binary permissions** - chmod +x after push +10. **Apache SSL needs mod_ssl enabled** - Run `a2enmod ssl` before starting Apache diff --git a/prompts/fail2ban-start.sh b/prompts/fail2ban-start.sh new file mode 100644 index 0000000..55b64bf --- /dev/null +++ b/prompts/fail2ban-start.sh @@ -0,0 +1,11 @@ +#!/bin/bash +# fail2ban startup script for Incus containers +# Usage: Place in /opt/gbo/bin/ and run as root in container + +LOGFILE=/opt/gbo/logs/fail2ban.log + +mkdir -p /opt/gbo/logs +nohup /usr/bin/fail2ban-server -x -f > $LOGFILE 2>&1 & +sleep 2 +fail2ban-client reload +echo "Fail2ban started - check status with: fail2ban-client status" \ No newline at end of file diff --git a/prompts/go.md b/prompts/go.md new file mode 100644 index 0000000..c20d520 --- /dev/null +++ b/prompts/go.md @@ -0,0 +1,15 @@ +# Production Smoke Test Plan + +## Status: Caddy running ✅ + +## URLs to Test (YOLO) +1. https://webmail.pragmatismo.com.br/ +2. https://chat.pragmatismo.com.br/cristo +3. https://alm.pragmatismo.com.br/ +4. https://tables.pragmatismo.com.br/ + +## Steps +1. Navigate to each URL +2. Take screenshot +3. Check for errors in console/network +4. Report pass/fail per URL