Files
pvemanager/backend/app/config.py
markmorado 6a79dd2048
All checks were successful
Backend Tests / pytest (Python 3.12) (push) Successful in 1m1s
Deploy to Production / Trigger production update (push) Successful in 8s
fix(appstore): DNS, per-server golden template, bind-mount perms, delete race
App Store install/update pipeline reliability fixes:
- explicit nameserver (APPSTORE_NAMESERVER) on App Store LXCs and the golden
  template build CT, instead of inheriting a node resolver that may be
  unreachable from the container network (e.g. Tailscale-only DNS)
- golden template storage/resolution moved from one global setting to
  per-server_id (migration 37: app_operations.server_id) — building it on one
  Proxmox server no longer overwrites the template configured for another
- golden template automated build fixed: vzdump export step no longer fails
  on "unable to parse directory volume name 'vztmpl/probe'"
- bind-mount host directories are pre-created with permissive ownership
  before `docker compose up`, fixing Permission denied for non-root
  containers (e.g. MariaDB) writing to Docker-auto-created root-owned dirs
- delete/teardown now waits for Proxmox to actually confirm the destroy task
  finished (was silently proceeding after a short timeout, letting a new
  install reuse the VMID while the node was still wiping the old disk)
- pct create retries a few times on transient "got lock" config errors
- failed pipeline steps now report the tail of command output (4000 chars)
  instead of the head, so the real error isn't buried under pull/progress noise
- install error box in the UI wraps long text and scrolls instead of
  stretching the dialog

Also carries forward in-progress script catalog / instances / RBAC changes
that were already uncommitted in the working tree before this session.

Bump to v1.9.1, update WIKI.md/docs/CHANGELOG accordingly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-17 04:49:40 +05:00

