EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
meshcentralSync.js
Go to the documentation of this file.
1/**
2 * MeshCentral Auto-Sync Worker (Backup/Safety Net)
3 *
4 * SYNC HIERARCHY:
5 * 1. PRIMARY: meshcentral-device-sync.js (Direct DB queries, runs every 5 minutes)
6 * 2. BACKUP: This worker (API-based, runs daily at 3 AM as safety net)
7 * 3. DEPRECATED: POST /api/meshcentral/sync (Manual API endpoint, use /api/sync/meshcentral instead)
8 *
9 * This worker runs daily as a backup to catch devices missed by the primary sync.
10 * The primary sync method (meshcentral-device-sync.js) queries the MeshCentral
11 * PostgreSQL database directly every 5 minutes, which is much faster and more reliable.
12 *
13 * This worker serves as a safety net to:
14 * - Catch devices missed by primary sync due to transient errors
15 * - Verify sync accuracy once daily
16 * - Update connection status for all devices comprehensive check
17 *
18 * Uses hardware identifiers (hostname, MAC, motherboard UUID) for matching
19 *
20 * TODO: When primary sync is proven stable for 30+ days, this worker can be
21 * deprecated or converted to a weekly health check instead of daily.
22 */
23
24const { Worker, Queue } = require('bullmq');
25const Redis = require('ioredis');
26const db = require('../db');
27const MeshCentralAPI = require('../lib/meshcentral-api');
28
29const redisConfig = require('../config/redis');
30const connection = new Redis({
31 ...redisConfig,
32 maxRetriesPerRequest: null // BullMQ requires this
33});
34
35// Create queue for MeshCentral sync jobs
36const meshSyncQueue = new Queue('meshcentral-sync', { connection });
37
38/**
39 * Get MeshCentral API instance.
40 * Creates authenticated MeshCentral API client with WebSocket connection.
41 * @function getMeshAPI
42 * @returns {Promise<MeshCentralAPI>} Authenticated API instance
43 */
44async function getMeshAPI() {
45 const api = new MeshCentralAPI({
46 url: process.env.MESHCENTRAL_URL || 'https://rmm-psa-meshcentral-aq48h.ondigitalocean.app',
47 username: process.env.MESHCENTRAL_ADMIN_USER || 'admin',
48 password: process.env.MESHCENTRAL_ADMIN_PASS || 'admin'
49 });
50
51 await api.login();
52 await api.connectWebSocket();
53 return api;
54}
55
56/**
57 * Perform MeshCentral device sync.
58 * Fetches devices from MeshCentral and matches with database agents.
59 * @function syncMeshCentralDevices
60 * @returns {Promise<void>}
61 */
62async function syncMeshCentralDevices() {
63 console.log('\nšŸ”„ [MeshSync] Starting automatic sync...');
64
65 try {
66 const api = await getMeshAPI();
67 const nodes = await api.getNodes();
68
69 console.log(`šŸ“Š [MeshSync] Found ${nodes.length} MeshCentral devices`);
70
71 let linkedCount = 0;
72 let updatedCount = 0;
73 let newCount = 0;
74 let skippedCount = 0;
75 const matchDetails = [];
76
77 for (const node of nodes) {
78 try {
79 const parsed = MeshCentralAPI.parseNodeData(node);
80
81 if (!parsed.hostname) {
82 console.log(`āš ļø [MeshSync] Skipping device without hostname: ${parsed.nodeId}`);
83 skippedCount++;
84 continue;
85 }
86
87 // Check if device already linked
88 const existingLink = await db.query(
89 'SELECT agent_id, hostname, hardware_data FROM agents WHERE meshcentral_nodeid = $1',
90 [parsed.nodeId]
91 );
92
93 if (existingLink.rows.length > 0) {
94 // Update connection status
95 await db.query(`
96 UPDATE agents
97 SET meshcentral_connected = $1,
98 meshcentral_last_seen = $2,
99 last_seen = GREATEST(last_seen, $2)
100 WHERE meshcentral_nodeid = $3
101 `, [
102 parsed.connected,
103 parsed.lastSeen,
104 parsed.nodeId
105 ]);
106 updatedCount++;
107 continue;
108 }
109
110 // Try to find matching agent by hostname
111 const hostnameMatch = await db.query(
112 'SELECT agent_id, agent_uuid, hostname FROM agents WHERE LOWER(hostname) = LOWER($1) AND meshcentral_nodeid IS NULL LIMIT 1',
113 [parsed.hostname]
114 );
115
116 if (hostnameMatch.rows.length > 0) {
117 const agent = hostnameMatch.rows[0];
118
119 // Link agent to MeshCentral device
120 await db.query(
121 `UPDATE agents
122 SET meshcentral_nodeid = $1,
123 meshcentral_connected = $2,
124 meshcentral_last_seen = $3
125 WHERE agent_id = $4`,
126 [parsed.nodeId, parsed.connected, parsed.lastSeen, agent.agent_id]
127 );
128
129 linkedCount++;
130 matchDetails.push({
131 agentId: agent.agent_id,
132 agentUuid: agent.agent_uuid,
133 hostname: agent.hostname,
134 nodeId: parsed.nodeId,
135 method: 'hostname'
136 });
137
138 console.log(`āœ… [MeshSync] Linked agent ${agent.agent_id} (${agent.hostname}) to MeshCentral node ${parsed.nodeId.substring(0, 20)}...`);
139 } else {
140 // Try MAC address matching if available
141 let macMatched = false;
142
143 if (parsed.hardware && parsed.hardware.macAddresses && parsed.hardware.macAddresses.length > 0) {
144 for (const mac of parsed.hardware.macAddresses) {
145 const macMatch = await db.query(`
146 SELECT agent_id, agent_uuid, hostname, hardware_data
147 FROM agents
148 WHERE meshcentral_nodeid IS NULL
149 AND hardware_data IS NOT NULL
150 AND hardware_data::text ILIKE $1
151 LIMIT 1
152 `, [`%${mac}%`]);
153
154 if (macMatch.rows.length > 0) {
155 const agent = macMatch.rows[0];
156
157 await db.query(
158 `UPDATE agents
159 SET meshcentral_nodeid = $1,
160 meshcentral_connected = $2,
161 meshcentral_last_seen = $3
162 WHERE agent_id = $4`,
163 [parsed.nodeId, parsed.connected, parsed.lastSeen, agent.agent_id]
164 );
165
166 linkedCount++;
167 macMatched = true;
168 matchDetails.push({
169 agentId: agent.agent_id,
170 agentUuid: agent.agent_uuid,
171 hostname: agent.hostname,
172 nodeId: parsed.nodeId,
173 method: 'mac_address',
174 mac: mac
175 });
176
177 console.log(`āœ… [MeshSync] Linked agent ${agent.agent_id} (${agent.hostname}) to MeshCentral node via MAC ${mac}`);
178 break;
179 }
180 }
181 }
182
183 if (!macMatched) {
184 // No match found - could create new agent or skip
185 console.log(`ā„¹ļø [MeshSync] No matching agent for device: ${parsed.hostname} (${parsed.nodeId.substring(0, 20)}...)`);
186 skippedCount++;
187 }
188 }
189 } catch (err) {
190 console.error(`āŒ [MeshSync] Error processing node:`, err.message);
191 }
192 }
193
194 const summary = {
195 timestamp: new Date().toISOString(),
196 total: nodes.length,
197 linked: linkedCount,
198 updated: updatedCount,
199 new: newCount,
200 skipped: skippedCount,
201 matchDetails: matchDetails
202 };
203
204 console.log(`\nšŸ“ˆ [MeshSync] Sync complete:`);
205 console.log(` Total devices: ${summary.total}`);
206 console.log(` Newly linked: ${summary.linked}`);
207 console.log(` Updated: ${summary.updated}`);
208 console.log(` Skipped: ${summary.skipped}`);
209 console.log('');
210
211 return summary;
212
213 } catch (error) {
214 console.error('āŒ [MeshSync] Sync failed:', error.message);
215 throw error;
216 }
217}
218
219/**
220 * BullMQ Worker - Processes sync jobs
221 */
222const worker = new Worker('meshcentral-sync', async (job) => {
223 console.log(`\nšŸ”„ [MeshSync Worker] Processing job ${job.id}...`);
224
225 try {
226 const result = await syncMeshCentralDevices();
227 return result;
228 } catch (error) {
229 console.error(`āŒ [MeshSync Worker] Job ${job.id} failed:`, error.message);
230 throw error;
231 }
232}, {
233 connection,
234 concurrency: 1, // Only run one sync job at a time
235 limiter: {
236 max: 1,
237 duration: 60000 // Max 1 job per minute
238 }
239});
240
241worker.on('completed', (job, result) => {
242 console.log(`āœ… [MeshSync Worker] Job ${job.id} completed: ${result.linked} linked, ${result.updated} updated`);
243});
244
245worker.on('failed', (job, err) => {
246 console.error(`āŒ [MeshSync Worker] Job ${job?.id} failed:`, err.message);
247});
248
249/**
250 * Schedule recurring sync job (every 10 minutes)
251 */
252async function scheduleRecurringSync() {
253 // Remove any existing repeatable jobs
254 const repeatableJobs = await meshSyncQueue.getRepeatableJobs();
255 for (const job of repeatableJobs) {
256 await meshSyncQueue.removeRepeatableByKey(job.key);
257 }
258
259 // Add new repeatable job - daily at 3 AM (backup to webhook system)
260 await meshSyncQueue.add(
261 'sync-devices',
262 {},
263 {
264 repeat: {
265 pattern: '0 3 * * *', // Daily at 3:00 AM
266 immediately: true // Run immediately on startup
267 },
268 removeOnComplete: {
269 count: 10 // Keep last 10 completed jobs
270 },
271 removeOnFail: {
272 count: 20 // Keep last 20 failed jobs
273 }
274 }
275 );
276
277 console.log('šŸ• [MeshSync] Scheduled to run daily at 3:00 AM (backup sync)');
278 console.log('ā„¹ļø [MeshSync] Primary sync via webhook: /api/webhooks/meshcentral');
279}
280
281// Initialize on startup
282scheduleRecurringSync().catch(err => {
283 console.error('āŒ [MeshSync] Failed to schedule sync:', err);
284});
285
286// Manual trigger function (can be called from API)
287/**
288 * Trigger manual MeshCentral device sync.
289 * Adds high-priority sync job to queue.
290 * @function triggerManualSync
291 * @returns {Promise<string>} Job ID of queued sync task
292 */
293async function triggerManualSync() {
294 const job = await meshSyncQueue.add('manual-sync', {}, { priority: 1 });
295 console.log(`šŸ”„ [MeshSync] Manual sync triggered - Job ID: ${job.id}`);
296 return job.id;
297}
298
299console.log('āœ… [MeshSync] Worker initialized and ready');
300
301module.exports = {
302 meshSyncQueue,
303 triggerManualSync,
304 syncMeshCentralDevices
305};