ServiceChannel webhook processor, proposal approval cards, attachment auto-post, CollabSupport commands, and Docker deployment configuration. Co-authored-by: Cursor <cursoragent@cursor.com>
345 lines
No EOL
12 KiB
JavaScript
345 lines
No EOL
12 KiB
JavaScript
import fs from 'fs';
|
||
import csv from 'csv-parser';
|
||
import path from 'path';
|
||
import axios from 'axios';
|
||
import { match } from 'assert';
|
||
|
||
var merakiNetworks = [];
|
||
|
||
|
||
var config = JSON.parse(fs.readFileSync('./config/config.json'));
|
||
|
||
|
||
|
||
// 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 processCSVAndEnrichMeraki(csvFilePath) {
|
||
const results = [];
|
||
|
||
// Step 1: Parse the CSV
|
||
await new Promise((resolve, reject) => {
|
||
fs.createReadStream(csvFilePath)
|
||
.pipe(csv())
|
||
.on('data', (row) => results.push(row))
|
||
.on('end', resolve)
|
||
.on('error', reject);
|
||
});
|
||
|
||
// Step 2: Process each row
|
||
const enrichedData = [];
|
||
for (const row of results) {
|
||
const friendlyName = row.device_friendly_name?.trim() || ''; // Column 2
|
||
|
||
if (!friendlyName) continue;
|
||
|
||
// Extract store number: e.g., "US003897MSCAERIE" → "003897"
|
||
const storeMatch = friendlyName.match(/(\d{6})/); // Pull 6 digits
|
||
const storeNum = storeMatch ? storeMatch[0] : null;
|
||
|
||
if (!storeNum) {
|
||
console.log(`Skipping row with invalid friendlyName: ${friendlyName}`);
|
||
continue;
|
||
}
|
||
|
||
try {
|
||
// Step 3: Find Meraki Network ID
|
||
const networkId = await findMerakiNetworkId(storeNum);
|
||
|
||
if (!networkId) {
|
||
console.log(`No Meraki network found for store: ${storeNum}`);
|
||
enrichedData.push({ ...row, meraki: null });
|
||
continue;
|
||
}
|
||
|
||
// Step 4: Get Meraki clients for the network
|
||
const clients = await getMerakiClients(networkId);
|
||
|
||
// Step 5: Find matching client by description === friendlyName (case-insensitive)
|
||
const matchingClient = clients.find(client =>
|
||
client.description?.toUpperCase().trim() === friendlyName.toUpperCase()
|
||
);
|
||
|
||
if (!matchingClient) {
|
||
console.log(`No matching Meraki client for device: ${friendlyName} in store ${storeNum}`);
|
||
enrichedData.push({ ...row, meraki: null });
|
||
continue;
|
||
} else {
|
||
console.log(matchingClient)
|
||
}
|
||
|
||
// Step 6: Extract relevant Meraki info (device, port, vlan, port status)
|
||
const merakiInfo = {
|
||
deviceName: matchingClient.recentDeviceName || 'N/A',
|
||
deviceSerial: matchingClient.recentDeviceSerial || 'N/A',
|
||
switchport: matchingClient.switchport || 'N/A',
|
||
vlan: matchingClient.vlan || 'N/A',
|
||
status: matchingClient.status || 'Unknown',
|
||
connection: matchingClient.recentDeviceConnection || 'N/A',
|
||
lastSeen: matchingClient.lastSeen || 'N/A',
|
||
ip: matchingClient.ip || 'N/A',
|
||
mac: matchingClient.mac || 'N/A'
|
||
// Add more fields as needed, e.g., portStatus if available from another API
|
||
};
|
||
|
||
// Note: If "port status" requires another Meraki call (e.g., get switch ports), add it here:
|
||
// const ports = await getMerakiSwitchPorts(matchingClient.recentDeviceSerial);
|
||
// Then find the port matching switchport and get its status (enabled, link, etc.)
|
||
|
||
enrichedData.push({ ...row, meraki: merakiInfo });
|
||
} catch (error) {
|
||
console.error(`Error processing store ${storeNum}, device ${friendlyName}:`, error.message);
|
||
enrichedData.push({ ...row, meraki: null, error: error.message });
|
||
}
|
||
}
|
||
|
||
// Step 7: Return or save the enriched data (e.g., as JSON)
|
||
return enrichedData;
|
||
}
|
||
|
||
async function getMerakiClients(merakiNetwork) {
|
||
if (merakiNetwork) {
|
||
try {
|
||
let allClients = [];
|
||
let nextUrl = `https://api.meraki.com/api/v1/networks/${merakiNetwork}/clients?perPage=5000×pan=2592000`; // Start with high perPage + 24h window
|
||
|
||
while (nextUrl) {
|
||
const response = await axios.get(nextUrl, {
|
||
headers: {
|
||
'X-Cisco-Meraki-API-Key': config.auth.meraki.apiKey,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
});
|
||
|
||
const pageClients = await response.data;
|
||
allClients = allClients.concat(pageClients);
|
||
|
||
//console.log(`Fetched ${pageClients.length} clients from this page (total so far: ${allClients.length})`);
|
||
|
||
// Check Link header for next page
|
||
const linkHeader = await response.headers.link;
|
||
if (!linkHeader) {
|
||
nextUrl = null;
|
||
break;
|
||
}
|
||
|
||
// Parse the 'next' link from Link header (format: <url>; rel="next", ...)
|
||
const nextMatch = await linkHeader.match(/<([^>]+)>;\s*rel="next"/);
|
||
nextUrl = nextMatch ? nextMatch[1] : null;
|
||
}
|
||
return allClients;
|
||
} catch (error) {
|
||
logger(`getMerakiClients(${merakiNetwork})`, `Error: ${error}`)
|
||
return null;
|
||
}
|
||
} else {
|
||
return [];
|
||
}
|
||
}
|
||
|
||
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';
|
||
await cacheMerakiNetworks();
|
||
processCSVAndEnrichMeraki(csvFile)
|
||
.then(enriched => {
|
||
//console.log('Enriched Data:', JSON.stringify(enriched, null, 2));
|
||
// Optional: save to file
|
||
fs.writeFileSync('enriched-meraki.csv.json', JSON.stringify(enriched, null, 2));
|
||
})
|
||
.catch(err => console.error('Fatal error:', err)); |