diff --git a/.vscodeignore b/.vscodeignore index 150497379..de60e02af 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -35,4 +35,7 @@ test.clab.yml #clab files clab-*/ -*.clab.yml* \ No newline at end of file +*.clab.yml* + +#others +.claude/** \ No newline at end of file diff --git a/README.md b/README.md index 2a182c2f3..ec18669cf 100644 --- a/README.md +++ b/README.md @@ -90,11 +90,7 @@ Customize your experience under `containerlab.*` in VS Code Settings: Map a node’s `kind` to its preferred exec command (e.g. `{ "nokia_srlinux": "sr_cli" }`). - **`containerlab.node.sshUserMapping`** (object) - Map a node’s `kind` to its preferred ssh user (e.g. `{ "nokia_srlinux": "clab" }`). - -- **`containerlab.wsl.wiresharkPath`** (string) - Path to Wireshark in Windows from inside WSL. - _Default: `/mnt/c/Program Files/Wireshark/wireshark.exe`_ + Map a node's `kind` to its preferred ssh user (e.g. `{ "nokia_srlinux": "clab" }`). - **`containerlab.remote.hostname`** (string) Hostname or IP used for remote connections (affects packet capture). diff --git a/package.json b/package.json index 9b9c09183..37783ff35 100644 --- a/package.json +++ b/package.json @@ -305,6 +305,10 @@ "command": "containerlab.interface.captureWithEdgeshark", "title": "Capture interface (Edgeshark)" }, + { + "command": "containerlab.interface.captureWithEdgesharkVNC", + "title": "Capture interface (Edgeshark VNC)" + }, { "command": "containerlab.interface.setDelay", "title": "Set delay" @@ -339,6 +343,11 @@ "title": "Uninstall Edgeshark", "category": "Containerlab" }, + { + "command": "containerlab.capture.killAllWiresharkVNC", + "title": "Kill all Wireshark VNC containers", + "category": "Containerlab" + }, { "command": "containerlab.set.sessionHostname", "title": "Configure hostname for this session", @@ -823,6 +832,11 @@ "when": "viewItem == containerlabInterfaceUp", "group": "captureContextMenu@1" }, + { + "command": "containerlab.interface.captureWithEdgesharkVNC", + "when": "viewItem == containerlabInterfaceUp", + "group": "captureContextMenu@2" + }, { "command": "containerlab.interface.setDelay", "when": "viewItem == containerlabInterfaceUp", @@ -1059,11 +1073,6 @@ "default": {}, "markdownDescription": "Custom SSH users for different node kinds. Enter the mapping between the kind and SSH username.\n\nFor example: `{\"nokia_srlinux\": \"clab\"}` means that `ssh clab@` will be used if `` is the `nokia_srlinux` kind." }, - "containerlab.wsl.wiresharkPath": { - "type": "string", - "default": "/mnt/c/Program Files/Wireshark/wireshark.exe", - "markdownDescription": "The path to the wireshark executable on windows from inside WSL. The default path is `/mnt/c/Program Files/Wireshark/wireshark.exe`." - }, "containerlab.remote.hostname": { "type": "string", "default": "", @@ -1123,6 +1132,50 @@ "type": "string", "default": "", "description": "Additional docker (or podman) arguments to append to the fcli command" + }, + "containerlab.capture.preferredAction": { + "type": "string", + "default": "Wireshark VNC", + "enum": [ + "Edgeshark", + "Wireshark VNC" + ], + "description": "The preferred capture method when using the capture interface quick action on the interface tree item" + }, + "containerlab.capture.wireshark.dockerImage": { + "type": "string", + "default": "ghcr.io/kaelemc/wireshark-vnc-docker:latest", + "description": "The docker image to use for Wireshark/Edgeshark VNC capture. Requires full image name + tag" + }, + "containerlab.capture.wireshark.pullPolicy": { + "type": "string", + "default": "always", + "enum": [ + "always", + "missing", + "never" + ], + "description": "The pull policy of the Wireshark docker image" + }, + "containerlab.capture.wireshark.extraDockerArgs": { + "type": "string", + "default": "", + "description": "Extra arguments to pass to the run command for the wireshark VNC container. Useful for things like bind mounts etc." + }, + "containerlab.capture.wireshark.theme": { + "type": "string", + "default": "Follow VS Code theme", + "enum": [ + "Follow VS Code theme", + "Dark", + "Light" + ], + "description": "The theme, or colour scheme of the wireshark application." + }, + "containerlab.capture.wireshark.stayOpenInBackground": { + "type": "boolean", + "default": "true", + "description": "Keep Wireshark VNC sessions alive, even when the capture tab is not active. Enabling this will consume more memory on both the client and remote containerlab host system." } } } diff --git a/src/commands/capture.ts b/src/commands/capture.ts index eff95d5ce..122e8c902 100644 --- a/src/commands/capture.ts +++ b/src/commands/capture.ts @@ -1,146 +1,118 @@ import * as vscode from "vscode" import { execSync } from "child_process"; -import { runWithSudo } from "../helpers/containerlabUtils"; import { outputChannel } from "../extension"; import * as utils from "../utils"; import { ClabInterfaceTreeNode } from "../treeView/common"; +import { EDGESHARK_INSTALL_CMD } from "./edgeshark"; let sessionHostname: string = ""; /** * Begin packet capture on an interface. - * - If remoteName = ssh-remote, we always do edgeshark/packetflix. - * - If on OrbStack (Mac), we also do edgeshark because netns approach doesn't work well on macOS. - * - Otherwise, we spawn tcpdump + Wireshark locally (or in WSL). */ export async function captureInterface(node: ClabInterfaceTreeNode) { - if (!node) { - return vscode.window.showErrorMessage("No interface to capture found."); - } - - outputChannel.appendLine(`[DEBUG] captureInterface() called for node=${node.parentName}, interface=${node.name}`); - outputChannel.appendLine(`[DEBUG] remoteName = ${vscode.env.remoteName || "(none)"}; isOrbstack=${utils.isOrbstack()}`); - - // SSH-remote => use edgeshark/packetflix - if (vscode.env.remoteName === "ssh-remote") { - outputChannel.appendLine("[DEBUG] In SSH-Remote environment → captureInterfaceWithPacketflix()"); - return captureInterfaceWithPacketflix(node); - } - - // On OrbStack macOS, netns is typically not workable => edgeshark - if (utils.isOrbstack()) { - outputChannel.appendLine("[DEBUG] Detected OrbStack environment → captureInterfaceWithPacketflix()"); - return captureInterfaceWithPacketflix(node); - } - - // Otherwise, we do local capture with tcpdump|Wireshark - const captureCmd = `ip netns exec ${node.parentName} tcpdump -U -nni ${node.name} -w -`; - const wifiCmd = await resolveWiresharkCommand(); - const finalCmd = `${captureCmd} | ${wifiCmd} -k -i -`; + if (!node) { + return vscode.window.showErrorMessage("No interface to capture found."); + } - outputChannel.appendLine(`[DEBUG] Attempting local capture with command:\n ${finalCmd}`); + outputChannel.appendLine(`[DEBUG] captureInterface() called for node=${node.parentName}, interface=${node.name}`); + outputChannel.appendLine(`[DEBUG] remoteName = ${vscode.env.remoteName || "(none)"}; isOrbstack=${utils.isOrbstack()}`); - vscode.window.showInformationMessage(`Starting capture on ${node.parentName}/${node.name}... check "Containerlab" output for logs.`); + // Settings override + const preferredCaptureMethod = vscode.workspace.getConfiguration("containerlab").get("capture.preferredAction"); + switch (preferredCaptureMethod) { + case "Edgeshark": + return captureInterfaceWithPacketflix(node); + case "Wireshark VNC": + return captureEdgesharkVNC(node); + } - runCaptureWithPipe(finalCmd, node.parentName, node.name); + // Default to VNC capture + return captureEdgesharkVNC(node); } -/** - * Spawn Wireshark or Wireshark.exe in WSL. - */ -async function resolveWiresharkCommand(): Promise { - if (vscode.env.remoteName === "wsl") { - const cfgWiresharkPath = vscode.workspace - .getConfiguration("containerlab") - .get("wsl.wiresharkPath", "/mnt/c/Program Files/Wireshark/wireshark.exe"); - return `"${cfgWiresharkPath}"`; - } - return "wireshark"; -} -/** - * Actually run the pipeline with sudo if needed. No extra 'bash -c' here; - * let runWithSudo handle the quoting. - */ -function runCaptureWithPipe(pipeCmd: string, parentName: string, ifName: string) { - outputChannel.appendLine(`[DEBUG] runCaptureWithPipe() => runWithSudo(command=${pipeCmd})`); - - runWithSudo( - pipeCmd, - `TCPDump capture on ${parentName}/${ifName}`, - outputChannel, - "generic" - ) - .then(() => { - outputChannel.appendLine("[DEBUG] Capture process completed or exited"); - }) - .catch(err => { - vscode.window.showErrorMessage(`Failed to start tcpdump capture:\n${err.message || err}`); - outputChannel.appendLine(`[ERROR] runCaptureWithPipe() => ${err.message || err}`); - }); -} - -/** - * Start capture on an interface using edgeshark/packetflix. - * This method builds a 'packetflix:' URI that calls edgeshark. - */ -export async function captureInterfaceWithPacketflix( - node: ClabInterfaceTreeNode, +// Build the packetflix:ws: URI +async function genPacketflixURI(node: ClabInterfaceTreeNode, allSelectedNodes?: ClabInterfaceTreeNode[] // [CHANGED] ) { - if (!node) { - return vscode.window.showErrorMessage("No interface to capture found."); + if (!node) { + return vscode.window.showErrorMessage("No interface to capture found."); + } + outputChannel.appendLine(`[DEBUG] captureInterfaceWithPacketflix() called for node=${node.parentName} if=${node.name}`); + + // Check edgeshark is available on the host + // - make a simple API call to get version of packetflix + let edgesharkOk = false + try { + const res = await fetch('http://127.0.0.1:5001/version'); + edgesharkOk = res.ok + } catch { + // Port is probably closed, edgeshark not running + } + if(!edgesharkOk) { + const selectedOpt = await vscode.window.showInformationMessage("Capture: Edgeshark is not running. Would you like to start it?", { modal: false }, "Yes") + if(selectedOpt === "Yes") { + execSync(EDGESHARK_INSTALL_CMD) } - outputChannel.appendLine(`[DEBUG] captureInterfaceWithPacketflix() called for node=${node.parentName} if=${node.name}`); - - // If user multi‐selected items, we capture them all. - const selected = allSelectedNodes && allSelectedNodes.length > 0 - ? allSelectedNodes - : [node]; - - // If multiple selected - if (selected.length > 1) { - // Check if they are from the same container - const uniqueContainers = new Set(selected.map(i => i.parentName)); - if (uniqueContainers.size > 1) { - // from different containers => spawn multiple edgeshark sessions - outputChannel.appendLine("[DEBUG] Edgeshark multi selection => multiple containers => launching individually"); - for (const nd of selected) { - await captureInterfaceWithPacketflix(nd); // re-call for single - } - return; + else { + return + } + } + + // If user multi‐selected items, we capture them all. + const selected = allSelectedNodes && allSelectedNodes.length > 0 + ? allSelectedNodes + : [node]; + + // If multiple selected + if (selected.length > 1) { + // Check if they are from the same container + const uniqueContainers = new Set(selected.map(i => i.parentName)); + if (uniqueContainers.size > 1) { + // from different containers => spawn multiple edgeshark sessions + outputChannel.appendLine("[DEBUG] Edgeshark multi selection => multiple containers => launching individually"); + for (const nd of selected) { + await captureInterfaceWithPacketflix(nd); // re-call for single } + return; + } // All from same container => build multi-interface edgeshark link - return captureMultipleEdgeshark(selected); - } + return await captureMultipleEdgeshark(selected); + } - // [ORIGINAL SINGLE-INTERFACE EDGESHARK LOGIC] - outputChannel.appendLine(`[DEBUG] captureInterfaceWithPacketflix() single mode for node=${node.parentName}/${node.name}`); + // [ORIGINAL SINGLE-INTERFACE EDGESHARK LOGIC] + outputChannel.appendLine(`[DEBUG] captureInterfaceWithPacketflix() single mode for node=${node.parentName}/${node.name}`); - // Make sure we have a valid hostname - const hostname = await getHostname(); - if (!hostname) { - return vscode.window.showErrorMessage( - "No known hostname/IP address to connect to for packet capture." - ); - } + // Make sure we have a valid hostname + const hostname = await getHostname(); + if (!hostname) { + return vscode.window.showErrorMessage( + "No known hostname/IP address to connect to for packet capture." + ); + } - // If it's an IPv6 literal, bracket it. e.g. ::1 => [::1] - const bracketed = hostname.includes(":") ? `[${hostname}]` : hostname; + // If it's an IPv6 literal, bracket it. e.g. ::1 => [::1] + const bracketed = hostname.includes(":") ? `[${hostname}]` : hostname; - const config = vscode.workspace.getConfiguration("containerlab"); - const packetflixPort = config.get("remote.packetflixPort", 5001); + const config = vscode.workspace.getConfiguration("containerlab"); + const packetflixPort = config.get("remote.packetflixPort", 5001); - const packetflixUri = `packetflix:ws://${bracketed}:${packetflixPort}/capture?container={"network-interfaces":["${node.name}"],"name":"${node.parentName}","type":"docker"}&nif=${node.name}`; - outputChannel.appendLine(`[DEBUG] single-edgeShark => ${packetflixUri}`); + const containerStr = encodeURIComponent(`{"network-interfaces":["${node.name}"],"name":"${node.parentName}","type":"docker"}`) - vscode.window.showInformationMessage( - `Starting edgeshark capture on ${node.parentName}/${node.name}...` - ); - vscode.env.openExternal(vscode.Uri.parse(packetflixUri)); + const uri = `packetflix:ws://${bracketed}:${packetflixPort}/capture?container=${containerStr}&nif=${node.name}` + + vscode.window.showInformationMessage( + `Starting edgeshark capture on ${node.parentName}/${node.name}...` + ); + + outputChannel.appendLine(`[DEBUG] single-edgeShark => ${uri.toString()}`); + + return [uri, bracketed] } +// Capture multiple interfaces with Edgeshark async function captureMultipleEdgeshark(nodes: ClabInterfaceTreeNode[]) { const base = nodes[0]; const ifNames = nodes.map(n => n.name); @@ -171,7 +143,232 @@ async function captureMultipleEdgeshark(nodes: ClabInterfaceTreeNode[]) { ); outputChannel.appendLine(`[DEBUG] multi-edgeShark => ${packetflixUri}`); - vscode.env.openExternal(vscode.Uri.parse(packetflixUri)); + return [packetflixUri, bracketed] +} + +/** + * Start capture on an interface using edgeshark/packetflix. + * This method builds a 'packetflix:' URI that calls edgeshark. + */ +export async function captureInterfaceWithPacketflix( + node: ClabInterfaceTreeNode, + allSelectedNodes?: ClabInterfaceTreeNode[] // [CHANGED] +) { + + const packetflixUri = await genPacketflixURI(node, allSelectedNodes) + if (!packetflixUri) { + return + } + + vscode.env.openExternal(vscode.Uri.parse(packetflixUri[0])); +} + +// Capture using Edgeshark + Wireshark via VNC in a webview +export async function captureEdgesharkVNC( + node: ClabInterfaceTreeNode, + allSelectedNodes?: ClabInterfaceTreeNode[] // [CHANGED] +) { + + const packetflixUri = await genPacketflixURI(node, allSelectedNodes) + if (!packetflixUri) { + return + } + + const wsConfig = vscode.workspace.getConfiguration("containerlab") + const dockerImage = wsConfig.get("capture.wireshark.dockerImage", "ghcr.io/kaelemc/wireshark-vnc-docker:latest") + const dockerPullPolicy = wsConfig.get("capture.wireshark.pullPolicy", "always") + const extraDockerArgs = wsConfig.get("capture.wireshark.extraDockerArgs") + const wiresharkThemeSetting = wsConfig.get("capture.wireshark.theme") + const keepOpenInBackground = wsConfig.get("capture.wireshark.stayOpenInBackground") + + let darkModeEnabled = false; + + switch (wiresharkThemeSetting) { + case "Dark": + darkModeEnabled = true + break; + case "Light": + darkModeEnabled = false + break; + default: { + // Follow VS Code system theme + const vscThemeKind = vscode.window.activeColorTheme.kind + switch (vscThemeKind) { + case vscode.ColorThemeKind.Dark: + darkModeEnabled = true + break; + case vscode.ColorThemeKind.HighContrast: + darkModeEnabled = true + break; + default: + darkModeEnabled = false + break; + } + } + } + + const darkModeSetting = darkModeEnabled ? "-e DARK_MODE=1" : ""; + + // Check if Edgeshark is running and get its network + let edgesharkNetwork = ""; + try { + const edgesharkInfo = execSync(`docker ps --filter "name=edgeshark" --format "{{.Names}}" | head -1`, { encoding: 'utf-8' }).trim(); + if (edgesharkInfo) { + const networks = execSync(`docker inspect ${edgesharkInfo} --format '{{range .NetworkSettings.Networks}}{{.NetworkID}} {{end}}'`, { encoding: 'utf-8' }).trim(); + const networkId = networks.split(' ')[0]; + if (networkId) { + const networkName = execSync(`docker network inspect ${networkId} --format '{{.Name}}'`, { encoding: 'utf-8' }).trim(); + edgesharkNetwork = `--network ${networkName}`; + } + } + } catch { + // If we can't find the network, continue without it + } + + // Get the lab directory from the container labels to mount for saving pcap files + let volumeMount = ""; + try { + const labDir = execSync(`docker inspect ${node.parentName} --format '{{index .Config.Labels "clab-node-lab-dir"}}' 2>/dev/null`, { encoding: 'utf-8' }).trim(); + if (labDir && labDir !== '') { + // Go up two levels to get the actual lab directory (from node-specific dir to lab root) + const pathParts = labDir.split('/'); + pathParts.pop(); // Remove node name (e.g., "srl1") + pathParts.pop(); // Remove lab name (e.g., "clab-vlan") + const labRootDir = pathParts.join('/'); + volumeMount = `-v "${labRootDir}:/pcaps"`; + outputChannel.appendLine(`[DEBUG] Mounting lab directory: ${labRootDir} as /pcaps`); + } + } catch { + // If we can't get the lab directory, continue without mounting + } + + // Replace localhost with host.docker.internal or the actual host IP + let modifiedPacketflixUri = packetflixUri[0]; + if (modifiedPacketflixUri.includes('localhost') || modifiedPacketflixUri.includes('127.0.0.1')) { + // When using the edgeshark network, we need to use the edgeshark container name + if (edgesharkNetwork) { + modifiedPacketflixUri = modifiedPacketflixUri.replace(/localhost|127\.0\.0\.1/g, 'edgeshark-edgeshark-1'); + } else { + // Otherwise use host.docker.internal which works on Docker Desktop + modifiedPacketflixUri = modifiedPacketflixUri.replace(/localhost|127\.0\.0\.1/g, 'host.docker.internal'); + } + } + + const port = await utils.getFreePort() + const containerId = await utils.execWithProgress(`docker run -d --rm --pull ${dockerPullPolicy} -p 127.0.0.1:${port}:5800 ${edgesharkNetwork} ${volumeMount} ${darkModeSetting} -e PACKETFLIX_LINK="${modifiedPacketflixUri}" ${extraDockerArgs} --name clab_vsc_ws-${node.parentName}_${node.name}-${Date.now()} ${dockerImage}`, "Starting Wireshark") + + // let vscode port forward for us + const localUri = vscode.Uri.parse(`http://localhost:${port}`); + const externalUri = (await vscode.env.asExternalUri(localUri)).toString(); + + const panel = vscode.window.createWebviewPanel( + 'clabWiresharkVNC', + `Wireshark (${node.parentName}:${node.name})`, + vscode.ViewColumn.One, + { + enableScripts: true, + retainContextWhenHidden: keepOpenInBackground + } + ); + + panel.onDidDispose(() => { + execSync(`docker rm -f ${containerId}`) + }) + + const iframeUrl = externalUri; + + // Show info about where to save pcap files if volume is mounted + if (volumeMount) { + vscode.window.showInformationMessage( + `Wireshark started. Save pcap files to /pcaps to persist them in the lab directory.` + ); + } + + // Wait a bit for the VNC server to be ready + setTimeout(() => { + panel.webview.html = ` + + + + +
+ Loading Wireshark... + ${volumeMount ? '
Tip: Save pcap files to /pcaps to persist them in the lab directory
' : ''} +
+ + + + + `; + }, 1000); + +} + +export async function killAllWiresharkVNCCtrs() { + const dockerImage = vscode.workspace.getConfiguration("containerlab").get("capture.wireshark.dockerImage", "ghcr.io/kaelemc/wireshark-vnc-docker:latest") + utils.execWithProgress(`docker rm -f $(docker ps --filter "name=clab_vsc_ws-" --filter "ancestor=${dockerImage}" --format "{{.ID}}")`, "Killing Wireshark container:") } /** @@ -179,24 +376,24 @@ async function captureMultipleEdgeshark(nodes: ClabInterfaceTreeNode[]) { * overriding the auto-detected or config-based hostname until the user closes VS Code. */ export async function setSessionHostname() { - const opts: vscode.InputBoxOptions = { - title: `Configure hostname for Containerlab remote (this session only)`, - placeHolder: `IPv4, IPv6 or DNS resolvable hostname of the system where containerlab is running`, - prompt: "This will persist for only this session of VS Code.", - validateInput: (input: string) => { - if (input.trim().length === 0) { - return "Input should not be empty"; - } - } - }; - - const val = await vscode.window.showInputBox(opts); - if (!val) { - return false; + const opts: vscode.InputBoxOptions = { + title: `Configure hostname for Containerlab remote (this session only)`, + placeHolder: `IPv4, IPv6 or DNS resolvable hostname of the system where containerlab is running`, + prompt: "This will persist for only this session of VS Code.", + validateInput: (input: string) => { + if (input.trim().length === 0) { + return "Input should not be empty"; + } } - sessionHostname = val.trim(); - vscode.window.showInformationMessage(`Session hostname is set to: ${sessionHostname}`); - return true; + }; + + const val = await vscode.window.showInputBox(opts); + if (!val) { + return false; + } + sessionHostname = val.trim(); + vscode.window.showInformationMessage(`Session hostname is set to: ${sessionHostname}`); + return true; } /** @@ -276,5 +473,4 @@ export async function getHostname(): Promise { // 6. Fallback: default to "localhost". outputChannel.appendLine("[DEBUG] No suitable hostname found; defaulting to 'localhost'"); return "localhost"; -} - +} \ No newline at end of file diff --git a/src/commands/clabCommand.ts b/src/commands/clabCommand.ts index c1fc3ecd6..9aa7d3bd3 100644 --- a/src/commands/clabCommand.ts +++ b/src/commands/clabCommand.ts @@ -1,7 +1,11 @@ import * as vscode from "vscode"; import * as cmd from './command'; +import * as utils from '../utils'; import { ClabLabTreeNode } from "../treeView/common"; - +import { DefaultOptions } from "./command"; +import { exec } from "child_process"; +import { outputChannel } from "../extension"; +import path from "path"; /** * A helper class to build a 'containerlab' command (with optional sudo, etc.) * and run it either in the Output channel or in a Terminal. @@ -10,21 +14,18 @@ export class ClabCommand extends cmd.Command { private node?: ClabLabTreeNode; private action: string; private runtime: string; + private labPath?: string; constructor( action: string, node: ClabLabTreeNode, - spinnerMsg?: cmd.SpinnerMsg, - useTerminal?: boolean, - terminalName?: string ) { - const options: cmd.CmdOptions = { - command: "containerlab", - useSpinner: useTerminal ? false : true, - spinnerMsg, - terminalName, - }; - super(options); + + const opts: DefaultOptions = { + command: "containerlab" + } + + super(opts); // Read the runtime from configuration. const config = vscode.workspace.getConfiguration("containerlab"); @@ -36,7 +37,6 @@ export class ClabCommand extends cmd.Command { public async run(flags?: string[]): Promise { // Try node.details -> fallback to active editor - let labPath: string; if (!this.node) { const editor = vscode.window.activeTextEditor; if (!editor) { @@ -45,13 +45,13 @@ export class ClabCommand extends cmd.Command { ); return; } - labPath = editor.document.uri.fsPath; + this.labPath = editor.document.uri.fsPath; } else { - labPath = this.node.labPath.absolute + this.labPath = this.node.labPath.absolute } - if (!labPath) { + if (!this.labPath) { vscode.window.showErrorMessage( `No labPath found for command "${this.action}".` ); @@ -78,9 +78,42 @@ export class ClabCommand extends cmd.Command { allFlags.push(...flags); } - const cmdArgs = [this.action, "-r", this.runtime, ...allFlags, "-t", labPath]; + const cmdArgs = [this.action, "-r", this.runtime, ...allFlags, "-t", this.labPath]; // Return the promise from .execute() so we can await return this.execute(cmdArgs); } + + override async execProgress(cmd: string[]): Promise { + const title = utils.titleCase(this.action); + vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: title, + cancellable: false + }, + (progress) => new Promise((resolve, reject) => { + const child = exec(cmd.join(" "), { encoding: 'utf-8' }, (err, stdout, stderr) => { + if (err) { + vscode.window.showErrorMessage(`${title}: ${stderr.trimEnd().split("\n").reverse()[0]}`); + return reject(err); + } + outputChannel.append(stdout); + resolve(stdout.trim()); + }); + + child.stderr?.on('data', (data) => { + const line = data.toString().trim(); + if (line) { + progress.report({ message: line.replace(/^\d{2}:\d{2}:\d{2} \w+ /, "") }); + outputChannel.appendLine(line); + } + }); + }) + ).then(() => { + vscode.window.showInformationMessage(`✔ ${title} success for ${path.basename(this.labPath!)}`); + // trigger a refresh after execution + vscode.commands.executeCommand('containerlab.refresh'); + }) + } } diff --git a/src/commands/command.ts b/src/commands/command.ts index e6370bd1b..02e6f7b79 100644 --- a/src/commands/command.ts +++ b/src/commands/command.ts @@ -41,18 +41,18 @@ export function execCommandInTerminal(command: string, terminalName: string) { export async function execCommandInOutput(command: string, show?: boolean, stdoutCb?: Function, stderrCb?: Function) { let proc = exec(command); - if(show) { outputChannel.show(); } + if (show) { outputChannel.show(); } proc.stdout?.on('data', (data) => { const cleaned = utils.stripAnsi(data.toString()); outputChannel.append(cleaned); - if(stdoutCb) { stdoutCb(proc, cleaned); } + if (stdoutCb) { stdoutCb(proc, cleaned); } }); proc.stderr?.on('data', (data) => { const cleaned = utils.stripAnsi(data.toString()); outputChannel.append(cleaned); - if(stderrCb) { stderrCb(proc, cleaned); } + if (stderrCb) { stderrCb(proc, cleaned); } }); proc.on('close', (code) => { @@ -62,12 +62,31 @@ export async function execCommandInOutput(command: string, show?: boolean, stdou }); } -export type CmdOptions = { +export type SpinnerOptions = { + useSpinner?: true; command: string; - useSpinner: boolean; - terminalName?: string; - spinnerMsg?: SpinnerMsg; -} + spinnerMsg: SpinnerMsg; + terminalName?: never; + useProgress?: false; +}; + +export type TerminalOptions = { + useSpinner?: false; + command: string; + terminalName: string; + spinnerMsg?: never; + useProgress?: false; +}; + +export type DefaultOptions = { + command: string; + useSpinner?: false; + terminalName?: never; + spinnerMsg?: never; + useProgress?: true; +}; + +export type CmdOptions = SpinnerOptions | TerminalOptions | DefaultOptions; export type SpinnerMsg = { progressMsg: string; @@ -84,37 +103,32 @@ export class Command { protected useSudo: boolean; protected spinnerMsg?: SpinnerMsg; protected terminalName?: string; + protected useProgress: boolean; constructor(options: CmdOptions) { this.command = options.command; - this.useSpinner = options.useSpinner; - - if(this.useSpinner) { - if(options.terminalName) {throw new Error("useSpinner is true. terminalName should NOT be defined.");} - if(!options.spinnerMsg) {throw new Error("useSpinner is true, but spinnerMsg is undefined.");} - this.spinnerMsg = options.spinnerMsg; - } - else { - if(!options.terminalName) {throw new Error("UseSpinner is false. terminalName must be defined.");} - this.terminalName = options.terminalName; - } - - const config = vscode.workspace.getConfiguration("containerlab"); - this.useSudo = config.get("sudoEnabledByDefault", true); + this.useSpinner = options.useSpinner || false; + this.spinnerMsg = options.spinnerMsg; + this.terminalName = options.terminalName; + this.useProgress = options.useProgress || true; + this.useSudo = utils.getConfig('sudoEnabledByDefault'); } protected execute(args?: string[]): Promise { let cmd: string[] = []; - if(this.useSudo) {cmd.push("sudo");} + if (this.useSudo) { cmd.push("sudo"); } cmd.push(this.command); - if(args) {cmd.push(...args);} + if (args) { cmd.push(...args); } outputChannel.appendLine(`[${this.command}] Running: ${cmd.join(" ")}`); - if(this.useSpinner) { + if (this.useSpinner) { return this.execSpinner(cmd); } + else if (this.useProgress) { + return this.execProgress(cmd); + } else { execCommandInTerminal(cmd.join(" "), this.terminalName!); return Promise.resolve(); @@ -133,7 +147,7 @@ export class Command { progress.report({ message: " [View Logs](command:containerlab.viewLogs)" - }); + }); const workspaceFolder = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; const cwd = workspaceFolder ?? path.join(os.homedir(), ".clab"); @@ -205,7 +219,38 @@ export class Command { const failMsg = this.spinnerMsg?.failMsg ? `this.spinnerMsg.failMsg. Err: ${err}` : `${utils.titleCase(command)} failed: ${err.message}`; const viewOutputBtn = await vscode.window.showErrorMessage(failMsg, "View logs"); // If view logs button was clicked. - if(viewOutputBtn === "View logs") { outputChannel.show(); } + if (viewOutputBtn === "View logs") { outputChannel.show(); } } } + + protected async execProgress(cmd: string[]): Promise { + const title = utils.titleCase(this.useSudo ? cmd[2] : cmd[1]) + vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: title, + cancellable: false + }, + (progress) => new Promise((resolve, reject) => { + const child = exec(cmd.join(" "), { encoding: 'utf-8' }, (err, stdout, stderr) => { + if (err) { + vscode.window.showErrorMessage(`${title} failed: ${stderr}`); + return reject(err); + } + outputChannel.append(stdout); + resolve(stdout.trim()); + }); + + child.stderr?.on('data', (data) => { + const line = data.toString().trim(); + if (line) { + progress.report({ message: line }); + outputChannel.appendLine(line); + } + }); + }) + ).then(() => { + vscode.window.showInformationMessage(`${title}: Success`); + }) + } } \ No newline at end of file diff --git a/src/commands/deploy.ts b/src/commands/deploy.ts index 91848f599..269a77eae 100644 --- a/src/commands/deploy.ts +++ b/src/commands/deploy.ts @@ -1,6 +1,5 @@ import { ClabLabTreeNode } from "../treeView/common"; import { ClabCommand } from "./clabCommand"; -import { SpinnerMsg } from "./command"; import * as vscode from "vscode"; import { deployPopularLab } from "./deployPopular"; import { getSelectedLabNode } from "./utils"; @@ -11,11 +10,7 @@ export async function deploy(node?: ClabLabTreeNode) { return; } - const spinnerMessages: SpinnerMsg = { - progressMsg: "Deploying Lab... ", - successMsg: "Lab deployed successfully!" - }; - const deployCmd = new ClabCommand("deploy", node, spinnerMessages); + const deployCmd = new ClabCommand("deploy", node); deployCmd.run(); } @@ -40,11 +35,8 @@ export async function deployCleanup(node?: ClabLabTreeNode) { await config.update("skipCleanupWarning", true, vscode.ConfigurationTarget.Global); } } - const spinnerMessages: SpinnerMsg = { - progressMsg: "Deploying Lab (cleanup)... ", - successMsg: "Lab deployed (cleanup) successfully!" - }; - const deployCmd = new ClabCommand("deploy", node, spinnerMessages); + + const deployCmd = new ClabCommand("deploy", node); deployCmd.run(["-c"]); } diff --git a/src/commands/destroy.ts b/src/commands/destroy.ts index 7bb31c1c4..84f71cae8 100644 --- a/src/commands/destroy.ts +++ b/src/commands/destroy.ts @@ -1,7 +1,6 @@ import * as vscode from "vscode"; import { ClabLabTreeNode } from "../treeView/common"; import { ClabCommand } from "./clabCommand"; -import { SpinnerMsg } from "./command"; import { getSelectedLabNode } from "./utils"; export async function destroy(node?: ClabLabTreeNode) { @@ -10,11 +9,7 @@ export async function destroy(node?: ClabLabTreeNode) { return; } - const spinnerMessages: SpinnerMsg = { - progressMsg: "Destroying Lab... ", - successMsg: "Lab destroyed successfully!" - }; - const destroyCmd = new ClabCommand("destroy", node, spinnerMessages); + const destroyCmd = new ClabCommand("destroy", node); destroyCmd.run(); } @@ -39,10 +34,7 @@ export async function destroyCleanup(node?: ClabLabTreeNode) { await config.update("skipCleanupWarning", true, vscode.ConfigurationTarget.Global); } } - const spinnerMessages: SpinnerMsg = { - progressMsg: "Destroying Lab (cleanup)... ", - successMsg: "Lab destroyed (cleanup) successfully!" - }; - const destroyCmd = new ClabCommand("destroy", node, spinnerMessages); + + const destroyCmd = new ClabCommand("destroy", node); destroyCmd.run(["-c"]); } diff --git a/src/commands/dockerCommand.ts b/src/commands/dockerCommand.ts index 1d9b93a04..a26a9b353 100644 --- a/src/commands/dockerCommand.ts +++ b/src/commands/dockerCommand.ts @@ -8,15 +8,14 @@ import * as vscode from "vscode"; export class DockerCommand extends cmd.Command { private action: string; - constructor(action: string, spinnerMsg?: cmd.SpinnerMsg) { + constructor(action: string, spinnerMsg: cmd.SpinnerMsg) { const config = vscode.workspace.getConfiguration("containerlab"); const runtime = config.get("runtime", "docker"); - const options: cmd.CmdOptions = { + const options: cmd.SpinnerOptions = { command: runtime, - useSpinner: true, - spinnerMsg: spinnerMsg, - }; + spinnerMsg: spinnerMsg + } super(options); this.action = action; @@ -25,7 +24,6 @@ export class DockerCommand extends cmd.Command { public run(containerID: string) { // Build the command const cmd = [this.action, containerID]; - this.execute(cmd); } } \ No newline at end of file diff --git a/src/commands/edgeshark.ts b/src/commands/edgeshark.ts index dc3e3431c..665812f25 100644 --- a/src/commands/edgeshark.ts +++ b/src/commands/edgeshark.ts @@ -1,13 +1,17 @@ -import { execCommandInTerminal } from "./command"; +import { execCommandInTerminal } from './command'; -export async function installEdgeshark() { - execCommandInTerminal("curl -sL \ +export const EDGESHARK_INSTALL_CMD = "curl -sL \ +https://github.com/siemens/edgeshark/raw/main/deployments/wget/docker-compose.yaml \ +| DOCKER_DEFAULT_PLATFORM= docker compose -f - up -d" + +export const EDGESHARK_UNINSTALL_CMD = "curl -sL \ https://github.com/siemens/edgeshark/raw/main/deployments/wget/docker-compose.yaml \ -| DOCKER_DEFAULT_PLATFORM= docker compose -f - up -d", "Edgeshark Installation"); +| DOCKER_DEFAULT_PLATFORM= docker compose -f - down" + +export async function installEdgeshark() { + execCommandInTerminal(EDGESHARK_INSTALL_CMD, "Edgeshark Installation"); } export async function uninstallEdgeshark() { - execCommandInTerminal("curl -sL \ -https://github.com/siemens/edgeshark/raw/main/deployments/wget/docker-compose.yaml \ -| DOCKER_DEFAULT_PLATFORM= docker compose -f - down", "Edgeshark Uninstallation"); + execCommandInTerminal(EDGESHARK_UNINSTALL_CMD, "Edgeshark Uninstallation"); } \ No newline at end of file diff --git a/src/commands/graph.ts b/src/commands/graph.ts index 480ea6ebf..c56246d63 100644 --- a/src/commands/graph.ts +++ b/src/commands/graph.ts @@ -1,7 +1,6 @@ import * as vscode from "vscode"; import * as fs from "fs"; import { ClabCommand } from "./clabCommand"; -import { SpinnerMsg } from "./command"; import { ClabLabTreeNode } from "../treeView/common"; import { TopoViewer } from "../topoViewer/backend/topoViewerWebUiFacade"; @@ -18,13 +17,7 @@ async function runGraphDrawIO(node: ClabLabTreeNode | undefined, layout: "horizo return; } - const spinnerMessages: SpinnerMsg = { - progressMsg: "Generating DrawIO graph...", - successMsg: "DrawIO Graph Completed!", - failMsg: "Graph (draw.io) Failed", - }; - - const graphCmd = new ClabCommand("graph", node, spinnerMessages); + const graphCmd = new ClabCommand("graph", node); // Figure out the .drawio filename if (!node.labPath.absolute) { @@ -71,7 +64,7 @@ export async function graphDrawIOInteractive(node?: ClabLabTreeNode) { return; } - const graphCmd = new ClabCommand("graph", node, undefined, true, "Graph - drawio Interactive"); + const graphCmd = new ClabCommand("graph", node); graphCmd.run(["--drawio", "--drawio-args", `"-I"`]); } diff --git a/src/commands/redeploy.ts b/src/commands/redeploy.ts index 4e38a82dd..fd2370e63 100644 --- a/src/commands/redeploy.ts +++ b/src/commands/redeploy.ts @@ -1,7 +1,6 @@ import * as vscode from "vscode"; import { ClabLabTreeNode } from "../treeView/common"; import { ClabCommand } from "./clabCommand"; -import { SpinnerMsg } from "./command"; import { getSelectedLabNode } from "./utils"; export async function redeploy(node?: ClabLabTreeNode) { @@ -10,11 +9,7 @@ export async function redeploy(node?: ClabLabTreeNode) { return; } - const spinnerMessages: SpinnerMsg = { - progressMsg: "Redeploying Lab... ", - successMsg: "Lab redeployed successfully!" - }; - const redeployCmd = new ClabCommand("redeploy", node, spinnerMessages); + const redeployCmd = new ClabCommand("redeploy", node); redeployCmd.run(); } @@ -40,10 +35,6 @@ export async function redeployCleanup(node?: ClabLabTreeNode) { } } - const spinnerMessages: SpinnerMsg = { - progressMsg: "Redeploying Lab (cleanup)... ", - successMsg: "Lab redeployed (cleanup) successfully!" - }; - const redeployCmd = new ClabCommand("redeploy", node, spinnerMessages); + const redeployCmd = new ClabCommand("redeploy", node); redeployCmd.run(["-c"]); } diff --git a/src/commands/save.ts b/src/commands/save.ts index 7b7a76706..210a798f6 100644 --- a/src/commands/save.ts +++ b/src/commands/save.ts @@ -1,6 +1,5 @@ // src/commands/save.ts import * as vscode from "vscode"; -import { SpinnerMsg } from "./command"; import { ClabCommand } from "./clabCommand"; import { ClabLabTreeNode, ClabContainerTreeNode } from "../treeView/common"; import * as path from "path"; @@ -20,14 +19,8 @@ export async function saveLab(node: ClabLabTreeNode) { return; } - const spinnerMessages: SpinnerMsg = { - progressMsg: `Saving lab configuration for ${node.label}...`, - successMsg: `Lab configuration for ${node.label} saved successfully!`, - failMsg: `Could not save lab configuration for ${node.label}` - }; - // Create a ClabCommand for "save" using the lab node. - const saveCmd = new ClabCommand("save", node, spinnerMessages); + const saveCmd = new ClabCommand("save", node); // ClabCommand automatically appends "-t ". saveCmd.run(); } @@ -50,12 +43,6 @@ export async function saveNode(node: ClabContainerTreeNode) { // Extract the short node name by removing the "clab-{labname}-" prefix const shortNodeName = node.name.replace(/^clab-[^-]+-/, ''); - const spinnerMessages: SpinnerMsg = { - progressMsg: `Saving configuration for node ${shortNodeName}...`, - successMsg: `Configuration for node ${shortNodeName} saved successfully!`, - failMsg: `Could not save configuration for node ${shortNodeName}` - }; - const tempLabNode = new ClabLabTreeNode( path.basename(node.labPath.absolute), vscode.TreeItemCollapsibleState.None, @@ -66,7 +53,7 @@ export async function saveNode(node: ClabContainerTreeNode) { "containerlabLabDeployed" ); - const saveCmd = new ClabCommand("save", tempLabNode, spinnerMessages); + const saveCmd = new ClabCommand("save", tempLabNode); // Use --node-filter instead of -n and use the short name saveCmd.run(["--node-filter", shortNodeName]); } diff --git a/src/extension.ts b/src/extension.ts index 3eaed2c5f..4a71d3237 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -440,6 +440,14 @@ export async function activate(context: vscode.ExtensionContext) { ) ); + context.subscriptions.push( + vscode.commands.registerCommand( + 'containerlab.interface.captureWithEdgesharkVNC', + (clickedNode, allSelectedNodes) => { + cmd.captureEdgesharkVNC(clickedNode, allSelectedNodes); + }) + ); + context.subscriptions.push( vscode.commands.registerCommand('containerlab.interface.setDelay', cmd.setLinkDelay) ); @@ -469,6 +477,10 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push( vscode.commands.registerCommand('containerlab.uninstall.edgeshark', cmd.uninstallEdgeshark) ); + // Kill Wireshark VNC + context.subscriptions.push( + vscode.commands.registerCommand('containerlab.capture.killAllWiresharkVNC', cmd.killAllWiresharkVNCCtrs) + ); // Session hostname command context.subscriptions.push( @@ -566,15 +578,15 @@ export async function activate(context: vscode.ExtensionContext) { const refreshInterval = config.get('refreshInterval', 10000); const refreshTaskID = setInterval( - async ()=> { - ins.update().then( () => { + async () => { + ins.update().then(() => { // Only refresh running labs - local labs use file watchers runningLabsProvider.softRefresh(); }) }, refreshInterval ) - context.subscriptions.push({ dispose: () => clearInterval(refreshTaskID)}); + context.subscriptions.push({ dispose: () => clearInterval(refreshTaskID) }); } export function deactivate() { diff --git a/src/topoViewer/backend/topoViewerAdaptorClab.ts b/src/topoViewer/backend/topoViewerAdaptorClab.ts index ff79f5928..72099d541 100644 --- a/src/topoViewer/backend/topoViewerAdaptorClab.ts +++ b/src/topoViewer/backend/topoViewerAdaptorClab.ts @@ -520,40 +520,94 @@ export class TopoViewerAdaptorClab { * @param clabName - Optional container name. * @returns The matching ClabInterfaceTreeNode, or null if not found. */ - public getClabContainerInterfaceTreeNode(nodeName: string, interfaceName: string, clabTreeDataToTopoviewer: Record, clabName: string | undefined): ClabInterfaceTreeNode | null { + // public getClabContainerInterfaceTreeNode(nodeName: string, interfaceName: string, clabTreeDataToTopoviewer: Record, clabName: string | undefined): ClabInterfaceTreeNode | null { + + // log.info(`clabName: ${clabName}`); + // log.info(`nodeName: ${nodeName}`); + // log.info(`interfaceName: ${interfaceName}`); + + // // Retrieve the container tree node (assuming this method exists and returns a ClabContainerTreeNode instance) + // const foundContainerData: ClabContainerTreeNode | null = this.getClabContainerTreeNode( + // nodeName, + // clabTreeDataToTopoviewer, + // clabName + // ); + + // log.info(`ContainerData: ${foundContainerData}`); + + // if (!foundContainerData) { + // log.info(`Container not found for node: ${nodeName}`); + // return null; + // } + + // // Assuming containerData.interfaces is an array of ClabInterfaceTreeNode instances, + // // use the find method to locate the interface with the matching name. + // const foundInterface = foundContainerData.interfaces.find( + // // (intf: ClabInterfaceTreeNode) => intf.name === interfaceName + // (intf: ClabInterfaceTreeNode) => intf.label === interfaceName // aarafat-tag: intf.name is replaced with intf.label; why not intf.alias? intf.alias not available when default for interfaceName is used. + // ); + + // if (foundInterface) { + // log.info(`Found interface: ${foundInterface.label}`); + // log.debug(`Output of foundInterfaceData ${JSON.stringify(foundInterface, null, "\t")}`); + // return foundInterface; + // } else { + // log.info(`Interface ${interfaceName} not found in container ${nodeName}`); + // return null; + // } + // } - log.info(`clabName: ${clabName}`); - log.info(`nodeName: ${nodeName}`); - log.info(`interfaceName: ${interfaceName}`); - - // Retrieve the container tree node (assuming this method exists and returns a ClabContainerTreeNode instance) - const foundContainerData: ClabContainerTreeNode | null = this.getClabContainerTreeNode( - nodeName, - clabTreeDataToTopoviewer, - clabName - ); - - log.info(`ContainerData: ${foundContainerData}`); + /** + * + * @param nodeName - The name of the node (container name). + * @param interfaceName - The interface name to match. + * @param clabTreeDataToTopoviewer - The tree data record keyed by clab name. + * @param clabName - The Containerlab topology name. + * @returns The matched ClabInterfaceTreeNode or `null` if not found. + */ + public getClabContainerInterfaceTreeNode( + nodeName: string, + interfaceName: string, + clabTreeDataToTopoviewer: Record, + clabName: string | undefined + ): ClabInterfaceTreeNode | null { + + log.info(`Resolving interface in container: node=${nodeName}, interface=${interfaceName}, clab=${clabName}`); + + const container = this.getClabContainerTreeNode(nodeName, clabTreeDataToTopoviewer, clabName); + if (!container) { + log.warn(`Container "${nodeName}" not found under clab "${clabName}"`); + return null; + } - if (!foundContainerData) { - log.info(`Container not found for node: ${nodeName}`); + if (!Array.isArray(container.interfaces)) { + log.warn(`No interfaces defined in container "${nodeName}"`); return null; } - // Assuming containerData.interfaces is an array of ClabInterfaceTreeNode instances, - // use the find method to locate the interface with the matching name. - const foundInterface = foundContainerData.interfaces.find( - // (intf: ClabInterfaceTreeNode) => intf.name === interfaceName - (intf: ClabInterfaceTreeNode) => intf.label === interfaceName // aarafat-tag: intf.name is replaced with intf.label; why not intf.alias? intf.alias not available when default for interfaceName is used. - ); + let matchedBy: string | null = null; + const foundInterface = container.interfaces.find((intf) => { + if (intf.name === interfaceName) { + matchedBy = "label"; + return true; + } else if (intf.label === interfaceName) { + matchedBy = "name"; + return true; + } else if (intf.alias && intf.alias === interfaceName) { + matchedBy = "alias"; + return true; + } + return false; + }); if (foundInterface) { - log.info(`Found interface: ${foundInterface.label}`); - log.debug(`Output of foundInterfaceData ${JSON.stringify(foundInterface, null, "\t")}`); + log.info(`✅ Matched interface "${interfaceName}" using "${matchedBy}"`); + log.debug(`Found interface: ${JSON.stringify(foundInterface, null, 2)}`); return foundInterface; } else { - log.info(`Interface ${interfaceName} not found in container ${nodeName}`); + log.warn(`⚠️ Interface "${interfaceName}" not found in container "${nodeName}"`); return null; } } + } diff --git a/src/topoViewer/backend/topoViewerWebUiFacade.ts b/src/topoViewer/backend/topoViewerWebUiFacade.ts index 8e1dd7dc6..3f1798dc7 100644 --- a/src/topoViewer/backend/topoViewerWebUiFacade.ts +++ b/src/topoViewer/backend/topoViewerWebUiFacade.ts @@ -17,6 +17,7 @@ import { sshToNode, showLogs, captureInterfaceWithPacketflix, + captureEdgesharkVNC } from '../../commands/index'; /** @@ -514,6 +515,35 @@ export class TopoViewer { } break; } + case 'clab-link-capture-edgeshark-vnc': { + try { + interface LinkEndpointInfo { + nodeName: string; + interfaceName: string; + } + const linkInfo: LinkEndpointInfo = JSON.parse(payload as string); + log.info(`clab-link-capture called with payload: ${JSON.stringify(linkInfo, null, 2)}`); + + const updatedClabTreeDataToTopoviewer = this.cacheClabTreeDataToTopoviewer; + if (updatedClabTreeDataToTopoviewer) { + const containerInterfaceData = this.adaptor.getClabContainerInterfaceTreeNode( + linkInfo.nodeName, + linkInfo.interfaceName, + updatedClabTreeDataToTopoviewer, + this.adaptor.currentClabTopo?.name as string + ); + if (containerInterfaceData) { + captureEdgesharkVNC(containerInterfaceData); + } + result = `Endpoint "${endpointName}" executed successfully. Return payload is ${containerInterfaceData}`; + log.info(result); + } + } catch (innerError) { + result = `Error executing endpoint "${endpointName}".`; + log.error(`Error executing endpoint "${endpointName}": ${JSON.stringify(innerError)}`); + } + break; + } case 'clab-link-subinterfaces': { try { interface LinkEndpointInfo { diff --git a/src/topoViewer/webview-ui/html-static/js/dev.js b/src/topoViewer/webview-ui/html-static/js/dev.js index defb2c032..1448327e2 100644 --- a/src/topoViewer/webview-ui/html-static/js/dev.js +++ b/src/topoViewer/webview-ui/html-static/js/dev.js @@ -963,259 +963,168 @@ document.addEventListener("DOMContentLoaded", async function () { document.getElementById("endpoint-a-edgeshark").textContent = `Edgeshark :: ${clickedEdge.data("source")} :: ${clickedEdge.data("sourceEndpoint")}` document.getElementById("endpoint-b-edgeshark").textContent = `Edgeshark :: ${clickedEdge.data("target")} :: ${clickedEdge.data("targetEndpoint")}` - + document.getElementById("endpoint-a-edgeshark-vnc").textContent = `Edgeshark VNC :: ${clickedEdge.data("source")} :: ${clickedEdge.data("sourceEndpoint")}` + document.getElementById("endpoint-b-edgeshark-vnc").textContent = `Edgeshark VNC :: ${clickedEdge.data("target")} :: ${clickedEdge.data("targetEndpoint")}` //render sourceSubInterfaces let clabSourceSubInterfacesClabData - if (isVscodeDeployment) { - try { - console.log("########################################################### source subInt") - const response = await sendMessageToVscodeEndpointPost("clab-link-subinterfaces", { - nodeName: clickedEdge.data("extraData").clabSourceLongName, - interfaceName: clickedEdge.data("extraData").clabSourcePort - }); - clabSourceSubInterfacesClabData = response.map(item => item.name); // Output: ["e1-1-1", "e1-1-2"] - console.log("Source SubInterface list:", clabSourceSubInterfacesClabData); - - if (Array.isArray(clabSourceSubInterfacesClabData) && clabSourceSubInterfacesClabData.length > 0) { - // Map sub-interfaces with prefix - const sourceSubInterfaces = clabSourceSubInterfacesClabData - // Render sub-interfaces - renderSubInterfaces(sourceSubInterfaces, 'endpoint-a-top', 'endpoint-a-bottom', nodeName); - } else if (Array.isArray(clabSourceSubInterfacesClabData)) { - console.info("No sub-interfaces found. The input data array is empty."); - renderSubInterfaces(null, 'endpoint-a-top', 'endpoint-a-bottom', nodeName); - } else { - console.info("No sub-interfaces found. The input data is null, undefined, or not an array."); - renderSubInterfaces(null, 'endpoint-a-top', 'endpoint-a-bottom', nodeName); - } + try { + console.log("########################################################### source subInt") + const nodeName = clickedEdge.data("extraData").clabSourceLongName; + const response = await sendMessageToVscodeEndpointPost("clab-link-subinterfaces", { + nodeName: nodeName, + interfaceName: clickedEdge.data("extraData").clabSourcePort + }); + + console.log("########################################################### source subInt response", response) + + clabSourceSubInterfacesClabData = response.map(item => item.name); // Output: ["e1-1-1", "e1-1-2"] + console.log("###########################################") + console.log("Source SubInterface list:", clabSourceSubInterfacesClabData); - } catch (error) { - console.error("Failed to get SubInterface list:", error); - } - } else { - let clabSourceSubInterfacesArgList = [ - clickedEdge.data("extraData").clabSourceLongName, - clickedEdge.data("extraData").clabSourcePort - ]; - clabSourceSubInterfacesClabData = await sendRequestToEndpointGetV3("/clab-link-subinterfaces", clabSourceSubInterfacesArgList); - console.info("clabSourceSubInterfacesClabData: ", clabSourceSubInterfacesClabData); if (Array.isArray(clabSourceSubInterfacesClabData) && clabSourceSubInterfacesClabData.length > 0) { // Map sub-interfaces with prefix - const sourceSubInterfaces = clabSourceSubInterfacesClabData.map( - item => `${item.ifname}` - ); + const sourceSubInterfaces = clabSourceSubInterfacesClabData // Render sub-interfaces - renderSubInterfaces(sourceSubInterfaces, 'endpoint-a-edgeshark', 'endpoint-a-clipboard', nodeName); - renderSubInterfaces(sourceSubInterfaces, 'endpoint-a-clipboard', 'endpoint-a-bottom', nodeName); + renderSubInterfaces(sourceSubInterfaces, 'endpoint-a-top', 'endpoint-a-bottom', `${clickedEdge.data("source")}`); + renderSubInterfaces(sourceSubInterfaces, 'endpoint-a-vnc-top', 'endpoint-a-vnc-bottom', `${clickedEdge.data("source")}`); + } else if (Array.isArray(clabSourceSubInterfacesClabData)) { console.info("No sub-interfaces found. The input data array is empty."); - renderSubInterfaces(null, 'endpoint-a-edgeshark', 'endpoint-a-clipboard', nodeName); - renderSubInterfaces(null, 'endpoint-a-clipboard', 'endpoint-a-bottom', nodeName); + renderSubInterfaces(null, 'endpoint-a-top', 'endpoint-a-bottom', `${clickedEdge.data("source")}`); + renderSubInterfaces(null, 'endpoint-a-vnc-top', 'endpoint-a-vnc-bottom', `${clickedEdge.data("source")}`); } else { console.info("No sub-interfaces found. The input data is null, undefined, or not an array."); - renderSubInterfaces(null, 'endpoint-a-edgeshark', 'endpoint-a-clipboard', nodeName); - renderSubInterfaces(null, 'endpoint-a-clipboard', 'endpoint-a-bottom', nodeName); + renderSubInterfaces(null, 'endpoint-a-top', 'endpoint-a-bottom', `${clickedEdge.data("source")}`); + renderSubInterfaces(null, 'endpoint-a-vnc-top', 'endpoint-a-vnc-bottom', `${clickedEdge.data("source")}`); } + } catch (error) { + console.error("Failed to get SubInterface list:", error); } + //render targetSubInterfaces + try { + console.log("########################################################### target subInt") + const nodeName = clickedEdge.data("extraData").clabTargetLongName; - //render targetSubInterfaces - if (isVscodeDeployment) { - try { - console.log("########################################################### target subInt") - const response = await sendMessageToVscodeEndpointPost("clab-link-subinterfaces", { - nodeName: clickedEdge.data("extraData").clabTargetLongName, - interfaceName: clickedEdge.data("extraData").clabTargetPort - }); - clabTargetSubInterfacesClabData = response.map(item => item.name); // Output: ["e1-1-1", "e1-1-2"] - console.log("###########################################") - console.log("Target SubInterface list:", clabTargetSubInterfacesClabData); - - if (Array.isArray(clabTargetSubInterfacesClabData) && clabTargetSubInterfacesClabData.length > 0) { - // Map sub-interfaces with prefix - const TargetSubInterfaces = clabTargetSubInterfacesClabData - // Render sub-interfaces - renderSubInterfaces(TargetSubInterfaces, 'endpoint-b-top', 'endpoint-b-bottom', nodeName); - } else if (Array.isArray(clabTargetSubInterfacesClabData)) { - console.info("No sub-interfaces found. The input data array is empty."); - renderSubInterfaces(null, 'endpoint-b-top', 'endpoint-b-bottom', nodeName); - } else { - console.info("No sub-interfaces found. The input data is null, undefined, or not an array."); - renderSubInterfaces(null, 'endpoint-b-top', 'endpoint-b-bottom', nodeName); - } + const response = await sendMessageToVscodeEndpointPost("clab-link-subinterfaces", { + nodeName: nodeName, + interfaceName: clickedEdge.data("extraData").clabTargetPort + }); - } catch (error) { - console.error("Failed to get SubInterface list:", error); - } - } - else { - let clabTargetSubInterfacesArgList = [ - clickedEdge.data("extraData").clabTargetLongName, - clickedEdge.data("extraData").clabTargetPort - ]; - let clabTargetSubInterfacesClabData = await sendRequestToEndpointGetV3("/clab-link-subinterfaces", clabTargetSubInterfacesArgList); - console.info("clabTargetSubInterfacesClabData: ", clabTargetSubInterfacesClabData); + console.log("########################################################### target subInt response", response) + + clabTargetSubInterfacesClabData = response.map(item => item.name); // Output: ["e1-1-1", "e1-1-2"] + console.log("###########################################") + console.log("Target SubInterface list:", clabTargetSubInterfacesClabData); if (Array.isArray(clabTargetSubInterfacesClabData) && clabTargetSubInterfacesClabData.length > 0) { // Map sub-interfaces with prefix - const TargetSubInterfaces = clabTargetSubInterfacesClabData.map( - item => `${item.ifname}` - ); - + const TargetSubInterfaces = clabTargetSubInterfacesClabData // Render sub-interfaces - renderSubInterfaces(TargetSubInterfaces, 'endpoint-b-edgeshark', 'endpoint-b-clipboard'); - renderSubInterfaces(TargetSubInterfaces, 'endpoint-b-clipboard', 'endpoint-b-bottom'); + renderSubInterfaces(TargetSubInterfaces, 'endpoint-b-top', 'endpoint-b-bottom', `${clickedEdge.data("target")}`); + renderSubInterfaces(TargetSubInterfaces, 'endpoint-b-vnc-top', 'endpoint-b-vnc-bottom', `${clickedEdge.data("target")}`); } else if (Array.isArray(clabTargetSubInterfacesClabData)) { console.info("No sub-interfaces found. The input data array is empty."); - renderSubInterfaces(null, 'endpoint-b-edgeshark', 'endpoint-b-clipboard'); - renderSubInterfaces(null, 'endpoint-b-clipboard', 'endpoint-b-bottom'); + renderSubInterfaces(null, 'endpoint-b-top', 'endpoint-b-bottom', `${clickedEdge.data("target")}`); + renderSubInterfaces(null, 'endpoint-b-vnc-top', 'endpoint-b-vnc-bottom', `${clickedEdge.data("target")}`); + } else { console.info("No sub-interfaces found. The input data is null, undefined, or not an array."); - renderSubInterfaces(null, 'endpoint-b-edgeshark', 'endpoint-b-clipboard'); - renderSubInterfaces(null, 'endpoint-b-clipboard', 'endpoint-b-bottom'); + renderSubInterfaces(null, 'endpoint-b-top', 'endpoint-b-bottom', `${clickedEdge.data("target")}`); + renderSubInterfaces(null, 'endpoint-b-vnc-top', 'endpoint-b-vnc-bottom', `${clickedEdge.data("target")}`); + } - } + } catch (error) { + console.error("Failed to get SubInterface list:", error); + } - let actualLinkMacPair - if (isVscodeDeployment) { - // get Source MAC Address - try { - console.log("########################################################### Source MAC Address") - // const response = await sendMessageToVscodeEndpointPost("clab-link-mac-address", { - // nodeName: clickedEdge.data("extraData").clabSourceLongName, - // interfaceName: clickedEdge.data("extraData").clabSourcePort - // }); - // clabSourceMacAddress = response - - clabSourceMacAddress = clickedEdge.data("sourceMac") // aarafat-tag: get source MAC address from the edge data; suplied by the backend socket - console.log("###########################################") - console.log("Source MAC address:", clabSourceMacAddress); - if (clabSourceMacAddress) { - // render MAC address - document.getElementById("panel-link-endpoint-a-mac-address").textContent = clabSourceMacAddress - } - console.log("clicked-edge-sourceMac", clickedEdge.data("sourceMac")) - - clabSourceMtu = clickedEdge.data("sourceMtu") // aarafat-tag: get source MTU from the edge data; suplied by the backend socket - console.log("###########################################") - console.log("Source MAC address:", clabSourceMtu); - if (clabSourceMtu) { - // render MAC address - document.getElementById("panel-link-endpoint-a-mtu").textContent = clabSourceMtu - } - console.log("clicked-edge-sourceMtu", clickedEdge.data("sourceMtu")) - - clabSourceType = clickedEdge.data("sourceType") // aarafat-tag: get source MTU from the edge data; suplied by the backend socket - console.log("###########################################") - console.log("Source MAC address:", clabSourceType); - if (clabSourceType) { - // render MAC address - document.getElementById("panel-link-endpoint-a-type").textContent = clabSourceType - } - console.log("clicked-edge-sourceType", clickedEdge.data("sourceType")) - - } catch (error) { - console.error("Failed to get SubInterface list:", error); + // get Source MAC Address + try { + console.log("########################################################### Source MAC Address") + // const response = await sendMessageToVscodeEndpointPost("clab-link-mac-address", { + // nodeName: clickedEdge.data("extraData").clabSourceLongName, + // interfaceName: clickedEdge.data("extraData").clabSourcePort + // }); + // clabSourceMacAddress = response + + clabSourceMacAddress = clickedEdge.data("sourceMac") // aarafat-tag: get source MAC address from the edge data; suplied by the backend socket + console.log("###########################################") + console.log("Source MAC address:", clabSourceMacAddress); + if (clabSourceMacAddress) { + // render MAC address + document.getElementById("panel-link-endpoint-a-mac-address").textContent = clabSourceMacAddress } - - // get Target MAC Address - try { - console.log("########################################################### Target MAC Address") - // const response = await sendMessageToVscodeEndpointPost("clab-link-mac-address", { - // nodeName: clickedEdge.data("extraData").clabTargetLongName, - // interfaceName: clickedEdge.data("extraData").clabTargetPort - // }); - // clabTargetMacAddress = response - - clabTargetMacAddress = clickedEdge.data("targetMac") // aarafat-tag: get target MAC address from the edge data; suplied by the backend socket - console.log("###########################################") - console.log("Target MAC address:", clabTargetMacAddress); - if (clabTargetMacAddress) { - // render MAC address - document.getElementById("panel-link-endpoint-b-mac-address").textContent = clabTargetMacAddress - } - console.log("clicked-edge-targetMac", clickedEdge.data("targetMac")) - - clabTargetMtu = clickedEdge.data("targetMtu") // aarafat-tag: get target MTU from the edge data; suplied by the backend socket - console.log("###########################################") - console.log("Target MAC address:", clabTargetMtu); - if (clabTargetMtu) { - // render MAC address - document.getElementById("panel-link-endpoint-b-mtu").textContent = clabTargetMtu - } - console.log("clicked-edge-targetMtu", clickedEdge.data("targetMtu")) - - clabTargetType = clickedEdge.data("targetType") // aarafat-tag: get target MTU from the edge data; suplied by the backend socket - console.log("###########################################") - console.log("Target MAC address:", clabTargetType); - if (clabTargetType) { - // render MAC address - document.getElementById("panel-link-endpoint-b-type").textContent = clabTargetType - } - console.log("clicked-edge-targetType", clickedEdge.data("targetType")) - - } catch (error) { - console.error("Failed to get SubInterface list:", error); + console.log("clicked-edge-sourceMac", clickedEdge.data("sourceMac")) + + clabSourceMtu = clickedEdge.data("sourceMtu") // aarafat-tag: get source MTU from the edge data; suplied by the backend socket + console.log("###########################################") + console.log("Source MAC address:", clabSourceMtu); + if (clabSourceMtu) { + // render MAC address + document.getElementById("panel-link-endpoint-a-mtu").textContent = clabSourceMtu } - - - } else { - // setting MAC address endpoint-a values by getting the data from clab via /clab-link-mac GET API - clabLinkMacArgsList = [`${clickedEdge.data("extraData").clabSourceLongName}`, `${clickedEdge.data("extraData").clabTargetLongName}`] - actualLinkMacPair = await sendRequestToEndpointGetV3("/clab-link-macaddress", clabLinkMacArgsList) - - - console.info("actualLinkMacPair: ", actualLinkMacPair) - - // // setting MAC address endpoint-a values by getting the data from clab via /clab/link/${source_container}/${target_container}/mac GET API - // const actualLinkMacPair = await sendRequestToEndpointGetV2(`/clab/link/${source_container}/${target_container}/mac-address`, clabLinkMacArgsList=[]) - - sourceClabNode = `${clickedEdge.data("extraData").clabSourceLongName}` - targetClabNode = `${clickedEdge.data("extraData").clabTargetLongName}` - sourceIfName = `${clickedEdge.data("sourceEndpoint")}` - targetIfName = `${clickedEdge.data("targetEndpoint")}` - - const getMacAddressesResult = getMacAddresses(actualLinkMacPair["data"], sourceClabNode, targetClabNode, sourceIfName, targetIfName); - if (typeof getMacAddressesResult === "object") { // Ensure result is an object - console.info("Source If MAC:", getMacAddressesResult.sourceIfMac); // Access sourceIfMac - console.info("Target If MAC:", getMacAddressesResult.targetIfMac); // Access targetIfMac - - document.getElementById("panel-link-endpoint-a-mac-address").textContent = getMacAddressesResult.sourceIfMac - document.getElementById("panel-link-endpoint-b-mac-address").textContent = getMacAddressesResult.targetIfMac - - } else { - console.info(getMacAddressesResult); // Handle error message - - document.getElementById("panel-link-endpoint-a-mac-address").textContent = "Oops, no MAC address here!" - document.getElementById("panel-link-endpoint-b-mac-address").textContent = "Oops, no MAC address here!" + console.log("clicked-edge-sourceMtu", clickedEdge.data("sourceMtu")) + + clabSourceType = clickedEdge.data("sourceType") // aarafat-tag: get source MTU from the edge data; suplied by the backend socket + console.log("###########################################") + console.log("Source MAC address:", clabSourceType); + if (clabSourceType) { + // render MAC address + document.getElementById("panel-link-endpoint-a-type").textContent = clabSourceType } + console.log("clicked-edge-sourceType", clickedEdge.data("sourceType")) + } catch (error) { + console.error("Failed to get SubInterface list:", error); + } - function getMacAddresses(data, sourceClabNode, targetClabNode, sourceIfName, targetIfName) { - const result = data.find(item => - item.sourceClabNode === sourceClabNode && - item.targetClabNode === targetClabNode && - item.sourceIfName === sourceIfName && - item.targetIfName === targetIfName - ); - if (result) { - return { - sourceIfMac: result.sourceIfMac, - targetIfMac: result.targetIfMac - }; - } else { - return "No matching data found."; - } + // get Target MAC Address + try { + console.log("########################################################### Target MAC Address") + // const response = await sendMessageToVscodeEndpointPost("clab-link-mac-address", { + // nodeName: clickedEdge.data("extraData").clabTargetLongName, + // interfaceName: clickedEdge.data("extraData").clabTargetPort + // }); + // clabTargetMacAddress = response + + clabTargetMacAddress = clickedEdge.data("targetMac") // aarafat-tag: get target MAC address from the edge data; suplied by the backend socket + console.log("###########################################") + console.log("Target MAC address:", clabTargetMacAddress); + if (clabTargetMacAddress) { + // render MAC address + document.getElementById("panel-link-endpoint-b-mac-address").textContent = clabTargetMacAddress + } + console.log("clicked-edge-targetMac", clickedEdge.data("targetMac")) + + clabTargetMtu = clickedEdge.data("targetMtu") // aarafat-tag: get target MTU from the edge data; suplied by the backend socket + console.log("###########################################") + console.log("Target MAC address:", clabTargetMtu); + if (clabTargetMtu) { + // render MAC address + document.getElementById("panel-link-endpoint-b-mtu").textContent = clabTargetMtu } + console.log("clicked-edge-targetMtu", clickedEdge.data("targetMtu")) + + clabTargetType = clickedEdge.data("targetType") // aarafat-tag: get target MTU from the edge data; suplied by the backend socket + console.log("###########################################") + console.log("Target MAC address:", clabTargetType); + if (clabTargetType) { + // render MAC address + document.getElementById("panel-link-endpoint-b-type").textContent = clabTargetType + } + console.log("clicked-edge-targetType", clickedEdge.data("targetType")) + + } catch (error) { + console.error("Failed to get SubInterface list:", error); } let clabSourceLinkImpairmentClabData @@ -2283,7 +2192,6 @@ async function linkWireshark(event, option, endpoint, referenceElementAfterId) { try { let environments - let deploymentType let cytoTopologyJson let edgeData let clabUser @@ -2296,14 +2204,9 @@ async function linkWireshark(event, option, endpoint, referenceElementAfterId) { if (isVscodeDeployment) { - // call backend to get hostname - environments = await getEnvironments(event); console.info("linkWireshark - environments: ", environments); - // edgesharkHostUrl = await sendMessageToVscodeEndpointPost("clab-host-get-hostname", routerName); - // console.log("############### endpoint clab-host-get-hostname response from backend:", edgesharkHostUrl); - edgesharkHostUrl = environments["clab-allowed-hostname01"] || environments["clab-allowed-hostname"]; // used for edgeshark console.log("############### endpoint clab-host-get-hostname response from backend:", edgesharkHostUrl); @@ -2311,18 +2214,12 @@ async function linkWireshark(event, option, endpoint, referenceElementAfterId) { edgeData = findCytoElementById(cytoTopologyJson, edgeId); console.log("edgeData: ", edgeData); - clabSourceLongName = edgeData.data.extraData.clabSourceLongName; // used for edgeshark + clabSourceLongName = edgeData.data.extraData.clabSourceLongName; console.log("edgeData.data.extraData.clabSourceLongName: ", clabSourceLongName); - clabSourcePort = edgeData.data.extraData.clabSourcePort; // used for edgeshark - console.log("edgeData.data.extraData.clabSourcePort: ", clabSourcePort); - - clabTargetLongName = edgeData.data.extraData.clabTargetLongName; // used for edgeshark + clabTargetLongName = edgeData.data.extraData.clabTargetLongName; console.log("edgeData.data.extraData.clabTargetLongName: ", clabTargetLongName); - clabTargetPort = edgeData.data.extraData.clabTargetPort; // used for edgeshark - console.log("edgeData.data.extraData.clabTargetPort: ", clabTargetPort); - } else { environments = await getEnvironments(event); @@ -2344,19 +2241,9 @@ async function linkWireshark(event, option, endpoint, referenceElementAfterId) { clabTargetPort = edgeData.data.extraData.clabTargetPort; // used for edgeshark } - let wiresharkHref, baseUrl, urlParams, netNsResponse, netNsId, wiresharkSshCommand; + let baseUrl, wiresharkSshCommand; switch (option) { - case "app": - if (endpoint === "source") { - wiresharkHref = `clab-capture://${clabUser}@${clabServerAddress}?${clabSourceLongName}?${clabSourcePort}`; - } else if (endpoint === "target") { - wiresharkHref = `clab-capture://${clabUser}@${clabServerAddress}?${clabTargetLongName}?${clabTargetPort}`; - } - console.info("linkWireshark- wiresharkHref: ", wiresharkHref); - window.open(wiresharkHref); - break; - case "edgeSharkInterface": { baseUrl = `packetflix:ws://${edgesharkHostUrl}:5001/capture?`; if (endpoint === "source") { @@ -2364,52 +2251,30 @@ async function linkWireshark(event, option, endpoint, referenceElementAfterId) { try { const response = await sendMessageToVscodeEndpointPost("clab-link-capture", { nodeName: clabSourceLongName, - interfaceName: clabSourcePort + interfaceName: edgeData.data.extraData.clabSourcePort // resolve actual endpoint source }); console.info("External URL opened successfully:", response); } catch (error) { console.error("Failed to open external URL:", error); } - } else { - netNsResponse = await sendRequestToEndpointGetV3("/clab-node-network-namespace", [clabSourceLongName]); - netNsId = extractNamespaceId(netNsResponse.namespace_id); - console.info("linkWireshark - netNsSource: ", netNsId); - urlParams = `container={"netns":${netNsId},"network-interfaces":["${clabSourcePort}"],"name":"${clabSourceLongName.toLowerCase()}","type":"docker","prefix":""}&nif=${clabSourcePort}`; - const edgeSharkHref = baseUrl + urlParams; - console.info("linkWireshark - edgeSharkHref: ", edgeSharkHref); } } else if (endpoint === "target") { if (isVscodeDeployment) { try { const response = await sendMessageToVscodeEndpointPost("clab-link-capture", { nodeName: clabTargetLongName, - interfaceName: clabTargetPort + interfaceName: edgeData.data.extraData.clabTargetPort // resolve actual endpoint target }); console.info("External URL opened successfully:", response); } catch (error) { console.error("Failed to open external URL:", error); } - } else { - netNsResponse = await sendRequestToEndpointGetV3("/clab-node-network-namespace", [clabTargetLongName]); - netNsId = extractNamespaceId(netNsResponse.namespace_id); - console.info("linkWireshark - netNsTarget: ", netNsId); - urlParams = `container={"netns":${netNsId},"network-interfaces":["${clabTargetPort}"],"name":"${clabTargetLongName.toLowerCase()}","type":"docker","prefix":""}&nif=${clabTargetPort}`; - const edgeSharkHref = baseUrl + urlParams; - console.info("linkWireshark - edgeSharkHref: ", edgeSharkHref); } } - - - // window.open(edgeSharkHref); - - if (isVscodeDeployment) { - } else { - window.open(edgeSharkHref); - } break; } - case "edgeSharkSubInterface": + case "edgeSharkSubInterface": { if (referenceElementAfterId === "endpoint-a-top" || referenceElementAfterId === "endpoint-b-top") { baseUrl = `packetflix:ws://${edgesharkHostUrl}:5001/capture?`; if (isVscodeDeployment) { @@ -2418,7 +2283,7 @@ async function linkWireshark(event, option, endpoint, referenceElementAfterId) { try { const response = await sendMessageToVscodeEndpointPost("clab-link-capture", { nodeName: clabSourceLongName, - interfaceName: clabSourcePort + interfaceName: endpoint // passing directly endpoint, as ths subInterface endpoint calculated dyamically }); console.info("External URL opened successfully:", response); } catch (error) { @@ -2427,9 +2292,9 @@ async function linkWireshark(event, option, endpoint, referenceElementAfterId) { } else if (referenceElementAfterId === "endpoint-b-top") { console.info("linkWireshark - endpoint-b-subInterface"); try { - const response = await sendMessageToVscodeEndpointPost("link-capture", { + const response = await sendMessageToVscodeEndpointPost("clab-link-capture", { nodeName: clabTargetLongName, - interfaceName: clabTargetPort + interfaceName: endpoint // passing directly endpoint, as ths subInterface endpoint calculated dyamically }); console.info("External URL opened successfully:", response); } catch (error) { @@ -2437,43 +2302,80 @@ async function linkWireshark(event, option, endpoint, referenceElementAfterId) { } } - - } else { - if (referenceElementAfterId === "endpoint-a-edgeshark") { - netNsResponse = await sendRequestToEndpointGetV3("/clab-node-network-namespace", [clabSourceLongName]); - netNsId = extractNamespaceId(netNsResponse.namespace_id); - urlParams = `container={"netns":${netNsId},"network-interfaces":["${endpoint}"],"name":"${clabSourceLongName.toLowerCase()}","type":"docker","prefix":""}&nif=${endpoint}`; - } else { - console.info("linkWireshark - endpoint-b-edgeshark"); - netNsResponse = await sendRequestToEndpointGetV3("/clab-node-network-namespace", [clabTargetLongName]); - netNsId = extractNamespaceId(netNsResponse.namespace_id); - urlParams = `container={"netns":${netNsId},"network-interfaces":["${endpoint}"],"name":"${clabSourceLongName.toLowerCase()}","type":"docker","prefix":""}&nif=${endpoint}`; - } - const edgeSharkHref = baseUrl + urlParams; - console.info("linkWireshark - edgeSharkHref: ", edgeSharkHref); - window.open(edgeSharkHref); } - } else if (referenceElementAfterId === "endpoint-a-clipboard" || referenceElementAfterId === "endpoint-b-clipboard") { - console.info(`linkWireshark - ${referenceElementAfterId}`); - const targetLongName = referenceElementAfterId === "endpoint-a-clipboard" ? clabSourceLongName : clabTargetLongName; - const targetPort = referenceElementAfterId === "endpoint-a-clipboard" ? clabSourcePort : clabTargetPort; - - // Both container and colocated use the same command in this case. - wiresharkSshCommand = `ssh ${clabUser}@${environments["clab-allowed-hostname"]} "sudo -S /sbin/ip netns exec ${targetLongName} tcpdump -U -nni ${endpoint} -w -" | wireshark -k -i -`; - await copyToClipboard(wiresharkSshCommand); } + // else if (referenceElementAfterId === "endpoint-a-clipboard" || referenceElementAfterId === "endpoint-b-clipboard") { + // console.info(`linkWireshark - ${referenceElementAfterId}`); + // const targetLongName = referenceElementAfterId === "endpoint-a-clipboard" ? clabSourceLongName : clabTargetLongName; + // const targetPort = referenceElementAfterId === "endpoint-a-clipboard" ? clabSourcePort : clabTargetPort; + + // // Both container and colocated use the same command in this case. + // wiresharkSshCommand = `ssh ${clabUser}@${environments["clab-allowed-hostname"]} "sudo -S /sbin/ip netns exec ${targetLongName} tcpdump -U -nni ${endpoint} -w -" | wireshark -k -i -`; + // await copyToClipboard(wiresharkSshCommand); + // } break; + } - case "copy": + case "edgeSharkInterfaceVnc": { + // this feature only maintaned for vscode only + baseUrl = `packetflix:ws://${edgesharkHostUrl}:5001/capture?`; if (endpoint === "source") { - wiresharkSshCommand = `ssh ${clabUser}@${environments["clab-allowed-hostname"]} "sudo -S /sbin/ip netns exec ${clabSourceLongName} tcpdump -U -nni ${clabSourcePort} -w -" | wireshark -k -i -`; + try { + const response = await sendMessageToVscodeEndpointPost("clab-link-capture-edgeshark-vnc", { + nodeName: clabSourceLongName, + interfaceName: edgeData.data.extraData.clabSourcePort // resolve actual endpoint source + }); + console.info("External URL opened successfully:", response); + } catch (error) { + console.error("Failed to open external URL:", error); + } } else if (endpoint === "target") { - wiresharkSshCommand = `ssh ${clabUser}@${environments["clab-allowed-hostname"]} "sudo -S /sbin/ip netns exec ${clabTargetLongName} tcpdump -U -nni ${clabTargetPort} -w -" | wireshark -k -i -`; + try { + const response = await sendMessageToVscodeEndpointPost("clab-link-capture-edgeshark-vnc", { + nodeName: clabTargetLongName, + interfaceName: edgeData.data.extraData.clabTargetPort // resolve actual endpoint target + }); + console.info("External URL opened successfully:", response); + } catch (error) { + console.error("Failed to open external URL:", error); + } } - console.info("linkWireshark- wiresharkSshCommand: ", wiresharkSshCommand); - await copyToClipboard(wiresharkSshCommand); break; + } + + case "edgeSharkSubInterfaceVnc": { + if (referenceElementAfterId === "endpoint-a-vnc-top" || referenceElementAfterId === "endpoint-b-vnc-top") { + baseUrl = `packetflix:ws://${edgesharkHostUrl}:5001/capture?`; + if (referenceElementAfterId === "endpoint-a-vnc-top") { + console.info("linkWireshark - endpoint-a-vnc-subInterface"); + try { + console.log("sourceSubInterface", endpoint) + const response = await sendMessageToVscodeEndpointPost("clab-link-capture-edgeshark-vnc", { + nodeName: clabSourceLongName, + interfaceName: endpoint + // interfaceName: "ethernet-1/1.1" + }); + console.info("External URL opened successfully:", response); + } catch (error) { + console.error("Failed to open external URL:", error); + } + } else if (referenceElementAfterId === "endpoint-b-vnc-top") { + console.info("linkWireshark - endpoint-b-vnc-subInterface"); + try { + console.log("targetSubInterface", endpoint) + const response = await sendMessageToVscodeEndpointPost("clab-link-capture-edgeshark-vnc", { + nodeName: clabTargetLongName, + interfaceName: endpoint + }); + console.info("External URL opened successfully:", response); + } catch (error) { + console.error("Failed to open external URL:", error); + } + } + } + break; + } default: console.warn("linkWireshark - Unknown option provided:", option); @@ -4891,17 +4793,20 @@ async function fetchAndLoadData() { } } - - async function renderSubInterfaces(subInterfaces, referenceElementAfterId, referenceElementBeforeId, nodeName) { console.log("##### renderSubInterfaces is called") console.log("##### subInterfaces: ", subInterfaces) const containerSelectorId = 'panel-link-action-dropdown-menu-dropdown-content'; + // Determine the capture mode + const captureMode = referenceElementAfterId.includes("vnc") + ? "edgeSharkSubInterfaceVnc" + : "edgeSharkSubInterface"; + const onClickHandler = (event, subInterface) => { console.info(`Clicked on: ${subInterface}`); - linkWireshark(event, "edgeSharkSubInterface", subInterface, referenceElementAfterId); + linkWireshark(event, captureMode, subInterface, referenceElementAfterId); }; // Validate container @@ -4923,18 +4828,12 @@ async function renderSubInterfaces(subInterfaces, referenceElementAfterId, refer let currentNode = referenceElementAfter.nextSibling; while (currentNode && currentNode !== referenceElementBefore) { const nextNode = currentNode.nextSibling; - currentNode.remove(); // Remove the current node + currentNode.remove(); currentNode = nextNode; } - // Handle case when subInterfaces is null if (!subInterfaces) { console.info("Sub-interfaces is null. Cleared existing items and performed no further actions."); - // Optionally, you could display a placeholder message or take other actions: - // const placeholder = document.createElement("div"); - // placeholder.textContent = "No sub-interfaces available."; - // placeholder.style.textAlign = "center"; - // insertAfter(placeholder, referenceElementAfter); return; } @@ -4951,7 +4850,6 @@ async function renderSubInterfaces(subInterfaces, referenceElementAfterId, refer }); } - // Helper function to insert an element after a reference element function insertAfter(newNode, referenceNode) { referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling); diff --git a/src/topoViewer/webview-ui/html-static/template/vscodeHtmlTemplate.ts b/src/topoViewer/webview-ui/html-static/template/vscodeHtmlTemplate.ts index a0ca74f8a..cd9a6a92c 100644 --- a/src/topoViewer/webview-ui/html-static/template/vscodeHtmlTemplate.ts +++ b/src/topoViewer/webview-ui/html-static/template/vscodeHtmlTemplate.ts @@ -1271,29 +1271,47 @@ return ` Capture E.Point-A (Edgeshark) - + id="endpoint-a-edgeshark">Capture E.Point-A (Edgeshark) + - - - + - + id="endpoint-a-bottom"> + + Capture E.Point-B (Edgeshark) - + id="endpoint-b-edgeshark">Capture E.Point-B (Edgeshark) + + + + - + + + Capture E.Point-A VNC (Edgeshark) + + + + id="endpoint-a-vnc-bottom"> + + + Capture E.Point-B VNC (Edgeshark) + + + + + + diff --git a/src/utils.ts b/src/utils.ts index f6e9376ca..baa8e0a60 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -2,7 +2,8 @@ import * as vscode from "vscode"; import * as path from 'path'; import * as fs from "fs"; import * as os from "os"; -import { execSync } from "child_process"; +import { exec, execSync } from "child_process"; +import * as net from 'net'; export function stripAnsi(input: string): string { const esc = String.fromCharCode(27); @@ -114,3 +115,50 @@ export function getUsername(): string { } return username; } + +// eslint-disable-next-line no-undef +export function execWithProgress(command: string, progressMessage: string): Thenable { + return vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: progressMessage, + cancellable: false + }, + (progress) => new Promise((resolve, reject) => { + const child = exec(command, { encoding: 'utf-8' }, (err, stdout, stderr) => { + if (err) { + vscode.window.showErrorMessage(`Failed: ${stderr}`); + return reject(err); + } + resolve(stdout.trim()); + }); + + child.stderr?.on('data', (data) => { + const line = data.toString().trim(); + if (line) progress.report({ message: line }); + }); + }) + ); +} + +export async function getFreePort(): Promise { + return new Promise((resolve, reject) => { + const server = net.createServer(); + server.listen(0, '127.0.0.1'); + server.on('listening', () => { + const address = server.address(); + server.close(); + if (typeof address === 'object' && address?.port) { + resolve(address.port); + } else { + reject(new Error('Could not get free port')); + } + }); + server.on('error', reject); + }); +} + +// Get the config, set the default to undefined as all defaults **SHOULD** be set in package.json +export function getConfig(relCfgPath: string): any { + return vscode.workspace.getConfiguration("containerlab").get(relCfgPath, undefined) +} \ No newline at end of file diff --git a/test/unit/commands/clabCommand.test.ts b/test/unit/commands/clabCommand.test.ts index ecf7e7946..f5eeb5eeb 100644 --- a/test/unit/commands/clabCommand.test.ts +++ b/test/unit/commands/clabCommand.test.ts @@ -61,7 +61,7 @@ describe('ClabCommand', () => { vscodeStub.TreeItemCollapsibleState.None, { absolute: '/tmp/lab.yml', relative: 'lab.yml' } ); - const clab = new ClabCommand('deploy', node, undefined, true, 'term'); + const clab = new ClabCommand('deploy', node); await clab.run(['--foo']); expect(cmdStub.instances).to.have.lengthOf(1);