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: ; 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));