EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
tenant-meshcentral-sync.js
Go to the documentation of this file.
1/**
2 * Startup Tenant-MeshCentral Sync
3 *
4 * Automatically runs on backend startup to ensure all tenants
5 * have device groups in MeshCentral. Now that MeshCentral uses
6 * PostgreSQL, mesh groups persist across restarts.
7 *
8 * Process:
9 * 1. Fetch all tenants from PostgreSQL database
10 * 2. Fetch all existing mesh groups from MeshCentral
11 * 3. Match tenants to mesh groups by ID or name
12 * 4. Create missing mesh groups for any unmatched tenants
13 * 5. Tag all devices in each mesh with tenant UUID
14 */
15
16const db = require('../db');
17const MeshCentralAPI = require('../lib/meshcentral-api');
18
19const MESHCENTRAL_URL = process.env.MESHCENTRAL_URL || 'https://rmm-psa-meshcentral-aq48h.ondigitalocean.app';
20const MESHCENTRAL_USER = process.env.MESHCENTRAL_ADMIN_USER || 'admin';
21const MESHCENTRAL_PASSWORD = process.env.MESHCENTRAL_ADMIN_PASS || 'admin';
22
23// Delay before running sync (to let MeshCentral start up)
24const STARTUP_DELAY = parseInt(process.env.TENANT_SYNC_DELAY || '30000'); // 30 seconds
25
26/**
27 * Ensure tenant has a device group in MeshCentral
28 * Matches existing groups by ID or name, creates new ones if needed
29 * @param api
30 * @param tenant
31 * @param existingMeshes
32 */
33async function ensureTenantMesh(api, tenant, existingMeshes) {
34 try {
35 const expectedMeshName = `${tenant.name} Devices`;
36
37 // Check if tenant already has a mesh ID
38 if (tenant.meshcentral_group_id) {
39 // Verify it still exists in MeshCentral
40 const meshExists = existingMeshes.some(m => m._id === tenant.meshcentral_group_id);
41
42 if (meshExists) {
43 console.log(` āœ… Tenant "${tenant.name}" has mesh: ${tenant.meshcentral_group_id}`);
44 await tagDevicesInMesh(api, tenant);
45 return { meshId: tenant.meshcentral_group_id, created: false, linked: false };
46 } else {
47 console.log(` āš ļø Tenant "${tenant.name}" mesh ${tenant.meshcentral_group_id} not found`);
48 // Continue to try finding by name or create new
49 }
50 }
51
52 // Try to find existing mesh by name
53 const matchingMesh = existingMeshes.find(m => {
54 const meshName = m.name || '';
55 return meshName === expectedMeshName ||
56 meshName === tenant.name ||
57 meshName.includes(tenant.name);
58 });
59
60 if (matchingMesh) {
61 console.log(` šŸ”— Found existing mesh "${matchingMesh.name}" - linking to tenant "${tenant.name}"`);
62
63 // Link this mesh to the tenant
64 await db.query(
65 'UPDATE tenants SET meshcentral_group_id = $1, meshcentral_setup_complete = true WHERE tenant_id = $2',
66 [matchingMesh._id, tenant.tenant_id]
67 );
68
69 console.log(` āœ… Linked mesh ${matchingMesh._id} to tenant "${tenant.name}"`);
70 await tagDevicesInMesh(api, { ...tenant, meshcentral_group_id: matchingMesh._id });
71
72 return { meshId: matchingMesh._id, created: false, linked: true };
73 }
74
75 // No existing mesh found - create new one
76 console.log(` šŸ“¦ Creating new mesh: ${expectedMeshName}`);
77
78 const result = await api.createMesh(expectedMeshName, `Device group for ${tenant.name} (tenant:${tenant.tenant_id})`);
79
80 if (!result || !result.meshId) {
81 throw new Error('Failed to create mesh in MeshCentral');
82 }
83
84 // Store mesh ID in database
85 await db.query(
86 'UPDATE tenants SET meshcentral_group_id = $1, meshcentral_setup_complete = true WHERE tenant_id = $2',
87 [result.meshId, tenant.tenant_id]
88 );
89
90 console.log(` āœ… Created mesh ${result.meshId} for "${tenant.name}"`);
91
92 return { meshId: result.meshId, created: true, linked: false };
93
94 } catch (error) {
95 console.error(` āŒ Error for tenant "${tenant.name}":`, error.message);
96 throw error;
97 }
98}
99
100/**
101 * Tag all devices in a mesh with tenant UUID
102 * @param api
103 * @param tenant
104 */
105async function tagDevicesInMesh(api, tenant) {
106 try {
107 const nodes = await api.getNodes();
108 const meshNodes = nodes.filter(n => n.meshid === tenant.meshcentral_group_id);
109
110 if (meshNodes.length === 0) {
111 console.log(` ā„¹ļø No devices in mesh for tenant "${tenant.name}"`);
112 return;
113 }
114
115 console.log(` šŸ·ļø Tagging ${meshNodes.length} device(s) for tenant "${tenant.name}"`);
116
117 for (const node of meshNodes) {
118 const parsed = api.constructor.parseNodeData(node);
119 const tagData = api.constructor.parseDeviceTags(node.tags);
120
121 // Only update if tenant tag is missing or different
122 if (tagData.tenantId !== tenant.tenant_id) {
123 console.log(` └─ ${parsed.hostname} → tenant:${tenant.tenant_id}`);
124 await api.updateDeviceMetadata(parsed.nodeId, {
125 tenantId: tenant.tenant_id,
126 customerId: tagData.customerId,
127 tags: tagData.customTags
128 });
129 }
130 }
131 } catch (error) {
132 console.error(` āš ļø Error tagging devices for tenant "${tenant.name}":`, error.message);
133 }
134}
135
136/**
137 * Run the startup sync
138 */
139async function runStartupSync() {
140 console.log(`\n${'='.repeat(60)}`);
141 console.log('šŸš€ TENANT-MESHCENTRAL STARTUP SYNC');
142 console.log(`${'='.repeat(60)}\n`);
143
144 try {
145 // Wait for MeshCentral to be ready
146 console.log(`ā³ Waiting ${STARTUP_DELAY}ms for MeshCentral to start...`);
147 await new Promise(resolve => setTimeout(resolve, STARTUP_DELAY));
148
149 // Get all tenants
150 const tenantsResult = await db.query(
151 'SELECT tenant_id, name, meshcentral_group_id, meshcentral_setup_complete FROM tenants ORDER BY created_at'
152 );
153
154 if (tenantsResult.rows.length === 0) {
155 console.log('ā„¹ļø No tenants found, skipping sync\n');
156 return;
157 }
158
159 console.log(`šŸ“Š Found ${tenantsResult.rows.length} tenants to sync\n`);
160
161 // Connect to MeshCentral
162 console.log(`šŸ”Œ Connecting to MeshCentral: ${MESHCENTRAL_URL}`);
163 const api = new MeshCentralAPI({
164 url: MESHCENTRAL_URL,
165 username: MESHCENTRAL_USER,
166 password: MESHCENTRAL_PASSWORD
167 });
168
169 await api.login();
170 console.log(`āœ… Connected to MeshCentral as ${MESHCENTRAL_USER}\n`);
171
172 // Fetch all existing mesh groups
173 console.log(`šŸ“¦ Fetching existing mesh groups from MeshCentral...`);
174 const existingMeshes = await api.getMeshes();
175 console.log(`āœ… Found ${existingMeshes.length} existing mesh group(s)\n`);
176
177 // Process each tenant
178 let created = 0;
179 let existing = 0;
180 let linked = 0;
181 let failed = 0;
182
183 for (const tenant of tenantsResult.rows) {
184 console.log(`\nšŸ”„ Processing tenant: ${tenant.name} (${tenant.tenant_id})`);
185 try {
186 const result = await ensureTenantMesh(api, tenant, existingMeshes);
187 if (result.created) {
188 created++;
189 } else if (result.linked) {
190 linked++;
191 } else {
192 existing++;
193 }
194 } catch (error) {
195 failed++;
196 }
197 }
198
199 console.log(`\n${'='.repeat(60)}`);
200 console.log('āœ… STARTUP SYNC COMPLETE');
201 console.log(`${'='.repeat(60)}`);
202 console.log(`šŸ“Š Results:`);
203 console.log(` - Created: ${created} new mesh group(s)`);
204 console.log(` - Linked: ${linked} existing mesh group(s)`);
205 console.log(` - Already synced: ${existing} mesh group(s)`);
206 console.log(` - Failed: ${failed} tenant(s)`);
207 console.log(`${'='.repeat(60)}\n`);
208
209 } catch (error) {
210 console.error('\nāŒ STARTUP SYNC FAILED:', error.message);
211 console.error('Stack:', error.stack);
212 console.log('\nā„¹ļø Note: This is non-fatal. You can manually trigger sync via:');
213 console.log(' POST /api/tenant-meshcentral-sync/sync-all\n');
214 }
215}
216
217/**
218 * Initialize startup sync if enabled
219 */
220function initStartupSync() {
221 const enabled = process.env.TENANT_SYNC_ON_STARTUP === 'true'; // Disabled by default (changed to prevent duplicates)
222
223 if (!enabled) {
224 console.log('ā„¹ļø Tenant-MeshCentral startup sync is disabled (set TENANT_SYNC_ON_STARTUP=true to enable)');
225 return;
226 }
227
228 // Run in background (don't block server startup)
229 setImmediate(() => {
230 runStartupSync().catch(err => {
231 console.error('āŒ Startup sync error:', err);
232 });
233 });
234}
235
236module.exports = {
237 runStartupSync,
238 initStartupSync
239};