ServiceChannel webhook processor, proposal approval cards, attachment auto-post, CollabSupport commands, and Docker deployment configuration. Co-authored-by: Cursor <cursoragent@cursor.com>
708 lines
No EOL
24 KiB
JavaScript
708 lines
No EOL
24 KiB
JavaScript
import fs from 'fs';
|
||
import csv from 'csv-parser';
|
||
import path from 'path';
|
||
import axios from 'axios';
|
||
import pLimit from 'p-limit';
|
||
import { Parser } from 'json2csv';
|
||
|
||
var merakiNetworks = [];
|
||
|
||
|
||
var config = JSON.parse(fs.readFileSync('./config/config.json'));
|
||
|
||
let lastPortFetchTime = 0;
|
||
const MIN_DELAY_MS = 150; // ~6–7 calls/sec — safe under Meraki's 10/sec steady limit
|
||
const MAX_RETRIES = 5;
|
||
const BASE_BACKOFF_MS = 1000; // 1s, 2s, 4s, 8s, 16s...
|
||
|
||
// Global cache for device info (serial → {model, name, type, ...})
|
||
const deviceInfoCache = new Map();
|
||
|
||
|
||
// Assume you have these functions from your previous code
|
||
// e.g., findMerakiNetworkId(storeNum) → returns network ID
|
||
// getMerakiClients(networkId) → returns array of clients with description, vlan, switchport, status, etc.
|
||
|
||
async function getMerakiDeviceInfo(serial) {
|
||
if (!serial || serial === 'N/A') return null;
|
||
|
||
if (deviceInfoCache.has(serial)) {
|
||
return deviceInfoCache.get(serial);
|
||
}
|
||
|
||
const url = `https://api.meraki.com/api/v1/devices/${serial}`;
|
||
|
||
for (let attempt = 1; attempt <= 3; attempt++) {
|
||
try {
|
||
const response = await fetch(url, {
|
||
method: 'GET',
|
||
headers: {
|
||
'X-Cisco-Meraki-API-Key': config.auth.meraki.apiKey,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
});
|
||
|
||
if (response.status === 429) {
|
||
let retryAfterSec = 2;
|
||
const retryAfter = response.headers.get('retry-after');
|
||
if (retryAfter) retryAfterSec = parseInt(retryAfter, 10) || 2;
|
||
console.warn(`429 on device info for ${serial} - wait ${retryAfterSec}s`);
|
||
await new Promise(r => setTimeout(r, retryAfterSec * 1000 + 300));
|
||
continue;
|
||
}
|
||
|
||
if (!response.ok) {
|
||
console.warn(`Device info fetch failed for ${serial}: HTTP ${response.status}`);
|
||
return null;
|
||
}
|
||
|
||
const info = await response.json();
|
||
deviceInfoCache.set(serial, info);
|
||
return info;
|
||
} catch (err) {
|
||
console.warn(`Device info attempt ${attempt} error for ${serial}: ${err.message}`);
|
||
if (attempt === 3) return null;
|
||
await new Promise(r => setTimeout(r, 1000 * attempt));
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
async function throttledGetMerakiSwitchPortConfig(serial, portId) {
|
||
if (!serial || !portId) {
|
||
throw new Error('Missing serial or portId for port config fetch');
|
||
}
|
||
|
||
const url = `https://api.meraki.com/api/v1/devices/${serial}/switch/ports/${portId}`;
|
||
|
||
// Step 1: Enforce minimum delay between calls
|
||
const now = Date.now();
|
||
const timeSinceLast = now - lastPortFetchTime;
|
||
if (timeSinceLast < MIN_DELAY_MS) {
|
||
const waitMs = MIN_DELAY_MS - timeSinceLast;
|
||
console.log(`Throttling port fetch for ${serial}/${portId} — waiting ${waitMs}ms`);
|
||
await new Promise(resolve => setTimeout(resolve, waitMs));
|
||
}
|
||
|
||
// Update timestamp right before the request (more accurate for burst control)
|
||
lastPortFetchTime = Date.now();
|
||
|
||
// Step 2: Attempt the request with retries on 429 and transient errors
|
||
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
||
try {
|
||
const response = await fetch(url, {
|
||
method: 'GET',
|
||
headers: {
|
||
'X-Cisco-Meraki-API-Key': config.auth.meraki.apiKey,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
});
|
||
|
||
// Handle 429 specifically
|
||
if (response.status === 429) {
|
||
let retryAfterSec = 2; // default fallback
|
||
|
||
// Respect Retry-After header if present
|
||
const retryAfterHeader = response.headers.get('retry-after');
|
||
if (retryAfterHeader) {
|
||
const parsed = parseInt(retryAfterHeader, 10);
|
||
if (!isNaN(parsed) && parsed > 0) {
|
||
retryAfterSec = parsed;
|
||
}
|
||
}
|
||
|
||
console.warn(
|
||
`429 Too Many Requests on ${serial}/${portId} ` +
|
||
`(attempt ${attempt}/${MAX_RETRIES}) — waiting ${retryAfterSec}s`
|
||
);
|
||
|
||
await new Promise(resolve => setTimeout(resolve, retryAfterSec * 1000 + 200)); // +200ms jitter
|
||
continue; // retry
|
||
}
|
||
|
||
if (!response.ok) {
|
||
const errorText = await response.text().catch(() => 'Unknown error');
|
||
throw new Error(`Meraki API failed (${response.status}): ${errorText}`);
|
||
}
|
||
|
||
const data = await response.json();
|
||
console.log(`Successfully fetched port config for ${serial}/${portId}`);
|
||
return data;
|
||
|
||
} catch (err) {
|
||
console.error(
|
||
`Port fetch error for ${serial}/${portId} (attempt ${attempt}/${MAX_RETRIES}): ${err.message}`
|
||
);
|
||
|
||
if (attempt === MAX_RETRIES) {
|
||
throw new Error(
|
||
`Failed to fetch port ${portId} on switch ${serial} after ${MAX_RETRIES} attempts: ${err.message}`
|
||
);
|
||
}
|
||
|
||
// Exponential backoff for non-429 errors too (e.g., 500s, network blips)
|
||
const backoffMs = BASE_BACKOFF_MS * Math.pow(2, attempt - 1) + Math.random() * 200; // jitter
|
||
console.log(`Backing off for ${Math.round(backoffMs)}ms before retry ${attempt + 1}`);
|
||
await new Promise(resolve => setTimeout(resolve, backoffMs));
|
||
}
|
||
}
|
||
|
||
// Should never reach here due to throw on last attempt
|
||
throw new Error('Unexpected exit from retry loop');
|
||
}
|
||
// Helper: Fetch a single switch port configuration from Meraki API
|
||
async function getMerakiSwitchPortConfig(serial, portId) {
|
||
if (!serial || !portId) {
|
||
throw new Error('Missing serial or portId for port config fetch');
|
||
}
|
||
|
||
const url = `https://api.meraki.com/api/v1/devices/${serial}/switch/ports/${portId}`;
|
||
|
||
try {
|
||
const response = await fetch(url, {
|
||
method: 'GET',
|
||
headers: {
|
||
'X-Cisco-Meraki-API-Key': config.auth.meraki.apiKey,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const errorText = await response.text().catch(() => 'Unknown error');
|
||
throw new Error(`Meraki API failed (${response.status}): ${errorText}`);
|
||
}
|
||
|
||
return await response.json();
|
||
} catch (err) {
|
||
throw new Error(`Failed to fetch port ${portId} on switch ${serial}: ${err.message}`);
|
||
}
|
||
}
|
||
|
||
// Helper: Fetch all clients for a network (with basic pagination support)
|
||
async function getMerakiClients(networkId) {
|
||
let allClients = [];
|
||
let startingAfter = null;
|
||
const perPage = 1000;
|
||
|
||
do {
|
||
let url = `https://api.meraki.com/api/v1/networks/${networkId}/clients?perPage=${perPage}`;
|
||
if (startingAfter) {
|
||
url += `&startingAfter=${encodeURIComponent(startingAfter)}`;
|
||
}
|
||
|
||
for (let attempt = 1; attempt <= 5; attempt++) {
|
||
try {
|
||
const response = await fetch(url, {
|
||
method: 'GET',
|
||
headers: {
|
||
'X-Cisco-Meraki-API-Key': config.auth.meraki.apiKey,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
});
|
||
|
||
if (response.status === 429) {
|
||
let retryAfterSec = 2;
|
||
const retryAfterHeader = response.headers.get('retry-after');
|
||
if (retryAfterHeader) {
|
||
const parsed = parseInt(retryAfterHeader, 10);
|
||
if (!isNaN(parsed) && parsed > 0) retryAfterSec = parsed;
|
||
}
|
||
console.warn(`429 on clients page for ${networkId} (attempt ${attempt}) - waiting ${retryAfterSec}s`);
|
||
await new Promise(r => setTimeout(r, retryAfterSec * 1000 + 300)); // jitter
|
||
continue;
|
||
}
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`Clients fetch failed: ${response.status}`);
|
||
}
|
||
|
||
const clients = await response.json();
|
||
allClients = allClients.concat(clients);
|
||
startingAfter = clients.length === perPage ? clients[clients.length - 1].id : null;
|
||
break; // success → next page
|
||
|
||
} catch (err) {
|
||
if (attempt === 5) throw err;
|
||
const backoff = 1000 * Math.pow(2, attempt - 1) + Math.random() * 500;
|
||
console.log(`Retry backoff ${Math.round(backoff)}ms for ${networkId}`);
|
||
await new Promise(r => setTimeout(r, backoff));
|
||
}
|
||
}
|
||
} while (startingAfter);
|
||
|
||
return allClients;
|
||
}
|
||
|
||
async function getAllMerakiSwitchPorts(serial) {
|
||
if (!serial || serial === 'N/A') return [];
|
||
|
||
// Check if it's actually a switch
|
||
const device = await getMerakiDeviceInfo(serial);
|
||
if (!device || !device.model?.startsWith('MS')) {
|
||
const model = device?.model || 'unknown';
|
||
console.log(`Skipping ports fetch for non-switch device ${serial} (model: ${model})`);
|
||
return [];
|
||
}
|
||
|
||
console.log(`Fetching ports for confirmed switch ${serial} (model: ${device.model})`);
|
||
|
||
const url = `https://api.meraki.com/api/v1/devices/${serial}/switch/ports`;
|
||
|
||
for (let attempt = 1; attempt <= 3; attempt++) {
|
||
try {
|
||
const response = await fetch(url, {
|
||
method: 'GET',
|
||
headers: {
|
||
'X-Cisco-Meraki-API-Key': config.auth.meraki.apiKey,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
});
|
||
|
||
if (response.status === 429) {
|
||
let retryAfterSec = 2;
|
||
const retryAfter = response.headers.get('retry-after');
|
||
if (retryAfter) retryAfterSec = parseInt(retryAfter, 10) || 2;
|
||
console.warn(`429 on ports fetch for ${serial} - wait ${retryAfterSec}s`);
|
||
await new Promise(r => setTimeout(r, retryAfterSec * 1000 + 300));
|
||
continue;
|
||
}
|
||
|
||
if (!response.ok) {
|
||
const errText = await response.text().catch(() => 'Unknown');
|
||
throw new Error(`HTTP ${response.status}: ${errText}`);
|
||
}
|
||
|
||
const ports = await response.json();
|
||
console.log(`Fetched ${ports.length} ports for switch ${serial}`);
|
||
return ports;
|
||
} catch (err) {
|
||
console.warn(`Ports fetch attempt ${attempt} failed for ${serial}: ${err.message}`);
|
||
if (attempt === 3) return [];
|
||
await new Promise(r => setTimeout(r, 1000 * attempt));
|
||
}
|
||
}
|
||
|
||
return [];
|
||
}
|
||
|
||
// Main function – now outputs CSV directly
|
||
async function processCSVAndEnrichMeraki(csvFilePath, outputCsvPath = 'enriched-meraki-output.csv') {
|
||
const results = [];
|
||
|
||
// 1. Parse input CSV
|
||
await new Promise((resolve, reject) => {
|
||
fs.createReadStream(csvFilePath)
|
||
.pipe(csv())
|
||
.on('data', row => results.push(row))
|
||
.on('end', () => {
|
||
console.log(`Input CSV parsed - ${results.length} rows`);
|
||
resolve();
|
||
})
|
||
.on('error', err => reject(err));
|
||
});
|
||
|
||
// 2. Cache networks if not already done
|
||
if (merakiNetworks.length === 0) {
|
||
await cacheMerakiNetworks();
|
||
}
|
||
|
||
// 3. Build unique store → network map
|
||
const storeToNetwork = new Map();
|
||
const limit = pLimit(4);
|
||
|
||
const networkPromises = results.map(row => {
|
||
const friendlyName = row.device_friendly_name?.trim() || '';
|
||
if (!friendlyName) return Promise.resolve();
|
||
const storeNum = friendlyName.match(/(\d{6})/)?.[0];
|
||
if (!storeNum || storeToNetwork.has(storeNum)) return Promise.resolve();
|
||
|
||
return limit(() =>
|
||
findMerakiNetworkId(storeNum)
|
||
.then(netId => { if (netId) storeToNetwork.set(storeNum, netId); })
|
||
.catch(() => { })
|
||
);
|
||
});
|
||
|
||
await Promise.all(networkPromises);
|
||
console.log(`Found ${storeToNetwork.size} unique networks`);
|
||
|
||
// 4. Pre-fetch clients per network
|
||
const networkClientCache = new Map();
|
||
const clientPromises = [...storeToNetwork.entries()].map(([storeNum, netId]) =>
|
||
limit(async () => {
|
||
try {
|
||
const clients = await getMerakiClients(netId);
|
||
networkClientCache.set(netId, clients);
|
||
console.log(`Clients fetched: ${clients.length} for store ${storeNum}`);
|
||
} catch (err) {
|
||
console.error(`Clients fetch failed for store ${storeNum}: ${err.message}`);
|
||
networkClientCache.set(netId, []);
|
||
}
|
||
})
|
||
);
|
||
|
||
await Promise.all(clientPromises);
|
||
|
||
// 5. Enrich rows – collect unique serials first
|
||
const enrichedRows = results.map(row => ({ ...row, meraki: null, error: null }));
|
||
const serialToPorts = new Map();
|
||
const uniqueSerials = new Set();
|
||
|
||
// Phase A: Match clients & collect serials
|
||
for (let i = 0; i < results.length; i++) {
|
||
const row = results[i];
|
||
const friendlyName = row.device_friendly_name?.trim() || '';
|
||
if (!friendlyName) {
|
||
enrichedRows[i].error = 'Missing friendly name';
|
||
continue;
|
||
}
|
||
|
||
if (match.recentDeviceConnection !== 'Wired' || !match.switchport) {
|
||
enrichedRows[i].error = 'Wireless or non-switch connected client';
|
||
continue;
|
||
}
|
||
|
||
const storeNum = friendlyName.match(/(\d{6})/)?.[0];
|
||
if (!storeNum) continue;
|
||
|
||
const netId = storeToNetwork.get(storeNum);
|
||
if (!netId) continue;
|
||
|
||
const clients = networkClientCache.get(netId) || [];
|
||
const match = clients.find(c =>
|
||
c.description?.toUpperCase().trim() === friendlyName.toUpperCase().trim()
|
||
);
|
||
|
||
if (!match) {
|
||
enrichedRows[i].error = 'No matching Meraki client';
|
||
continue;
|
||
}
|
||
|
||
const merakiInfo = {
|
||
deviceName: match.recentDeviceName || 'N/A',
|
||
deviceSerial: match.recentDeviceSerial || 'N/A',
|
||
switchport: match.switchport || 'N/A',
|
||
vlan: match.vlan || 'N/A',
|
||
status: match.status || 'Unknown',
|
||
connection: match.recentDeviceConnection || 'N/A',
|
||
lastSeen: match.lastSeen || 'N/A',
|
||
ip: match.ip || 'N/A',
|
||
mac: match.mac || 'N/A'
|
||
};
|
||
|
||
enrichedRows[i].meraki = merakiInfo;
|
||
|
||
if (merakiInfo.deviceSerial !== 'N/A') {
|
||
uniqueSerials.add(merakiInfo.deviceSerial);
|
||
}
|
||
}
|
||
|
||
// Phase B: Pre-fetch all ports for unique switches
|
||
if (uniqueSerials.size > 0) {
|
||
console.log(`Fetching ports for ${uniqueSerials.size} unique switches...`);
|
||
const portPromises = [...uniqueSerials].map(serial =>
|
||
limit(async () => {
|
||
const ports = await getAllMerakiSwitchPorts(serial);
|
||
serialToPorts.set(serial, ports);
|
||
})
|
||
);
|
||
await Promise.all(portPromises);
|
||
}
|
||
|
||
// Phase C: Final enrichment from cache
|
||
for (const row of enrichedRows) {
|
||
const meraki = row.meraki;
|
||
if (!meraki || meraki.deviceSerial === 'N/A' || meraki.switchport === 'N/A') {
|
||
if (meraki) {
|
||
Object.assign(meraki, {
|
||
portType: 'N/A',
|
||
accessPolicyType: 'N/A',
|
||
stickyMacEnabled: false,
|
||
stickyMacList: [],
|
||
stickyMacAllowListLimit: 0,
|
||
stickyMacCount: 0,
|
||
portEnabled: 'N/A'
|
||
});
|
||
}
|
||
continue;
|
||
}
|
||
|
||
const ports = serialToPorts.get(meraki.deviceSerial) || [];
|
||
const portConfig = ports.find(p =>
|
||
String(p.port || p.portId || p.number) === String(meraki.switchport)
|
||
);
|
||
|
||
if (portConfig) {
|
||
meraki.portType = portConfig.type || 'N/A';
|
||
meraki.accessPolicyType = portConfig.accessPolicyType || 'N/A';
|
||
meraki.stickyMacEnabled = portConfig.accessPolicyType === 'Sticky MAC allow list';
|
||
meraki.stickyMacList = portConfig.stickyMacAllowList || [];
|
||
meraki.stickyMacAllowListLimit = portConfig.stickyMacAllowListLimit || 0;
|
||
meraki.stickyMacCount = meraki.stickyMacList.length;
|
||
meraki.portEnabled = portConfig.enabled ? 'Enabled' : 'Disabled';
|
||
} else {
|
||
meraki.portType = 'Not Found';
|
||
meraki.portEnabled = 'Not Found';
|
||
}
|
||
}
|
||
|
||
// 6. Convert enriched data → CSV
|
||
const flattened = enrichedRows.map(item => {
|
||
const m = item.meraki || {};
|
||
const stickyList = Array.isArray(m.stickyMacList) ? m.stickyMacList.join(', ') : '';
|
||
|
||
return {
|
||
device_id: item.device_id || '',
|
||
device_friendly_name: item.device_friendly_name || '',
|
||
device_enrollment_user_name: item.device_enrollment_user_name || '',
|
||
_device_platform: item._device_platform || '',
|
||
device_os_version: item.device_os_version || '',
|
||
device_last_seen_utc: item.device_last_seen_utc || '',
|
||
device_enrollment_status: item.device_enrollment_status || '',
|
||
device_model_name: item.device_model_name || '',
|
||
_device_mac_address: item._device_mac_address || '',
|
||
device_mac_address: item.device_mac_address || '',
|
||
|
||
meraki_deviceName: m.deviceName || '',
|
||
meraki_deviceSerial: m.deviceSerial || '',
|
||
meraki_switchport: m.switchport || '',
|
||
meraki_vlan: m.vlan || '',
|
||
meraki_status: m.status || '',
|
||
meraki_connection: m.connection || '',
|
||
meraki_lastSeen: m.lastSeen || '',
|
||
meraki_ip: m.ip || '',
|
||
meraki_mac: m.mac || '',
|
||
|
||
meraki_portType: m.portType || 'N/A',
|
||
meraki_accessPolicyType: m.accessPolicyType || 'N/A',
|
||
meraki_portEnabled: m.portEnabled || 'N/A',
|
||
meraki_stickyMacEnabled: m.stickyMacEnabled === true ? 'true' : 'false',
|
||
meraki_stickyMacList: stickyList,
|
||
meraki_stickyMacAllowListLimit: m.stickyMacAllowListLimit ?? '',
|
||
meraki_stickyMacCount: m.stickyMacCount ?? '',
|
||
error: item.error || ''
|
||
};
|
||
});
|
||
|
||
const fields = [
|
||
'device_id', 'device_friendly_name', 'device_enrollment_user_name', '_device_platform',
|
||
'device_os_version', 'device_last_seen_utc', 'device_enrollment_status', 'device_model_name',
|
||
'_device_mac_address', 'device_mac_address',
|
||
'meraki_deviceName', 'meraki_deviceSerial', 'meraki_switchport', 'meraki_vlan',
|
||
'meraki_status', 'meraki_connection', 'meraki_lastSeen', 'meraki_ip', 'meraki_mac',
|
||
'meraki_portType', 'meraki_accessPolicyType', 'meraki_portEnabled',
|
||
'meraki_stickyMacEnabled', 'meraki_stickyMacList', 'meraki_stickyMacAllowListLimit',
|
||
'meraki_stickyMacCount', 'error'
|
||
];
|
||
|
||
const parser = new Parser({ fields });
|
||
const csvContent = parser.parse(flattened);
|
||
fs.writeFileSync('enriched-meraki-intermediate.json', JSON.stringify(enrichedRows, null, 2));
|
||
fs.writeFileSync(outputCsvPath, csvContent);
|
||
console.log(`Enrichment complete. Output CSV saved to: ${outputCsvPath}`);
|
||
console.log(`Processed ${enrichedRows.length} rows`);
|
||
|
||
return enrichedRows; // optional – for further chaining if needed
|
||
}
|
||
|
||
async function findMerakiNetworkId(storeNum) {
|
||
var startTime = new Date().getTime();
|
||
var networkSearchTerm = Number(storeNum).toString().padStart(5, "0");
|
||
try {
|
||
// Filter networks by partial name match (case-insensitive)
|
||
const matchingNetworks = merakiNetworks.filter(net =>
|
||
(net.name || '').toLowerCase().includes(networkSearchTerm.toLowerCase())
|
||
);
|
||
|
||
if (matchingNetworks.length === 0) {
|
||
return null;
|
||
}
|
||
|
||
if (matchingNetworks.length > 1) {
|
||
console.log(`Multiple networks match '${networkSearchTerm}':`);
|
||
matchingNetworks.forEach(net => console.log(`- ${net.name} (ID: ${net.id})`));
|
||
console.log('Using the first match...');
|
||
}
|
||
|
||
|
||
return matchingNetworks[0].id;
|
||
} catch (error) {
|
||
console.error(`Error finding Meraki Network: ${error}`);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
async function cacheMerakiNetworks() {
|
||
|
||
try {
|
||
let allNetworks = [];
|
||
let nextUrl = `https://api.meraki.com/api/v1/organizations/${config.auth.meraki.orgId}/networks?perPage=1000`;
|
||
|
||
while (nextUrl) {
|
||
const response = await axios.get(nextUrl, {
|
||
headers: {
|
||
'X-Cisco-Meraki-API-Key': config.auth.meraki.apiKey,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
});
|
||
|
||
const pageNetworks = response.data;
|
||
allNetworks = allNetworks.concat(pageNetworks);
|
||
|
||
//logger(`cacheMerakiNetworks()`, `Fetched ${pageNetworks.length} networks (total so far: ${allNetworks.length})`);
|
||
|
||
// Log raw header
|
||
//logger(`cacheMerakiNetworks()`, `Link header raw: ${response.headers.link || '(none)'}`);
|
||
|
||
// Robust next URL extraction
|
||
const linkHeader = response.headers.link;
|
||
let foundNext = null;
|
||
|
||
if (linkHeader) {
|
||
//logger(`cacheMerakiNetworks()`, `Full Link header (raw): ${linkHeader}`);
|
||
|
||
const parts = linkHeader.split(',');
|
||
//logger(`cacheMerakiNetworks()`, `Split into ${parts.length} parts`);
|
||
|
||
for (const part of parts) {
|
||
const trimmed = part.trim();
|
||
//logger(`cacheMerakiNetworks()`, `Examining part: "${trimmed}"`);
|
||
|
||
// Forgiving checks: lower case, no quotes required, partial match
|
||
const lowerTrimmed = trimmed.toLowerCase();
|
||
if (lowerTrimmed.includes('rel=next') || lowerTrimmed.includes('rel="next"') || lowerTrimmed.includes("rel='next'")) {
|
||
//logger(`cacheMerakiNetworks()`, `→ Detected rel=next in: "${trimmed}"`);
|
||
|
||
const urlMatch = trimmed.match(/<([^>]+)>/);
|
||
if (urlMatch && urlMatch[1]) {
|
||
foundNext = urlMatch[1].trim(); // extra trim just in case
|
||
//logger(`cacheMerakiNetworks()`, `→ Extracted next URL: ${foundNext}`);
|
||
break;
|
||
} else {
|
||
//logger(`cacheMerakiNetworks()`, `→ URL match failed on that part`);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
nextUrl = foundNext;
|
||
|
||
if (!nextUrl) {
|
||
//logger(`cacheMerakiNetworks()`, `No next page detected – ending loop`);
|
||
} else {
|
||
//logger(`cacheMerakiNetworks()`, `Advancing to next URL: ${nextUrl}`);
|
||
}
|
||
}
|
||
|
||
merakiNetworks = allNetworks;
|
||
logger(`cacheMerakiNetworks()`, `Cached ${merakiNetworks.length} networks. (${new Date().getTime() - startTime}ms)`);
|
||
|
||
} catch (error) {
|
||
console.error('Meraki Networks API error:', error.message);
|
||
if (error.response) {
|
||
console.error('Status:', error.response.status);
|
||
console.error('Data:', error.response.data);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function rebootWorkspaceOneDevice(deviceId) {
|
||
const baseUrl = `https://${awHost}/api`; // e.g., as123.awmdm.com
|
||
const headers = {
|
||
'Authorization': `Basic ${Buffer.from('your_username:your_password').toString('base64')}`, // or use API key method
|
||
'aw-tenant-code': tenantCode,
|
||
'Accept': 'application/json',
|
||
'Content-Type': 'application/json'
|
||
};
|
||
|
||
try {
|
||
const response = await axios.post(
|
||
`${baseUrl}/mdm/devices/${deviceId}/commands`,
|
||
{ Command: 'RebootDevice' }, // or 'RestartDevice' for iOS/tvOS
|
||
{ headers }
|
||
);
|
||
|
||
console.log('Reboot command sent successfully:', response.data);
|
||
return response.data;
|
||
} catch (error) {
|
||
console.error('Error sending reboot:');
|
||
console.error(error.response?.data || error.message);
|
||
throw error;
|
||
}
|
||
}
|
||
/*
|
||
async function updateSwitchPort(networkId, serial, portId, settings) {
|
||
const apiKey = 'YOUR_MERAKI_API_KEY_HERE';
|
||
const baseUrl = 'https://api.meraki.com/api/v1';
|
||
|
||
try {
|
||
const response = await axios.put(
|
||
`${baseUrl}/networks/${networkId}/devices/${serial}/switch/ports/${portId}`,
|
||
settings,
|
||
{
|
||
headers: {
|
||
'X-Cisco-Meraki-API-Key': apiKey,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
}
|
||
);
|
||
|
||
console.log('Success:', response.data);
|
||
return response.data;
|
||
} catch (error) {
|
||
console.error('Error updating port:');
|
||
console.error(error.response?.data || error.message);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// Usage examples
|
||
updateSwitchPort('L_123456789012345678', 'Q3LU-ABCDE-12345', '8', false); // Disable port 8
|
||
// updateSwitchPort('L_123456789012345678', 'Q3LU-ABCDE-12345', '8', true); // Enable port 8*/
|
||
function logger(functionName, message) {
|
||
var d = new Date();
|
||
var year = d.getFullYear();
|
||
var month = (d.getMonth() + 1).toString().padStart(2, "0");
|
||
var day = d.getDate().toString().padStart(2, "0");
|
||
let logFile = path.join(`./logs/${year}${month}${day}.log`);
|
||
console.log(d.toLocaleString() + " " + functionName + ": " + message);
|
||
fs.appendFileSync(logFile, d.toLocaleString() + " " + functionName + ": " + message + "\n");
|
||
}
|
||
|
||
async function sendMdmQuery(deviceId, apiKey, tenantCode, awHost) {
|
||
const baseUrl = `https://${awHost}/api`; // e.g., as1991.awmdm.com
|
||
|
||
const headers = {
|
||
'Authorization': `Basic ${Buffer.from('your_username:your_password').toString('base64')}`, // or API key method
|
||
'aw-tenant-code': tenantCode,
|
||
'Accept': 'application/json',
|
||
'Content-Type': 'application/json'
|
||
};
|
||
|
||
try {
|
||
const response = await axios.post(
|
||
`${baseUrl}/mdm/devices/${deviceId}/commands`,
|
||
{ Command: 'QueryDevice' },
|
||
{ headers }
|
||
);
|
||
|
||
console.log('Query command sent successfully:', response.data);
|
||
return response.data;
|
||
} catch (error) {
|
||
console.error('Error sending MDM Query:');
|
||
if (error.response) {
|
||
console.error('Status:', error.response.status);
|
||
console.error('Response:', error.response.data);
|
||
} else {
|
||
console.error(error.message);
|
||
}
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// Usage example
|
||
const csvFile = 'appleTV.csv';
|
||
const inputCsv = 'appleTV.csv';
|
||
await processCSVAndEnrichMeraki(inputCsv, 'enriched-meraki-output.csv')
|
||
.catch(err => console.error('Fatal error:', err)); |