UNPKG

iobroker.js-controller

Version:

Updated by reinstall.js on 2018-06-11T15:19:56.688Z

1,149 lines (1,043 loc) • 50 kB
/** * Setup * * Copyright 2013-2022 bluefox <dogafox@gmail.com> * * MIT License * */ 'use strict'; const COLOR_RED = '\x1b[31m'; const COLOR_YELLOW = '\x1b[33m'; const COLOR_RESET = '\x1b[0m'; const COLOR_GREEN = '\x1b[32m'; /** @class */ function Setup(options) { const fs = require('fs-extra'); const path = require('path'); const { tools, EXIT_CODES } = require('@iobroker/js-controller-common'); const dbTools = require('@iobroker/js-controller-common-db').tools; const Backup = require('./setupBackup'); const deepClone = require('deep-clone'); const pluginInfos = require('./pluginInfos'); options = options || {}; const processExit = options.processExit; const dbConnect = options.dbConnect; const params = options.params; const cleanDatabase = options.cleanDatabase; const resetDbConnect = options.resetDbConnect; const restartController = options.restartController; let objects; let states; function mkpathSync(rootpath, dirpath) { // Remove filename dirpath = dirpath.split('/'); dirpath.pop(); if (!dirpath.length) { return; } for (const dir of dirpath) { rootpath = path.join(rootpath, dir); if (!fs.existsSync(rootpath)) { if (dir !== '..') { fs.mkdirSync(rootpath); } else { throw new Error(`Cannot create ${rootpath}${dirpath.join('/')}`); } } } } async function informAboutPlugins(systemConfig, callback) { let ioPackage; let ioConfig; const configFile = tools.getConfigFileName(); try { ioPackage = JSON.parse(fs.readFileSync(path.join(__dirname, '../../io-package.json'), 'utf8')); } catch { console.error('Cannot read js-controller io-package.json. Ignore plugins defined there.'); } try { ioConfig = JSON.parse(fs.readFileSync(configFile, 'utf8')); } catch { console.error('Can not read js-controller config file. Ignore plugins defined there.'); } const plugins = {}; if (ioPackage && ioPackage.common && ioPackage.common.plugins) { for (const [plugin, pluginData] of Object.entries(ioPackage.common.plugins)) { if (pluginData.enabled !== false) { plugins[plugin] = pluginData; } } } if (ioConfig && ioConfig.plugins) { for (const [plugin, pluginData] of Object.entries(ioConfig.plugins)) { if (!plugins[plugin] && pluginData.enabled !== false) { plugins[plugin] = pluginData; } } } let systemLang = 'en'; let systemDiag = 'extended'; if (systemConfig && systemConfig.common) { systemDiag = systemConfig.common.diag || 'extended'; systemLang = systemConfig.common.language || 'en'; } for (const plugin of Object.keys(plugins)) { const pluginInfo = pluginInfos.PLUGIN_INFOS[plugin]; if (!pluginInfo) { // We do not have relevant information to display continue; } if (systemDiag === 'none' && pluginInfos.isReportingPlugin(plugin)) { // Reporting plugins respect "diag" and do not send information if diag is disabled continue; } let enabledState; try { enabledState = await states.getStateAsync( `system.host.${tools.getHostName()}.plugins.${plugin}.enabled` ); } catch { // ignore } if (enabledState && enabledState.val !== undefined) { // already configured, so do not output again continue; } const infoHeadLine = pluginInfo.headline[systemLang] || pluginInfo.headline.en; const infoText = pluginInfo.text[systemLang] || pluginInfo.text.en; console.error(COLOR_RED); console.error(infoHeadLine); console.error(COLOR_YELLOW); console.error(infoText); console.error(); console.error(COLOR_RESET); } callback(); } /** * Called after io-package objects are created * * @param {Record<string, any>} systemConfig * @param {() => void} callback * @return {Promise<void>} */ async function setupReady(systemConfig, callback) { if (!callback) { console.log('database setup done. You can add adapters and start ' + tools.appName + ' now'); return processExit(EXIT_CODES.NO_ERROR); } // clean up invalid user group assignments (non-existing user in a group) try { await _cleanupInvalidGroupAssignments(); } catch (e) { console.error(`Cannot clean up invalid user group assignments: ${e.message}`); } if (!objects.syncFileDirectory || !objects.dirExists) { return void informAboutPlugins(systemConfig, callback); } // check meta.user try { const objExists = await objects.objectExists('meta.user'); if (objExists) { // check if dir is missing const dirExists = objects.dirExists('meta.user'); if (!dirExists) { // create meta.user, so users see them as upload target await objects.mkdirAsync('meta.user'); console.log('Successfully created "meta.user" directory'); } } } catch (err) { console.warn(`Could not create directory "meta.user": ${err.message}`); } try { const { numberSuccess, notifications } = objects.syncFileDirectory(); numberSuccess && console.log( `${numberSuccess} file(s) successfully synchronized with ioBroker storage. Please DO NOT copy files manually into ioBroker storage directories!` ); if (notifications.length) { console.log(); console.log('The following notifications happened during sync: '); notifications.forEach(el => console.log(`- ${el}`)); console.log(); } return void informAboutPlugins(systemConfig, callback); } catch (err) { console.error(`Error on file directory sync: ${err.message}`); return void informAboutPlugins(systemConfig, callback); } } async function dbSetup(iopkg, ignoreExisting, callback) { if (typeof ignoreExisting === 'function') { callback = ignoreExisting; ignoreExisting = false; } if (iopkg.objects && iopkg.objects.length > 0) { const obj = iopkg.objects.pop(); objects.getObject(obj._id, (err, _obj) => { if (err || !_obj || obj._id.startsWith('_design/')) { obj.from = `system.host.${tools.getHostName()}.cli`; obj.ts = Date.now(); objects.setObject(obj._id, obj, () => { console.log(`object ${obj._id} ${err || !_obj ? 'created' : 'updated'}`); setTimeout(dbSetup, 25, iopkg, ignoreExisting, callback); }); } else { !ignoreExisting && console.log('object ' + obj._id + ' yet exists'); setTimeout(dbSetup, 25, iopkg, ignoreExisting, callback); } }); } else { await tools.createUuid(objects); // check if encrypt secret exists objects.getObject('system.config', (err, obj) => { let configFixed = false; if (obj && obj.type !== 'config') { obj.type = 'config'; obj.from = `system.host.${tools.getHostName()}.cli`; obj.ts = Date.now(); configFixed = true; } if (obj && (!obj.native || !obj.native.secret)) { require('crypto').randomBytes(24, (ex, buf) => { obj.native = obj.native || {}; obj.native.secret = buf.toString('hex'); obj.from = `system.host.${tools.getHostName()}.cli`; obj.ts = Date.now(); objects.setObject('system.config', obj, () => setupReady(obj, callback)); }); } else { if (configFixed) { objects.setObject('system.config', obj, () => setupReady(obj, callback)); } else { setupReady(obj, callback); } } }); } } /** * Creates objects and does object related cleanup * * @param {() => void} callback * @param {boolean} checkCertificateOnly */ function setupObjects(callback, checkCertificateOnly) { dbConnect(params, async (_objects, _states) => { objects = _objects; states = _states; const iopkg = fs.readJsonSync(`${__dirname}/../../io-package.json`); await _maybeMigrateSets(); if (checkCertificateOnly) { let certObj; if (iopkg && iopkg.objects) { for (let i = 0; i < iopkg.objects.length; i++) { if (iopkg.objects[i] && iopkg.objects[i]._id === 'system.certificates') { certObj = iopkg.objects[i]; break; } } } if (certObj) { let obj; try { obj = await objects.getObjectAsync('system.certificates'); } catch { // ignore } if ( obj && obj.native && obj.native.certificates && obj.native.certificates.defaultPublic !== undefined ) { let cert = tools.getCertificateInfo(obj.native.certificates.defaultPublic); if (cert) { const dateCertStart = Date.parse(cert.validityNotBefore); const dateCertEnd = Date.parse(cert.validityNotAfter); // check, if certificate is invalid (too old, longer then 825 days or keylength too short) if ( dateCertEnd <= Date.now() || cert.keyLength < 2048 || dateCertEnd - dateCertStart > 365 * 24 * 60 * 60 * 1000 ) { // generate new certificates if (cert.certificateFilename) { console.log( `Existing file certificate (${cert.certificateFilename}) is invalid (too old, validity longer then 345 days or keylength too short). Please check it!` ); } else { console.log( 'Existing earlier generated certificate is invalid (too old, validity longer then 345 days or keylength too short). Generating new Certificate!' ); cert = null; } } } if (!cert) { const newCert = tools.generateDefaultCertificates(); obj.native.certificates.defaultPrivate = newCert.defaultPrivate; obj.native.certificates.defaultPublic = newCert.defaultPublic; try { await objects.setObjectAsync(obj._id, obj); console.log(`object ${obj._id} updated`); } catch { //ignore } dbSetup(iopkg, true, callback); return; } } dbSetup(iopkg, true, callback); } else { dbSetup(iopkg, true, callback); } } else { dbSetup(iopkg, callback); } }); } /** * Asks the user if he wants to migrate objects if it makes sense and performs migration according to input * * @param {object} newConfig - updated config * @param {object} oldConfig - previous config * @param {import("readline").ReadLine} rl - readline object * @param {function(number=):void} callback - callback function */ function migrateObjects(newConfig, oldConfig, rl, callback) { // allow migration if one of the db types changed or host changed of redis const oldStatesHasServer = dbTools.statesDbHasServer(oldConfig.states.type); const oldObjectsHasServer = dbTools.statesDbHasServer(oldConfig.objects.type); const newStatesHasServer = dbTools.statesDbHasServer(newConfig.states.type); const newObjectsHasServer = dbTools.statesDbHasServer(newConfig.objects.type); const oldStatesLocalServer = dbTools.isLocalStatesDbServer(oldConfig.states.type, oldConfig.states.host); const oldObjectsLocalServer = dbTools.isLocalObjectsDbServer(oldConfig.objects.type, oldConfig.objects.host); const newStatesLocalServer = dbTools.isLocalStatesDbServer(newConfig.states.type, newConfig.states.host); const newObjectsLocalServer = dbTools.isLocalObjectsDbServer(newConfig.objects.type, newConfig.objects.host); if ( oldConfig && (oldConfig.states.type !== newConfig.states.type || oldConfig.objects.type !== newConfig.objects.type || (!oldStatesHasServer && oldConfig.states.host !== newConfig.states.host) || (!oldObjectsHasServer && oldConfig.objects.host !== newConfig.objects.host)) ) { let fromMaster = oldStatesLocalServer || oldObjectsLocalServer; let toMaster = newStatesLocalServer || newObjectsLocalServer; if (!oldStatesHasServer && !oldObjectsHasServer) { fromMaster = null; // Master can not be detected, check new } if (!newStatesHasServer && !newObjectsHasServer) { toMaster = null; // new } let allowMigration; if (fromMaster) { if (!toMaster) { const answer = rl.question( `Please choose if this is a Master/single host (enter "m") or a Slave host (enter "S") you are about to edit. For Slave hosts the data migration will be skipped. [S/m]: `, { limit: /^[SsMm]?$/, defaultInput: 'S' } ); allowMigration = !(answer === 'S' || answer === 's'); } else { const answer = rl.question( `This host appears to be a Master or a Single host system. Is this correct? [Y/n]: `, { limit: /^[YyNnJj]?$/, defaultInput: 'Y' } ); allowMigration = answer === 'Y' || answer === 'y' || answer === 'J' || answer === 'j'; } } else { if (toMaster) { const answer = rl.question( `It appears that you want to convert this slave host into a Master or Single host system. Is this correct? [Y/n]: `, { limit: /^[YyNnJj]?$/, defaultInput: 'Y' } ); allowMigration = answer === 'Y' || answer === 'y' || answer === 'J' || answer === 'j'; } else { const answer = rl.question( `This host appears to be an ioBroker SLAVE system. Migration will be skipped. Is this correct? [Y/n]: `, { limit: /^[YyNnJj]?$/, defaultInput: 'Y' } ); allowMigration = !(answer === 'Y' || answer === 'y' || answer === 'J' || answer === 'j'); } } if (oldObjectsHasServer && !newObjectsHasServer) { console.log(COLOR_YELLOW); console.log(`Important: Using ${newConfig.objects.type} for the Objects database is only supported`); console.log('with js-controller 2.0 or higher!'); console.log('When your system consists of multiple hosts please make sure to have'); console.log('js-controller 2.0 or higher installed on ALL hosts *before* continuing!'); if (allowMigration) { console.log(''); console.log(''); console.log('Important #2: If you already did the migration on an other host'); console.log('please *do not* migrate again! This can destroy your system!'); console.log(''); console.log(''); console.log('Important #3: The process will migrate all files that were officially'); console.log('uploaded into the ioBroker system. If you have manually copied files into'); console.log('iobroker-data/files/... into own directories then these files will NOT be'); console.log('migrated! Make sure all files are in adapter directories inside the files'); console.log('directory!'); } console.log(COLOR_RESET); } // FileDB -> JSONL migration is handled in the DB classes. Skip migration if both DBs are changed from File -> JsonL if ( (oldConfig.states.type === newConfig.states.type || (oldConfig.states.type === 'file' && newConfig.states.type === 'jsonl')) && (oldConfig.objects.type === newConfig.objects.type || (oldConfig.objects.type === 'file' && newConfig.objects.type === 'jsonl')) ) { console.log('Explicit migration from file to jsonl is not necessary, skipping...'); allowMigration = false; } let answer = 'N'; if (allowMigration) { console.log(); answer = rl.question( `Do you want to migrate objects and states from "${oldConfig.objects.type}/${oldConfig.states.type}" to "${newConfig.objects.type}/${newConfig.states.type}" [y/N]: `, { limit: /^[YyNnJj]?$/, defaultInput: 'N' } ); if ( newConfig.objects.type !== oldConfig.objects.type && (answer === 'Y' || answer === 'y' || answer === 'J' || answer === 'j') ) { console.log(COLOR_YELLOW); answer = rl.question( `Migrating the objects database will overwrite all objects! Are you sure that this is not a slave host and you want to migrate the data? [y/N]: `, { limit: /^[YyNnJj]?$/, defaultInput: 'N' } ); console.log(COLOR_RESET); } } if (answer === 'Y' || answer === 'y' || answer === 'J' || answer === 'j') { console.log(`Connecting to previous DB "${oldConfig.objects.type}"...`); dbConnect(params, async (objects, states, isOffline) => { if (!isOffline) { console.error(COLOR_RED); console.error('Cannot migrate DB while js-controller is still running!'); console.error( 'Please stop ioBroker and try again. No settings have been changed.' + COLOR_RESET ); return void callback(EXIT_CODES.CONTROLLER_RUNNING); } const backup = new Backup({ states, objects, cleanDatabase, restartController, processExit: callback }); console.log('Creating backup ...'); console.log(`${COLOR_GREEN}This can take some time ... please be patient!${COLOR_RESET}`); let filePath = await backup.createBackup('', true); const origBackupPath = filePath; filePath = filePath.replace('.tar.gz', '-migration.tar.gz'); try { fs.renameSync(origBackupPath, filePath); } catch { filePath = origBackupPath; console.log('[Not Critical Error] Could not rename Backup file'); } console.log('Backup created: ' + filePath); await resetDbConnect(); console.log(`updating conf/${tools.appName}.json`); fs.writeFileSync(tools.getConfigFileName() + '.bak', JSON.stringify(oldConfig, null, 2)); fs.writeFileSync(tools.getConfigFileName(), JSON.stringify(newConfig, null, 2)); console.log(''); console.log(`Connecting to new DB "${newConfig.objects.type}" (can take up to 20s) ...`); dbConnect(true, Object.assign(params, { timeout: 20000 }), (objects, states) => { if (!states || !objects) { console.error(COLOR_RED); console.log( 'New Database could not be connected. Please check your settings. No settings have been changed.' + COLOR_RESET ); console.log('restoring conf/' + tools.appName + '.json'); fs.writeFileSync(tools.getConfigFileName(), JSON.stringify(oldConfig, null, 2)); fs.unlinkSync(tools.getConfigFileName() + '.bak'); return void callback(EXIT_CODES.MIGRATION_ERROR); } const backup = new Backup({ states, objects, cleanDatabase, restartController, processExit: callback, dbMigration: true }); console.log('Restore backup ...'); console.log(`${COLOR_GREEN}This can take some time ... please be patient!${COLOR_RESET}`); backup.restoreBackup(filePath, false, true, async err => { if (err) { console.log(`Error happened during restore: ${err.message}`); console.log(); console.log('restoring conf/' + tools.appName + '.json'); fs.writeFileSync(tools.getConfigFileName(), JSON.stringify(oldConfig, null, 2)); fs.unlinkSync(tools.getConfigFileName() + '.bak'); } else { await _maybeMigrateSets(); console.log('Backup restored - Migration successful'); console.log(COLOR_YELLOW); console.log('Important: If your system consists of multiple hosts please execute '); console.log('"iobroker upload all" on the master AFTER all other hosts/slaves have '); console.log('also been updated to this states/objects database configuration AND are'); console.log('running!' + COLOR_RESET); } callback(err ? EXIT_CODES.MIGRATION_ERROR : 0); }); }); }); return; } else if (!newObjectsHasServer) { console.log(''); console.log('No Database migration was done.'); console.log( `${COLOR_YELLOW}If this was done on your master host please execute "iobroker setup first" to newly initialize all objects.${COLOR_RESET}` ); console.log(''); } } console.log(`updating conf/${tools.appName}.json`); fs.writeFileSync(tools.getConfigFileName(), JSON.stringify(newConfig, null, 2)); callback(); } this.setupCustom = callback => { const rl = require('readline-sync'); let config; let originalConfig; // read actual configuration try { if (fs.existsSync(tools.getConfigFileName())) { config = fs.readJSONSync(tools.getConfigFileName()); originalConfig = deepClone(config); } else { config = require(`../../conf/${tools.appName}-dist.json`); } } catch { config = require(`../../conf/${tools.appName}-dist.json`); } const currentObjectsType = originalConfig.objects.type || 'jsonl'; const currentStatesType = originalConfig.states.type || 'jsonl'; console.log('Current configuration:'); console.log('- Objects database:'); console.log(` - Type: ${originalConfig.objects.type}`); console.log(` - Host/Unix Socket: ${originalConfig.objects.host}`); console.log(` - Port: ${originalConfig.objects.port}`); if (Array.isArray(originalConfig.objects.host)) { console.log( ` - Sentinel-Master-Name: ${ originalConfig.objects.sentinelName ? originalConfig.objects.sentinelName : 'mymaster' }` ); } console.log('- States database:'); console.log(` - Type: ${originalConfig.states.type}`); console.log(` - Host/Unix Socket: ${originalConfig.states.host}`); console.log(` - Port: ${originalConfig.states.port}`); if (Array.isArray(originalConfig.states.host)) { console.log( ` - Sentinel-Master-Name: ${ originalConfig.states.sentinelName ? originalConfig.states.sentinelName : 'mymaster' }` ); } if ( dbTools.objectsDbHasServer(originalConfig.objects.type) || dbTools.statesDbHasServer(originalConfig.states.type) ) { console.log(`- Data Directory: ${tools.getDefaultDataDir()}`); } if (originalConfig && originalConfig.system && originalConfig.system.hostname) { console.log(`- Host name: ${originalConfig.system.hostname}`); } console.log(''); let otype = rl.question( `Type of objects DB [(j)sonl, (f)ile, (r)edis, ...], default [${currentObjectsType}]: `, { defaultInput: currentObjectsType } ); otype = otype.toLowerCase(); if (otype === 'r') { otype = 'redis'; } else if (otype === 'f') { otype = 'file'; } else if (otype === 'j') { otype = 'jsonl'; } let getDefaultObjectsPort; try { const path = require.resolve(`@iobroker/db-objects-${otype}`); getDefaultObjectsPort = require(path).getDefaultPort; } catch { console.log(`${COLOR_RED}Unknown objects type: ${otype}${COLOR_RESET}`); if (otype !== 'file' && otype !== 'redis') { console.log(COLOR_YELLOW); console.log(`Please check that the objects db type you entered is really correct!`); console.log(`If yes please use "npm i @iobroker/db-objects-${otype}" to install it manually.`); console.log(`You also need to make sure you stay up to date with this package in the future!`); console.log(COLOR_RESET); } return void callback(EXIT_CODES.INVALID_ARGUMENTS); } if (otype === 'redis' && originalConfig.objects.type !== 'redis') { console.log(COLOR_YELLOW); console.log('When Objects and Files are stored in a Redis database please consider the following:'); console.log('1. All data will be stored in RAM, make sure to have enough free RAM available!'); console.log( '2. Make sure to check Redis persistence options to make sure a Redis problem will not cause data loss!' ); console.log('3. The Redis persistence files can get big, make sure not to use an SD card to store them.'); console.log(COLOR_RESET); } const defaultObjectsHost = otype === originalConfig.objects.type ? originalConfig.objects.host : '127.0.0.1'; let ohost = rl.question( `Host / Unix Socket of objects DB(${otype}), default[${ Array.isArray(defaultObjectsHost) ? defaultObjectsHost.join(',') : defaultObjectsHost }]: `, { defaultInput: Array.isArray(defaultObjectsHost) ? defaultObjectsHost.join(',') : defaultObjectsHost } ); ohost = ohost.toLowerCase(); const op = getDefaultObjectsPort(ohost); const oSentinel = otype === 'redis' && ohost.includes(','); if (oSentinel) { ohost = ohost.split(','); ohost.forEach((host, idx) => (ohost[idx] = host.trim())); } const defaultObjectsPort = otype === originalConfig.objects.type && ohost === originalConfig.objects.host ? originalConfig.objects.port : op; const userObjPort = rl.question( `Port of objects DB(${otype}), default[${ Array.isArray(defaultObjectsPort) ? defaultObjectsPort.join(',') : defaultObjectsPort }]: `, { defaultInput: Array.isArray(defaultObjectsPort) ? defaultObjectsPort.join(',') : defaultObjectsPort, limit: /^[0-9, ]+$/ } ); let oport; if (userObjPort.includes(',')) { oport = userObjPort.split(','); oport.forEach((port, idx) => { oport[idx] = parseInt(port.trim(), 10); if (isNaN(oport[idx])) { console.log(`${COLOR_RED}Invalid objects port: ${oport[idx]}${COLOR_RESET}`); return void callback(EXIT_CODES.INVALID_ARGUMENTS); } }); } else { oport = parseInt(userObjPort, 10); if (isNaN(oport)) { console.log(`${COLOR_RED}Invalid objects port: ${oport}${COLOR_RESET}`); return void callback(EXIT_CODES.INVALID_ARGUMENTS); } } let oSentinelName = null; if (oSentinel) { const defaultSentinelName = originalConfig.objects.sentinelName ? originalConfig.objects.sentinelName : 'mymaster'; oSentinelName = rl.question(`Objects Redis Sentinel Master Name [${defaultSentinelName}]: `, { defaultInput: defaultSentinelName }); } let defaultStatesType = currentStatesType; try { require.resolve(`@iobroker/db-states-${otype}`); defaultStatesType = otype; // if states db is also available with same type we use as default } catch { // ignore, unchanged } let stype = rl.question( `Type of states DB [(j)sonl, (f)file, (r)edis, ...], default [${defaultStatesType}]: `, { defaultInput: defaultStatesType } ); stype = stype.toLowerCase(); if (stype === 'r') { stype = 'redis'; } else if (stype === 'f') { stype = 'file'; } else if (stype === 'j') { stype = 'jsonl'; } let getDefaultStatesPort; try { const path = require.resolve(`@iobroker/db-states-${stype}`); getDefaultStatesPort = require(path).getDefaultPort; } catch { console.log(`${COLOR_RED}Unknown states type: ${stype}${COLOR_RESET}`); if (stype !== 'file' && stype !== 'redis') { console.log(COLOR_YELLOW); console.log(`Please check that the states db type you entered is really correct!`); console.log(`If yes please use "npm i @iobroker/db-states-${stype}" to install it manually.`); console.log(`You also need to make sure you stay up to date with this package in the future!`); console.log(COLOR_RESET); } return void callback(EXIT_CODES.INVALID_ARGUMENTS); } if (stype === 'redis' && originalConfig.states.type !== 'redis' && otype !== 'redis') { console.log(COLOR_YELLOW); console.log('When States are stored in a Redis database please make sure to configure Redis'); console.log('persistence to make sure a Redis problem will not cause data loss!'); console.log(COLOR_RESET); } let defaultStatesHost = stype === originalConfig.states.type ? originalConfig.states.host : ohost || '127.0.0.1'; if (stype === otype) { defaultStatesHost = ohost; } let shost = rl.question( `Host / Unix Socket of states DB (${stype}), default[${ Array.isArray(defaultStatesHost) ? defaultStatesHost.join(',') : defaultStatesHost }]: `, { defaultInput: Array.isArray(defaultStatesHost) ? defaultStatesHost.join(',') : defaultStatesHost } ); shost = shost.toLowerCase(); const sp = getDefaultStatesPort(shost); const sSentinel = stype === 'redis' && shost.includes(','); if (sSentinel) { shost = shost.split(','); shost.forEach((host, idx) => (shost[idx] = host.trim())); } let defaultStatesPort = stype === originalConfig.states.type && shost === originalConfig.states.host ? originalConfig.states.port : sp; if (stype === otype && !dbTools.statesDbHasServer(stype) && shost === ohost) { defaultStatesPort = oport; } const userStatePort = rl.question( `Port of states DB (${stype}), default[${ Array.isArray(defaultStatesPort) ? defaultStatesPort.join(',') : defaultStatesPort }]: `, { defaultInput: Array.isArray(defaultStatesPort) ? defaultStatesPort.join(',') : defaultStatesPort, limit: /^[0-9, ]+$/ } ); let sport; if (userStatePort.includes(',')) { sport = userStatePort.split(','); sport.forEach((port, idx) => { sport[idx] = parseInt(port.trim(), 10); if (isNaN(sport[idx])) { console.log(`${COLOR_RED}Invalid states port: ${sport[idx]}${COLOR_RESET}`); return void callback(EXIT_CODES.INVALID_ARGUMENTS); } }); } else { sport = parseInt(userStatePort, 10); if (isNaN(sport)) { console.log(`${COLOR_RED}Invalid states port: ${sport}${COLOR_RESET}`); return void callback(EXIT_CODES.INVALID_ARGUMENTS); } } let sSentinelName = null; if (sSentinel) { const defaultSentinelName = originalConfig.states.sentinelName ? originalConfig.states.sentinelName : oSentinelName && oport === sport ? oSentinelName : 'mymaster'; sSentinelName = rl.question(`States Redis Sentinel Master Name [${defaultSentinelName}]: `, { defaultInput: defaultSentinelName }); } let dir; let hname; if (dbTools.isLocalStatesDbServer(stype, shost) || dbTools.isLocalObjectsDbServer(otype, ohost)) { dir = rl.question(`Data directory (file), default[${tools.getDefaultDataDir()}]: `, { defaultInput: tools.getDefaultDataDir() }); hname = rl.question( `Host name of this machine [${ originalConfig && originalConfig.system ? originalConfig.system.hostname || require('os').hostname() : require('os').hostname() }]: `, { defaultInput: (originalConfig && originalConfig.system && originalConfig.system.hostname) || '' } ); } else { hname = rl.question(`Host name of this machine [${require('os').hostname()}]: `, { defaultInput: '' }); } if (hname.match(/\s/)) { console.log(`${COLOR_RED}Invalid host name: ${hname}${COLOR_RESET}`); return void callback(EXIT_CODES.INVALID_ARGUMENTS); } config.system = config.system || {}; config.system.hostname = hname; config.objects.host = ohost; config.objects.type = otype; config.objects.port = oport; config.states.host = shost; config.states.type = stype; config.states.port = sport; config.states.dataDir = undefined; config.objects.dataDir = undefined; if (dir) { config.objects.dataDir = dir; } if (dir) { config.states.dataDir = dir; } if (config.objects.type === 'redis' && oSentinel && oSentinelName && oSentinelName !== 'mymaster') { config.objects.sentinelName = oSentinelName; } if (config.states.type === 'redis' && sSentinel && sSentinelName && sSentinelName !== 'mymaster') { config.states.sentinelName = sSentinelName; } migrateObjects(config, originalConfig, rl, callback); }; /** * Checks if single host setup and if so migrates and activates Redis Sets Usage * @return {Promise<void>} * @private */ async function _maybeMigrateSets() { try { // if we have a single host system we need to ensure that existing objects are migrated to sets before doing anything else if (await tools.isSingleHost(objects)) { await objects.activateSets(); const noMigrated = await objects.migrateToSets(); if (noMigrated) { console.log(`Successfully migrated ${noMigrated} objects to Redis Sets`); } } } catch (e) { console.warn(`Could not migrate objects to corresponding sets: ${e.message}`); } } /** * Removes non-existing users from groups * * @return {Promise<void>} * @private */ async function _cleanupInvalidGroupAssignments() { const usersView = await objects.getObjectViewAsync('system', 'user'); const groupView = await objects.getObjectViewAsync('system', 'group'); const existingUsers = usersView.rows.map(obj => obj.value._id); for (const group of groupView.rows) { // reference for readability const groupMembers = group.value.common.members; if (!Array.isArray(groupMembers)) { // fix legacy objects const obj = group.value; obj.common.members = []; await objects.setObjectAsync(obj._id, obj); continue; } let changed = false; for (let i = groupMembers.length - 1; i >= 0; i--) { if (!existingUsers.includes(groupMembers[i])) { // we have found a non-existing user, so remove it changed = true; console.log(`Removed non-existing user "${groupMembers[i]}" from group "${group.value._id}"`); groupMembers.splice(i, 1); } } if (changed) { await objects.setObjectAsync(group.value._id, group.value); } } } this.setup = function (callback, ignoreIfExist, useRedis) { let config; let isCreated = false; const platform = require('os').platform(); const otherInstallDirs = []; // copy reinstall.js file into root if (fs.existsSync(__dirname + '/../../../../node_modules/')) { try { if (fs.existsSync(__dirname + '/../../reinstall.js')) { fs.writeFileSync( __dirname + '/../../../../reinstall.js', fs.readFileSync(__dirname + '/../../reinstall.js') ); } } catch (e) { console.warn(`Cannot write file. Not critical: ${e.message}`); } } // Delete files for other OS if (platform.startsWith('win')) { otherInstallDirs.push(__dirname + '/../../' + tools.appName); otherInstallDirs.push(__dirname + '/../../' + tools.appName.substring(0, 3)); otherInstallDirs.push(__dirname + '/../../killall.sh'); otherInstallDirs.push(__dirname + '/../../reinstall.sh'); } else { otherInstallDirs.push(__dirname + '/../../_service_' + tools.appName + '.bat'); otherInstallDirs.push(__dirname + '/../../' + tools.appName + '.bat'); otherInstallDirs.push(__dirname + '/../../' + tools.appName.substring(0, 3) + '.bat'); // copy scripts to root directory if (fs.existsSync(__dirname + '/../../../../node_modules/')) { const startFile = `#!/usr/bin/env node require('${path.normalize(__dirname + '/..')}/setup').execute();`; try { if (fs.existsSync(__dirname + '/../../killall.sh')) { fs.writeFileSync( __dirname + '/../../../../killall.sh', fs.readFileSync(__dirname + '/../../killall.sh'), { mode: 492 /* 0754 */ } ); } if (fs.existsSync(__dirname + '/../../reinstall.sh')) { fs.writeFileSync( __dirname + '/../../../../reinstall.sh', fs.readFileSync(__dirname + '/../../reinstall.sh'), { mode: 492 /* 0754 */ } ); } if (!fs.existsSync(__dirname + '/../../../../' + tools.appName.substring(0, 3))) { fs.writeFileSync(__dirname + '/../../../../' + tools.appName.substring(0, 3), startFile, { mode: 492 /* 0754 */ }); } if (!fs.existsSync(__dirname + '/../../../../' + tools.appName)) { fs.writeFileSync(__dirname + '/../../../../' + tools.appName, startFile, { mode: 492 /* 0754 */ }); } } catch (e) { console.warn(`Cannot write file. Not critical: ${e.message}`); } } } for (let t = 0; t < otherInstallDirs.length; t++) { if (fs.existsSync(otherInstallDirs[t])) { const stat = fs.statSync(otherInstallDirs[t]); if (stat.isDirectory()) { const files = fs.readdirSync(otherInstallDirs[t]); for (let f = 0; f < files.length; f++) { fs.unlinkSync(otherInstallDirs[t] + '/' + files[f]); } fs.rmdirSync(otherInstallDirs[t]); } else { try { fs.unlinkSync(otherInstallDirs[t]); } catch (e) { console.warn(`Cannot delete file. Not critical: ${e.message}`); } } } } // Create log and tmp directory if (!fs.existsSync(__dirname + '/../../tmp')) { fs.mkdirSync(__dirname + '/../../tmp'); } const configFileName = tools.getConfigFileName(); // only change config if non existing - else setup custom has to be used if (!fs.existsSync(configFileName)) { isCreated = true; if (fs.existsSync(__dirname + '/../../conf/' + tools.appName + '-dist.json')) { config = require(`../../conf/${tools.appName}-dist.json`); } else { config = require(`../../conf/${tools.appName.toLowerCase()}-dist.json`); } console.log(`creating conf/${tools.appName}.json`); config.objects.host = params.objects || '127.0.0.1'; config.states.host = params.states || '127.0.0.1'; if (useRedis) { config.states.type = 'redis'; config.states.port = params.port || 6379; config.objects.type = 'redis'; config.objects.port = params.port || 6379; } // this path is relative to js-controller config.dataDir = tools.getDefaultDataDir(); const _path = path .normalize(__dirname + '/../../../node_modules/' + tools.appName + '.js-controller') .replace(/\\/g, '/'); if (fs.existsSync(_path)) { if (_path.indexOf('/node_modules/') !== -1) { mkpathSync(__dirname + '/../../', config.dataDir); } else { mkpathSync(__dirname + '../../', config.dataDir); } } else { mkpathSync(__dirname + '/../', '../' + config.dataDir); } const dirName = path.dirname(configFileName); if (!fs.existsSync(dirName)) { mkpathSync('', dirName.replace(/\\/g, '/')); } // Create default data dir fs.writeFileSync(configFileName, JSON.stringify(config, null, 2)); try { // Create if ( __dirname .toLowerCase() .replace(/\\/g, '/') .indexOf('node_modules/' + tools.appName + '.js-controller') !== -1 ) { const parts = config.dataDir.split('/'); // Remove appName-data/ parts.pop(); parts.pop(); const path_ = parts.join('/'); if (!fs.existsSync(__dirname + '/../../' + path_ + '/log')) { fs.mkdirSync(__dirname + '/../../' + path_ + '/log'); } } else { if (!fs.existsSync(__dirname + '/../../log')) { fs.mkdirSync(__dirname + '/../../log'); } } } catch (err) { console.log(`Non-critical error: ${err.message}`); } } else if (ignoreIfExist) { // it is a setup first run and config exists yet try { config = fs.readJSONSync(configFileName); if (!Object.prototype.hasOwnProperty.call(config, 'dataDir')) { // Workaround: there was a bug with admin v5 which could remove the dataDir attribute -> fix this // TODO: remove it as soon as all adapters are fixed which use systemConfig.dataDir config.dataDir = tools.getDefaultDataDir(); fs.writeJSONSync(configFileName, config, { spaces: 2 }); } } catch (err) { console.warn(`Cannot check config file: ${err.message}`); } setupObjects(() => callback && callback(), true); return; } setupObjects(() => callback && callback(isCreated)); }; } module.exports = Setup;