From 65808456889061e2c2258e05769f3c72577233d7 Mon Sep 17 00:00:00 2001 From: Jean Date: Thu, 18 Sep 2025 22:58:56 -0300 Subject: [PATCH 01/19] feat: Add ContainerLabs menu, Running/Create CLabs submenus, and file upload functionality (PoC) --- .gitignore | 1 + apps/__init__.py | 35 ++- apps/clabs/__init__.py | 10 + apps/clabs/mock_data.py | 89 ++++++ apps/clabs/routes.py | 159 +++++++++++ apps/clabs/templates/clabs/create.html | 349 ++++++++++++++++++++++++ apps/clabs/templates/clabs/open.html | 202 ++++++++++++++ apps/clabs/templates/clabs/running.html | 223 +++++++++++++++ apps/templates/includes/sidebar.html | 28 +- 9 files changed, 1088 insertions(+), 8 deletions(-) create mode 100644 apps/clabs/__init__.py create mode 100644 apps/clabs/mock_data.py create mode 100644 apps/clabs/routes.py create mode 100644 apps/clabs/templates/clabs/create.html create mode 100644 apps/clabs/templates/clabs/open.html create mode 100644 apps/clabs/templates/clabs/running.html diff --git a/.gitignore b/.gitignore index b267f5f..61ec9f3 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ __pycache__/ # venv venv +*.venv # .env variables file .env diff --git a/apps/__init__.py b/apps/__init__.py index be7ee67..6dbdeab 100644 --- a/apps/__init__.py +++ b/apps/__init__.py @@ -40,10 +40,33 @@ def register_extensions(app): def register_blueprints(app): for module_name in ('authentication', 'home', 'api', 'k8s', 'cli'): - module = import_module('apps.{}.routes'.format(module_name)) + module = import_module(f'apps.{module_name}.routes') app.register_blueprint(module.blueprint) - # Load socketio endpoints - import_module("apps.events") + + # (PoC) ContainerLabs: registra blueprint se habilitado + enable_clabs = app.config.get( + "ENABLE_CLABS", + str(os.getenv("ENABLE_CLABS", "false")).lower() in ("1", "true", "yes", "on") + ) + if enable_clabs: + try: + clabs = import_module("apps.clabs.routes") + app.register_blueprint(clabs.blueprint) + app.logger.info("ContainerLabs blueprint registrado (ENABLE_CLABS=true).") + except Exception as e: + app.logger.error(f"Falha ao registrar ContainerLabs: {e}") + else: + app.logger.info("ContainerLabs desativado (ENABLE_CLABS=false).") + + # Carrega endpoints do Socket.IO (xterm) + # -> Pula no Windows porque usa pty/termios/fcntl (só Unix) + try: + if os.name != "nt": + import_module("apps.events") + else: + app.logger.warning("Skipping apps.events on Windows (no termios/pty).") + except Exception as e: + app.logger.warning(f"Skipping apps.events due to error: {e}") def configure_database(app): @@ -79,6 +102,11 @@ def configure_log(app): def create_app(config): app = Flask(__name__) app.config.from_object(config) + + # Default da feature flag (pode vir do .env) + if "ENABLE_CLABS" not in app.config: + app.config["ENABLE_CLABS"] = str(os.getenv("ENABLE_CLABS", "false")).lower() in ("1", "true", "yes", "on") + register_extensions(app) register_blueprints(app) #configure_database(app) @@ -86,3 +114,4 @@ def create_app(config): configure_log(app) return app + diff --git a/apps/clabs/__init__.py b/apps/clabs/__init__.py new file mode 100644 index 0000000..0b0ba0e --- /dev/null +++ b/apps/clabs/__init__.py @@ -0,0 +1,10 @@ +# -*- encoding: utf-8 -*- +from flask import Blueprint + +blueprint = Blueprint( + "clabs_blueprint", + __name__, + url_prefix="/containerlabs", + template_folder="templates", + static_folder="static", +) diff --git a/apps/clabs/mock_data.py b/apps/clabs/mock_data.py new file mode 100644 index 0000000..d86e413 --- /dev/null +++ b/apps/clabs/mock_data.py @@ -0,0 +1,89 @@ +# -*- encoding: utf-8 -*- + +# Lista simples (usada na página de Running CLabs) +CLABS = [ + { + "id": "clab-001", + "name": "Edge Fabric", + "owner": "alice@example.com", + "status": "running", + "created_at": "2025-09-10 14:32", + "nodes": 6, + }, + { + "id": "clab-002", + "name": "Core BGP", + "owner": "bob@example.com", + "status": "running", + "created_at": "2025-09-12 09:05", + "nodes": 4, + }, +] + +# Recursos detalhados por CLab (para a página Open CLab) +# Estrutura inspirada em view_lab_instance: uma lista de "resources" por nó/serviço +CLABS_DETAILS = { + "clab-001": { + "title": "Edge Fabric", + "lab_instance_id": "clab-001", + "user": "Alice (alice@example.com)", + "created": "2025-09-10 14:32", + "expires_at": None, # PoC: sem expiração + "resources": [ + { + "kind": "node", + "name": "leaf1", + "ready": "Running", + "links": [ + {"label": "Console", "href": "#"}, + ], + "services": [ + {"name": "Mgmt SSH", "url": "#"}, + {"name": "gNMI", "url": "#"}, + ], + "age": "1d", + "node_name": "edge-host-01", + "pod_ip": "10.0.0.11", + }, + { + "kind": "node", + "name": "leaf2", + "ready": "Running", + "links": [{"label": "Console", "href": "#"}], + "services": [{"name": "Mgmt SSH", "url": "#"}], + "age": "1d", + "node_name": "edge-host-02", + "pod_ip": "10.0.0.12", + }, + ], + }, + "clab-002": { + "title": "Core BGP", + "lab_instance_id": "clab-002", + "user": "Bob (bob@example.com)", + "created": "2025-09-12 09:05", + "expires_at": None, + "resources": [ + { + "kind": "node", + "name": "r1", + "ready": "Running", + "links": [{"label": "Console", "href": "#"}], + "services": [{"name": "Mgmt SSH", "url": "#"}], + "age": "2h", + "node_name": "core-host-01", + "pod_ip": "10.1.0.21", + }, + { + "kind": "node", + "name": "r2", + "ready": "Running", + "links": [{"label": "Console", "href": "#"}], + "services": [{"name": "Mgmt SSH", "url": "#"}], + "age": "2h", + "node_name": "core-host-02", + "pod_ip": "10.1.0.22", + }, + ], + }, +} diff --git a/apps/clabs/routes.py b/apps/clabs/routes.py new file mode 100644 index 0000000..e2495b1 --- /dev/null +++ b/apps/clabs/routes.py @@ -0,0 +1,159 @@ +# -*- encoding: utf-8 -*- +from flask import render_template, request, jsonify +from flask_login import current_user +from datetime import datetime + +from . import blueprint +from .mock_data import CLABS, CLABS_DETAILS + +# Tentamos usar PyYAML; se não estiver instalado, seguimos sem quebrar +try: + import yaml # PyYAML +except Exception: + yaml = None + +@blueprint.route("/running") +def running(): + labs = [] + for c in CLABS: + labs.append({ + "lab_instance_id": c["id"], + "user": c.get("owner", "--"), + "title": c.get("name", "--"), + "created": c.get("created_at", "--"), + }) + return render_template("clabs/running.html", labs=labs) + + +@blueprint.route("/open/") +def open_clab(clab_id): + lab = CLABS_DETAILS.get(clab_id) + if not lab: + return render_template("pages/error.html", title="Open CLab", msg="CLab not found") + + return render_template("clabs/open.html", lab=lab) + +def _resources_from_clab_yaml(yaml_text: str): + """ + Constrói a lista de 'resources' (nós) a partir de um YAML ContainerLab. + Se PyYAML não estiver disponível ou o YAML estiver inválido, retorna []. + """ + if not yaml or not yaml_text: + return [] + + try: + data = yaml.safe_load(yaml_text) + except Exception: + return [] + + if not isinstance(data, dict): + return [] + + topo = data.get("topology") or {} + nodes = topo.get("nodes") or {} + if not isinstance(nodes, dict): + return [] + + resources = [] + for name, cfg in nodes.items(): + # cfg costuma ser dict com 'kind', 'image', 'mgmt_ipv4' etc. + kind = "node" + ready = "Running" # PoC: considera subido + pod_ip = None + node_name = None + links = [{"label": "Console", "href": "#"}] # placeholder + services = [] + + if isinstance(cfg, dict): + pod_ip = cfg.get("mgmt_ipv4") + node_name = cfg.get("kind") or cfg.get("image") or "--" + + resources.append({ + "kind": kind, + "name": str(name), + "ready": ready, + "links": links, + "services": services, + "age": "--", + "node_name": node_name or "--", + "pod_ip": pod_ip or "--", + }) + + return resources + +@blueprint.route("/create", methods=["GET", "POST"]) +def create(): + if request.method == "GET": + return render_template("clabs/create.html") + + # --- POST (PoC) --- + # Recebe dados do formulário e arquivos (mock – não persiste em disco) + name = (request.form.get("clab_name") or "").strip() + namespace = (request.form.get("clab_namespace") or "").strip() + yaml_manifest = (request.form.get("clab_yaml") or "").strip() + + # Conta arquivos enviados (arquivos soltos + pastas via webkitdirectory) + files_count = 0 + files_list = [] # Vamos armazenar os nomes dos arquivos enviados aqui + try: + # Adiciona arquivos individuais + files_list.extend(request.files.getlist("clab_files") or []) + + # Adiciona arquivos de pastas com caminho completo + for file in request.files.getlist("clab_folders"): + # O caminho completo da pasta será adicionado, evitando duplicação + files_list.append(file) + + files_count = len(files_list) + except Exception: + files_count = 0 + + # Validação mínima (PoC): exigir YAML ou ao menos 1 arquivo + if not yaml_manifest and files_count == 0: + return jsonify({"ok": False, "result": "Provide YAML and/or files"}), 400 + + # ---- Monta recursos a partir do YAML (se possível) ---- + resources = _resources_from_clab_yaml(yaml_manifest) + nodes_count = len(resources) + + # ---- Atualiza os mocks para aparecer em Running CLabs e Open CLab ---- + new_id = f"clab-{datetime.utcnow().strftime('%Y%m%d%H%M%S')}" + created_str = datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S') + + # Dados do "owner" a partir do usuário logado (fallbacks seguros) + owner_name = getattr(current_user, "name", None) or getattr(current_user, "username", "User") + owner_email = getattr(current_user, "email", None) or "user@example.com" + owner_display = f"{owner_name} ({owner_email})" + + # Lista simples (Running CLabs) + CLABS.append({ + "id": new_id, + "name": name or "Unnamed", + "owner": owner_email, + "status": "running", + "created_at": created_str, + "nodes": nodes_count, + }) + + # Detalhes (Open CLab) — Incluindo os arquivos enviados + CLABS_DETAILS[new_id] = { + "title": name or "Unnamed", + "lab_instance_id": new_id, + "user": owner_display, + "created": created_str, + "expires_at": None, + "resources": resources, # agora populado a partir do YAML + "files": files_list, # Armazena os arquivos enviados com o caminho completo + } + + return jsonify({ + "ok": True, + "result": f"CLab '{name or 'Unnamed'}' created (mock).", + "received": { + "id": new_id, + "namespace": namespace or "--", + "has_yaml": bool(yaml_manifest), + "files": files_count, + "resources": nodes_count, + } + }), 201 diff --git a/apps/clabs/templates/clabs/create.html b/apps/clabs/templates/clabs/create.html new file mode 100644 index 0000000..c5082f0 --- /dev/null +++ b/apps/clabs/templates/clabs/create.html @@ -0,0 +1,349 @@ +{% extends "layouts/base.html" %} + +{% block title %} Create CLabs {% endblock %} + +{% block body_class %} sidebar-mini layout-navbar-fixed {% endblock body_class %} + +{% block stylesheets %} + + + + + + + + + +{% endblock stylesheets %} + +{% block content %} + +
+ +
+
+
+
+

Create CLabs

+ Paste your containerlab YAML and/or upload files/folders +
+
+ +
+
+
+
+ +
+ +
+
+
+
+

New ContainerLab

+
+
+
+
+ + +
+
+ + +
+
+ +
+ + + Use the same style of editor we já utilizamos para + Kubernetes Manifest (CodeMirror). +
+ +
+ +
+ +
Drag & drop files here, or click to select
+ Accepted: multiple files +
+ +
+
+ +
+ + + + Folder upload works on Chromium-based browsers (Chrome/Edge). Firefox não suporta; como + alternativa, compacte em ZIP e envie nos arquivos acima. + +
+ +
+ +
+
+
+ +
+ +
+ +{% endblock content %} + +{% block javascripts %} + + + + + + + + + + + + +{% endblock javascripts %} \ No newline at end of file diff --git a/apps/clabs/templates/clabs/open.html b/apps/clabs/templates/clabs/open.html new file mode 100644 index 0000000..1517ce9 --- /dev/null +++ b/apps/clabs/templates/clabs/open.html @@ -0,0 +1,202 @@ +{% extends "layouts/base.html" %} + +{% block title %} Open CLab {% endblock %} + +{% block body_class %} sidebar-mini layout-navbar-fixed {% endblock body_class %} + +{% block stylesheets %} + + + + + + + + + +{% endblock stylesheets %} + +{% block content %} + +
+ +
+
+
+
+

