Add 'fiscal/' from commit '102ba84500dbd9655a0471288c3e278652593ca9'
git-subtree-dir: fiscal git-subtree-mainline:d95165fc8fgit-subtree-split:102ba84500
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
# GTI Fiscal Hub — production environment (copy to .env, never commit .env)
|
||||
GTI_FISCAL_VERSION=1.3.6
|
||||
|
||||
# REQUIRED. URL-safe secret (used inside a connection URL): openssl rand -hex 24
|
||||
POSTGRES_PASSWORD=
|
||||
|
||||
# REQUIRED. Public HTTPS URL that Agents use (TLS terminates in the reverse proxy in front of the Hub).
|
||||
BASE_URL=https://fiscal.example.gti
|
||||
|
||||
# Interface the Hub listens on. Keep 127.0.0.1 when a reverse proxy runs on the same host; use the LAN IP only behind a firewall.
|
||||
# Docker-published ports bypass UFW: restrict the source in the DOCKER-USER chain (see docs/PILOT_DEPLOYMENT.md).
|
||||
HUB_BIND=127.0.0.1
|
||||
HUB_PORT=8000
|
||||
|
||||
# IP(s) of the reverse proxy allowed to set X-Forwarded-For/-Proto (comma-separated). 127.0.0.1 = same host.
|
||||
FORWARDED_ALLOW_IPS=127.0.0.1
|
||||
# 1 once HTTPS terminates in front of the Hub (session/CSRF cookies become Secure).
|
||||
WEB_COOKIE_SECURE=0
|
||||
|
||||
# SSO from GTI Gerenciador (docs/PILOT_DEPLOYMENT.md, "Human access"). Needs HTTPS (WEB_COOKIE_SECURE=1).
|
||||
# PUBLIC key only (raw Ed25519, base64url) — the private key lives only in the panel.
|
||||
SSO_GERENCIADOR_CHAVE_PUBLICA=
|
||||
SSO_START_URL=https://gtigerenciador.example.gti/sso/fiscal
|
||||
# Collaborator session lifetime; afterwards they are signed in again transparently.
|
||||
FEDERATED_SESSION_SECONDS=3600
|
||||
|
||||
LOG_LEVEL=INFO
|
||||
AGENT_ONLINE_THRESHOLD_SECONDS=120
|
||||
|
||||
# Agent distribution (docs/PILOT_DEPLOYMENT.md, "Agent distribution"). Defaults
|
||||
# are safe; override only if the pilot needs a longer/shorter window.
|
||||
# AGENT_DOWNLOAD_LINK_TTL_SECONDS=1800
|
||||
# AGENT_DOWNLOAD_MAX_USES_DEFAULT=1
|
||||
# AGENT_ENROLLMENT_TTL_SECONDS=86400
|
||||
|
||||
# Client mirror from GTI Gerenciador (docs/PILOT_DEPLOYMENT.md, "Client integration").
|
||||
# GTI Gerenciador is the single source of truth for clients — the Fiscal Hub never
|
||||
# gets MySQL credentials. Both empty = sync disabled (no client selector data).
|
||||
GERENCIADOR_INTERNAL_URL=https://gtigerenciador.example.gti/internal/fiscal/clients
|
||||
# Dedicated bearer token, shared only with the panel's FISCAL_INTERNAL_API_TOKEN —
|
||||
# never SESSION_SECRET, the SSO key, an admin password or an Agent token.
|
||||
FISCAL_INTERNAL_API_TOKEN=
|
||||
|
||||
# Directory (host) where pg_dump backups are written by backup.sh
|
||||
BACKUP_DIR=./backups
|
||||
# Max whole-database jobs (INITIAL_SYNC / REPROCESS_ALL) running at once across all Agents; others queue as PENDING. <=0 disables.
|
||||
HEAVY_JOB_CONCURRENCY_LIMIT=3
|
||||
|
||||
# Product-enrichment providers (free plans) - fill on the SERVER only, never commit values
|
||||
OSCBR_TOKEN=
|
||||
OSCBR_USER=
|
||||
OSCBR_PASSWORD=
|
||||
KODEBAR_API_KEY=
|
||||
COSMOS_ENABLED=0
|
||||
OSCBR_BACKFILL_ENABLED=0
|
||||
PROVIDER_OPERATIONAL_RESERVE_PERCENT=20
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env sh
|
||||
# PostgreSQL + release-storage backup of the GTI Fiscal Hub (restorable with
|
||||
# restore.sh + restore_releases.sh). Run from deploy/hub.
|
||||
# ./backup.sh -> ./backups/gti_fiscal_YYYYmmdd_HHMMSS.dump (Postgres, last 14 kept)
|
||||
# -> ./backups/gti_fiscal_releases_YYYYmmdd_HHMMSS.tgz (release binaries + AgentRelease metadata is IN the dump above)
|
||||
set -eu
|
||||
COMPOSE="docker compose -f docker-compose.prod.yml --env-file .env"
|
||||
# BACKUP_DIR (like every other setting) lives in .env, but --env-file only
|
||||
# feeds the container — a plain shell script never gets it unless it loads
|
||||
# the file itself. Without this, BACKUP_DIR silently falls back to ./backups
|
||||
# here while the Hub's containers still read the real value from .env.
|
||||
if [ -f .env ]; then set -a; . ./.env; set +a; fi
|
||||
DIR="${BACKUP_DIR:-./backups}"
|
||||
mkdir -p "$DIR"
|
||||
STAMP="$(date +%Y%m%d_%H%M%S)"
|
||||
FILE="gti_fiscal_${STAMP}.dump"
|
||||
$COMPOSE exec -T postgres pg_dump -U gti -d gti_fiscal -Fc -f "/backups/${FILE}"
|
||||
[ -s "$DIR/$FILE" ] || { echo "backup empty or missing: $DIR/$FILE" >&2; exit 1; }
|
||||
# Retention runs inside the container: Docker creates the bind-mounted
|
||||
# backup directory as root, so a host-side rm would fail from the 15th dump.
|
||||
$COMPOSE exec -T postgres sh -c 'ls -1t /backups/gti_fiscal_*.dump | tail -n +15 | xargs -r rm -f'
|
||||
echo "backup ok: $DIR/$FILE"
|
||||
|
||||
# Release storage (central/app/release_storage.py: every published Agent
|
||||
# package zip/exe, never in Postgres — only its metadata is, in the dump
|
||||
# above). Streamed out of the fiscal-hub container's stdout so this works
|
||||
# regardless of the compose project/container name.
|
||||
RELEASES_FILE="gti_fiscal_releases_${STAMP}.tgz"
|
||||
$COMPOSE exec -T fiscal-hub tar -C /app/data -cz releases > "$DIR/$RELEASES_FILE"
|
||||
[ -s "$DIR/$RELEASES_FILE" ] || { echo "release backup empty or missing: $DIR/$RELEASES_FILE" >&2; exit 1; }
|
||||
find "$DIR" -maxdepth 1 -name 'gti_fiscal_releases_*.tgz' -type f -printf '%T@ %p\n' 2>/dev/null | sort -rn | tail -n +15 | cut -d' ' -f2- | xargs -r rm -f
|
||||
echo "release storage backup ok: $DIR/$RELEASES_FILE"
|
||||
@@ -0,0 +1,100 @@
|
||||
# GTI Fiscal Hub — production compose (RC1). Only two containers: PostgreSQL + Hub. No Kubernetes/Redis/RabbitMQ.
|
||||
#
|
||||
# cd deploy/hub && cp .env.example .env && (edit .env) && docker compose -f docker-compose.prod.yml --env-file .env up -d --build
|
||||
#
|
||||
# PostgreSQL is on an internal network only: it publishes NO port and is not reachable from the LAN/internet.
|
||||
# The Hub binds to HUB_BIND (default 127.0.0.1): put a TLS reverse proxy (HTTPS) in front of it for the Agents.
|
||||
# Migrations run automatically at Hub start (Dockerfile CMD: alembic upgrade head).
|
||||
name: gti-fiscal-hub
|
||||
|
||||
x-logging: &logging
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "5"
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: gti_fiscal
|
||||
POSTGRES_USER: gti
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required (URL-safe, e.g. openssl rand -hex 24)}
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
- ${BACKUP_DIR:-./backups}:/backups
|
||||
networks: [internal]
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U gti -d gti_fiscal"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 12
|
||||
logging: *logging
|
||||
|
||||
fiscal-hub:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: central/Dockerfile
|
||||
image: gti-fiscal-hub:${GTI_FISCAL_VERSION:-1.3.6}
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
APP_ENV: production
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}
|
||||
LOG_LEVEL: ${LOG_LEVEL:-INFO}
|
||||
# Product-enrichment providers (free plans). Secrets live ONLY in deploy/hub/.env on the server (never committed, never in the Agent/installer/panel/logs).
|
||||
OSCBR_TOKEN: ${OSCBR_TOKEN:-}
|
||||
OSCBR_USER: ${OSCBR_USER:-}
|
||||
OSCBR_PASSWORD: ${OSCBR_PASSWORD:-}
|
||||
KODEBAR_API_KEY: ${KODEBAR_API_KEY:-}
|
||||
COSMOS_ENABLED: ${COSMOS_ENABLED:-0}
|
||||
OSCBR_BACKFILL_ENABLED: ${OSCBR_BACKFILL_ENABLED:-0}
|
||||
PROVIDER_OPERATIONAL_RESERVE_PERCENT: ${PROVIDER_OPERATIONAL_RESERVE_PERCENT:-20}
|
||||
BASE_URL: ${BASE_URL:?BASE_URL is required (public HTTPS URL of the Hub)}
|
||||
AGENT_ONLINE_THRESHOLD_SECONDS: ${AGENT_ONLINE_THRESHOLD_SECONDS:-120}
|
||||
# Reverse proxy on another host: uvicorn only honours X-Forwarded-For/-Proto
|
||||
# from these addresses (real client IP in audit/session records, scheme=https).
|
||||
FORWARDED_ALLOW_IPS: ${FORWARDED_ALLOW_IPS:-127.0.0.1}
|
||||
# 1 = session/CSRF cookies are sent only over HTTPS. Set once TLS is in front.
|
||||
WEB_COOKIE_SECURE: ${WEB_COOKIE_SECURE:-0}
|
||||
# SSO from GTI Gerenciador (central/app/sso.py): the panel's Ed25519 PUBLIC key
|
||||
# and its /sso/fiscal URL. Both empty = SSO off (local login only).
|
||||
SSO_GERENCIADOR_CHAVE_PUBLICA: ${SSO_GERENCIADOR_CHAVE_PUBLICA:-}
|
||||
SSO_START_URL: ${SSO_START_URL:-}
|
||||
FEDERATED_SESSION_SECONDS: ${FEDERATED_SESSION_SECONDS:-3600}
|
||||
# AGENT_DEV_TOKEN is intentionally NOT set: real Agents are registered with central/scripts/register_agent.py
|
||||
# (manually) or auto-provisioned via a successful POST /v1/agents/enroll (secure enrollment).
|
||||
# Agent distribution (central/app/agent_distribution.py): the universal package zip lives on the
|
||||
# agent_releases volume below, never in PostgreSQL. TTLs have safe defaults; override only if needed.
|
||||
AGENT_RELEASE_STORAGE_PATH: /app/data/releases
|
||||
AGENT_DOWNLOAD_LINK_TTL_SECONDS: ${AGENT_DOWNLOAD_LINK_TTL_SECONDS:-1800}
|
||||
AGENT_DOWNLOAD_MAX_USES_DEFAULT: ${AGENT_DOWNLOAD_MAX_USES_DEFAULT:-1}
|
||||
AGENT_ENROLLMENT_TTL_SECONDS: ${AGENT_ENROLLMENT_TTL_SECONDS:-86400}
|
||||
# Client mirror from GTI Gerenciador (central/app/client_sync.py): the panel is
|
||||
# the single source of truth for clients. Both empty = sync disabled.
|
||||
GERENCIADOR_INTERNAL_URL: ${GERENCIADOR_INTERNAL_URL:-}
|
||||
FISCAL_INTERNAL_API_TOKEN: ${FISCAL_INTERNAL_API_TOKEN:-}
|
||||
volumes:
|
||||
- agent_releases:/app/data/releases
|
||||
ports:
|
||||
- "${HUB_BIND:-127.0.0.1}:${HUB_PORT:-8000}:8000"
|
||||
networks: [internal, edge]
|
||||
healthcheck:
|
||||
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"]
|
||||
interval: 15s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 30s
|
||||
logging: *logging
|
||||
|
||||
networks:
|
||||
internal:
|
||||
internal: true # no route to the outside: PostgreSQL is never exposed
|
||||
edge: {}
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
agent_releases:
|
||||
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env sh
|
||||
# Run on the machine that holds the validated fiscal datasets (development Hub). Exports ONLY the reference tables
|
||||
# (datasets + fiscal entries), never tenants/agents/jobs/classification data, so a fresh production Hub can be seeded:
|
||||
#
|
||||
# ./export_datasets_seed.sh projeto-postgres-1 > gti_datasets_seed.dump
|
||||
# (copy to the production host, then) ./seed_datasets.sh gti_datasets_seed.dump
|
||||
set -eu
|
||||
CONTAINER="${1:-projeto-postgres-1}"
|
||||
TABLES="datasets ncm_entries legacy_ncm_entries cest_entries cest_ncm_entries tipi_entries ba_st_mva_entries ba_st_mva_ncm_sh fiscal_code_entries cfop_entries fcp_rates anp_product_codes ncm_utrib_entries rtc_biofuel_index_entries rtc_cred_pres_entries rtc_cclasstrib_entries rtc_cst_ibscbs_entries rtc_cbs_rate_entries"
|
||||
ARGS=""
|
||||
for t in $TABLES; do ARGS="$ARGS -t $t"; done
|
||||
docker exec "$CONTAINER" pg_dump -U gti -d gti_fiscal -Fc --data-only $ARGS
|
||||
@@ -0,0 +1,20 @@
|
||||
# Re-applies the Hub port restriction (restrict_hub_port.sh) whenever Docker starts.
|
||||
# Install:
|
||||
# sudo install -m 0755 restrict_hub_port.sh /usr/local/sbin/gti-fiscal-restrict-hub-port
|
||||
# printf 'HUB_BIND=10.0.10.117\nHUB_PORT=8000\nPROXY_IP=10.0.10.100\nIFACE=ens18\n' | sudo tee /etc/default/gti-fiscal-hub-port
|
||||
# sudo install -m 0644 gti-fiscal-hub-port.service /etc/systemd/system/
|
||||
# sudo systemctl daemon-reload && sudo systemctl enable --now gti-fiscal-hub-port.service
|
||||
[Unit]
|
||||
Description=GTI Fiscal: allow the Hub port only from the reverse proxy
|
||||
After=docker.service
|
||||
Requires=docker.service
|
||||
PartOf=docker.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
EnvironmentFile=/etc/default/gti-fiscal-hub-port
|
||||
ExecStart=/usr/local/sbin/gti-fiscal-restrict-hub-port
|
||||
|
||||
[Install]
|
||||
WantedBy=docker.service
|
||||
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env sh
|
||||
# Restore a Hub backup into PostgreSQL. DESTRUCTIVE for the current data: asks for confirmation.
|
||||
# ./restore.sh ./backups/gti_fiscal_20260919_120000.dump
|
||||
set -eu
|
||||
[ $# -eq 1 ] || { echo "usage: $0 <backup.dump>" >&2; exit 2; }
|
||||
FILE="$(basename "$1")"
|
||||
COMPOSE="docker compose -f docker-compose.prod.yml --env-file .env"
|
||||
# See backup.sh: BACKUP_DIR lives in .env, which --env-file only feeds to
|
||||
# the container, not to this shell script.
|
||||
if [ -f .env ]; then set -a; . ./.env; set +a; fi
|
||||
[ -s "${BACKUP_DIR:-./backups}/$FILE" ] || { echo "backup not found in ${BACKUP_DIR:-./backups}: $FILE" >&2; exit 1; }
|
||||
printf "This REPLACES the current Hub database with %s. Type RESTORE to continue: " "$FILE"
|
||||
read -r answer
|
||||
[ "$answer" = "RESTORE" ] || { echo "aborted"; exit 1; }
|
||||
$COMPOSE stop fiscal-hub
|
||||
$COMPOSE exec -T postgres pg_restore -U gti -d gti_fiscal --clean --if-exists --no-owner "/backups/$FILE"
|
||||
$COMPOSE up -d fiscal-hub # migrations run at start (alembic upgrade head)
|
||||
echo "restore ok: $FILE"
|
||||
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env sh
|
||||
# Restore the release-storage volume (backup.sh's *.tgz) into the Hub.
|
||||
# DESTRUCTIVE for the current release binaries: asks for confirmation. The
|
||||
# AgentRelease metadata (version/sha256/status) lives in Postgres, not here —
|
||||
# restore that separately with restore.sh from a dump taken around the same time.
|
||||
# ./restore_releases.sh ./backups/gti_fiscal_releases_20260923_224551.tgz
|
||||
set -eu
|
||||
[ $# -eq 1 ] || { echo "usage: $0 <releases_backup.tgz>" >&2; exit 2; }
|
||||
FILE="$(basename "$1")"
|
||||
COMPOSE="docker compose -f docker-compose.prod.yml --env-file .env"
|
||||
# See backup.sh: BACKUP_DIR lives in .env, which --env-file only feeds to
|
||||
# the container, not to this shell script.
|
||||
if [ -f .env ]; then set -a; . ./.env; set +a; fi
|
||||
[ -s "${BACKUP_DIR:-./backups}/$FILE" ] || { echo "backup not found in ${BACKUP_DIR:-./backups}: $FILE" >&2; exit 1; }
|
||||
printf "This REPLACES the current release-storage contents with %s. Type RESTORE to continue: " "$FILE"
|
||||
read -r answer
|
||||
[ "$answer" = "RESTORE" ] || { echo "aborted"; exit 1; }
|
||||
$COMPOSE exec -T fiscal-hub sh -c 'rm -rf /app/data/releases && mkdir -p /app/data/releases'
|
||||
$COMPOSE exec -T fiscal-hub tar -C /app/data -xz < "${BACKUP_DIR:-./backups}/$FILE"
|
||||
echo "restore ok: $FILE"
|
||||
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env sh
|
||||
# Restrict the published Hub port so that only the reverse proxy host reaches it.
|
||||
#
|
||||
# HUB_BIND=10.0.10.117 HUB_PORT=8000 PROXY_IP=10.0.10.100 IFACE=ens18 sh restrict_hub_port.sh
|
||||
#
|
||||
# Docker-published ports never pass through UFW/INPUT: the packets are DNATed
|
||||
# and forwarded, so the only supported place for this rule is DOCKER-USER.
|
||||
# Matching on the conntrack *original* destination selects this published port
|
||||
# (after DNAT the packet already points at the container); --ctdir ORIGINAL keeps
|
||||
# the container's replies out of the DROP rule. Idempotent: safe to re-run.
|
||||
# DOCKER-USER is not persisted across reboots — gti-fiscal-hub-port.service
|
||||
# re-applies it every time docker.service starts.
|
||||
set -eu
|
||||
: "${HUB_BIND:?HUB_BIND is required}" "${PROXY_IP:?PROXY_IP is required}" "${IFACE:?IFACE is required}"
|
||||
HUB_PORT="${HUB_PORT:-8000}"
|
||||
MATCH="-i $IFACE -p tcp -m conntrack --ctdir ORIGINAL --ctorigdst $HUB_BIND --ctorigdstport $HUB_PORT"
|
||||
|
||||
iptables -N DOCKER-USER 2>/dev/null || true
|
||||
while iptables -D DOCKER-USER $MATCH -s "$PROXY_IP" -j RETURN 2>/dev/null; do :; done
|
||||
while iptables -D DOCKER-USER $MATCH -j DROP 2>/dev/null; do :; done
|
||||
iptables -I DOCKER-USER 1 $MATCH -j DROP
|
||||
iptables -I DOCKER-USER 1 $MATCH -s "$PROXY_IP" -j RETURN
|
||||
iptables -S DOCKER-USER
|
||||
@@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env sh
|
||||
# Seed a NEW production Hub (already migrated: the Hub container ran `alembic upgrade head`) with the validated fiscal datasets.
|
||||
# ./seed_datasets.sh gti_datasets_seed.dump
|
||||
set -eu
|
||||
[ $# -eq 1 ] || { echo "usage: $0 <gti_datasets_seed.dump>" >&2; exit 2; }
|
||||
COMPOSE="docker compose -f docker-compose.prod.yml --env-file .env"
|
||||
EXISTING="$($COMPOSE exec -T postgres psql -U gti -d gti_fiscal -At -c 'select count(*) from datasets')"
|
||||
[ "$EXISTING" = "0" ] || { echo "refusing: datasets table is not empty ($EXISTING rows)" >&2; exit 1; }
|
||||
$COMPOSE exec -T postgres pg_restore -U gti -d gti_fiscal --data-only --no-owner --disable-triggers < "$1"
|
||||
$COMPOSE exec -T postgres psql -U gti -d gti_fiscal -At -c "select kind, status, record_count from datasets where status='ACTIVE' order by kind"
|
||||
Reference in New Issue
Block a user