Volver a la wiki

Agente · dev-rack-editor

Agente · dev-rack-editor

Propósito

Desarrollo y mantenimiento del Rack Editor de CreaRack-Pro. Cubre la app racks/, los 20 módulos JS del editor, el canvas Konva.js, el sistema de stencils, SSH integrado y backups de configuración.


Estructura de archivos

Frontend: static/js/editor/ (20 archivos, ~7.500 LOC)

ArchivoLOCResponsabilidad
main.js~363Bootstrap: carga datos del DOM, inicializa Konva, orquesta módulos
state.js~55Objeto STATE global, constantes de grid, helpers de posición
konva_setup.js~189Stage/Layer Konva.js, zoom, pan, frame del rack
devices.js~432Creación, drag-drop, snap, colisión de dispositivos en canvas
history_adapter.js~172Undo/Redo via StateManager — snapshots inmutables
editor_api.js~86Auto-save debounce 500ms, serialización hacia backend
ui_library.js~272Panel de stencils: búsqueda, carpetas, drag al canvas
PropertiesPanelRenderer.js~563HTML del panel de propiedades del dispositivo seleccionado
PropertiesPanelEvents.js~925Eventos del panel: nombre, puertos, acciones (SSH, Backups, QR…)
auto_provision_panel.js~239Panel flotante de dispositivos pendientes de Auto-Provision
ssh_integration.js~411Terminal SSH embebido, sesiones, integración SFTP
SshConfigModal.js~254Formulario SSH: IP, puerto, credenciales, vendor autodetect
import_export.js~506Importación Visio (.vssx/.vdx/.vss), categorías de stencils
backups.js~623Snapshots de configuración de red, diff viewer, restore
sftp_ui.js~340Navegador de archivos SFTP via Local Agent
ConnectionModal.js—Modal de conexiones inter-dispositivo (cables, puertos)
DeviceInfoModal.js—Modal de información detallada del dispositivo
PortConfigDiff.js—Visor de diferencias de configuración de puertos
modal_helper.js—Utilidades compartidas para modales del editor

Backend: racks/

racks/
├── models.py              # BoxCategory, RackGroup, Stencil, Rack, Device, ConfigBackup
├── views.py               # rack_editor (canvas), index (lista racks)
├── schemas.py             # Pydantic schemas
└── api/
    ├── racks.py           # CRUD racks + dispositivos (~360 LOC)
    ├── library.py         # Stencils + Visio import (~745 LOC)
    ├── export.py          # CSV/JSON/PDF export (~1023 LOC)
    ├── labels.py          # QR labels API
    └── trash.py           # Papelera racks

Estado global (state.js)

export const STATE = {
    rackData: null,         // Datos del rack cargados del DOM (#rack-data)
    devices: [],            // Array de dispositivos con nodos Konva
    selectedNode: null,     // Nodo Konva actualmente seleccionado
    isLocked: false,        // Bloqueo de interacción del canvas
    currentDragItem: null,  // Item arrastrándose desde la librería
    stage: null,            // Konva.Stage
    layer: null,            // Konva.Layer principal
    rackGroup: null,        // Konva.Group del frame del rack
    pos: { x: 50, y: 50 }, // Posición del rack en el canvas
    CONFIG: {
        U_HEIGHT: 35,       // Pixeles por unidad de rack (U)
        RACK_WIDTH: 400,    // Ancho fijo del rack en pixeles
        RACK_HEIGHT: 0      // Calculado: height_u × U_HEIGHT
    }
};

export const SLOT = { FULL: 'full', LEFT: 'left', RIGHT: 'right' };
export const HALF_WIDTH = 195;  // (RACK_WIDTH - 10) / 2

Estructura de un dispositivo en STATE.devices[]

{
    type: "switch",
    originalName: "Cisco 2960",
    u_height: 1,
    u_position: 15,          // Posición U desde abajo
    notes: "",
    image: "/media/uploads/...",
    db_id: 42,               // null si es nuevo (no guardado aún)
    half_width: false,
    slot: "full",            // "full" | "left" | "right"
    ports: {
        "eth0": { live: "up", label: "WAN", desired: "trunk" },  // formato enriquecido
        "sfp1": "10G"        // formato legacy (string)
    },
    management_config: { ip: "192.168.1.1", vendor: "cisco" },
    meta: { ports: {...}, notes: "...", half_width: false },
    konvaNode: Konva.Group   // Referencia al nodo visual
}

Konva.js — estructura visual

Konva.Stage (draggable, zoom con wheel)
└── Konva.Layer
    ├── STATE.rackGroup (Konva.Group)
    │   ├── Rect (fondo del rack)
    │   ├── Lines (separadores de U)
    │   └── Text (números de U: 1, 2, 3...)
    ├── DeviceGroup_1 (Konva.Group, draggable)
    │   ├── Rect (fondo), Image (stencil), Text (nombre)
    ├── DeviceGroup_2 ...
    └── DeviceGroup_N ...

Conversión coordenadas ↔ posición U

// U → Y pixel
y = STATE.pos.y + (RACK_HEIGHT - u_position × U_HEIGHT)

// Y pixel → U (al hacer drop/drag-end)
uPosition = (RACK_HEIGHT - (yRelative + deviceHeight)) / U_HEIGHT + 1

Snap y colisiones (devices.js)

Snap (dragBoundFunc)

Detección de colisiones

FunciónDescripción
checkCollisionU(targetU, uHeight, ignoreNode)Solapamiento vertical (1D)
checkCollision(targetU, uHeight, slot, ignoreNode)Vertical + horizontal (slot). FULL bloquea cualquier posición. LEFT y RIGHT coexisten si no se solapan verticalmente

Flujo drag-end

dragend →
  1. Validar dentro de límites del rack
  2. Calcular u_position desde Y pixel
  3. checkCollision(targetU, uHeight, slot)
  4. Si colisión → revertir posición anterior
  5. Si válido → actualizar device.u_position y device.slot
  6. Disparar auto-save

Undo/Redo (history_adapter.js)

Usuario edita → saveState() → createSnapshot() → STATE_MANAGER.push(snapshot)
Usuario Ctrl+Z → STATE_MANAGER.undo() → restoreState(snapshot)
                                              → limpia STATE.devices
                                              → re-crea nodos Konva
                                              → restaura selección
                                              → dispara auto-save

Auto-save (editor_api.js)

Debounce 500ms. Serializa STATE.devices y envía:

PUT /api/racks/{rackId}/devices
Body: { devices: [{ db_id, type, name, u_position, u_height, notes,
                    image, half_width, slot, ports, management_config }] }

Panel de propiedades

Secciones (PropertiesPanelRenderer.js)

  1. Cabecera: nombre editable, tipo
  2. Imagen: preview stencil
  3. Posición: U position (lectura) + altura en U
  4. Management Config: IP, vendor, hostname
  5. Puertos Ethernet: grid hasta 48 puertos (formato enriquecido o legacy)
  6. Puertos SFP: grid hasta 8 puertos (layout 2 columnas)
  7. Notas: textarea editable
  8. Acciones: Delete, Clone, Save to Library, QR, SSH, Backups

Formato de puertos

// Enriquecido (preferido)
{ "eth0": { "live": "up", "label": "WAN", "desired": "trunk" } }

// Legacy (string simple)
{ "eth0": "connected" }

El renderer detecta automáticamente el formato.

Eventos clave (PropertiesPanelEvents.js)

EventoAcción
Input nombreActualiza konvaNode.findOne('Text') + debounce auto-save
Input puertoActualiza device.ports[key] + auto-save
Blur cualquier campodispatchEvent('request-save-history') → snapshot Undo
Botón CloneDuplica en siguiente U disponible
Botón Save to LibraryPOST /api/racks/stencils/create_from_device
Botón SSHAbre SshConfigModal → ssh_integration.openSSHSession()
Botón BackupsAbre modal de snapshots de configuración

Sistema de stencils (ui_library.js)

Estructura de un stencil

{
    id: 123,
    name: "Cisco Catalyst 2960",
    image_path: "/media/uploads/stencils/cisco_2960.png",
    default_u_height: 1,
    manufacturer: "Cisco",
    category: "Switches",
    extra_data: '{"half_width": false, "ports": {...}}'
}

Drag desde librería al canvas

  1. Stencil con data-id, data-name, data-type, data-height, data-image
  2. dragstart → metadata en STATE.currentDragItem
  3. drop sobre canvas → calcula U desde coordenadas del mouse
  4. addDeviceToRack() con datos del stencil
  5. STATE.currentDragItem = null

Importación Visio (pipeline 2 pasos)

  1. POST /api/racks/visio/analyze (FormData .vssx/.vdx/.vss) → session_id + masters encontrados
  2. POST /api/racks/visio/confirm { session_id, masters, category_name } → crea stencils
  3. DELETE /api/racks/cleanup/session/{id} → limpia archivos temporales

Auto-Provision Panel (auto_provision_panel.js)

Lee localStorage['auto_provision_pending'] al cargar el editor:

// Formato v2 (actual)
{
    version: 2,
    racks: {
        "rack_42": [
            { name: "SW-Core-01", type: "switch", stencil_id: 123,
              management_config: { ip: "10.0.1.1", vendor: "cisco" } }
        ]
    }
}

Solo muestra dispositivos pendientes para el rack actual. Al arrastrar un stencil al canvas, el management_config se auto-rellena desde el pending device y se elimina del localStorage.


SSH Integration

Flujo de conexión

  1. Usuario click “SSH” en panel de propiedades
  2. SshConfigModal muestra formulario con datos de management_config
  3. Check de alcanzabilidad: GET http://localhost:5050/check?host={ip}&port={port}
  4. Usuario confirma → openSSHSession(deviceId, displayName)
  5. Modal con terminal xterm.js embebido
  6. WebSocket: ws://localhost:5050/ws/terminal/{sessionId} via Local Agent

Estado global SSH

