mirror of
https://github.com/hansjone/netx.git
synced 2026-10-10 11:20:48 +08:00
feat(cli): UME lazy exec via shared profiles and ume_ne_id MCP support
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
ba1a40f725
commit
38a1c7f3a9
21 changed files with 1504 additions and 54 deletions
265
web/src/components/UmeCliConnectPanel.tsx
Normal file
265
web/src/components/UmeCliConnectPanel.tsx
Normal file
|
|
@ -0,0 +1,265 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
apiDelete,
|
||||
apiPatch,
|
||||
apiPost,
|
||||
fetchCliMeta,
|
||||
fetchCliProfiles,
|
||||
fetchManagedNeMeta,
|
||||
postUmeConnectTest,
|
||||
} from "../services/api";
|
||||
import { HopProxyFields, emptyHopProxyFields, type HopProxyFieldsState } from "./HopProxyFields";
|
||||
import { HelpHint } from "./HelpHint";
|
||||
import { queryKeys } from "../constants/queryKeys";
|
||||
import { useI18n } from "../i18n";
|
||||
import { useToast } from "../hooks/useToast";
|
||||
import type { CliConnectProfileItem } from "../types";
|
||||
|
||||
type ProfileForm = {
|
||||
id: string;
|
||||
name: string;
|
||||
username: string;
|
||||
password: string;
|
||||
port: number;
|
||||
protocol: string;
|
||||
device_type_default: string;
|
||||
vendor_default: string;
|
||||
is_default: boolean;
|
||||
hop: HopProxyFieldsState;
|
||||
};
|
||||
|
||||
const emptyForm = (): ProfileForm => ({
|
||||
id: "",
|
||||
name: "default",
|
||||
username: "",
|
||||
password: "",
|
||||
port: 22,
|
||||
protocol: "ssh",
|
||||
device_type_default: "zte_zxros",
|
||||
vendor_default: "ZTE",
|
||||
is_default: true,
|
||||
hop: { ...emptyHopProxyFields(), hop_vendor: "bastion", hop_port: 22 },
|
||||
});
|
||||
|
||||
function profileToForm(row: CliConnectProfileItem): ProfileForm {
|
||||
return {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
username: row.username,
|
||||
password: "",
|
||||
port: row.port,
|
||||
protocol: row.protocol,
|
||||
device_type_default: row.device_type_default,
|
||||
vendor_default: row.vendor_default,
|
||||
is_default: row.is_default,
|
||||
hop: {
|
||||
hop_vendor: row.hop_vendor as HopProxyFieldsState["hop_vendor"],
|
||||
hop_host: row.hop_host,
|
||||
hop_port: row.hop_port,
|
||||
hop_protocol: row.hop_protocol,
|
||||
hop_username: row.hop_username,
|
||||
hop_password: "",
|
||||
hop_command_template: row.hop_command_template,
|
||||
hop_vrf: row.hop_vrf,
|
||||
hop_target_auth_mode: row.hop_target_auth_mode,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function UmeCliConnectPanel() {
|
||||
const { t } = useI18n();
|
||||
const { showOk, showError } = useToast();
|
||||
const queryClient = useQueryClient();
|
||||
const [form, setForm] = useState<ProfileForm>(emptyForm);
|
||||
const [sampleUmeNeId, setSampleUmeNeId] = useState("");
|
||||
|
||||
const metaQuery = useQuery({ queryKey: queryKeys.cliMeta, queryFn: fetchCliMeta });
|
||||
const neMetaQuery = useQuery({ queryKey: queryKeys.managedNeMeta, queryFn: fetchManagedNeMeta });
|
||||
const profilesQuery = useQuery({ queryKey: queryKeys.cliProfiles, queryFn: fetchCliProfiles });
|
||||
|
||||
useEffect(() => {
|
||||
const items = profilesQuery.data?.items || [];
|
||||
const current = items.find((x) => x.id === form.id);
|
||||
const picked = current || items.find((x) => x.is_default) || items[0];
|
||||
if (picked && (!form.id || !current)) {
|
||||
setForm(profileToForm(picked));
|
||||
}
|
||||
}, [profilesQuery.data, form.id]);
|
||||
|
||||
const saveMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
const body = {
|
||||
name: form.name.trim(),
|
||||
username: form.username.trim(),
|
||||
password: form.password || undefined,
|
||||
port: form.port,
|
||||
protocol: form.protocol,
|
||||
device_type_default: form.device_type_default,
|
||||
vendor_default: form.vendor_default,
|
||||
is_default: form.is_default,
|
||||
hop_enabled: form.hop.hop_vendor !== "linux" ? Boolean(form.hop.hop_host.trim()) : true,
|
||||
hop_vendor: form.hop.hop_vendor,
|
||||
hop_host: form.hop.hop_host,
|
||||
hop_port: form.hop.hop_port,
|
||||
hop_protocol: form.hop.hop_protocol,
|
||||
hop_username: form.hop.hop_username,
|
||||
hop_password: form.hop.hop_password || undefined,
|
||||
hop_command_template: form.hop.hop_command_template,
|
||||
hop_vrf: form.hop.hop_vrf,
|
||||
hop_target_auth_mode: form.hop.hop_target_auth_mode,
|
||||
};
|
||||
if (form.id) {
|
||||
return apiPatch<CliConnectProfileItem>(`/v1/cli/profiles/${form.id}`, body);
|
||||
}
|
||||
return apiPost<CliConnectProfileItem>("/v1/cli/profiles", { ...body, password: form.password });
|
||||
},
|
||||
onSuccess: async (row) => {
|
||||
setForm(profileToForm(row));
|
||||
await queryClient.invalidateQueries({ queryKey: queryKeys.cliProfiles });
|
||||
await queryClient.invalidateQueries({ queryKey: queryKeys.cliMeta });
|
||||
showOk(t("ume.cli.saved"));
|
||||
},
|
||||
onError: (e: Error) => showError(e.message),
|
||||
});
|
||||
|
||||
const connectTestMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
const id = sampleUmeNeId.trim();
|
||||
if (!id) throw new Error(t("ume.cli.sampleNeRequired"));
|
||||
return postUmeConnectTest([id]);
|
||||
},
|
||||
onSuccess: () => {
|
||||
showOk(t("ume.cli.connectTestSubmitted"));
|
||||
},
|
||||
onError: (e: Error) => showError(e.message),
|
||||
});
|
||||
|
||||
const deviceTypes = neMetaQuery.data?.device_types ?? [];
|
||||
const vendors = neMetaQuery.data?.vendors ?? [];
|
||||
const cliReady = Boolean(metaQuery.data?.cli_profile_ready);
|
||||
|
||||
return (
|
||||
<section className="panel">
|
||||
<div className="panel__toolbar">
|
||||
<h2>{t("ume.cli.title")}</h2>
|
||||
<HelpHint text={t("ume.cli.hint")} />
|
||||
</div>
|
||||
<p className="form-field-hint" style={{ marginBottom: 12 }}>
|
||||
{cliReady ? t("ume.cli.statusReady") : t("ume.cli.statusNotReady")}
|
||||
</p>
|
||||
<div className="form-grid" style={{ maxWidth: 960 }}>
|
||||
<label>
|
||||
<span className="form-label">{t("ume.cli.profileName")}</span>
|
||||
<input value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} />
|
||||
</label>
|
||||
<label>
|
||||
<span className="form-label">{t("ume.cli.targetUsername")}</span>
|
||||
<input
|
||||
required
|
||||
value={form.username}
|
||||
onChange={(e) => setForm({ ...form, username: e.target.value })}
|
||||
placeholder="ca-oper"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span className="form-label">{t("ume.cli.targetPassword")}</span>
|
||||
<input
|
||||
type="password"
|
||||
value={form.password}
|
||||
onChange={(e) => setForm({ ...form, password: e.target.value })}
|
||||
placeholder={form.id ? t("ume.cli.passwordKeep") : ""}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span className="form-label">{t("managedNe.form.deviceType")}</span>
|
||||
<select
|
||||
value={form.device_type_default}
|
||||
onChange={(e) => setForm({ ...form, device_type_default: e.target.value })}
|
||||
>
|
||||
{deviceTypes.map((dt) => (
|
||||
<option key={dt} value={dt}>
|
||||
{dt}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span className="form-label">{t("managedNe.form.vendor")}</span>
|
||||
<select value={form.vendor_default} onChange={(e) => setForm({ ...form, vendor_default: e.target.value })}>
|
||||
{vendors.map((v) => (
|
||||
<option key={v} value={v}>
|
||||
{v}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<HopProxyFields
|
||||
value={form.hop}
|
||||
onChange={(hop) => setForm({ ...form, hop })}
|
||||
hopPasswordRequired={!form.id}
|
||||
hopPasswordOptional={Boolean(form.id)}
|
||||
/>
|
||||
<div className="filter-inline" style={{ marginTop: 16 }}>
|
||||
<button type="button" onClick={() => saveMutation.mutate()} disabled={saveMutation.isPending}>
|
||||
{saveMutation.isPending ? t("managedNe.form.saving") : t("managedNe.form.save")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setForm(emptyForm());
|
||||
}}
|
||||
>
|
||||
{t("ume.cli.newProfile")}
|
||||
</button>
|
||||
{form.id ? (
|
||||
<button
|
||||
type="button"
|
||||
className="danger-btn"
|
||||
onClick={async () => {
|
||||
if (!window.confirm(t("ume.cli.deleteConfirm"))) return;
|
||||
try {
|
||||
await apiDelete(`/v1/cli/profiles/${form.id}`);
|
||||
setForm(emptyForm());
|
||||
await queryClient.invalidateQueries({ queryKey: queryKeys.cliProfiles });
|
||||
showOk(t("ume.cli.deleted"));
|
||||
} catch (e) {
|
||||
showError(e instanceof Error ? e.message : String(e));
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t("managedNe.delete")}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="panel" style={{ marginTop: 20 }}>
|
||||
<h3>{t("ume.cli.connectTestTitle")}</h3>
|
||||
<div className="filter-inline">
|
||||
<input
|
||||
value={sampleUmeNeId}
|
||||
onChange={(e) => setSampleUmeNeId(e.target.value)}
|
||||
placeholder={t("ume.cli.sampleNePh")}
|
||||
style={{ minWidth: 320 }}
|
||||
/>
|
||||
<button type="button" onClick={() => connectTestMutation.mutate()} disabled={connectTestMutation.isPending}>
|
||||
{t("managedNe.connect.run")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{(profilesQuery.data?.items || []).length > 1 ? (
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<span className="form-label">{t("ume.cli.existingProfiles")}</span>
|
||||
<div className="filter-inline">
|
||||
{(profilesQuery.data?.items || []).map((p) => (
|
||||
<button key={p.id} type="button" className="link-btn" onClick={() => setForm(profileToForm(p))}>
|
||||
{p.name}
|
||||
{p.is_default ? ` (${t("ume.cli.default")})` : ""}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
|
@ -32,6 +32,10 @@ export const queryKeys = {
|
|||
neCollectionRunsAll: ["neCollectionRuns"] as const,
|
||||
neCollectionRuns: (jobId: string, page: number, status: string, keyword: string) =>
|
||||
["neCollectionRuns", jobId, page, status, keyword] as const,
|
||||
cliMeta: ["cliMeta"] as const,
|
||||
cliProfiles: ["cliProfiles"] as const,
|
||||
cliTargets: (keyword: string, page: number, pageSize: number) =>
|
||||
["cliTargets", keyword, page, pageSize] as const,
|
||||
umeCurrentAlarms: (
|
||||
severity: string,
|
||||
cleared: string,
|
||||
|
|
|
|||
|
|
@ -465,6 +465,31 @@ const en = {
|
|||
clearTitle: "Clear keyword and reset to page 1",
|
||||
expand: "Expand details",
|
||||
collapse: "Collapse details",
|
||||
cliConnect: "CLI connect",
|
||||
},
|
||||
tabs: {
|
||||
main: "UME sync",
|
||||
cli: "CLI connect",
|
||||
},
|
||||
cli: {
|
||||
title: "UME CLI connection profile",
|
||||
hint: "Unified SSH credentials and hop/bastion for UME inventory NEs. OClaw MCP can exec show commands via ume_ne_id without per-device managed_ne entries.",
|
||||
statusReady: "CLI profile is ready for UME NE command execution.",
|
||||
statusNotReady: "Set target username and hop/bastion settings (if enabled).",
|
||||
profileName: "Profile name",
|
||||
targetUsername: "Target SSH username",
|
||||
targetPassword: "Target SSH password",
|
||||
passwordKeep: "Leave blank to keep unchanged",
|
||||
newProfile: "New profile",
|
||||
deleteConfirm: "Delete this CLI profile?",
|
||||
deleted: "Profile deleted",
|
||||
saved: "CLI profile saved",
|
||||
connectTestTitle: "Sample NE connectivity test",
|
||||
sampleNePh: "ume_ne_id (copy from NE list below)",
|
||||
sampleNeRequired: "ume_ne_id is required",
|
||||
connectTestSubmitted: "Connectivity test submitted",
|
||||
existingProfiles: "Existing profiles",
|
||||
default: "default",
|
||||
},
|
||||
alarms: {
|
||||
title: "Current alarms",
|
||||
|
|
|
|||
|
|
@ -463,6 +463,31 @@ const zh = {
|
|||
clearTitle: "清空 keyword,回到第 1 页",
|
||||
expand: "展开详情",
|
||||
collapse: "收起详情",
|
||||
cliConnect: "CLI 连通",
|
||||
},
|
||||
tabs: {
|
||||
main: "UME 同步",
|
||||
cli: "CLI 连接",
|
||||
},
|
||||
cli: {
|
||||
title: "UME CLI 连接配置",
|
||||
hint: "为 UME 清单网元配置统一 SSH 凭据与跳板/堡垒机;OClaw MCP 可通过 ume_ne_id 直接执行 show 命令,无需逐台录入 managed_ne。",
|
||||
statusReady: "CLI 配置已就绪,可对 UME 网元执行命令。",
|
||||
statusNotReady: "请填写目标用户名,并配置跳板/堡垒机(若启用)。",
|
||||
profileName: "配置名称",
|
||||
targetUsername: "目标 SSH 用户名",
|
||||
targetPassword: "目标 SSH 密码",
|
||||
passwordKeep: "留空则不修改",
|
||||
newProfile: "新建配置",
|
||||
deleteConfirm: "确定删除该 CLI 配置?",
|
||||
deleted: "配置已删除",
|
||||
saved: "CLI 配置已保存",
|
||||
connectTestTitle: "样本网元连通性测试",
|
||||
sampleNePh: "ume_ne_id(从下方网元清单复制)",
|
||||
sampleNeRequired: "请填写 ume_ne_id",
|
||||
connectTestSubmitted: "已提交连通性测试",
|
||||
existingProfiles: "已有配置",
|
||||
default: "默认",
|
||||
},
|
||||
alarms: {
|
||||
title: "当前告警",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Fragment, useState } from "react";
|
||||
import { Fragment, useMemo, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
apiPost,
|
||||
|
|
@ -15,6 +15,7 @@ import {
|
|||
fetchUmeNotificationIds,
|
||||
fetchUmeSyncStatus,
|
||||
fetchUmeTokenStatus,
|
||||
fetchCliTargets,
|
||||
refreshUmeToken,
|
||||
upsertUmeKeyAlertRule,
|
||||
deleteUmeKeyAlertRule,
|
||||
|
|
@ -22,6 +23,7 @@ import {
|
|||
updateUmeKeyAlertMonitorConfig,
|
||||
} from "../services/api";
|
||||
import { HelpHint } from "../components/HelpHint";
|
||||
import { UmeCliConnectPanel } from "../components/UmeCliConnectPanel";
|
||||
import { queryKeys } from "../constants/queryKeys";
|
||||
import { useI18n } from "../i18n";
|
||||
import { useToast } from "../hooks/useToast";
|
||||
|
|
@ -67,6 +69,7 @@ export function UmePage() {
|
|||
const [keyAlertNeTypes, setKeyAlertNeTypes] = useState<string[]>([]);
|
||||
const [keyAlertEditRule, setKeyAlertEditRule] = useState<UmeKeyAlertRuleItem | null>(null);
|
||||
const [keyAlertEditNeTypes, setKeyAlertEditNeTypes] = useState<string[]>([]);
|
||||
const [umeViewTab, setUmeViewTab] = useState<"main" | "cli">("main");
|
||||
|
||||
const syncMutation = useMutation({
|
||||
mutationFn: async (domains: string[]) => apiPost<{ ok: boolean; jobs: unknown[] }>("/v1/ume/sync", { domains }),
|
||||
|
|
@ -254,6 +257,20 @@ export function UmePage() {
|
|||
queryFn: () => fetchUmeNe({ keyword: neKeyword, page: nePage, pageSize: nePageSize }),
|
||||
staleTime: 5000,
|
||||
});
|
||||
const cliTargetsQuery = useQuery({
|
||||
queryKey: queryKeys.cliTargets(neKeyword, nePage, nePageSize),
|
||||
queryFn: () =>
|
||||
fetchCliTargets({ source: "ume", keyword: neKeyword, page: nePage, pageSize: nePageSize }),
|
||||
enabled: nePanelOpen && umeViewTab === "main",
|
||||
staleTime: 5000,
|
||||
});
|
||||
const cliStatusByNeId = useMemo(() => {
|
||||
const map = new Map<string, string>();
|
||||
for (const row of cliTargetsQuery.data?.items || []) {
|
||||
map.set(row.id, row.connect_status);
|
||||
}
|
||||
return map;
|
||||
}, [cliTargetsQuery.data]);
|
||||
const currentQuery = useQuery({
|
||||
queryKey: queryKeys.umeCurrentAlarms(curSeverity, curCleared, curHostName, curKeyword, curPage, curPageSize),
|
||||
queryFn: () =>
|
||||
|
|
@ -442,6 +459,30 @@ export function UmePage() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<section className="panel" style={{ marginBottom: 12 }}>
|
||||
<div className="filter-inline">
|
||||
<button
|
||||
type="button"
|
||||
className={umeViewTab === "main" ? "link-btn" : "link-btn"}
|
||||
style={{ fontWeight: umeViewTab === "main" ? 700 : 400 }}
|
||||
onClick={() => setUmeViewTab("main")}
|
||||
>
|
||||
{t("ume.tabs.main")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="link-btn"
|
||||
style={{ fontWeight: umeViewTab === "cli" ? 700 : 400 }}
|
||||
onClick={() => setUmeViewTab("cli")}
|
||||
>
|
||||
{t("ume.tabs.cli")}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
{umeViewTab === "cli" ? (
|
||||
<UmeCliConnectPanel />
|
||||
) : (
|
||||
<>
|
||||
<section className="cards">
|
||||
<article className="card card--full">
|
||||
<h3>{t("ume.token.title")}</h3>
|
||||
|
|
@ -1245,6 +1286,7 @@ export function UmePage() {
|
|||
<th>type</th>
|
||||
<th>device_level</th>
|
||||
<th>host_name</th>
|
||||
<th>{t("ume.ne.cliConnect")}</th>
|
||||
<th>hw_ver</th>
|
||||
<th>last_seen</th>
|
||||
</tr>
|
||||
|
|
@ -1267,12 +1309,13 @@ export function UmePage() {
|
|||
<td>{x.ne_type}</td>
|
||||
<td>{x.device_level || t("common.empty")}</td>
|
||||
<td>{x.host_name || t("common.empty")}</td>
|
||||
<td>{cliStatusByNeId.get(x.ne_id) || "unknown"}</td>
|
||||
<td>{x.hardware_version || t("common.empty")}</td>
|
||||
<td>{x.last_seen_at ? formatSystemTime(x.last_seen_at) : t("common.empty")}</td>
|
||||
</tr>
|
||||
{expandedNeId === x.ne_id ? (
|
||||
<tr>
|
||||
<td colSpan={8}>
|
||||
<td colSpan={9}>
|
||||
<div style={{ fontSize: 12, display: "grid", gridTemplateColumns: "repeat(3, minmax(180px, 1fr))", gap: 8 }}>
|
||||
<div>consistent_state: {x.consistent_state || t("common.empty")}</div>
|
||||
<div>admin_status: {x.admin_status || t("common.empty")}</div>
|
||||
|
|
@ -1511,5 +1554,7 @@ export function UmePage() {
|
|||
</div>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,9 @@ import type {
|
|||
UmeNeItem,
|
||||
UmeSyncStatusResponse,
|
||||
UmeTokenStatus,
|
||||
CliConnectProfileItem,
|
||||
CliMeta,
|
||||
CliTargetListResponse,
|
||||
} from "../types";
|
||||
|
||||
const parseApiResponse = async (res: Response): Promise<Record<string, unknown>> => {
|
||||
|
|
@ -306,6 +309,28 @@ export const collectionRunDownloadUrl = (runId: string) => `/v1/ne-collections/r
|
|||
|
||||
export const collectionJobDownloadUrl = (jobId: string) => `/v1/ne-collections/${jobId}/download`;
|
||||
|
||||
export const fetchCliMeta = () => apiGet<CliMeta>("/v1/cli/meta");
|
||||
|
||||
export const fetchCliProfiles = () =>
|
||||
apiGet<{ items: CliConnectProfileItem[] }>("/v1/cli/profiles");
|
||||
|
||||
export const fetchCliTargets = (params: {
|
||||
source?: "managed" | "ume" | "all";
|
||||
keyword?: string;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
}) => {
|
||||
const p = new URLSearchParams();
|
||||
p.set("source", params.source || "all");
|
||||
if (params.keyword?.trim()) p.set("keyword", params.keyword.trim());
|
||||
p.set("page", String(Math.max(1, Number(params.page || 1))));
|
||||
p.set("page_size", String(Math.max(1, Math.min(500, Number(params.pageSize || 50)))));
|
||||
return apiGet<CliTargetListResponse>(`/v1/cli/targets?${p.toString()}`);
|
||||
};
|
||||
|
||||
export const postUmeConnectTest = (umeNeIds: string[]) =>
|
||||
apiPost<{ ok: boolean; submitted: number }>("/v1/cli/ume-connect-test", { ume_ne_ids: umeNeIds });
|
||||
|
||||
export const fetchUmeCurrentAlarms = (params: {
|
||||
severity: string;
|
||||
isCleared: string;
|
||||
|
|
|
|||
|
|
@ -219,6 +219,55 @@ export type ManagedNeImportResult = {
|
|||
failed: Array<{ row: number; reason: string }>;
|
||||
};
|
||||
|
||||
export type CliConnectProfileItem = {
|
||||
id: string;
|
||||
name: string;
|
||||
is_default: boolean;
|
||||
username: string;
|
||||
port: number;
|
||||
protocol: string;
|
||||
device_type_default: string;
|
||||
vendor_default: string;
|
||||
ne_type_rules: string;
|
||||
hop_enabled: boolean;
|
||||
hop_vendor: string;
|
||||
hop_host: string;
|
||||
hop_port: number;
|
||||
hop_protocol: string;
|
||||
hop_username: string;
|
||||
hop_command_template: string;
|
||||
hop_vrf: string;
|
||||
hop_target_auth_mode: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
export type CliMeta = {
|
||||
credentials_configured: boolean;
|
||||
default_profile_configured: boolean;
|
||||
cli_profile_ready: boolean;
|
||||
};
|
||||
|
||||
export type CliTargetItem = {
|
||||
source: string;
|
||||
id: string;
|
||||
ume_ne_id?: string | null;
|
||||
name: string;
|
||||
ip_address: string;
|
||||
ne_type?: string;
|
||||
vendor?: string;
|
||||
device_type?: string;
|
||||
connect_status: string;
|
||||
cli_profile_ready?: boolean;
|
||||
};
|
||||
|
||||
export type CliTargetListResponse = {
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
items: CliTargetItem[];
|
||||
};
|
||||
|
||||
export type EligibleNeItem = {
|
||||
id: string;
|
||||
name: string;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue