Shinobi/tools/copySystemToNewServer.js

226 lines
9.3 KiB
JavaScript

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() };
// ── CLI parsing ───────────────────────────────────────────────────────────────
const [
, , host,
sshUsername,
sshPassword,
sqlUsername = 'majesticflame',
sqlPassword = '',
sqlDatabase = 'ccio',
sqlPort = 3306,
shinobiPath = '/home/Shinobi', // <─ NEW default
] = 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');
console.log(`=============`)
console.log(`## Usage Parameters :`)
console.log(`=============`)
console.log(`# HOST : The Server IP Address or Domain Name. Required.`)
console.log(`# SSH_USER : The username to login to SSH. Required.`)
console.log(`# SSH_PASS : The password to login to SSH. To use RSA passwordless login do "__RSA:/path/to/keyfile". Required.`)
console.log(`# SQL_USER : The username for the MySQL (MariaDB) DATABASE. Default is "majesticflame". Optional.`)
console.log(`# SQL_PASS : The password for the MySQL (MariaDB) DATABASE. Default is blank. Optional.`)
console.log(`# SQL_DB : The database name. Default is "ccio". Optional.`)
console.log(`# SQL_PORT : The database port. Default is "3306". Optional.`)
console.log(`# SHINOBI_PATH : The path where Shinobi is installed. Default is "/home/Shinobi" Optional.`)
console.log(`=============`)
process.exit(1);
}
// ── Source-side DB info (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/5] Dumping local DB ⇒ ${dumpPath}`);
const args = [
`-h${localDbConf.host}`,
`-P${localDbConf.port}`,
`-u${localDbConf.user}`,
`-p${localDbConf.password}`,
'--single-transaction', '--routines', '--triggers',
localDbConf.database,
];
await run('mysqldump', args, {
stdio: ['ignore', await fs.open(dumpPath, 'w'), 'inherit'],
});
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. Copy dump to target ────────────────────────────────────────────────────
async function uploadDump(conn, sftp) {
console.log(`[2/5] Uploading dump ⇒ ${host}:${remoteDump}`);
await new Promise((res, rej) =>
sftp.fastPut(dumpPath, remoteDump, {}, err => (err ? rej(err) : res())),
);
console.log(' ✓ Upload complete');
}
// ── 3. Copy conf.json to target ───────────────────────────────────────────────
async function uploadConf(conn, sftp) {
console.log(`[3/5] Uploading conf.json ⇒ ${host}:${remoteConf}`);
// 3a. make sure remote Shinobi dir exists
await new Promise((resolve, reject) => {
conn.exec(`mkdir -p '${shinobiPath.replace(/'/g, `'\\''`)}'`, (err, stream) => {
if (err) return reject(err);
stream.on('close', code => (code === 0 ? resolve() : reject(new Error(`mkdir exited ${code}`))));
});
});
// 3b. 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');
}
// ── 4. Ensure DB / privileges on target ──────────────────────────────────────
async function ensureDatabase(conn) {
console.log('[4/6] 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}`))))
.stderr.pipe(process.stderr);
stream.pipe(process.stdout);
});
});
console.log(' ✓ Database ready');
}
// ── 5. Restore DB on target ───────────────────────────────────────────────────
async function importOnTarget(conn) {
console.log('[5/6] 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}`))))
.stderr.pipe(process.stderr);
stream.pipe(process.stdout);
});
});
console.log(' ✓ Import complete');
}
// ── 6. Cleanup ────────────────────────────────────────────────────────────────
async function cleanup(conn) {
console.log('[6/6] 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();
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);
}
})();