Shinobi/tools/copySystemToNewServer.js

245 lines
10 KiB
JavaScript

/* eslint-disable no-console */
const path = require('path');
const fs = require('fs').promises;
const { spawn } = require('child_process');
const { Client } = require('ssh2');
const os = require('os');
// ── Shinobi helpers ───────────────────────────────────────────────────────────
const configRaw = require('../conf.json'); // <- will be copied
const s = require('../libs/process.js')(process);
const config = require('../libs/config.js')(s);
const s2 = { mainDirectory: process.cwd() }; // Shinobi expects this
// ── CLI parsing ───────────────────────────────────────────────────────────────
// HOST SSH_USER SSH_PASS SQL_USER SQL_PASS SQL_DB SQL_PORT SHINOBI_PATH ROOT_PASSWORD
const [
, , host,
sshUsername,
sshPassword,
sqlUsername = 'majesticflame',
sqlPassword = '',
sqlDatabase = 'ccio',
sqlPort = 3306,
shinobiPath = '/home/Shinobi',
rootPassword = '', // <- NEW
] = process.argv.map(String);
if (!host || !sshUsername || !sshPassword || !sqlUsername) {
console.log(`** Invalid parameters provided!`);
if (!host) console.log('Missing Host!');
if (!sshUsername) console.log('Missing SSH Username!');
if (!sshPassword) console.log('Missing SSH Password!');
console.log(`## Example Usage :`);
console.log(`=============`);
console.log('node tools/copySystemToNewServer.js HOST SSH_USER SSH_PASS SQL_USER SQL_PASS SQL_DB SQL_PORT SHINOBI_PATH ROOT_PASSWORD');
console.log(`=============`);
console.log(`## Usage Parameters :`);
console.log(`=============`);
console.log(`# HOST : Target Server IP/FQDN. Required.`);
console.log(`# SSH_USER : Username for SSH. Required.`);
console.log(`# SSH_PASS : Password for SSH (or "__RSA:/path/key"). Required.`);
console.log(`# SQL_USER : MySQL user. Default "majesticflame". Optional.`);
console.log(`# SQL_PASS : MySQL password. Default blank. Optional.`);
console.log(`# SQL_DB : Database name. Default "ccio". Optional.`);
console.log(`# SQL_PORT : Database port. Default "3306". Optional.`);
console.log(`# SHINOBI_PATH : Path of Shinobi install. Default "/home/Shinobi". Optional.`);
console.log(`# ROOT_PASSWORD : If supplied, script runs "sudo su" and feeds this password. Optional.`);
console.log(`=============`);
process.exit(1);
}
// ── Source-side DB info (from conf.json) ──────────────────────────────────────
const localDbConf = (configRaw.db || {
host : '127.0.0.1',
user : 'majesticflame',
password: '',
database: 'ccio',
port : 3306,
});
// ── Paths ─────────────────────────────────────────────────────────────────────
const dumpName = `shinobi_dump_${Date.now()}.sql`;
const dumpPath = path.join(os.tmpdir(), dumpName);
const remoteDump = `/tmp/${dumpName}`;
const localTmpConf = path.join(os.tmpdir(), `shinobi_conf_${Date.now()}.json`);
const remoteConf = `${shinobiPath.replace(/\/+$/, '')}/conf.json`;
// ── Helper — promisified spawn ------------------------------------------------
function run(cmd, args, opts = {}) {
return new Promise((resolve, reject) => {
const p = spawn(cmd, args, { stdio: 'inherit', ...opts });
p.on('close', code => (code === 0 ? resolve() : reject(new Error(`${cmd} exited ${code}`))));
});
}
// ── 1. Dump the local database ───────────────────────────────────────────────
async function dumpLocalDb() {
console.log(`\n[1/7] Dumping local DB ⇒ ${dumpPath}`);
const args = [
`-h${localDbConf.host}`,
`-P${localDbConf.port}`,
`-u${localDbConf.user}`,
`-p${localDbConf.password}`,
'--single-transaction', '--routines', '--triggers',
localDbConf.database,
];
const handle = await fs.open(dumpPath, 'w');
await run('mysqldump', args, { stdio: ['ignore', handle.fd, 'inherit'] });
await handle.close();
console.log(' ✓ Dump complete');
}
// ── SSH helpers ───────────────────────────────────────────────────────────────
function connectSSH() {
return new Promise((resolve, reject) => {
const conn = new Client();
const opts = { host, port: 22, username: sshUsername };
if (sshPassword.startsWith('__RSA:')) {
opts.privateKey = require('fs').readFileSync(sshPassword.slice(6));
} else {
opts.password = sshPassword;
}
conn.on('ready', () => resolve(conn))
.on('error', reject)
.connect(opts);
});
}
function getSftp(conn) {
return new Promise((res, rej) => conn.sftp((e, s) => (e ? rej(e) : res(s))));
}
// ── 2. Optional privilege elevation ───────────────────────────────────────────
async function escalate(conn) {
if (!rootPassword) return; // nothing requested
console.log('[2/7] Elevating privileges with sudo …');
await new Promise((resolve, reject) => {
conn.exec('sudo su', (err, stream) => {
if (err) return reject(err);
stream.on('close', code => (code === 0 ? resolve() : reject(new Error(`sudo exited ${code}`))));
// Echo any stderr output from sudo
stream.stderr.on('data', data => process.stderr.write(data));
// Feed password then EOF
stream.write(rootPassword + '\n');
});
});
console.log(' ✓ sudo credentials cached');
}
// ── 3. Copy dump to target ────────────────────────────────────────────────────
async function uploadDump(conn, sftp) {
console.log(`[3/7] Uploading dump ⇒ ${host}:${remoteDump}`);
await new Promise((res, rej) =>
sftp.fastPut(dumpPath, remoteDump, {}, err => (err ? rej(err) : res())),
);
console.log(' ✓ Upload complete');
}
// ── 4. Copy conf.json to target ───────────────────────────────────────────────
async function uploadConf(conn, sftp) {
console.log(`[4/7] Uploading conf.json ⇒ ${host}:${remoteConf}`);
// Write local temp conf then push
await fs.writeFile(localTmpConf, JSON.stringify(configRaw, null, 2));
await new Promise((res, rej) =>
sftp.fastPut(localTmpConf, remoteConf, {}, err => (err ? rej(err) : res())),
);
console.log(' ✓ conf.json uploaded');
}
// ── 5. Ensure DB / privileges on target ───────────────────────────────────────
async function ensureDatabase(conn) {
console.log('[5/7] Ensuring database & privileges …');
const pwdClause = sqlPassword
? `IDENTIFIED BY '${sqlPassword.replace(/'/g, `'\\''`)}'`
: '';
const sql = `
CREATE DATABASE IF NOT EXISTS \\\`${sqlDatabase}\\\`;
GRANT ALL PRIVILEGES ON \\\`${sqlDatabase}\\\`.* TO '${sqlUsername}'@'127.0.0.1' ${pwdClause};
FLUSH PRIVILEGES;
`;
const cmd = [
'mysql',
`-u${sqlUsername}`,
sqlPassword ? `-p'${sqlPassword.replace(/'/g, `'\\''`)}'` : '',
`-P${sqlPort}`,
`-e "${sql.replace(/\n/g, ' ')}"`,
].join(' ');
await new Promise((resolve, reject) => {
conn.exec(cmd, (err, stream) => {
if (err) return reject(err);
stream.on('close', code => (code === 0 ? resolve() : reject(new Error(`mysql exited ${code}`))));
stream.stderr.pipe(process.stderr);
stream.pipe(process.stdout);
});
});
console.log(' ✓ Database ready');
}
// ── 6. Restore DB on target ──────────────────────────────────────────────────
async function importOnTarget(conn) {
console.log('[6/7] Importing dump on target …');
const restoreCmd = [
'mysql',
`-u${sqlUsername}`,
sqlPassword ? `-p'${sqlPassword.replace(/'/g, `'\\''`)}'` : '',
`-P${sqlPort}`,
`${sqlDatabase} < ${remoteDump}`,
].join(' ');
await new Promise((resolve, reject) => {
conn.exec(restoreCmd, (err, stream) => {
if (err) return reject(err);
stream.on('close', code => (code === 0 ? resolve() : reject(new Error(`mysql exited ${code}`))));
stream.stderr.pipe(process.stderr);
stream.pipe(process.stdout);
});
});
console.log(' ✓ Import complete');
}
// ── 7. Cleanup ────────────────────────────────────────────────────────────────
async function cleanup(conn) {
console.log('[7/7] Cleaning up …');
await fs.unlink(dumpPath).catch(() => {});
await fs.unlink(localTmpConf).catch(() => {});
await new Promise(res => {
conn.exec(`rm -f '${remoteDump}'`, () => res()); // ignore errors
});
conn.end();
console.log(' ✓ All done!');
}
// ── Main orchestrator ─────────────────────────────────────────────────────────
(async () => {
try {
await dumpLocalDb();
const conn = await connectSSH();
await escalate(conn); // caches sudo creds if requested
const sftp = await getSftp(conn);
await uploadDump(conn, sftp);
await uploadConf(conn, sftp);
await ensureDatabase(conn);
await importOnTarget(conn);
await cleanup(conn);
} catch (err) {
console.error('‼ Error:', err.message);
process.exit(1);
}
})();