mirror of
https://github.com/hansjone/netx.git
synced 2026-10-19 06:43:15 +08:00
feat(managed-ne): add bulk import template download with device_type reference sheet
Expose GET /v1/managed-ne/import/template and a workbench button to download xlsx/csv templates listing supported Netmiko device types. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
042c015045
commit
3024e13812
6 changed files with 69 additions and 0 deletions
|
|
@ -1,6 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile
|
from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile
|
||||||
|
from fastapi.responses import Response
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from .db import get_db
|
from .db import get_db
|
||||||
|
|
@ -9,6 +10,7 @@ from .ne_connect import schedule_connect_tests
|
||||||
from .ne_crypto import credentials_configured
|
from .ne_crypto import credentials_configured
|
||||||
from .ne_schemas import ConnectTestRequest, ManagedNeCreate, ManagedNeUpdate
|
from .ne_schemas import ConnectTestRequest, ManagedNeCreate, ManagedNeUpdate
|
||||||
from .ne_service import (
|
from .ne_service import (
|
||||||
|
build_managed_ne_import_template,
|
||||||
create_managed_ne,
|
create_managed_ne,
|
||||||
delete_managed_ne,
|
delete_managed_ne,
|
||||||
get_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)
|
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")
|
@router.post("/import")
|
||||||
async def api_import_managed_ne(file: UploadFile = File(...), db: Session = Depends(get_db)):
|
async def api_import_managed_ne(file: UploadFile = File(...), db: Session = Depends(get_db)):
|
||||||
content = await file.read()
|
content = await file.read()
|
||||||
|
|
|
||||||
|
|
@ -195,6 +195,51 @@ def delete_managed_ne(db: Session, ne_id: str) -> dict[str, bool]:
|
||||||
return {"ok": True}
|
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:
|
def import_managed_ne(db: Session, content: bytes, filename: str) -> ImportResult:
|
||||||
_require_crypto()
|
_require_crypto()
|
||||||
name = str(filename or "").lower()
|
name = str(filename or "").lower()
|
||||||
|
|
|
||||||
|
|
@ -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.",
|
"NETX_CREDENTIAL_SECRET_KEY is not set. Configure a Fernet key in .env and restart the API to store passwords.",
|
||||||
add: "Add NE",
|
add: "Add NE",
|
||||||
importBtn: "Import",
|
importBtn: "Import",
|
||||||
|
downloadTemplate: "Download template",
|
||||||
importing: "Importing…",
|
importing: "Importing…",
|
||||||
edit: "Edit",
|
edit: "Edit",
|
||||||
delete: "Delete",
|
delete: "Delete",
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,7 @@ const zh = {
|
||||||
credsNotConfigured: "未配置 NETX_CREDENTIAL_SECRET_KEY,无法保存密码。请在 .env 中设置 Fernet 密钥后重启 API。",
|
credsNotConfigured: "未配置 NETX_CREDENTIAL_SECRET_KEY,无法保存密码。请在 .env 中设置 Fernet 密钥后重启 API。",
|
||||||
add: "新增网元",
|
add: "新增网元",
|
||||||
importBtn: "批量导入",
|
importBtn: "批量导入",
|
||||||
|
downloadTemplate: "下载导入模板",
|
||||||
importing: "导入中…",
|
importing: "导入中…",
|
||||||
edit: "编辑",
|
edit: "编辑",
|
||||||
delete: "删除",
|
delete: "删除",
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
fetchManagedNe,
|
fetchManagedNe,
|
||||||
fetchManagedNeMeta,
|
fetchManagedNeMeta,
|
||||||
importManagedNe,
|
importManagedNe,
|
||||||
|
managedNeImportTemplateUrl,
|
||||||
updateManagedNe,
|
updateManagedNe,
|
||||||
} from "../services/api";
|
} from "../services/api";
|
||||||
import { queryKeys } from "../constants/queryKeys";
|
import { queryKeys } from "../constants/queryKeys";
|
||||||
|
|
@ -233,6 +234,12 @@ export function NePage() {
|
||||||
<button type="button" onClick={openCreate} disabled={!credsOk}>
|
<button type="button" onClick={openCreate} disabled={!credsOk}>
|
||||||
{t("managedNe.add")}
|
{t("managedNe.add")}
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => window.location.assign(managedNeImportTemplateUrl("xlsx"))}
|
||||||
|
>
|
||||||
|
{t("managedNe.downloadTemplate")}
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => importRef.current?.click()}
|
onClick={() => importRef.current?.click()}
|
||||||
|
|
|
||||||
|
|
@ -129,6 +129,9 @@ export const deleteManagedNe = (id: string) => apiDelete<{ ok: boolean }>(`/v1/m
|
||||||
export const connectTestManagedNe = (ids: string[]) =>
|
export const connectTestManagedNe = (ids: string[]) =>
|
||||||
apiPost<{ ok: boolean; submitted: number }>("/v1/managed-ne/connect-test", { ids });
|
apiPost<{ ok: boolean; submitted: number }>("/v1/managed-ne/connect-test", { ids });
|
||||||
|
|
||||||
|
export const managedNeImportTemplateUrl = (format: "xlsx" | "csv" = "xlsx") =>
|
||||||
|
`/v1/managed-ne/import/template?format=${format}`;
|
||||||
|
|
||||||
export const importManagedNe = async (file: File): Promise<ManagedNeImportResult> => {
|
export const importManagedNe = async (file: File): Promise<ManagedNeImportResult> => {
|
||||||
const form = new FormData();
|
const form = new FormData();
|
||||||
form.append("file", file);
|
form.append("file", file);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue