// services/vcMonitorService.js // On-demand packet capture (ExtendedLogging + PacketDump) for Cisco RoomOS / Webex video endpoints. // Uses cloud xAPI (requires spark:xapi_commands scope on the Service App / Integration). // After capture, logs (containing the pcap) are retrieved from Control Hub diagnostics — not a direct file download. import webex from '../integrations/webex/WebexClient.js'; import xapi from '../integrations/webex/XapiClient.js'; import { logger } from '../utils/logger.js'; const VALID_PACKETDUMP = ['Full', 'Limited', 'FullRotate', 'None']; export async function startPacketCapture(bot, serialNumber, packetDump = 'Full') { if (!serialNumber) throw new Error('Serial number is required'); const dumpType = normalizePacketDump(packetDump); logger('vc-monitor', `Starting packet capture for serial ${serialNumber} (PacketDump=${dumpType})`); try { await bot.say('markdown', `🚀 Starting extended logging + packet capture on **${serialNumber}** (PacketDump: **${dumpType}**)...`); // 1. Find device (same pattern as vcProvision) const devicesResponse = await webex.request('GET', 'devices', null, { serial: serialNumber }); if (!devicesResponse.items || devicesResponse.items.length === 0) { throw new Error(`No device found with serial ${serialNumber}`); } if (devicesResponse.items.length > 1) { throw new Error(`Multiple devices found with serial ${serialNumber}`); } const device = devicesResponse.items[0]; const display = device.displayName || device.serial || serialNumber; logger('vc-monitor', `Found device ${display} (${device.id})`); await bot.say('markdown', `✅ Device found: **${display}**`); // 2. Issue the xCommand await bot.say('markdown', `📡 Issuing \`Logging ExtendedLogging Start PacketDump: ${dumpType}\` ...`); const result = await xapi.xCommandWithDevice( 'Logging.ExtendedLogging.Start', device.id, { PacketDump: dumpType } ); logger('vc-monitor', `xCommand result: ${JSON.stringify(result)}`); const durationHint = dumpType === 'Full' ? '~3 minutes (includes RTP/media)' : dumpType === 'Limited' ? '~10 minutes (non-RTP/signaling)' : dumpType === 'FullRotate' ? 'rolling (keeps recent ~1h worth)' : 'no packet dump'; await bot.say('markdown', `✅ **Capture started successfully**\n\n` + `**Device:** ${display}\n` + `**PacketDump:** ${dumpType} — ${durationHint}\n\n` + `**Next steps (important):**\n` + `1. Reproduce the problem **now** while the capture is running.\n` + `2. When finished, run:\n` + ` \`/vcMonitor ${serialNumber} stop\`\n` + `3. Download the logs from **Control Hub**:\n` + ` - Devices → find device → **Issues & Diagnostics** → **System Logs**\n` + ` - Look for the most recent log bundle (the packet capture .pcap files are included, typically in the \`run/\` folder or a dedicated "Packet Captures" section).\n\n` + `The capture will time out automatically, but stopping explicitly is recommended for a clean bundle.` ); return { success: true, deviceId: device.id, dumpType, result }; } catch (error) { const msg = error.response?.data?.message || error.message || 'Unknown error'; logger('vc-monitor', `startPacketCapture failed for ${serialNumber}: ${msg}`, 'error'); // Common permission hint if (msg.toLowerCase().includes('403') || msg.toLowerCase().includes('unauthorized') || msg.toLowerCase().includes('scope')) { await bot.say('markdown', `⚠️ **Permission error** — does your Webex integration have the \`spark:xapi_commands\` scope?`); } await bot.say('markdown', `❌ **Failed to start capture on ${serialNumber}**\n\n${msg}`); throw error; } } export async function stopPacketCapture(bot, serialNumber) { if (!serialNumber) throw new Error('Serial number is required'); logger('vc-monitor', `Stopping packet capture for serial ${serialNumber}`); try { await bot.say('markdown', `🛑 Stopping extended logging + packet capture on **${serialNumber}**...`); const devicesResponse = await webex.request('GET', 'devices', null, { serial: serialNumber }); if (!devicesResponse.items || devicesResponse.items.length === 0) { throw new Error(`No device found with serial ${serialNumber}`); } const device = devicesResponse.items[0]; const display = device.displayName || device.serial || serialNumber; const result = await xapi.xCommandWithDevice('Logging.ExtendedLogging.Stop', device.id, {}); logger('vc-monitor', `Stop result: ${JSON.stringify(result)}`); await bot.say('markdown', `✅ **Capture stopped** on **${display}**\n\n` + `Now retrieve the logs (which include the packet captures):\n` + `- Control Hub → Devices → select the device → **Issues & Diagnostics** → **System Logs** (download the latest bundle).\n` + `- Or use the device's local web UI (Issues and Diagnostics) if you have direct access.\n\n` + `Tip: Full bundles are usually what you want (they contain the PCAPs in the run/ directory).` ); return { success: true, deviceId: device.id, result }; } catch (error) { const msg = error.response?.data?.message || error.message || 'Unknown error'; logger('vc-monitor', `stopPacketCapture failed for ${serialNumber}: ${msg}`, 'error'); await bot.say('markdown', `❌ **Failed to stop capture on ${serialNumber}**\n\n${msg}`); throw error; } } export async function getExtendedLoggingStatus(bot, serialNumber) { if (!serialNumber) throw new Error('Serial number is required'); logger('vc-monitor', `Querying ExtendedLogging status for ${serialNumber}`); try { await bot.say('markdown', `📡 Querying status on **${serialNumber}**...`); const devicesResponse = await webex.request('GET', 'devices', null, { serial: serialNumber }); if (!devicesResponse.items || devicesResponse.items.length === 0) { throw new Error(`No device found with serial ${serialNumber}`); } const device = devicesResponse.items[0]; const display = device.displayName || device.serial || serialNumber; // Query the whole ExtendedLogging subtree const status = await xapi.xStatus(device.id, 'Logging.ExtendedLogging'); logger('vc-monitor', `Status response keys: ${Object.keys(status || {})}`); // Try to surface the useful bits (structure can vary slightly by RoomOS version) const ext = status?.result?.Logging?.ExtendedLogging || status?.result || {}; const mode = ext.Mode || ext.mode || '—'; const packetDump = ext.PacketDump || ext.packetDump || '—'; let summary = `**ExtendedLogging status for ${display}:**\n`; summary += `- Mode: **${mode}**\n`; summary += `- PacketDump: **${packetDump}**\n`; // If more detail is present (e.g. under PacketDump or files), surface a bit if (ext.PacketDump && typeof ext.PacketDump === 'object') { summary += `- Details: ${JSON.stringify(ext.PacketDump)}\n`; } await bot.say('markdown', summary); // Also give a compact raw snippet for debugging (don't overwhelm) const raw = JSON.stringify(status, null, 2); const snippet = raw.length > 1200 ? raw.slice(0, 1200) + '\n... (truncated)' : raw; await bot.say('markdown', `**Raw status (for diagnostics):**\n\`\`\`json\n${snippet}\n\`\`\``); return { success: true, deviceId: device.id, status, mode, packetDump }; } catch (error) { const msg = error.response?.data?.message || error.message || 'Unknown error'; logger('vc-monitor', `getExtendedLoggingStatus failed for ${serialNumber}: ${msg}`, 'error'); await bot.say('markdown', `❌ **Failed to get status for ${serialNumber}**\n\n${msg}`); throw error; } } function normalizePacketDump(input = 'Full') { const s = String(input || '').trim(); const match = VALID_PACKETDUMP.find(v => v.toLowerCase() === s.toLowerCase()); return match || 'Full'; } export default { startPacketCapture, stopPacketCapture, getExtendedLoggingStatus };