3 * Pre-Shutdown Verification Checklist
4 * Verifies all critical data has been migrated from WHM/WHMCS before server shutdown
7const mysql = require('mysql2/promise');
8const { Pool } = require('pg');
9const { exec } = require('child_process');
10const util = require('util');
11const execPromise = util.promisify(exec);
15 port: 3307, // SSH tunnel
16 user: 'precisewebhostin_migration',
17 password: 'Zrepowfg1-cFS,TP',
18 database: 'precisewebhostin_whmcs'
22 host: 'rmm-psa-db-do-user-28531160-0.i.db.ondigitalocean.com',
24 database: 'defaultdb',
26 password: 'AVNS_J8RJAmsEwsHFG52_-F2',
27 ssl: { rejectUnauthorized: false }
30const tenantId = '00000000-0000-0000-0000-000000000001';
35 * @param checkFunction
37async function checkItem(description, checkFunction) {
38 process.stdout.write(`${description.padEnd(60)}`);
40 const result = await checkFunction();
41 if (result.status === 'ok') {
42 console.log(`✓ ${result.message || 'OK'}`);
43 return { passed: true, ...result };
44 } else if (result.status === 'warning') {
45 console.log(`⚠ ${result.message}`);
46 return { passed: true, warning: true, ...result };
48 console.log(`✗ ${result.message}`);
49 return { passed: false, ...result };
52 console.log(`✗ ${error.message}`);
53 return { passed: false, error: error.message };
60async function main() {
61 console.log('╔══════════════════════════════════════════════════════════╗');
62 console.log('║ WHM/WHMCS Pre-Shutdown Verification Checklist ║');
63 console.log('╚══════════════════════════════════════════════════════════╝\n');
70 // Setup SSH tunnel first
71 console.log('Setting up SSH tunnel to WHMCS database...');
73 await execPromise('ssh -i ~/PreciseITServices.pem -L 3307:precisewebhosting.com.au:3306 -fN centos@54.253.193.6 -o StrictHostKeyChecking=no');
74 await new Promise(resolve => setTimeout(resolve, 2000)); // Wait for tunnel
75 console.log('✓ SSH tunnel established\n');
77 console.log('⚠ SSH tunnel may already exist\n');
80 mysqlConn = await mysql.createConnection(MYSQL_CONFIG);
81 pgPool = new Pool(PG_CONFIG);
83 console.log('DATA MIGRATION VERIFICATION');
84 console.log('===========================\n');
86 // 1. Customer count verification
87 results.push(await checkItem('1. Customer records migrated', async () => {
88 const [whmcsCount] = await mysqlConn.query('SELECT COUNT(*) as count FROM tblclients');
89 const pgCount = await pgPool.query('SELECT COUNT(*) as count FROM customers WHERE tenant_id = $1', [tenantId]);
91 if (pgCount.rows[0].count >= whmcsCount[0].count) {
92 return { status: 'ok', message: `${pgCount.rows[0].count} of ${whmcsCount[0].count}` };
94 return { status: 'error', message: `Only ${pgCount.rows[0].count} of ${whmcsCount[0].count} migrated` };
98 // 2. Active services/contracts
99 results.push(await checkItem('2. Active service contracts migrated', async () => {
100 const [whmcsServices] = await mysqlConn.query(
101 'SELECT COUNT(*) as count FROM tblhosting WHERE domainstatus IN ("Active", "Suspended")'
103 const pgContracts = await pgPool.query(
104 'SELECT COUNT(*) as count FROM contracts WHERE tenant_id = $1 AND status IN ($2, $3)',
105 [tenantId, 'active', 'suspended']
108 if (pgContracts.rows[0].count >= whmcsServices[0].count * 0.9) { // Allow 10% variance
109 return { status: 'ok', message: `${pgContracts.rows[0].count} contracts (${whmcsServices[0].count} services)` };
111 return { status: 'warning', message: `${pgContracts.rows[0].count} contracts vs ${whmcsServices[0].count} services` };
115 // 3. Invoice records
116 results.push(await checkItem('3. Invoice history migrated', async () => {
117 const [whmcsInvoices] = await mysqlConn.query('SELECT COUNT(*) as count FROM tblinvoices');
118 const pgInvoices = await pgPool.query('SELECT COUNT(*) as count FROM invoices WHERE tenant_id = $1', [tenantId]);
120 const percentage = (pgInvoices.rows[0].count / whmcsInvoices[0].count) * 100;
121 return { status: 'ok', message: `${pgInvoices.rows[0].count} of ${whmcsInvoices[0].count} (${percentage.toFixed(0)}%)` };
124 // 4. Products catalog
125 results.push(await checkItem('4. Product catalog migrated', async () => {
126 const [whmcsProducts] = await mysqlConn.query('SELECT COUNT(*) as count FROM tblproducts WHERE hidden = 0');
127 const pgProducts = await pgPool.query('SELECT COUNT(*) as count FROM products WHERE tenant_id = $1', [tenantId]);
129 if (pgProducts.rows[0].count >= whmcsProducts[0].count) {
130 return { status: 'ok', message: `${pgProducts.rows[0].count} products` };
132 return { status: 'warning', message: `${pgProducts.rows[0].count} of ${whmcsProducts[0].count}` };
136 // 5. Product pricing
137 results.push(await checkItem('5. Product pricing populated', async () => {
138 const pgPriced = await pgPool.query(
139 'SELECT COUNT(*) as count FROM products WHERE tenant_id = $1 AND price_retail > 0',
142 const pgTotal = await pgPool.query(
143 'SELECT COUNT(*) as count FROM products WHERE tenant_id = $1',
147 if (pgPriced.rows[0].count > 0) {
148 return { status: 'ok', message: `${pgPriced.rows[0].count} of ${pgTotal.rows[0].count} have pricing` };
150 return { status: 'error', message: 'No products have pricing!' };
154 console.log('\nDNS & DOMAINS');
155 console.log('=============\n');
157 // 6. DNS zones migrated
158 results.push(await checkItem('6. DNS zones on BIND server', async () => {
159 const { stdout } = await execPromise('sshpass -p "vocjecXqFIzwe8dbPoTv4Lhd4O8WbVg4" ssh root@209.38.80.86 "ls /etc/bind/zones/ | wc -l"');
160 const zoneCount = parseInt(stdout.trim());
162 if (zoneCount > 60) {
163 return { status: 'ok', message: `${zoneCount} zones on BIND` };
165 return { status: 'warning', message: `Only ${zoneCount} zones found` };
169 // 7. BIND service status
170 results.push(await checkItem('7. BIND DNS service running', async () => {
171 const { stdout } = await execPromise('sshpass -p "vocjecXqFIzwe8dbPoTv4Lhd4O8WbVg4" ssh root@209.38.80.86 "systemctl is-active named"');
173 if (stdout.trim() === 'active') {
174 return { status: 'ok', message: 'Active' };
176 return { status: 'error', message: 'Not active!' };
180 console.log('\nWORDPRESS SITES');
181 console.log('===============\n');
183 // 8. WordPress sites migrated
184 results.push(await checkItem('8. WordPress sites on new droplet', async () => {
185 const { stdout } = await execPromise('ssh -i ~/.ssh/wordpress_hosting_rsa root@209.38.90.228 "ls /var/www/wordpress-sites/ | wc -l"');
186 const siteCount = parseInt(stdout.trim());
188 if (siteCount >= 14) {
189 return { status: 'ok', message: `${siteCount} sites` };
191 return { status: 'warning', message: `Only ${siteCount} sites found` };
195 // 9. WordPress Apache configs
196 results.push(await checkItem('9. Apache vhosts configured', async () => {
197 const { stdout } = await execPromise('ssh -i ~/.ssh/wordpress_hosting_rsa root@209.38.90.228 "ls /etc/apache2/sites-enabled/ | grep -v 000-default | wc -l"');
198 const vhostCount = parseInt(stdout.trim());
200 if (vhostCount >= 14) {
201 return { status: 'ok', message: `${vhostCount} vhosts` };
203 return { status: 'warning', message: `Only ${vhostCount} vhosts` };
207 console.log('\nCRITICAL DATA CHECKS');
208 console.log('====================\n');
210 // 10. Domain-to-client mapping
211 results.push(await checkItem('10. Client domain associations', async () => {
212 const [domains] = await mysqlConn.query(`
213 SELECT COUNT(DISTINCT domain) as count
215 WHERE domainstatus NOT IN ('Cancelled', 'Terminated')
220 message: `${domains[0].count} domains in WHMCS (review needed)`,
221 data: { domainCount: domains[0].count }
225 // 11. Client custom fields
226 results.push(await checkItem('11. Client custom fields/notes', async () => {
227 const [notes] = await mysqlConn.query('SELECT COUNT(*) as count FROM tblclients WHERE notes != ""');
229 if (notes[0].count > 0) {
232 message: `${notes[0].count} clients have notes (may need manual review)`,
233 data: { notesCount: notes[0].count }
236 return { status: 'ok', message: 'No critical notes' };
240 // 12. Unpaid invoices
241 results.push(await checkItem('12. Outstanding invoices', async () => {
242 const [unpaid] = await mysqlConn.query(`
243 SELECT COUNT(*) as count, SUM(total) as total
245 WHERE status = 'Unpaid'
250 message: `${unpaid[0].count} unpaid ($${parseFloat(unpaid[0].total || 0).toFixed(2)})`,
251 data: { unpaidCount: unpaid[0].count, unpaidTotal: unpaid[0].total }
255 // 13. Email/support tickets
256 results.push(await checkItem('13. Support tickets', async () => {
258 const [tickets] = await mysqlConn.query('SELECT COUNT(*) as count FROM tbltickets WHERE status != "Closed"');
260 if (tickets[0].count > 0) {
263 message: `${tickets[0].count} open tickets (export recommended)`,
264 data: { openTickets: tickets[0].count }
267 return { status: 'ok', message: 'No open tickets' };
270 return { status: 'ok', message: 'Tickets table not accessible' };
274 console.log('\nBACKUP RECOMMENDATIONS');
275 console.log('======================\n');
277 // 14. Final database backup
278 results.push(await checkItem('14. Create final WHMCS DB backup', async () => {
281 message: 'Manual action required',
282 action: 'Run: mysqldump precisewebhostin_whmcs > whmcs_final_backup.sql'
287 results.push(await checkItem('15. Create final WHM backup', async () => {
290 message: 'Manual action required',
291 action: 'WHM > Backup > Generate Full Backup'
295 console.log('\n\n╔══════════════════════════════════════════════════════════╗');
296 console.log('║ VERIFICATION SUMMARY ║');
297 console.log('╚══════════════════════════════════════════════════════════╝\n');
299 const passed = results.filter(r => r.passed && !r.warning).length;
300 const warnings = results.filter(r => r.warning).length;
301 const failed = results.filter(r => !r.passed).length;
302 const total = results.length;
304 console.log(`Total Checks: ${total}`);
305 console.log(`Passed: ${passed} ✓`);
306 console.log(`Warnings: ${warnings} ⚠`);
307 console.log(`Failed: ${failed} ✗`);
310 console.log('\n⚠ CRITICAL ISSUES FOUND - DO NOT SHUTDOWN SERVER YET\n');
311 console.log('Failed checks:');
312 results.filter(r => !r.passed).forEach((r, i) => {
313 console.log(` ${i + 1}. ${r.error || 'Unknown error'}`);
315 } else if (warnings > 0) {
316 console.log('\n⚠ WARNINGS FOUND - REVIEW BEFORE SHUTDOWN\n');
317 console.log('Items requiring attention:');
318 results.filter(r => r.warning).forEach((r, i) => {
319 console.log(` ${i + 1}. ${r.message}`);
320 if (r.action) console.log(` Action: ${r.action}`);
323 console.log('\n✓ ALL CHECKS PASSED - SERVER CAN BE SAFELY SHUTDOWN\n');
326 // Additional recommendations
327 console.log('\nFINAL CHECKLIST BEFORE SHUTDOWN:');
328 console.log('================================');
329 console.log('[ ] Export any remaining cPanel email accounts to new provider');
330 console.log('[ ] Update all domain nameservers to Cloudflare (if not done)');
331 console.log('[ ] Verify SSL certificates are in place on new WordPress droplet');
332 console.log('[ ] Download final backup of /home/ directories from WHM');
333 console.log('[ ] Export WHMCS database: mysqldump precisewebhostin_whmcs > final_backup.sql');
334 console.log('[ ] Test all critical domains resolve correctly from new servers');
335 console.log('[ ] Notify clients of any planned downtime (if applicable)');
336 console.log('[ ] Keep WHM server running for 30 days minimum as fallback\n');
339 console.error('\n❌ VERIFICATION FAILED:', error.message);
342 if (mysqlConn) await mysqlConn.end();
343 if (pgPool) await pgPool.end();
345 // Clean up SSH tunnel
347 await execPromise('pkill -f "ssh.*3307:precisewebhosting"');