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: EJECUTADO
Migrar el backend DataVault de conectividad SSH/SFTP (Paramiko) hacia acceso local al filesystem mediante volumenes Docker compartidos, implementando el patron Facade para mantener compatibilidad con el modo SFTP como fallback.
| Principio |
Aplicacion |
| Open/Closed |
Nueva funcionalidad (local) sin modificar la existente (SFTP) |
| Dependency Inversion |
Los servicios dependen de la abstraccion RepositoryClient, no de implementaciones concretas |
| Single Responsibility |
Cada cliente (Local, SFTP, Docker) tiene una unica responsabilidad |
| Configuration over Convention |
STORAGE_MODE env var determina el comportamiento en runtime |
| Zero Breaking Changes |
El modo SFTP sigue disponible como fallback via configuracion |
develop
+-- feature/repository-client-facade (Rama 1)
+-- feature/migrate-services-to-facade (Rama 2)
+-- feature/docker-api-daemon-mgmt (Rama 3)
+-- feature/cleanup-deprecated (Rama 4)
Merge secuencial a develop tras validacion de cada rama.
| Archivo |
Descripcion |
app/clients/repository_client.py |
Interfaz abstracta RepositoryClient con 14 metodos |
app/clients/local_storage_client.py |
Implementacion local (pathlib/os/shutil) |
app/clients/storage_factory.py |
Factory global controlado por STORAGE_MODE env var |
| Archivo |
Cambio |
app/clients/sftp_client.py |
Hereda de RepositoryClient. Implementa metodos nuevos (stat, read_file, write_file, remove_file, chmod, list_dir, exists). Alias de compatibilidad (test_connection = test_access) |
app/clients/__init__.py |
Exporta nuevos componentes |
app/config.py |
Agrega settings storage_mode, oais_repo_path, oais_ingest_path, oais_status_path |
14 metodos abstractos que cubren todas las operaciones sobre el repositorio:
| Categoria |
Metodos |
| Acceso |
test_access() |
| Upload |
upload(local_path, subdir), upload_folder(local_folder, subdir) |
| Directorio |
list_directory(path), list_dir(path), exists(path) |
| Proyectos |
list_projects(), create_project_directory(name), create_subfolder(project, subfolder), scan_project_subfolders(project) |
| Archivos |
stat(path), read_file(path, mode), write_file(path, content), remove_file(path) |
| Permisos |
chmod(path, mode) |
# Comportamiento del factory
STORAGE_MODE=local → LocalStorageClient(base_path=OAIS_REPO_PATH)
STORAGE_MODE=sftp → SFTPClient(host, port, user, password, remote_base)
Variables de entorno:
| Variable |
Default |
Descripcion |
STORAGE_MODE |
local |
Modo de acceso al repositorio |
OAIS_REPO_PATH |
/home/egs/datavault/Repositorio |
Ruta raiz del repositorio |
OAIS_INGEST_PATH |
/home/egs/datavault/Repositorio/ingest_sip |
Carpeta de ingesta |
OAIS_STATUS_PATH |
/home/egs/datavault/datavault/preservation_status.json |
Estado de preservacion |
| Archivo |
Cambios Principales |
app/services/oais_integration_service.py |
Constructor recibe base_path opcional en lugar de Tenant. Usa RepositoryClient via factory. Elimina todo uso directo de Paramiko |
app/api/ingest.py |
Reemplaza SFTPClient por factory (get_ingest_client(), get_repository_client()). Elimina 6 select(Tenant) muertos. Elimina import Tenant |
app/api/preservation.py |
Reemplaza lectura SFTP de preservation_status.json por lectura local. Elimina 4 select(Tenant) muertos |
app/api/document_retention.py |
Usa OAISIntegrationService() sin parametro tenant. Elimina 5 select(Tenant) muertos |
app/api/oais_integration.py |
Elimina helper _get_current_tenant_record() y sus 4 llamadas. Instancia OAISIntegrationService() sin tenant |
app/api/legal_hold.py |
Mismos cambios que document_retention |
docker-compose.dev.yml |
Monta volumen oais-repositorio, agrega env vars de storage |
| Archivo |
Cambio |
app/models/tenant.py |
Eliminadas 15 declaraciones relationship() huerfanas. Eliminado import de relationship y ForeignKey |
app/models/user.py |
Revertida adicion incorrecta de tenant_id a UserTenant |
| Antes (Paramiko directo) |
Despues (RepositoryClient) |
sftp = sftp_client._connect() |
(eliminado — sin conexion) |
sftp.stat(path) |
self._client.stat(path) |
sftp.open(path, 'rb').read() |
self._client.read_file(path, 'rb') |
sftp.open(path, 'w').write(data) |
self._client.write_file(path, data) |
sftp.chmod(path, 0o444) |
self._client.chmod(path, 0o444) |
sftp.remove(path) |
self._client.remove_file(path) |
sftp.listdir(path) |
self._client.list_dir(path) |
try: ... finally: sftp.close() |
(simplificado — sin conexion) |
| Archivo |
Descripcion |
app/clients/docker_client.py |
Clase OAISDockerClient que gestiona contenedores de daemons via Docker API |
| Archivo |
Cambio |
app/api/cloud.py |
7 endpoints de daemon reescritos: de paramiko.SSHClient().exec_command() a OAISDockerClient |
docker-compose.dev.yml |
Monta /var/run/docker.sock para acceso a Docker Engine |
requirements.txt |
Agrega docker==7.1.0 |
| Daemon Logico |
Contenedor Docker |
Servicio Compose |
oais_watcher |
oais-watcher |
oais-watcher |
cloud_sync_daemon_aip |
oais-cloud-sync-aip |
oais-cloud-sync-aip |
cloud_sync_daemon_sip |
oais-cloud-sync-sip |
oais-cloud-sync-sip |
preservation_verifier |
oais-preservation-verifier |
oais-preservation-verifier |
| Antes (SSH) |
Despues (Docker API) |
ssh.exec_command("systemctl status watcher") |
client.containers.get("oais-watcher").status |
ssh.exec_command("systemctl start watcher") |
container.start() |
ssh.exec_command("systemctl stop watcher") |
container.stop(timeout=30) |
ssh.exec_command("journalctl -u watcher -n 50") |
container.logs(tail=50) |
| (no disponible) |
container.stats(stream=False) — CPU/memoria |
| Archivo |
Eliminado |
app/api/ingest.py |
from app.models.tenant import Tenant, helper _get_sftp_client_from_tenant() |
app/api/cloud.py |
_execute_ssh_command(), import paramiko |
app/api/oais_integration.py |
_get_current_tenant_record(), imports de select y Tenant |
app/api/preservation.py |
4 bloques select(Tenant).limit(1) muertos |
app/api/document_retention.py |
5 bloques select(Tenant).limit(1) muertos |
# Resultado esperado: 0 coincidencias
grep -rn "import paramiko" app/ --include="*.py" | grep -v sftp_client.py
grep -rn "SFTPClient(" app/ --include="*.py" | grep -v sftp_client.py | grep -v storage_factory.py
Nota: paramiko permanece en requirements.txt y sftp_client.py existe como implementacion alternativa del facade (STORAGE_MODE=sftp).
| Archivo |
Accion |
Rama |
app/clients/repository_client.py |
CREADO |
1 |
app/clients/local_storage_client.py |
CREADO |
1 |
app/clients/storage_factory.py |
CREADO |
1 |
app/clients/docker_client.py |
CREADO |
3 |
app/clients/sftp_client.py |
MODIFICADO |
1 |
app/clients/__init__.py |
MODIFICADO |
1 |
app/config.py |
MODIFICADO |
1 |
app/services/oais_integration_service.py |
REESCRITO |
2 |
app/models/tenant.py |
MODIFICADO |
2 |
app/models/user.py |
CORREGIDO |
2 |
app/api/ingest.py |
MODIFICADO |
2, 4 |
app/api/preservation.py |
MODIFICADO |
2, 4 |
app/api/cloud.py |
MODIFICADO |
2, 3 |
app/api/oais_integration.py |
MODIFICADO |
2, 4 |
app/api/legal_hold.py |
MODIFICADO |
2 |
app/api/document_retention.py |
MODIFICADO |
2, 4 |
docker-compose.dev.yml |
MODIFICADO |
2, 3 |
requirements.txt |
MODIFICADO |
3 |
deploy-centrica-dev.sh |
MODIFICADO |
— |
deploy-oais-dev.sh |
CREADO |
— |
| Riesgo |
Mitigacion |
Estado |
Path traversal en LocalStorageClient |
Paths provienen de BD, no de input de usuario |
Aceptado |
app/api/repository.py tiene 7 bloques Paramiko sin migrar |
Archivo legacy fuera del alcance. Documentado para fase posterior |
Pendiente |
Rutas hardcoded en create_project_directory |
Replica comportamiento original del SFTPClient. Alineado con OAIS_REPO_PATH |
Aceptado |
Departamento de Arquitectura — Centrica Soluciones