154 lines
8.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import os
from typing import Optional
from pydantic_settings import BaseSettings
from pydantic import Field
from pathlib import Path
def get_version() -> str:
"""Read version from VERSION file"""
# Try multiple locations for VERSION file
possible_paths = [
Path(__file__).parent.parent.parent.parent / "VERSION", # Development
Path(__file__).parent.parent / "VERSION", # Docker /app/VERSION
Path("/app/VERSION"), # Docker absolute
]
for version_file in possible_paths:
if version_file.exists():
return version_file.read_text().strip()
# Fallback only if the VERSION file is missing — keep in sync with /VERSION.
return "1.5.1"
class Settings(BaseSettings):
# Panel name - configurable but not shown in UI by default
PANEL_NAME: str = Field(default="PVEmanager", env="PANEL_NAME")
VERSION: str = Field(default_factory=get_version)
DEBUG: bool = Field(default=False, env="DEBUG")
# Database settings
DB_HOST: str = Field(default="localhost", env="DB_HOST")
DB_PORT: int = Field(default=5432, env="DB_PORT")
DB_USER: str = Field(default="pvemanager", env="DB_USER")
DB_PASSWORD: str = Field(default="pvemanager", env="DB_PASSWORD")
DB_NAME: str = Field(default="pvemanager", env="DB_NAME")
DATABASE_URL: Optional[str] = Field(default=None, env="DATABASE_URL")
# Security settings
SECRET_KEY: str = Field(default="your-secret-key-here", env="SECRET_KEY")
ALGORITHM: str = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES: int = Field(default=480, env="ACCESS_TOKEN_EXPIRE_MINUTES") # 8 hours
# Lifetime of self-service password-reset links (forgot-password flow)
PASSWORD_RESET_TOKEN_EXPIRE_MINUTES: int = Field(default=30, env="PASSWORD_RESET_TOKEN_EXPIRE_MINUTES")
# Logging settings
LOG_LEVEL: str = Field(default="INFO", env="LOG_LEVEL")
LOG_FILE: str = Field(default="logs/app.log", env="LOG_FILE")
# SSH settings
SSH_TIMEOUT: int = Field(default=10, env="SSH_TIMEOUT")
DEFAULT_SSH_USER: str = Field(default="root", env="DEFAULT_SSH_USER")
DEFAULT_SSH_PORT: int = Field(default=22, env="DEFAULT_SSH_PORT")
# Reject SSH connections to hosts with unknown/changed host keys (mitigates MITM).
# Default False preserves legacy behaviour (warn & accept); enable in production
# once your servers' host keys are in known_hosts.
SSH_REJECT_UNKNOWN_HOSTS: bool = Field(default=False, env="SSH_REJECT_UNKNOWN_HOSTS")
# Metrics history
METRICS_COLLECT_INTERVAL: int = Field(default=15, env="METRICS_COLLECT_INTERVAL")
METRICS_RETENTION_DAYS: int = Field(default=30, env="METRICS_RETENTION_DAYS")
# CORS — comma-separated list of allowed origins; "*" is insecure in production
CORS_ORIGINS: str = Field(default="*", env="CORS_ORIGINS")
# Default admin password — MUST be changed via env var in production
ADMIN_PASSWORD: str = Field(default="admin123", env="ADMIN_PASSWORD")
# Fernet key for encrypting sensitive DB fields (Proxmox passwords, cloud-init passwords).
# Generate with: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
# If not set, values are stored as plaintext (backward-compatible but insecure).
FERNET_KEY: Optional[str] = Field(default=None, env="FERNET_KEY")
# Timezone
TZ: str = Field(default="UTC", env="TZ")
# ── App Store (модуль каталога self-hosted приложений) ──────────────────
# Источник каталога. Рекомендуется фиксировать REF на конкретный tag/commit
# (митигация риска смены формата Runtipi, ТЗ п.13); дефолт master — «всегда свежий».
RUNTIPI_APPSTORE_REPO: str = Field(default="runtipi/runtipi-appstore", env="RUNTIPI_APPSTORE_REPO")
RUNTIPI_APPSTORE_REF: str = Field(default="master", env="RUNTIPI_APPSTORE_REF")
# Дополнительный источник каталога — Umbrel (getumbrel/umbrel-apps). Формат
# манифеста иной (umbrel-app.yml + docker-compose.yml), поэтому подключается
# отдельным провайдером; см. UmbrelCatalogProvider.
UMBREL_APPSTORE_REPO: str = Field(default="getumbrel/umbrel-apps", env="UMBREL_APPSTORE_REPO")
UMBREL_APPSTORE_REF: str = Field(default="master", env="UMBREL_APPSTORE_REF")
# Иконки Umbrel-приложений лежат не в umbrel-apps, а в галерее
# (<app-id>/icon.svg). Тянем их пофайлово с GitHub Pages CDN параллельно —
# это быстрее, чем качать тяжёлый tarball галереи (со скриншотами).
UMBREL_APPSTORE_GALLERY_CDN: str = Field(
default="https://getumbrel.github.io/umbrel-apps-gallery",
env="UMBREL_APPSTORE_GALLERY_CDN")
# Активные источники каталога (через запятую): "runtipi", "umbrel" или оба.
# По умолчанию только runtipi — Umbrel включается явно.
APPSTORE_SOURCES: str = Field(default="runtipi", env="APPSTORE_SOURCES")
# Каталог для кэша логотипов/данных App Store (монтируемый volume — переживает пересборку).
APPSTORE_DATA_DIR: str = Field(default="/app/data/appstore", env="APPSTORE_DATA_DIR")
# Периодичность авто-синхронизации каталога (ТЗ F-CAT-1).
CATALOG_SYNC_INTERVAL_HOURS: int = Field(default=24, env="CATALOG_SYNC_INTERVAL_HOURS")
# Архитектура целевых нод (для пометки неподдерживаемых приложений). amd64|arm64.
APPSTORE_HOST_ARCH: str = Field(default="amd64", env="APPSTORE_HOST_ARCH")
# Золотой LXC-шаблон (vztmpl) с предустановленным Docker — из него клонируются приложения.
# Напр. "local:vztmpl/golden-docker_12_amd64.tar.zst". Обязателен для установки (M2).
APPSTORE_GOLDEN_TEMPLATE: Optional[str] = Field(default=None, env="APPSTORE_GOLDEN_TEMPLATE")
# DNS для LXC, создаваемых App Store (через пробел — несколько серверов).
# Явно задаём публичный резолвер: без этого контейнер наследует DNS ноды,
# который может указывать на адрес, недоступный из сети контейнера
# (напр. Tailscale-резолвер хоста) — тогда `docker compose up` падает на
# pull образов с "i/o timeout" при резолве registry-1.docker.io.
APPSTORE_NAMESERVER: str = Field(default="1.1.1.1 8.8.8.8", env="APPSTORE_NAMESERVER")
model_config = {
"env_file": ".env",
"env_file_encoding": "utf-8",
"case_sensitive": False,
}
@property
def SQLALCHEMY_DATABASE_URI(self) -> str:
if self.DATABASE_URL:
return self.DATABASE_URL.replace("postgresql://", "postgresql+psycopg2://")
return (
f"postgresql+psycopg2://{self.DB_USER}:"
f"{self.DB_PASSWORD}@{self.DB_HOST}:"
f"{self.DB_PORT}/{self.DB_NAME}"
)
@property
def get_db_url(self) -> str:
"""Get database URL for alembic and other tools"""
return self.SQLALCHEMY_DATABASE_URI
settings = Settings()
# Timezone-aware datetime helper
from datetime import datetime, timezone
def utcnow() -> datetime:
"""Get current UTC time as timezone-aware datetime.
Use this instead of datetime.utcnow() to avoid timezone issues
with PostgreSQL columns that have timezone=True.
"""
return datetime.now(timezone.utc)
def make_tz_aware(dt: Optional[datetime]) -> Optional[datetime]:
"""Ensure datetime is timezone-aware. Defaults to UTC if naive."""
if dt is None:
return None
if dt.tzinfo is None:
return dt.replace(tzinfo=timezone.utc)
return dt