mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 03:10:46 +08:00
feat(ne): connect detail, Huawei/Cisco hop, Cisco hostname probe
Persist full connect test logs (connect_detail) with NE UI detail modal. Add Huawei/Cisco jump CLI templates and generic CLI hop session path. Probe Cisco hostname via show configuration | include hostname (60s timeout). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
778442dc57
commit
dc17f9d15a
15 changed files with 457 additions and 63 deletions
|
|
@ -2,10 +2,10 @@ import type { ReactNode } from "react";
|
|||
import { useI18n } from "../i18n";
|
||||
import {
|
||||
HOP_VENDORS,
|
||||
defaultHopTemplate,
|
||||
isAutoHopTemplate,
|
||||
isLinuxHopVendor,
|
||||
patchHopVendorChange,
|
||||
zteHopTemplate,
|
||||
type HopVendor,
|
||||
} from "../utils/hopProxy";
|
||||
|
||||
|
|
@ -27,7 +27,7 @@ export const emptyHopProxyFields = (): HopProxyFieldsState => ({
|
|||
hop_protocol: "ssh",
|
||||
hop_username: "",
|
||||
hop_password: "",
|
||||
hop_command_template: zteHopTemplate("ssh", ""),
|
||||
hop_command_template: defaultHopTemplate("zte", "ssh", ""),
|
||||
hop_vrf: "",
|
||||
});
|
||||
|
||||
|
|
@ -51,10 +51,32 @@ function applyHopTemplate(
|
|||
vrf: string,
|
||||
force = false,
|
||||
): Partial<HopProxyFieldsState> {
|
||||
if (!force && !isAutoHopTemplate(prev.hop_command_template, prev.hop_protocol, prev.hop_vrf)) {
|
||||
if (!force && !isAutoHopTemplate(prev.hop_command_template, prev.hop_vendor, prev.hop_protocol, prev.hop_vrf)) {
|
||||
return {};
|
||||
}
|
||||
return { hop_command_template: zteHopTemplate(protocol, vrf) };
|
||||
return { hop_command_template: defaultHopTemplate(prev.hop_vendor, protocol, vrf) };
|
||||
}
|
||||
|
||||
function hopHintKey(vendor: string): string {
|
||||
const v = String(vendor || "").toLowerCase();
|
||||
if (v === "linux") return "managedNe.hop.linuxHint";
|
||||
if (v === "huawei") return "managedNe.hop.huaweiHint";
|
||||
if (v === "cisco") return "managedNe.hop.ciscoHint";
|
||||
return "managedNe.hop.zteHint";
|
||||
}
|
||||
|
||||
function templateHintKey(vendor: string): string {
|
||||
const v = String(vendor || "").toLowerCase();
|
||||
if (v === "huawei") return "managedNe.hop.templateHintHuawei";
|
||||
if (v === "cisco") return "managedNe.hop.templateHintCisco";
|
||||
return "managedNe.hop.templateHint";
|
||||
}
|
||||
|
||||
function vrfLabelKey(vendor: string): string {
|
||||
const v = String(vendor || "").toLowerCase();
|
||||
if (v === "huawei") return "managedNe.hop.vpnInstance";
|
||||
if (v === "cisco") return "managedNe.hop.vrfCisco";
|
||||
return "managedNe.hop.vrf";
|
||||
}
|
||||
|
||||
type Props = {
|
||||
|
|
@ -72,6 +94,7 @@ export function HopProxyFields({
|
|||
}: Props) {
|
||||
const { t } = useI18n();
|
||||
const linux = isLinuxHopVendor(value.hop_vendor);
|
||||
const huawei = value.hop_vendor === "huawei";
|
||||
|
||||
const set = (patch: Partial<HopProxyFieldsState>) => onChange(patch);
|
||||
|
||||
|
|
@ -92,9 +115,7 @@ export function HopProxyFields({
|
|||
</option>
|
||||
))}
|
||||
</select>
|
||||
<span className="form-field-hint">
|
||||
{linux ? t("managedNe.hop.linuxHint") : t("managedNe.hop.zteHint")}
|
||||
</span>
|
||||
<span className="form-field-hint">{t(hopHintKey(value.hop_vendor))}</span>
|
||||
</label>
|
||||
<label>
|
||||
<FormLabel required>{t("managedNe.hop.host")}</FormLabel>
|
||||
|
|
@ -118,7 +139,7 @@ export function HopProxyFields({
|
|||
set({ hop_protocol, ...applyHopTemplate(value, hop_protocol, value.hop_vrf) });
|
||||
}}
|
||||
>
|
||||
<option value="ssh">ssh</option>
|
||||
<option value="ssh">{huawei ? t("managedNe.hop.protocolSshStelnet") : "ssh"}</option>
|
||||
<option value="telnet">telnet</option>
|
||||
</select>
|
||||
</label>
|
||||
|
|
@ -144,7 +165,7 @@ export function HopProxyFields({
|
|||
{!linux ? (
|
||||
<>
|
||||
<label>
|
||||
<FormLabel>{t("managedNe.hop.vrf")}</FormLabel>
|
||||
<FormLabel>{t(vrfLabelKey(value.hop_vendor))}</FormLabel>
|
||||
<input
|
||||
value={value.hop_vrf}
|
||||
onChange={(e) => {
|
||||
|
|
@ -158,9 +179,9 @@ export function HopProxyFields({
|
|||
<input
|
||||
value={value.hop_command_template}
|
||||
onChange={(e) => set({ hop_command_template: e.target.value })}
|
||||
placeholder={zteHopTemplate(value.hop_protocol, value.hop_vrf)}
|
||||
placeholder={defaultHopTemplate(value.hop_vendor, value.hop_protocol, value.hop_vrf)}
|
||||
/>
|
||||
<span className="form-field-hint">{t("managedNe.hop.templateHint")}</span>
|
||||
<span className="form-field-hint">{t(templateHintKey(value.hop_vendor))}</span>
|
||||
</label>
|
||||
</>
|
||||
) : null}
|
||||
|
|
|
|||
|
|
@ -163,7 +163,12 @@ const en = {
|
|||
run: "Connectivity test",
|
||||
running: "Testing…",
|
||||
submitted: "Submitted tests for {{n}} device(s)",
|
||||
retest: "Test again",
|
||||
},
|
||||
connectDetail: "Details",
|
||||
connectDetailTitle: "Connectivity test log",
|
||||
connectDetailEmpty:
|
||||
"No log yet. Run a connectivity test first; failures store full errors and hop context (passwords excluded).",
|
||||
importResult: {
|
||||
done: "Import done: {{inserted}} inserted, {{updated}} updated, {{failed}} failed row(s)",
|
||||
},
|
||||
|
|
@ -187,21 +192,34 @@ const en = {
|
|||
enable: "Connect to target via jump host",
|
||||
vendor: {
|
||||
zte: "ZTE device (CLI jump)",
|
||||
huawei: "Huawei device (CLI jump)",
|
||||
cisco: "Cisco device (CLI jump)",
|
||||
linux: "Linux server (SSH tunnel)",
|
||||
},
|
||||
zteHint: "Run ssh/telnet on the ZTE device to reach the target; target credentials use secondary auth.",
|
||||
huaweiHint: "Run telnet / stelnet (SSH) on the Huawei hop; stelnet is SSH. Target credentials use secondary auth.",
|
||||
ciscoHint: "Run Cisco ssh -vrf / telnet /vrf jump commands; target credentials use secondary auth.",
|
||||
linuxHint: "SSH to the Linux bastion, then direct-tcpip tunnel to target IP:port (ProxyJump-style).",
|
||||
host: "Jump host",
|
||||
port: "Jump port",
|
||||
protocol: "Jump protocol",
|
||||
protocolSshStelnet: "ssh (stelnet)",
|
||||
username: "Jump username",
|
||||
password: "Jump password",
|
||||
vrf: "Mgmt VRF (optional)",
|
||||
vpnInstance: "VPN-Instance (optional)",
|
||||
vrfCisco: "VRF (optional, e.g. MGMT)",
|
||||
commandTemplate: "Jump command template",
|
||||
templateHint:
|
||||
"ZTE CLI: telnet {target_ip}, telnet {target_ip} vrf {vrf}, ssh {target_ip}, ssh {target_ip} vrf {vrf}. Auto-suggested from jump protocol/VRF; same when left blank. Target credentials via secondary auth prompts.",
|
||||
templateHintHuawei:
|
||||
"Huawei CLI: telnet {target_ip}, telnet vpn-instance {vrf} {target_ip}, stelnet {target_ip}, stelnet {target_ip} -vpn-instance {vrf}. SSH protocol maps to stelnet. Auto-suggested when left blank.",
|
||||
templateHintCisco:
|
||||
"Cisco CLI: ssh {target_ip}, ssh -vrf {vrf} {target_ip}, telnet {target_ip}, telnet {target_ip} /vrf {vrf}. Auto-suggested when left blank.",
|
||||
badge: {
|
||||
zte: "ZTE hop",
|
||||
huawei: "Huawei hop",
|
||||
cisco: "Cisco hop",
|
||||
linux: "Linux hop",
|
||||
},
|
||||
hostRequired: "Jump host is required",
|
||||
|
|
|
|||
|
|
@ -162,7 +162,11 @@ const zh = {
|
|||
run: "连通性测试",
|
||||
running: "测试中…",
|
||||
submitted: "已提交 {{n}} 台设备测试",
|
||||
retest: "重新测试",
|
||||
},
|
||||
connectDetail: "详情",
|
||||
connectDetailTitle: "连通性测试日志",
|
||||
connectDetailEmpty: "暂无日志。请先执行连通性测试;失败时会记录完整错误与跳板上下文(不含密码)。",
|
||||
importResult: {
|
||||
done: "导入完成:新增 {{inserted}},更新 {{updated}},失败 {{failed}} 行",
|
||||
},
|
||||
|
|
@ -186,21 +190,34 @@ const zh = {
|
|||
enable: "经跳板登录目标网元",
|
||||
vendor: {
|
||||
zte: "ZTE 设备(CLI 跳登)",
|
||||
huawei: "华为设备(CLI 跳登)",
|
||||
cisco: "思科设备(CLI 跳登)",
|
||||
linux: "Linux 服务器(SSH 隧道)",
|
||||
},
|
||||
zteHint: "在 ZTE 设备上执行 ssh/telnet 命令跳转到目标,目标账号由二次认证输入。",
|
||||
huaweiHint: "在华为设备上执行 telnet / stelnet(SSH)跳登;stelnet 即 SSH。目标账号由二次认证输入。",
|
||||
ciscoHint: "在思科设备上执行 ssh -vrf / telnet /vrf 跳登,目标账号由二次认证输入。",
|
||||
linuxHint: "先 SSH 登录 Linux 跳板,经 direct-tcpip 隧道连接目标 IP:端口(等同 ProxyJump)。",
|
||||
host: "跳板地址",
|
||||
port: "跳板端口",
|
||||
protocol: "跳板协议",
|
||||
protocolSshStelnet: "ssh(stelnet)",
|
||||
username: "跳板用户名",
|
||||
password: "跳板密码",
|
||||
vrf: "管理 VRF(可选)",
|
||||
vpnInstance: "VPN-Instance(可选)",
|
||||
vrfCisco: "VRF(可选,如 MGMT)",
|
||||
commandTemplate: "跳登命令模板",
|
||||
templateHint:
|
||||
"ZTE 常用:telnet {target_ip}、telnet {target_ip} vrf {vrf}、ssh {target_ip}、ssh {target_ip} vrf {vrf}。按跳板协议与 VRF 自动推荐;留空时后端同样规则。目标账号密码由二次认证提示输入。",
|
||||
templateHintHuawei:
|
||||
"华为常用:telnet {target_ip}、telnet vpn-instance {vrf} {target_ip}、stelnet {target_ip}、stelnet {target_ip} -vpn-instance {vrf}。SSH 协议对应 stelnet。留空时按协议与 VPN-Instance 自动推荐。",
|
||||
templateHintCisco:
|
||||
"思科常用:ssh {target_ip}、ssh -vrf {vrf} {target_ip}、telnet {target_ip}、telnet {target_ip} /vrf {vrf}。留空时按协议与 VRF 自动推荐。",
|
||||
badge: {
|
||||
zte: "ZTE跳板",
|
||||
huawei: "华为跳板",
|
||||
cisco: "思科跳板",
|
||||
linux: "Linux跳板",
|
||||
},
|
||||
hostRequired: "请填写跳板地址",
|
||||
|
|
|
|||
|
|
@ -835,6 +835,31 @@ pre {
|
|||
box-shadow: 0 16px 48px rgba(15, 23, 42, 0.2);
|
||||
}
|
||||
|
||||
.modal--wide {
|
||||
width: min(920px, 100%);
|
||||
}
|
||||
|
||||
.connect-detail-summary {
|
||||
margin-left: 8px;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.connect-log {
|
||||
margin: 12px 0 0;
|
||||
padding: 12px;
|
||||
max-height: min(52vh, 480px);
|
||||
overflow: auto;
|
||||
background: #0f172a;
|
||||
color: #e2e8f0;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
}
|
||||
|
||||
.modal h3 {
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useMemo, useRef, useState, type ReactNode } from "react";
|
||||
import { useEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
batchApplyHopManagedNe,
|
||||
|
|
@ -19,8 +19,12 @@ import { useToast } from "../hooks/useToast";
|
|||
import type { ManagedNeItem } from "../types";
|
||||
import { pageCount } from "../utils/display";
|
||||
import { formatSystemTime } from "../utils/time";
|
||||
import { isAutoHopTemplate, patchHopVendorChange, zteHopTemplate } from "../utils/hopProxy";
|
||||
import type { HopVendor } from "../utils/hopProxy";
|
||||
import {
|
||||
defaultHopTemplate,
|
||||
isAutoHopTemplate,
|
||||
patchHopVendorChange,
|
||||
type HopVendor,
|
||||
} from "../utils/hopProxy";
|
||||
|
||||
type FormState = {
|
||||
name: string;
|
||||
|
|
@ -62,15 +66,15 @@ const emptyForm = (): FormState => ({
|
|||
hop_protocol: "ssh",
|
||||
hop_username: "",
|
||||
hop_password: "",
|
||||
hop_command_template: zteHopTemplate("ssh", ""),
|
||||
hop_command_template: defaultHopTemplate("zte", "ssh", ""),
|
||||
hop_vrf: "",
|
||||
});
|
||||
|
||||
function applyHopTemplate(prev: FormState, protocol: string, vrf: string, force = false): Partial<FormState> {
|
||||
if (!force && !isAutoHopTemplate(prev.hop_command_template, prev.hop_protocol, prev.hop_vrf)) {
|
||||
if (!force && !isAutoHopTemplate(prev.hop_command_template, prev.hop_vendor, prev.hop_protocol, prev.hop_vrf)) {
|
||||
return {};
|
||||
}
|
||||
return { hop_command_template: zteHopTemplate(protocol, vrf) };
|
||||
return { hop_command_template: defaultHopTemplate(prev.hop_vendor, protocol, vrf) };
|
||||
}
|
||||
|
||||
function FormLabel({ children, required }: { children: ReactNode; required?: boolean }) {
|
||||
|
|
@ -111,6 +115,7 @@ export function NePage() {
|
|||
const [editing, setEditing] = useState<ManagedNeItem | null>(null);
|
||||
const [form, setForm] = useState<FormState>(emptyForm);
|
||||
const [batchHop, setBatchHop] = useState<HopProxyFieldsState>(emptyHopProxyFields);
|
||||
const [connectDetailRow, setConnectDetailRow] = useState<ManagedNeItem | null>(null);
|
||||
|
||||
const metaQuery = useQuery({
|
||||
queryKey: queryKeys.managedNeMeta,
|
||||
|
|
@ -134,6 +139,20 @@ export function NePage() {
|
|||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!connectDetailRow) return;
|
||||
const updated = listQuery.data?.items?.find((x) => x.id === connectDetailRow.id);
|
||||
if (!updated) return;
|
||||
if (
|
||||
updated.connect_status !== connectDetailRow.connect_status ||
|
||||
updated.connect_message !== connectDetailRow.connect_message ||
|
||||
updated.connect_detail !== connectDetailRow.connect_detail ||
|
||||
updated.connect_tested_at !== connectDetailRow.connect_tested_at
|
||||
) {
|
||||
setConnectDetailRow(updated);
|
||||
}
|
||||
}, [listQuery.data, connectDetailRow]);
|
||||
|
||||
const total = listQuery.data?.total ?? 0;
|
||||
const pages = pageCount(total, pageSize);
|
||||
const perPage = (n: number) => t("common.perPage", { n });
|
||||
|
|
@ -279,7 +298,9 @@ export function NePage() {
|
|||
tags: row.tags,
|
||||
remark: row.remark,
|
||||
hop_enabled: row.hop_enabled,
|
||||
hop_vendor: (row.hop_vendor === "linux" ? "linux" : "zte") as HopVendor,
|
||||
hop_vendor: (["linux", "huawei", "cisco", "zte"].includes(row.hop_vendor)
|
||||
? row.hop_vendor
|
||||
: "zte") as HopVendor,
|
||||
hop_host: row.hop_host,
|
||||
hop_port: row.hop_port,
|
||||
hop_protocol: row.hop_protocol,
|
||||
|
|
@ -287,11 +308,12 @@ export function NePage() {
|
|||
hop_password: "",
|
||||
hop_command_template: isAutoHopTemplate(
|
||||
row.hop_command_template,
|
||||
row.hop_vendor,
|
||||
row.hop_protocol,
|
||||
row.hop_vrf,
|
||||
)
|
||||
? zteHopTemplate(row.hop_protocol, row.hop_vrf)
|
||||
: row.hop_command_template || zteHopTemplate(row.hop_protocol, row.hop_vrf),
|
||||
? defaultHopTemplate(row.hop_vendor, row.hop_protocol, row.hop_vrf)
|
||||
: row.hop_command_template || defaultHopTemplate(row.hop_vendor, row.hop_protocol, row.hop_vrf),
|
||||
hop_vrf: row.hop_vrf,
|
||||
});
|
||||
setModalOpen(true);
|
||||
|
|
@ -466,7 +488,9 @@ export function NePage() {
|
|||
className="table-tag"
|
||||
title={`${row.hop_host}:${row.hop_port} (${row.hop_vendor})`}
|
||||
>
|
||||
{t(`managedNe.hop.badge.${row.hop_vendor === "linux" ? "linux" : "zte"}`)}
|
||||
{t(
|
||||
`managedNe.hop.badge.${["linux", "huawei", "cisco", "zte"].includes(row.hop_vendor) ? row.hop_vendor : "zte"}`,
|
||||
)}
|
||||
</span>
|
||||
) : null}
|
||||
</td>
|
||||
|
|
@ -485,12 +509,20 @@ export function NePage() {
|
|||
: t("common.empty")}
|
||||
</td>
|
||||
<td className="table-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="link-btn"
|
||||
onClick={() => setConnectDetailRow(row)}
|
||||
disabled={!row.connect_tested_at && !row.connect_message && !row.connect_detail}
|
||||
>
|
||||
{t("managedNe.connectDetail")}
|
||||
</button>
|
||||
<button type="button" className="link-btn" onClick={() => openEdit(row)}>
|
||||
{t("managedNe.edit")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="link-btn"
|
||||
className="link-btn link-btn--danger"
|
||||
onClick={() => {
|
||||
if (window.confirm(t("managedNe.confirmDelete"))) deleteMutation.mutate(row.id);
|
||||
}}
|
||||
|
|
@ -706,6 +738,48 @@ export function NePage() {
|
|||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{connectDetailRow ? (
|
||||
<div className="modal-backdrop" role="presentation" onClick={() => setConnectDetailRow(null)}>
|
||||
<div className="modal modal--wide" role="dialog" onClick={(e) => e.stopPropagation()}>
|
||||
<h3>{t("managedNe.connectDetailTitle")}</h3>
|
||||
<p className="form-hint">
|
||||
{connectDetailRow.name || connectDetailRow.ip_address} · {connectDetailRow.ip_address}:
|
||||
{connectDetailRow.port}/{connectDetailRow.protocol}
|
||||
{connectDetailRow.connect_tested_at
|
||||
? ` · ${formatSystemTime(connectDetailRow.connect_tested_at, { assumeUtcNaive: true })}`
|
||||
: ""}
|
||||
</p>
|
||||
<p>
|
||||
<span className={`conn-pill conn-pill--${connectPillLevel(connectDetailRow.connect_status)}`}>
|
||||
{connectDetailRow.connect_status}
|
||||
</span>
|
||||
{connectDetailRow.connect_message ? (
|
||||
<span className="connect-detail-summary"> — {connectDetailRow.connect_message}</span>
|
||||
) : null}
|
||||
</p>
|
||||
<pre className="connect-log">
|
||||
{connectDetailRow.connect_detail?.trim() ||
|
||||
connectDetailRow.connect_message?.trim() ||
|
||||
t("managedNe.connectDetailEmpty")}
|
||||
</pre>
|
||||
<div className="modal__actions">
|
||||
<button
|
||||
type="button"
|
||||
disabled={connectMutation.isPending}
|
||||
onClick={() => {
|
||||
connectMutation.mutate([connectDetailRow.id]);
|
||||
}}
|
||||
>
|
||||
{connectMutation.isPending ? t("managedNe.connect.running") : t("managedNe.connect.retest")}
|
||||
</button>
|
||||
<button type="button" onClick={() => setConnectDetailRow(null)}>
|
||||
{t("managedNe.form.cancel")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -132,6 +132,7 @@ export type ManagedNeItem = {
|
|||
username: string;
|
||||
connect_status: ConnectStatus;
|
||||
connect_message: string;
|
||||
connect_detail: string;
|
||||
connect_tested_at: string | null;
|
||||
tags: string;
|
||||
remark: string;
|
||||
|
|
|
|||
|
|
@ -1,26 +1,43 @@
|
|||
import { isAutoHopTemplate, zteHopTemplate } from "./zteHop";
|
||||
/** Jump-host (hop) templates per vendor. */
|
||||
|
||||
export type HopVendor = "zte" | "linux";
|
||||
import { ciscoHopTemplate, huaweiHopTemplate, isAutoHopTemplate, zteHopTemplate } from "./zteHop";
|
||||
|
||||
export const HOP_VENDORS: HopVendor[] = ["zte", "linux"];
|
||||
export type HopVendor = "zte" | "huawei" | "cisco" | "linux";
|
||||
|
||||
export const HOP_VENDORS: HopVendor[] = ["zte", "huawei", "cisco", "linux"];
|
||||
|
||||
export function isLinuxHopVendor(vendor: string): boolean {
|
||||
return String(vendor || "").toLowerCase() === "linux";
|
||||
}
|
||||
|
||||
export function isCliHopVendor(vendor: string): boolean {
|
||||
const v = String(vendor || "").toLowerCase();
|
||||
return v === "zte" || v === "huawei" || v === "cisco";
|
||||
}
|
||||
|
||||
export function defaultHopTemplate(vendor: string, protocol: string, vrf: string): string {
|
||||
const v = String(vendor || "zte").toLowerCase();
|
||||
if (v === "huawei") return huaweiHopTemplate(protocol, vrf);
|
||||
if (v === "cisco") return ciscoHopTemplate(protocol, vrf);
|
||||
if (v === "linux") return "";
|
||||
return zteHopTemplate(protocol, vrf);
|
||||
}
|
||||
|
||||
export function patchHopVendorChange(
|
||||
vendor: HopVendor,
|
||||
prev: { hop_protocol: string; hop_vrf: string; hop_command_template: string },
|
||||
prev: { hop_protocol: string; hop_vrf: string; hop_command_template: string; hop_vendor?: string },
|
||||
): { hop_vendor: HopVendor; hop_protocol: string; hop_vrf: string; hop_command_template: string } {
|
||||
if (vendor === "linux") {
|
||||
return { hop_vendor: "linux", hop_protocol: "ssh", hop_vrf: "", hop_command_template: "" };
|
||||
}
|
||||
const protocol = prev.hop_protocol || "ssh";
|
||||
const vrf = prev.hop_vrf || "";
|
||||
return {
|
||||
hop_vendor: "zte",
|
||||
hop_protocol: prev.hop_protocol || "ssh",
|
||||
hop_vrf: prev.hop_vrf,
|
||||
hop_command_template: zteHopTemplate(prev.hop_protocol || "ssh", prev.hop_vrf),
|
||||
hop_vendor: vendor,
|
||||
hop_protocol: protocol,
|
||||
hop_vrf: vrf,
|
||||
hop_command_template: defaultHopTemplate(vendor, protocol, vrf),
|
||||
};
|
||||
}
|
||||
|
||||
export { isAutoHopTemplate, zteHopTemplate };
|
||||
export { ciscoHopTemplate, huaweiHopTemplate, isAutoHopTemplate, zteHopTemplate };
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/** ZTE device CLI jump commands: ssh/telnet <ip> [vrf <name>]. */
|
||||
/** Vendor CLI jump command templates (placeholders). */
|
||||
|
||||
const LEGACY_HOP_TEMPLATES = new Set([
|
||||
"ssh {target_user}@{target_ip}",
|
||||
|
|
@ -12,8 +12,35 @@ export function zteHopTemplate(protocol: string, vrf: string): string {
|
|||
return v ? `${cmd} {target_ip} vrf {vrf}` : `${cmd} {target_ip}`;
|
||||
}
|
||||
|
||||
export function isAutoHopTemplate(template: string, protocol: string, vrf: string): boolean {
|
||||
/** Cisco: ssh -vrf VRF IP; telnet IP [/vrf VRF]. */
|
||||
export function ciscoHopTemplate(protocol: string, vrf: string): string {
|
||||
const v = String(vrf || "").trim();
|
||||
if (String(protocol || "ssh").toLowerCase() === "telnet") {
|
||||
return v ? `telnet {target_ip} /vrf {vrf}` : `telnet {target_ip}`;
|
||||
}
|
||||
return v ? `ssh -vrf {vrf} {target_ip}` : `ssh {target_ip}`;
|
||||
}
|
||||
|
||||
/** Huawei: telnet [vpn-instance VRF] IP; SSH uses stelnet. */
|
||||
export function huaweiHopTemplate(protocol: string, vrf: string): string {
|
||||
const v = String(vrf || "").trim();
|
||||
if (String(protocol || "ssh").toLowerCase() === "telnet") {
|
||||
return v ? `telnet vpn-instance {vrf} {target_ip}` : `telnet {target_ip}`;
|
||||
}
|
||||
return v ? `stelnet {target_ip} -vpn-instance {vrf}` : `stelnet {target_ip}`;
|
||||
}
|
||||
|
||||
export function isAutoHopTemplate(
|
||||
template: string,
|
||||
vendor: string,
|
||||
protocol: string,
|
||||
vrf: string,
|
||||
): boolean {
|
||||
const t = String(template || "").trim();
|
||||
if (!t || LEGACY_HOP_TEMPLATES.has(t)) return true;
|
||||
const v = String(vendor || "zte").toLowerCase();
|
||||
if (v === "huawei") return t === huaweiHopTemplate(protocol, vrf);
|
||||
if (v === "cisco") return t === ciscoHopTemplate(protocol, vrf);
|
||||
if (v === "linux") return t === "";
|
||||
return t === zteHopTemplate(protocol, vrf);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue