diff --git a/netx_api/managed_ne_router.py b/netx_api/managed_ne_router.py
index f691641..75b25e8 100644
--- a/netx_api/managed_ne_router.py
+++ b/netx_api/managed_ne_router.py
@@ -1,6 +1,7 @@
from __future__ import annotations
from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile
+from fastapi.responses import Response
from sqlalchemy.orm import Session
from .db import get_db
@@ -9,6 +10,7 @@ from .ne_connect import schedule_connect_tests
from .ne_crypto import credentials_configured
from .ne_schemas import ConnectTestRequest, ManagedNeCreate, ManagedNeUpdate
from .ne_service import (
+ build_managed_ne_import_template,
create_managed_ne,
delete_managed_ne,
get_managed_ne,
@@ -70,6 +72,16 @@ def api_delete_managed_ne(ne_id: str, db: Session = Depends(get_db)):
return delete_managed_ne(db, ne_id)
+@router.get("/import/template")
+def api_managed_ne_import_template(format: str = Query(default="xlsx")):
+ filename, payload, media_type = build_managed_ne_import_template(format)
+ return Response(
+ content=payload,
+ media_type=media_type,
+ headers={"content-disposition": f'attachment; filename="{filename}"'},
+ )
+
+
@router.post("/import")
async def api_import_managed_ne(file: UploadFile = File(...), db: Session = Depends(get_db)):
content = await file.read()
diff --git a/netx_api/ne_service.py b/netx_api/ne_service.py
index 65d1c18..aff9b12 100644
--- a/netx_api/ne_service.py
+++ b/netx_api/ne_service.py
@@ -195,6 +195,51 @@ def delete_managed_ne(db: Session, ne_id: str) -> dict[str, bool]:
return {"ok": True}
+def build_managed_ne_import_template(fmt: str = "xlsx") -> tuple[str, bytes, str]:
+ """Return (filename, content, media_type) for bulk-import template."""
+ rows = [
+ {
+ "device_type": "cisco_ios",
+ "ip": "192.168.0.1",
+ "username": "admin",
+ "password": "your_password",
+ "port": 22,
+ "protocol": "ssh",
+ "name": "Core-SW1",
+ "vendor": "Cisco",
+ },
+ {
+ "device_type": "huawei",
+ "ip": "192.168.0.2",
+ "username": "admin",
+ "password": "your_password",
+ "port": 22,
+ "protocol": "ssh",
+ "name": "AGG-01",
+ "vendor": "Huawei",
+ },
+ ]
+ df = pd.DataFrame(rows, columns=list(IMPORT_COLUMNS))
+ buf = BytesIO()
+ kind = str(fmt or "xlsx").strip().lower()
+ if kind == "csv":
+ df.to_csv(buf, index=False, encoding="utf-8-sig")
+ return (
+ "managed_ne_import_template.csv",
+ buf.getvalue(),
+ "text/csv; charset=utf-8",
+ )
+ device_types_df = pd.DataFrame({"device_type": list(SUPPORTED_DEVICE_TYPES)})
+ with pd.ExcelWriter(buf, engine="openpyxl") as writer:
+ df.to_excel(writer, sheet_name="import", index=False)
+ device_types_df.to_excel(writer, sheet_name="device_type", index=False)
+ return (
+ "managed_ne_import_template.xlsx",
+ buf.getvalue(),
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+ )
+
+
def import_managed_ne(db: Session, content: bytes, filename: str) -> ImportResult:
_require_crypto()
name = str(filename or "").lower()
diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts
index bfa133b..a852cab 100644
--- a/web/src/i18n/en.ts
+++ b/web/src/i18n/en.ts
@@ -102,6 +102,7 @@ const en = {
"NETX_CREDENTIAL_SECRET_KEY is not set. Configure a Fernet key in .env and restart the API to store passwords.",
add: "Add NE",
importBtn: "Import",
+ downloadTemplate: "Download template",
importing: "Importing…",
edit: "Edit",
delete: "Delete",
diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts
index edb4437..3c160b5 100644
--- a/web/src/i18n/zh.ts
+++ b/web/src/i18n/zh.ts
@@ -101,6 +101,7 @@ const zh = {
credsNotConfigured: "未配置 NETX_CREDENTIAL_SECRET_KEY,无法保存密码。请在 .env 中设置 Fernet 密钥后重启 API。",
add: "新增网元",
importBtn: "批量导入",
+ downloadTemplate: "下载导入模板",
importing: "导入中…",
edit: "编辑",
delete: "删除",
diff --git a/web/src/pages/NePage.tsx b/web/src/pages/NePage.tsx
index d936e2a..3495ffd 100644
--- a/web/src/pages/NePage.tsx
+++ b/web/src/pages/NePage.tsx
@@ -7,6 +7,7 @@ import {
fetchManagedNe,
fetchManagedNeMeta,
importManagedNe,
+ managedNeImportTemplateUrl,
updateManagedNe,
} from "../services/api";
import { queryKeys } from "../constants/queryKeys";
@@ -233,6 +234,12 @@ export function NePage() {
+