Cliente: Centrica Soluciones
Proyecto: Nebula ERP — Componente Nebula Vault (DataVault)
Version: 1.0
Fecha: 16 de Abril, 2026
Autor: Carlos Alberto Torres Camargo — Arquitecto de Software
Clasificacion: Uso Interno — Equipo de Desarrollo
Estado: EN VALIDACION (rama feature)
Registro detallado de cada cambio realizado durante la migracion de SFTP a Docker. Sirve como trazabilidad para auditorias, rollbacks y referencia futura.
app/clients/repository_client.py (Nuevo — ~60 lineas)Interfaz abstracta que define el contrato de operaciones sobre el repositorio OAIS.
from abc import ABC, abstractmethod
from typing import List, Dict, Optional
class RepositoryClient(ABC):
@abstractmethod
def test_access(self) -> bool: ...
@abstractmethod
def upload(self, local_path: str, subdir: str) -> str: ...
@abstractmethod
def upload_folder(self, local_folder: str, subdir: str) -> str: ...
@abstractmethod
def list_directory(self, path: str) -> List[Dict]: ...
@abstractmethod
def list_projects(self, repositorio_path: Optional[str] = None) -> List[str]: ...
@abstractmethod
def create_project_directory(self, project_name: str) -> bool: ...
@abstractmethod
def create_subfolder(self, project_name: str, subfolder_name: str) -> bool: ...
@abstractmethod
def scan_project_subfolders(self, project_name: str) -> List[str]: ...
@abstractmethod
def stat(self, path: str) -> Optional[Dict]: ...
@abstractmethod
def read_file(self, path: str, mode: str = 'r') -> bytes | str: ...
@abstractmethod
def write_file(self, path: str, content: str) -> None: ...
@abstractmethod
def remove_file(self, path: str) -> None: ...
@abstractmethod
def chmod(self, path: str, mode: int) -> None: ...
@abstractmethod
def list_dir(self, path: str) -> List[str]: ...
@abstractmethod
def exists(self, path: str) -> bool: ...
app/clients/local_storage_client.py (Nuevo — ~220 lineas)Implementacion local del RepositoryClient usando pathlib, os y shutil.
Decisiones de diseno:
test_access() verifica existencia y permisos R/W del base_pathupload() y upload_folder() usan shutil.copy2/shutil.copytree preservando metadatalist_directory() retorna estructura identica a SFTPClient: {name, is_directory, size, modified}list_projects() filtra carpetas de sistema: AIP, DIP, SIP, _ERRORES, _PROCESADOS, INGEST_SIPcreate_project_directory() crea estructura OAIS estandar: {proyecto}/SIP/, {proyecto}/AIP/, {proyecto}/DIP/app/clients/storage_factory.py (Nuevo — ~40 lineas)Factory global que instancia el cliente correcto segun STORAGE_MODE:
def get_repository_client(base_path=None) -> RepositoryClient:
mode = os.getenv("STORAGE_MODE", "local")
if mode == "local":
return LocalStorageClient(base_path or os.getenv("OAIS_REPO_PATH"))
elif mode == "sftp":
return SFTPClient(host=..., port=..., user=..., password=..., remote_base=...)
raise ValueError(f"STORAGE_MODE invalido: {mode}")
def get_ingest_client() -> RepositoryClient:
return get_repository_client(base_path=os.getenv("OAIS_INGEST_PATH"))
app/clients/docker_client.py (Nuevo — ~250 lineas)Clase OAISDockerClient para gestion de contenedores de daemons OAIS.
Metodos implementados:
| Metodo | Descripcion | Retorno |
|---|---|---|
get_container_status(daemon) |
Estado del contenedor | {is_running, status, container_id, image, created, started_at} |
start_container(daemon) |
Inicia contenedor detenido | {exito, mensaje, daemon_name} |
stop_container(daemon) |
Detiene contenedor (timeout 30s) | {exito, mensaje, daemon_name} |
restart_container(daemon) |
Reinicia contenedor | {exito, mensaje, daemon_name} |
get_container_logs(daemon, lines) |
Logs recientes con timestamps | {daemon_name, lines, logs: []} |
get_container_stats(daemon) |
Metricas de uso | {cpu_percent, memory_usage, memory_limit, memory_percent} |
get_all_daemon_statuses() |
Estado de todos los daemons | Dict con status por daemon |
Mapeo hardcoded (seguridad):
DAEMON_CONTAINERS = {
"cloud_sync_daemon_aip": "oais-cloud-sync-aip",
"cloud_sync_daemon_sip": "oais-cloud-sync-sip",
"oais_watcher": "oais-watcher",
"preservation_verifier": "oais-preservation-verifier",
}
deploy-oais-dev.sh (Nuevo — ~90 lineas)Script de despliegue para el codigo OAIS:
__pycache__, *.pyc)docker-compose.dev.yml a nodo-01oais-watcherapp/clients/sftp_client.pyCambio: Hereda de RepositoryClient. Implementa 7 metodos nuevos requeridos por la interfaz.
# ANTES
class SFTPClient:
...
# DESPUES
class SFTPClient(RepositoryClient):
# Metodos existentes intactos
# Nuevos metodos:
def stat(self, path) -> Optional[Dict]: ...
def read_file(self, path, mode='r') -> bytes | str: ...
def write_file(self, path, content) -> None: ...
def remove_file(self, path) -> None: ...
def chmod(self, path, mode) -> None: ...
def list_dir(self, path) -> List[str]: ...
def exists(self, path) -> bool: ...
# Alias de compatibilidad:
test_connection = test_access
list_projects_from_server = list_projects
app/config.pyCambio: Agregadas 4 settings de infraestructura OAIS.
# Nuevas settings (lineas 37-40)
storage_mode: str = os.getenv("STORAGE_MODE", "local")
oais_repo_path: str = os.getenv("OAIS_REPO_PATH", "/home/egs/datavault/Repositorio")
oais_ingest_path: str = os.getenv("OAIS_INGEST_PATH", "/home/egs/datavault/Repositorio/ingest_sip")
oais_status_path: str = os.getenv("OAIS_STATUS_PATH", "/home/egs/datavault/datavault/preservation_status.json")
app/models/tenant.pyCambio: Eliminadas 15 declaraciones relationship() huerfanas. Eliminados imports de relationship y ForeignKey.
# ANTES (27 lineas de imports + relationships)
from sqlalchemy.orm import relationship
class Tenant(Base):
# ... columnas ...
user_tenants = relationship("UserTenant", back_populates="tenant")
employees = relationship("Employee", back_populates="tenant")
# ... 13 mas ...
# DESPUES (limpio, solo columnas)
class Tenant(Base):
__tablename__ = "tenants"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
name = Column(String(255), nullable=False, unique=True)
# ... resto de columnas sin relationships ...
app/models/user.pyCambio: Revertida adicion incorrecta de tenant_id a UserTenant.
Contexto: durante el diagnostico se intento agregar tenant_id y tenant = relationship("Tenant") a UserTenant como solucion al error del mapper. Esto era incorrecto — la migracion fada7e94b5eb removio tenant_id intencionalmente como parte de database-per-tenant. Se revirtio a su estado original.
app/services/oais_integration_service.pyCambio: Reescritura del constructor y todas las operaciones de filesystem.
# ANTES
class OAISIntegrationService:
def __init__(self, tenant: Tenant):
config = parse_server_config(tenant.server_config)
self.sftp_client = SFTPClient(host=..., port=..., ...)
# DESPUES
class OAISIntegrationService:
def __init__(self, base_path: str = None):
self._base_path = base_path or os.getenv("OAIS_REPO_PATH")
self._client: RepositoryClient = get_repository_client(self._base_path)
Todas las operaciones sftp.* reemplazadas por self._client.* — mismo contrato, distinta implementacion.
app/api/ingest.py (~1400 lineas)Cambios:
from app.models.tenant import Tenantselect(Tenant).limit(1) muertos (resultado nunca usado)_get_sftp_client_from_tenant(tenant) por _get_storage_client() usando factorysftp_client.xxx() cambiados a storage_client.xxx()app/api/preservation.pyCambios:
select(Tenant).limit(1) muertos (en diagnose, repair, mark-optimal, verify)tenant indefinido (lineas 204, 243, 804) — cambiadas a read_preservation_status_from_server() sin argumentoread_preservation_status_from_server() ahora lee directamente del filesystem local via settings.oais_status_pathapp/api/oais_integration.pyCambios:
_get_current_tenant_record(db)select y TenantOAISIntegrationService() instanciado sin parametro tenantapp/api/document_retention.pyCambios:
select(Tenant).limit(1) muertosOAISIntegrationService() instanciado sin parametro tenantapp/api/legal_hold.pyCambios:
OAISIntegrationService() instanciado sin parametro tenantapp/api/cloud.pyCambios:
OAISDockerClient_execute_ssh_command() y import paramikodb: AsyncSession ni tenantdocker-compose.dev.ymlCambios:
# Nuevos volumenes para datavault-backend
volumes:
- oais-repositorio:/home/egs/datavault/Repositorio # Acceso al repositorio OAIS
- ./oais:/home/egs/datavault/datavault:ro # preservation_status.json
- /var/run/docker.sock:/var/run/docker.sock # Docker API para daemons
# Nuevas variables de entorno
environment:
STORAGE_MODE: local
OAIS_REPO_PATH: /home/egs/datavault/Repositorio
OAIS_INGEST_PATH: /home/egs/datavault/Repositorio/ingest_sip
OAIS_STATUS_PATH: /home/egs/datavault/datavault/preservation_status.json
requirements.txtCambio: Agregado docker==7.1.0 para Docker API.
deploy-centrica-dev.shCambio: Agregado paso de sincronizacion de docker-compose.dev.yml a nodo-01 via SCP antes del despliegue.
tenants — Actualizacion de RegistrosNo se ejecutaron migraciones Alembic. Solo se actualizaron registros existentes:
UPDATE tenants SET server_type = 'local', server_url = NULL WHERE name = 'Red Summa';
UPDATE tenants SET server_type = 'local', server_url = NULL WHERE name = 'Egs';
Nota: Los campos server_url, server_type y server_config se mantienen en el schema de la tabla. El factory get_repository_client() usa STORAGE_MODE env var (infraestructura), no tenant.server_type (BD). Los campos de BD son documentales.
| Archivo | Razon |
|---|---|
app/api/repository.py (~3000 lineas) |
Contiene 7 bloques import paramiko directos. Archivo legacy con endpoints de gestion de repositorio. Se decidio no migrar en esta fase para limitar riesgo. Documentado como deuda tecnica |
app/database.py |
Sistema de resolucion de BD por tenant via Simappe. Intacto, no tocado |
app/utils/security.py |
Autenticacion y autorizacion. Intacto |
app/dependencies.py |
Dependencias FastAPI. Intacto |
alembic/ |
Sin nuevas migraciones |
| Check | Resultado |
|---|---|
0 models con tenant_id column (23 modelos verificados) |
PASS |
0 models con tenant = relationship("Tenant") |
PASS |
20/20 pares back_populates bidireccionales consistentes |
PASS |
23/23 modelos registrados en __init__.py |
PASS |
0 import paramiko fuera de sftp_client.py (excepto repository.py) |
PASS |
0 SFTPClient( fuera de sftp_client.py y storage_factory.py |
PASS |
14/14 metodos abstractos implementados en LocalStorageClient |
PASS |
14/14 metodos abstractos implementados en SFTPClient |
PASS |
60/60 endpoints con Depends(get_current_active_user) preservado |
PASS |
database.py intacto (get_tenant_db no modificado) |
PASS |
| Bug | Archivo | Lineas | Correccion |
|---|---|---|---|
_get_current_tenant_record(db) llamada pero funcion eliminada |
oais_integration.py |
87, 124, 166 | Eliminadas las 3 llamadas muertas |
Variable tenant sin definir pasada a funcion |
preservation.py |
204, 243, 804 | Cambiadas a llamada sin argumento |
Departamento de Arquitectura — Centrica Soluciones