window.editorSSHClient   // Instancia SSH activa
window.editorDeviceId    // ID del dispositivo conectado
window.editorSFTP        // Instancia SFTPFileManager

Backups de configuración (backups.js)

Snapshots de running-config via SSH.

AcciónEndpointDescripción
CrearPOST /api/device/{id}/backupSSH → running-config → almacena con hash SHA256
ListarGET /api/device/{id}/backupsHistorial con timestamps
DiffFrontendCompara dos backups lado a lado
RestorePOST /api/device/{id}/backup/{bid}/restoreAplica config al dispositivo

Vendors soportados: Cisco IOS/IOS-XE · Arista EOS · Juniper JunOS


SFTP Sidebar (sftp_ui.js)

sftp_ui.js → HTTP → Local Agent :5050 → asyncssh → Dispositivo de red

Endpoints Agent:

EndpointDescripción
POST /sftp/listListar directorio { client_id, path }
POST /sftp/downloadDescargar archivo
POST /sftp/uploadSubir archivo (multipart)
POST /sftp/mkdirCrear directorio
POST /sftp/deleteEliminar

Modelos Django

Rack

class Rack(models.Model):
    name, location, notes
    height_u = IntegerField(default=42)
    status   # active | planned | deprecated | maintenance
    organization = FK(Organization)
    groups = M2M(RackGroup)
    deleted_at = DateTimeField(null=True)  # Soft delete
    is_template = BooleanField(default=False)

Device

class Device(models.Model):
    rack = FK(Rack, on_delete=CASCADE)
    name, notes
    u_position = IntegerField()
    u_height = IntegerField(default=1)
    model_data = TextField(default="{}")         # JSON: half_width, slot, ports, image, type
    management_config = TextField(default="{}")   # JSON: ip, vendor (credenciales Fernet encrypted)
    status  # online | offline | warning | unknown

    @property
    def management_ip(self)     # Extrae IP de management_config
    @property
    def management_vendor(self)  # Extrae vendor de management_config

Stencil

class Stencil(models.Model):
    name, image_path
    default_u_height = IntegerField(default=1)
    manufacturer, category
    extra_data = TextField(default="{}")   # JSON: half_width, ports
    organization = FK(Organization, null=True)  # null = stencil global del sistema

ConfigBackup

class ConfigBackup(models.Model):
    device = FK(Device), rack = FK(Rack)
    config_text = TextField()              # Running-config completo
    config_hash = CharField(max_length=64) # SHA256
    backup_type  # auto | manual | pre_change
    vendor, device_version
    created_by = FK(User, null=True)

API Endpoints (41+ bajo /api/racks/)

Racks CRUD

MétodoURLDescripción
POST/api/racks/Crear rack
GET/api/racks/{id}Rack completo con dispositivos
PUT/api/racks/{id}Actualizar metadata
DELETE/api/racks/{id}Soft-delete
PUT/api/racks/{id}/devicesAuto-save — sincronizar dispositivos
POST/api/racks/{id}/cloneClonar rack completo
POST/api/racks/{id}/renameRenombrar
POST/api/racks/{id}/create_templateGuardar como template
POST/api/racks/{id}/apply_templateAplicar template
GET/api/racks/listLista ligera
PUT/api/racks/bulk-updateActualización masiva

Stencils y librería

MétodoURLDescripción
GET/api/racks/stencilsListar (sistema + org)
PUT/DELETE/api/racks/stencils/{id}Editar / eliminar
PUT/api/racks/stencils/{id}/moveMover categoría
POST/api/racks/stencils/create_from_imageDesde imagen upload
POST/api/racks/stencils/create_from_deviceDesde dispositivo en canvas
POST/api/racks/visio/analyzeAnalizar archivo Visio
POST/api/racks/visio/confirmConfirmar importación
GET/api/racks/library/backupExportar librería JSON
POST/api/racks/library/restore/analyzeAnalizar backup
POST/api/racks/library/restore/confirmConfirmar restauración
DELETE/PUT/api/racks/library/category/delete / renameGestión categorías

Export, Labels, Trash

MétodoURLDescripción
GET/api/racks/export/csvTodos los racks CSV
GET/api/racks/{id}/export/csvRack específico CSV
GET/api/racks/labels/{target}QR labels
GET/POST/DELETE/api/racks/trash/racks*Papelera (listar, restaurar, eliminar)

Flujo de carga del editor

GET /editor/{rack_id}/
    ↓ Django → template editor.html
    ↓ main.js bootstrap
    ├── Lee #rack-data del DOM → STATE.rackData
    ├── Calcula CONFIG.RACK_HEIGHT = height_u × U_HEIGHT
    ├── konva_setup.js → Stage, Layer, rackGroup
    ├── fitRackToView() → centra rack en viewport
    ├── devices.js → addDeviceToRack() por cada device
    ├── ui_library.js → carga stencils en panel izquierdo
    ├── auto_provision_panel.js → check localStorage pending
    └── history_adapter.js → snapshot inicial

Convenciones y restricciones

Véase también

Subir