Open CLab

+ View ContainerLab resources +
+
+ +
+
+
+
+ +
+ + +
+
+
+
+

{{ lab.title }}

+
+
+
+
+ Instance ID: +

{{ lab.lab_instance_id }}

+
+
+ User: +

{{ lab.user }}

+
+
+ Created (UTC): +

{{ lab.created or '--' }}

+
+
+ Expires at: +

{{ lab.expires_at or '--' }}

+
+
+
+
+
+
+ + +
+
+
+
+

Uploaded Files

+
+
+ {% if lab.files %} +
    + {% for file in lab.files %} +
  • + {{ file.filename if file.filename else file }} +
  • + {% endfor %} +
+ {% else %} +

No files uploaded.

+ {% endif %} +
+
+
+
+ + +
+
+
+
+

Resources

+
+
+ + + + + + + + + + + + + + + {% for r in lab.resources %} + + + + + + + + + + + {% endfor %} + +
KindNameStatusNodePod IPAgeServicesLinks
{{ r.kind }}{{ r.name }} + {% if r.ready == 'Running' %} + {{ r.ready }} + {% else %} + {{ r.ready }} + {% endif %} + {{ r.node_name or '--' }}{{ r.pod_ip or '--' }}{{ r.age or '--' }} + {% if r.services %} + {% for s in r.services %} + {{ + s.name }} + {% endfor %} + {% else %} + -- + {% endif %} + + {% if r.links %} + {% for l in r.links %} + {{ l.label }} + {% endfor %} + {% else %} + -- + {% endif %} +
+
+
+
+
+ +
+ +
+ +{% endblock content %} + +{% block javascripts %} + + + + + + + + + + + + + +{% endblock javascripts %} \ No newline at end of file diff --git a/apps/clabs/templates/clabs/running.html b/apps/clabs/templates/clabs/running.html new file mode 100644 index 0000000..bd242fc --- /dev/null +++ b/apps/clabs/templates/clabs/running.html @@ -0,0 +1,223 @@ +{% extends "layouts/base.html" %} + +{% block title %} Running CLabs {% endblock %} + +{% block body_class %} sidebar-mini layout-navbar-fixed {% endblock body_class %} + +{% block stylesheets %} + + + + + + + + + + + +{% endblock stylesheets %} + +{% block content %} + +
+
+
+
+
+

Running CLabs

+
+
+ +
+
+
+
+ +
+
+
+
+
+

Running CLabs

+
+ + +
+
+ +
+ + + + + + + + + + + + {% for lab in labs %} + + + + + + + + {% endfor %} + +
UserLab titleCreated (UTC)Actions
+
+ + +
+
{{ lab['user'] }}{{ lab['title'] }}{{ lab['created'] }} + Open CLab +
+ +
+ + +
+ +
+
+ + + +
+
+ +{% endblock content %} + +{% block javascripts %} + + + + + + + + + + + + + + + + +{% endblock javascripts %} \ No newline at end of file diff --git a/apps/templates/includes/sidebar.html b/apps/templates/includes/sidebar.html index 4b07429..a51dd68 100644 --- a/apps/templates/includes/sidebar.html +++ b/apps/templates/includes/sidebar.html @@ -96,15 +96,33 @@ - + {% endif %} + {% if current_user.category == "admin" or current_user.category == "teacher" %} - + + {% if config.get('ENABLE_CLABS') %}