UNPKG

iobroker.linux-control

Version:

Controlling Linux devices and get information about your system

1,506 lines (1,268 loc) 52.5 kB
'use strict'; /* * Created with @iobroker/create-adapter v1.24.2 */ // The adapter-core module gives you access to the core ioBroker functions // you need to create an adapter const utils = require('@iobroker/adapter-core'); // Load your modules here, e.g.: const NodeSSH = require('node-ssh').NodeSSH; const csvToJson = require('csvtojson'); const words = require('./admin/words.js'); const ping = require('ping'); const { exception } = require('console'); let language = 'en'; let _ = null; var requestInterval; var requestIntervalUserCommand; this.isAdapterStart = false; class LinuxControl extends utils.Adapter { /** * @param {Partial<utils.AdapterOptions>} [options={}] */ constructor(options) { super({ ...options, name: 'linux-control', }); this.on('ready', this.onReady.bind(this)); this.on('objectChange', this.onObjectChange.bind(this)); this.on('stateChange', this.onStateChange.bind(this)); // this.on('message', this.onMessage.bind(this)); this.on('unload', this.onUnload.bind(this)); } /** * Is called when databases are connected and adapter received configuration. */ async onReady() { try { // Initialize your adapter here await this.prepareTranslation(); await this.setSelectableHosts() this.isAdapterStart = true; await Promise.allSettled(this.config.hosts.map((host) => this.refreshHost(host))); this.isAdapterStart = false; } catch (err) { this.errorHandling(err, '[onReady]'); } } /** * @param {object} host */ async refreshHost(host) { if (host.enabled) { this.log.info(`getting data from ${host.name} (${host.ip}:${host.port}${this.isAdapterStart ? ', Adapter start' : ''})`); await this.createObjectButton(`${host.name}.refresh`, _('refreshHost')); this.subscribeStates(`${host.name}.refresh`); let connection = await this.getConnection(host); await this.getInfos(connection, host); if (connection) { await this.createControls(host); await this.distributionInfo(connection, host); await this.updateInfos(connection, host); await this.servicesInfo(connection, host); await this.needrestart(connection, host); await this.folderSizes(connection, host); await this.userCommand(connection, host); connection.dispose(); if (await this.getObjectAsync(`${host.name}.info.lastRefresh`)) { await this.setStateAsync(`${host.name}.info.lastRefresh`, new Date().getTime(), true); } this.log.info(`successful received data from ${host.name} (${host.ip}:${host.port})`); } if (this.isAdapterStart) { let objList = await this.getForeignObjectsAsync(`${this.namespace}.${host.name}.*`); for (const id in objList) { let obj = objList[id]; if (obj && obj.common && obj.common.role === 'button' && obj.common.type === 'boolean') { this.subscribeStates(id); this.debugHandling(`[refreshHost] ${host.name} (${host.ip}:${host.port}): button '${id}' subscribed`); } } } // interval using timeout if (requestInterval) requestInterval = null; if (host.interval && host.interval > 0) { requestInterval = setTimeout(() => { this.refreshHost(host); }, host.interval * 60000); } else { this.log.info(`polling interval is deactivated for ${host.name} (${host.ip}:${host.port})`); } } else { this.debugHandling(`getting data from ${host.name} (${host.ip}:${host.port}) -> not enabled!`); } } /** * @param {NodeSSH | undefined} connection * @param {object} host */ async getInfos(connection, host) { let logPrefix = `[getInfos] ${host.name} (${host.ip}:${host.port}):`; const objects = require('./admin/lib/info.json'); if (this.config.whitelist && this.config.whitelist["info"] && this.config.whitelist["info"].length > 0 && !this.config.blacklistDatapoints[host.name].includes('info.all')) { for (const propObj of objects) { if (this.config.whitelist["info"].includes(propObj.id) && !this.config.blacklistDatapoints[host.name].includes(`info.${propObj.id}`)) { let id = `${host.name.replace(' ', '_')}.info.${propObj.id}`; if (propObj.id === "is_online") { if (connection) { await this.createObjectBoolean(id, propObj.name); await this.setStateAsync(id, true, true); } else { await this.createObjectBoolean(id, propObj.name); await this.setStateAsync(id, false, true); } } else if (propObj.id === "ip") { await this.createObjectString(id, propObj.name); await this.setStateAsync(id, host.ip, true); } else { if (propObj.type === 'number') { await this.createObjectNumber(id, propObj.name); } } } else { await this.delMyObject(`${host.name.replace(' ', '_')}.info.${propObj.id}`, logPrefix); } } } else { if (this.isAdapterStart) { this.debugHandling(`${logPrefix} no datapoints selected -> removing existing datapoints`); for (const propObj of objects) { await this.delMyObject(`${host.name.replace(' ', '_')}.info.${propObj.id}`); } } } } //#region Command Functions /** * @param {NodeSSH | undefined} connection * @param {object} host */ async userCommand(connection, host) { let logPrefix = `[userCommand] ${host.name} (${host.ip}:${host.port}):`; try { /** @type {any[]} */ let commandsList = this.config.commands; if (commandsList.length > 0) { let commands = commandsList.filter(x => { return x.host === host.name; }); for (const cmd of commands) { if (cmd.enabled) { try { if ((!cmd.interval || cmd.interval === 0) && cmd.type !== 'button') { await this.userCommandExecute(connection, host, cmd); } else { if (this.isAdapterStart) { // adapter first run -> execute userCommands with diffrent polling interval if (cmd.type !== 'button') { this.debugHandling(`${logPrefix} datapoint-id: ${cmd.name}, description: ${cmd.description}: first start of adapter -> run also command with diffrent polling interval configured`); } else { this.debugHandling(`${logPrefix} datapoint-id: ${cmd.name}, description: ${cmd.description}: create button`); } await this.userCommandExecute(connection, host, cmd); } else { if (cmd.type !== 'button') { this.debugHandling(`${logPrefix} datapoint-id: ${cmd.name}, description: ${cmd.description}: diffrent polling interval configured`); } } } } catch (err) { this.log.error(`${logPrefix} datapoint-id: ${cmd.name}, description: ${cmd.description}`); // No Sentry error report for user commands this.errorHandling(err, logPrefix); } } else { this.debugHandling(`${logPrefix} datapoint-id: '${cmd.name}', description: '${cmd.description}' -> is not enabled!`); } } } } catch (err) { this.errorHandling(err, logPrefix); } } /** * @param {NodeSSH | undefined} connection * @param {object} host * @param {object} cmd */ async userCommandExecute(connection, host, cmd) { let logPrefix = `[userCommandExecute] ${host.name} (${host.ip}:${host.port}):`; let establishedNewConnection = false; try { if (connection === undefined && cmd.interval && cmd.interval > 0 && cmd.type !== 'button') { establishedNewConnection = true; this.debugHandling(`[userCommandExecute] ${host.name} (${host.ip}:${host.port}, id: ${cmd.name}, description: ${cmd.description}): diffrent polling interval -> create connection`); connection = await this.getConnection(host); } if (connection) { let id = `${host.name.replace(' ', '_')}.${cmd.name}`; if (cmd.type !== 'button') { let response = await this.sendCommand(connection, host, `${cmd.command}`, `[userCommandExecute] ${host.name} (${host.ip}:${host.port}, id: ${cmd.name}, description: ${cmd.description}):`); if (response) { if (cmd.type === 'string') { await this.createObjectString(id, cmd.description); await this.setStateAsync(id, response, true); } else if (cmd.type === 'number') { await this.createObjectNumber(id, cmd.description, cmd.unit); await this.setStateAsync(id, parseFloat(response), true); } else if (cmd.type === 'boolean') { await this.createObjectBoolean(id, cmd.description); await this.setStateAsync(id, (response === 'true' || parseInt(response) === 1) ? true : false, true); } else if (cmd.type === 'array') { await this.createObjectArray(id, cmd.description); await this.setStateAsync(id, JSON.parse(response), true); } } else { if (await this.getObjectAsync(id)) { if (cmd.type === 'string') { await this.setStateAsync(id, "", true); } else if (cmd.type === 'number') { await this.setStateAsync(id, 0, true); } else if (cmd.type === 'boolean') { await this.setStateAsync(id, false, true); } else if (cmd.type === 'array') { await this.setStateAsync(id, null, true); } } else { if (cmd.type === 'string') { await this.createObjectString(id, cmd.description); await this.setStateAsync(id, "", true); } else if (cmd.type === 'number') { await this.createObjectNumber(id, cmd.description, cmd.unit); await this.setStateAsync(id, 0, true); } else if (cmd.type === 'boolean') { await this.createObjectBoolean(id, cmd.description); await this.setStateAsync(id, false, true); } else if (cmd.type === 'array') { await this.createObjectArray(id, cmd.description); await this.setStateAsync(id, null, true); } } } } else { await this.createObjectButton(id, cmd.description); this.subscribeStates(id); } if (establishedNewConnection) { this.debugHandling(`[userCommandExecute] ${host.name} (${host.ip}:${host.port}, id: ${cmd.name}, description: ${cmd.description}): diffrent polling interval -> close connection`); connection.dispose(); } } if (cmd.interval && cmd.interval > 0 && cmd.type !== 'button') { // interval using timeout if (requestIntervalUserCommand) requestIntervalUserCommand = null; if (cmd.interval && cmd.interval > 0) { requestIntervalUserCommand = setTimeout(() => { this.userCommandExecute(undefined, host, cmd); }, cmd.interval * 1000); } } } catch (err) { this.log.error(`${logPrefix} datapoint-id: ${cmd.name}, description: ${cmd.description}`); // No Sentry error report for user commands this.errorHandling(err, logPrefix); } } /** * @param {NodeSSH | undefined} connection * @param {object} host */ async folderSizes(connection, host) { let logPrefix = `[folderSizes] ${host.name} (${host.ip}:${host.port}):`; try { if (connection) { // @ts-ignore let folderList = this.config.folders; if (folderList.length > 0) { let hostFolders = folderList.filter(x => { return x.host === host.name; }) for (const folder of hostFolders) { if (folder.enabled) { let unitFaktor = "/1024" if (folder.unit === 'GB') { unitFaktor = "/1024/1024" } else if (folder.unit === 'TB') { unitFaktor = "/1024/1024/1024" } let response = undefined; if (folder.fileNamePattern) { response = await this.sendCommand(connection, host, `${host.useSudo ? 'sudo -S ' : ''}find ${folder.path} -name "${folder.fileNamePattern}" -exec du -c {} + | tail -1 | awk '{printf $1${unitFaktor}}'`, logPrefix, undefined, false); } else { response = await this.sendCommand(connection, host, `${host.useSudo ? 'sudo -S ' : ''}du -sk ${folder.path} | awk '{ print $1 ${unitFaktor} }'`, logPrefix, undefined, false); } if (response) { let id = `${host.name.replace(' ', '_')}.folders.${folder.name}.size`; await this.createObjectNumber(id, _('folderSize'), folder.unit); let result = parseFloat(response).toFixed(parseInt(folder.digits) || 0); this.debugHandling(`${logPrefix} ${id}: ${parseFloat(result)} ${folder.unit}`); await this.setStateAsync(id, parseFloat(result), true); if (folder.countFiles) { response = await this.sendCommand(connection, host, `${host.useSudo ? 'sudo -S ' : ''}find ${folder.path} -name "${folder.fileNamePattern ? folder.fileNamePattern : '*'}" | wc -l`, logPrefix, undefined, false); if (response) { let id = `${host.name.replace(' ', '_')}.folders.${folder.name}.files`; await this.createObjectNumber(id, _('countFilesInFolder'), _('files')); this.debugHandling(`${logPrefix} ${id}: ${parseInt(response)} ${_('files')}`); await this.setStateAsync(id, parseInt(response), true); } } if (folder.lastChange) { response = await this.sendCommand(connection, host, `tmp=$(${host.useSudo ? 'sudo -S ' : ''}find ${folder.path} -name "${folder.fileNamePattern ? folder.fileNamePattern : '*'}" -type f -exec stat -c "%Y %n" -- {} \\; | sort -nr | head -n1 | awk '{print $2}') && date +%s -r $tmp`, logPrefix, undefined, false); if (response) { let id = `${host.name.replace(' ', '_')}.folders.${folder.name}.lastChange`; let timestamp = parseInt(response) * 1000; await this.createObjectNumber(id, _('last change')); this.debugHandling(`${logPrefix} ${id}: ${timestamp} -> ${this.formatDate(timestamp, 'DD.MM.YYYY hh:mm')}`); await this.setStateAsync(id, timestamp, true); } } } } else { this.debugHandling(`${logPrefix} getting size for '${host.name.replace(' ', '_')}.folders.${folder.name}' -> is not enabled!`); } } } } } catch (err) { this.errorHandling(err, logPrefix); } } /** * @param {NodeSSH | undefined} connection * @param {object} host */ async needrestart(connection, host) { let logPrefix = `[needrestart] ${host.name} (${host.ip}:${host.port}):`; const objects = require('./admin/lib/needrestart.json'); try { // @ts-ignore if (this.config.whitelist && this.config.whitelist["needrestart"] && this.config.whitelist["needrestart"].length > 0 && !this.config.blacklistDatapoints[host.name].includes('needrestart.all')) { if (connection) { if (await this.cmdPackageExist(connection, host, 'needrestart')) { let response = await this.sendCommand(connection, host, `(tmp=$(${host.useSudo ? 'sudo -S ' : ''}/usr/sbin/needrestart -p -l | head -1) && echo "$tmp" | awk '{print $1}' && echo ", $tmp" | sed 's/.*Services=\\([0-9]*\\);.*/\\1/' && echo "$tmp" | sed 's/.*Containers=\\([0-9]*\\);.*/\\1/' && echo "$tmp" | sed 's/.*Sessions=\\([0-9]*\\);.*/\\1/') | awk '{printf "%s" (NR%4==0?RS:FS),$1}'`, logPrefix, undefined, false); if (response) { /** @type {object} */ let parsed = await csvToJson({ noheader: true, headers: ['needrestart', 'services', 'containers', 'sessions'], delimiter: [" "] }).fromString(response); this.debugHandling(`${logPrefix} csvToJson result: ${JSON.stringify(parsed)}`); for (const obj of objects) { // @ts-ignore if (this.config.whitelist["needrestart"].includes(obj.id) && !this.config.blacklistDatapoints[host.name].includes(`needrestart.${obj.id}`)) { if (parsed && parsed[0] && parsed[0][obj.id]) { let id = `${host.name.replace(' ', '_')}.needrestart.${obj.id}`; if (obj.id === 'needrestart') { this.debugHandling(`${logPrefix} ${id}: ${parsed[0][obj.id] === 'OK' ? false : true}`); await this.createObjectBoolean(id, _(obj.name)); await this.setStateAsync(id, parsed[0][obj.id] === 'OK' ? false : true, true); } if (obj.type === 'number') { this.debugHandling(`${logPrefix} ${id}: ${parseInt(parsed[0][obj.id])}`); await this.createObjectNumber(id, _(obj.name)); await this.setStateAsync(id, parseInt(parsed[0][obj.id]), true); } } } else { await this.delMyObject(`${host.name.replace(' ', '_')}.needrestart.${obj.id}`, logPrefix); } } } } else { this.log.warn(`${logPrefix} package 'needrestart' not installed. You must install 'needrestart' to use this functions or deactivate the datapoints!`); let needRestartStates = await this.getStatesAsync(`${this.namespace}.${host.name.replace(' ', '_')}.needrestart.*`); for (const id of Object.keys(needRestartStates)) { await this.delMyObject(id); } } } } else { if (this.isAdapterStart) { this.debugHandling(`${logPrefix} no datapoints selected -> removing existing datapoints`); let needRestartStates = await this.getStatesAsync(`${this.namespace}.${host.name.replace(' ', '_')}.needrestart.*`); for (const id of Object.keys(needRestartStates)) { await this.delMyObject(id); } } } } catch (err) { this.errorHandling(err, logPrefix); } } /** * @param {NodeSSH | undefined} connection * @param {object} host * @param {string | undefined} serviceName */ async servicesInfo(connection, host, serviceName = undefined) { let logPrefix = `[servicesInfo] ${host.name} (${host.ip}:${host.port}):`; const objects = require('./admin/lib/services.json'); try { // @ts-ignore if (this.config.whitelist && this.config.whitelist["services"] && this.config.whitelist["services"].length > 0 && !this.config.blacklistDatapoints[host.name].includes('services.all')) { if (connection) { let response = await this.sendCommand(connection, host, `systemctl list-units --type service --all --no-legend | awk '{out=""; for(i=5;i<=NF;i++){out=out" "$i}; print $1","$2","$3","$4","out}'${serviceName ? ` | grep ${serviceName}` : ''}`, logPrefix, undefined, false); if (response) { response = response.replace(/\t/g, ',') /** @type {object} */ let parsed = await csvToJson({ noheader: true, headers: ['id', 'load', 'active', 'running', 'description'], delimiter: [","] }).fromString(response); this.debugHandling(`${logPrefix} csvToJson result: ${JSON.stringify(parsed)}`); // TODO: whitelist für services implementieren for (const result of parsed) { let idPrefix = `${host.name.replace(' ', '_')}.services.${result.id.replace('.service', '')}`; for (const obj of objects) { let id = `${idPrefix}.${obj.id}`; // @ts-ignore if (this.config.whitelist["services"].includes(obj.id) && !this.config.blacklistDatapoints[host.name].includes(`services.${obj.id}`) && (this.config.serviceWhiteList[host.name].includes(result.id.replace('.service', '')) || this.config.serviceWhiteList[host.name].length === 0)) { if (obj.type === 'string') { await this.createObjectString(id, obj.name); await this.setStateAsync(id, result[obj.id], true); } else if (obj.type === 'boolean') { await this.createObjectBoolean(id, obj.name); await this.setStateAsync(id, result[obj.id] === 'running' ? true : false, true); } else if (obj.type === 'button') { await this.createObjectButton(id, obj.name); this.subscribeStates(id); } } else { await this.delMyObject(id, logPrefix); } } } } } } else { if (this.isAdapterStart) { this.debugHandling(`${logPrefix} no datapoints selected -> removing existing datapoints`); let servicesStates = await this.getStatesAsync(`${this.namespace}.${host.name.replace(' ', '_')}.services.*`); for (const id of Object.keys(servicesStates)) { await this.delMyObject(id); } } } } catch (err) { this.errorHandling(err, logPrefix); } } /** * @param {NodeSSH | undefined} connection * @param {object} host */ async distributionInfo(connection, host) { let logPrefix = `[distributionInfo] ${host.name} (${host.ip}:${host.port}):`; const objects = require('./admin/lib/distribution.json'); try { // @ts-ignore if (this.config.whitelist && this.config.whitelist["distribution"] && this.config.whitelist["distribution"].length > 0 && !this.config.blacklistDatapoints[host.name].includes('distribution.all')) { if (connection) { let response = await this.sendCommand(connection, host, "cat /etc/os-release", logPrefix, undefined, false); if (response) { /** @type {object} */ let parsed = await csvToJson({ noheader: true, headers: ['prop', 'val'], delimiter: ["="] }).fromString(response); this.debugHandling(`${logPrefix} csvToJson result: ${JSON.stringify(parsed)}`); for (const propObj of objects) { let obj = parsed.find(x => x.prop === propObj.propName); // @ts-ignore if (this.config.whitelist["distribution"].includes(propObj.id) && !this.config.blacklistDatapoints[host.name].includes(`distribution.${propObj.id}`)) { if (obj && obj.prop && obj.val) { let id = `${host.name.replace(' ', '_')}.distribution.${propObj.id}`; await this.createObjectString(id, propObj.name); await this.setStateAsync(id, obj.val, true); } else { this.log.warn(`${logPrefix} property '${propObj.propName}' not exist in result!`); } } else { await this.delMyObject(`${host.name.replace(' ', '_')}.distribution.${propObj.id}`, logPrefix); } } } } } else { if (this.isAdapterStart) { this.debugHandling(`${logPrefix} no datapoints selected -> removing existing datapoints`); for (const propObj of objects) { await this.delMyObject(`${host.name.replace(' ', '_')}.distribution.${propObj.id}`); } } } } catch (err) { this.errorHandling(err, logPrefix); } } /** * @param {NodeSSH | undefined} connection * @param {object} host */ async updateInfos(connection, host) { let logPrefix = `[updateInfos] ${host.name} (${host.ip}:${host.port}):`; try { if (connection) { await this.cmdAptUpdate(connection, host); } } catch (err) { this.errorHandling(err, logPrefix); return undefined; } } /** * @param {NodeSSH | undefined} connection * @param {object} host * @param {Boolean} restart * @param {string | undefined} responseId */ async cmdShutdown(connection, host, restart = false, responseId = undefined) { let logPrefix = `[cmdShutdown] ${host.name} (${host.ip}:${host.port}):`; try { if (connection) { let cmd = `${host.useSudo ? 'sudo -S ' : ''}shutdown 0` if (restart) { cmd = `${host.useSudo ? 'sudo -S ' : ''}shutdown -r 0` } await this.sendCommand(connection, host, cmd, logPrefix, responseId); } } catch (err) { this.errorHandling(err, logPrefix); } } /** * @param {NodeSSH | undefined} connection * @param {object} host * @param {string} responseId */ async cmdAptUpgrade(connection, host, responseId) { let logPrefix = `[cmdAptUpgrade] ${host.name} (${host.ip}:${host.port}):`; try { if (connection) { let response = await this.sendCommand(connection, host, `${host.useSudo ? 'sudo -S ' : ''}DEBIAN_FRONTEND=noninteractive apt-get upgrade -y`, logPrefix, responseId); if (response) { await this.setStateAsync(responseId, response, true); await this.cmdAptUpdate(connection, host); } } } catch (err) { this.errorHandling(err, logPrefix); } } /** * @param {NodeSSH | undefined} connection * @param {object} host * @param {string} packageName * @returns {Promise<boolean>} */ async cmdPackageExist(connection, host, packageName) { let logPrefix = `[cmdPackageExist] ${host.name} (${host.ip}:${host.port}):`; try { if (connection) { let response = await this.sendCommand(connection, host, `dpkg-query --list | grep -i ${packageName}`, logPrefix); if (response) { return true; } else { return false; } } } catch (err) { this.errorHandling(err, logPrefix); } return false; } /** * @param {NodeSSH | undefined} connection * @param {object} host * @param {string | undefined} responseId */ async cmdAptUpdate(connection, host, responseId = undefined) { let logPrefix = `[cmdAptUpdate] ${host.name} (${host.ip}:${host.port}):`; const objects = require('./admin/lib/updates.json'); try { // @ts-ignore if (this.config.whitelist && this.config.whitelist["updates"] && this.config.whitelist["updates"].length > 0 && !this.config.blacklistDatapoints[host.name].includes('updates.all')) { if (connection) { // run apt update let response = await this.sendCommand(connection, host, `${host.useSudo ? 'sudo -S ' : ''}apt-get update`, logPrefix, responseId, false); if (response) { response = await this.sendCommand(connection, host, `apt-get --just-print upgrade 2>&1 | perl -ne 'if (/Inst\\s([\\w,\\-,\\d,\\.,~,:,\\+]+)\\s\\[([\\w,\\-,\\d,\\.,~,:,\\+]+)\\]\\s\\(([\\w,\\-,\\d,\\.,~,:,\\+]+)\\)? /i) {print \"$1,$2,$3\\n\"}' \| column -s \" \" -t`, logPrefix, undefined, false); let parsed = []; let newPackages = 0; if (response) { parsed = await csvToJson({ noheader: true, headers: ['name', 'installedVersion', 'availableVersion'], delimiter: [","] // @ts-ignore }).fromString(response); newPackages = parsed.length; } // Number of new Packages let id = `${host.name.replace(' ', '_')}.updates.newPackages`; // @ts-ignore if (this.config.whitelist["updates"].includes("newPackages") && !this.config.blacklistDatapoints[host.name].includes(`updates.newPackages`)) { this.debugHandling(`${logPrefix} ${id}: ${newPackages}`); await this.createObjectNumber(id, `newPackages`, `packages`); await this.setStateAsync(id, newPackages, true); } else { await this.delMyObject(id, logPrefix); } // is upgradable id = `${host.name.replace(' ', '_')}.updates.upgradable`; // @ts-ignore if (this.config.whitelist["updates"].includes("upgradable") && !this.config.blacklistDatapoints[host.name].includes(`updates.upgradable`)) { this.debugHandling(`${logPrefix} ${id}: ${newPackages > 0 ? true : false}`); await this.createObjectBoolean(id, `upgradable`); await this.setStateAsync(id, newPackages > 0 ? true : false, true); } else { await this.delMyObject(id, logPrefix); } // list of new packages id = `${host.name.replace(' ', '_')}.updates.newPackagesList`; // @ts-ignore if (this.config.whitelist["updates"].includes("newPackagesList") && !this.config.blacklistDatapoints[host.name].includes(`updates.newPackagesList`)) { if (newPackages > 0) { this.debugHandling(`${logPrefix} ${id}: ${JSON.stringify(parsed)}`); await this.createObjectString(id, `newPackagesList`); await this.setStateAsync(id, JSON.stringify(parsed), true); } else { await this.createObjectString(id, `newPackagesList`); await this.setStateAsync(id, '', true); } } else { await this.delMyObject(id, logPrefix); } } // last update let id = `${host.name.replace(' ', '_')}.updates.lastUpdate`; // @ts-ignore if (this.config.whitelist["updates"].includes("lastUpdate") && !this.config.blacklistDatapoints[host.name].includes(`updates.lastUpdate`)) { response = await this.sendCommand(connection, host, "dpkg-query -f '${db-fsys:Last-Modified}\n' -W | sort -nr | head -1", logPrefix, responseId, false); if (response) { let timestamp = parseInt(response) * 1000; this.debugHandling(`${logPrefix} ${id}: ${timestamp} -> ${this.formatDate(timestamp, 'DD.MM.YYYY hh:mm')}`); await this.createObjectNumber(id, `lastUpdate`); await this.setStateAsync(id, timestamp, true); } else { // Fallback method response = await this.sendCommand(connection, host, "grep installed /var/log/dpkg.log | tail -1 | cut -c1-19", logPrefix, responseId, false); if (response) { let timestamp = Date.parse(response); this.debugHandling(`${logPrefix} ${id}: Fallback method: ${timestamp} -> ${this.formatDate(timestamp, 'DD.MM.YYYY hh:mm')}`); await this.createObjectNumber(id, `lastUpdate`); await this.setStateAsync(id, timestamp, true); } } } else { await this.delMyObject(id, logPrefix); } } } else { if (this.isAdapterStart) { this.debugHandling(`${logPrefix} no datapoints selected -> removing existing datapoints`); for (const propObj of objects) { await this.delMyObject(`${host.name.replace(' ', '_')}.updates.${propObj.id}`); } } } } catch (err) { this.errorHandling(err, logPrefix); } } /** * @param {NodeSSH | undefined} connection * @param {string} cmd * @param {string} logPrefix * @param {string | undefined} responseId * @param {boolean | undefined} responseErrorSendToSentry * @returns {Promise<string | undefined>} */ async sendCommand(connection, host, cmd, logPrefix, responseId = undefined, responseErrorSendToSentry = false) { try { if (connection) { this.debugHandling(`${logPrefix} send command: '${cmd}'`); let response = undefined; if (host.useSudo && cmd.includes('sudo -S ')) { // using sudo let password = await this.getPassword(host); response = await connection.execCommand(cmd, { execOptions: { pty: true }, stdin: `${password}\n` }); if (!response.stderr) { response.stdout = response.stdout.replace(password, "") } } else if (cmd.includes('sudo ') && !cmd.includes('sudo -S ')) { this.errorHandling(new ResponseError(`${logPrefix} you must use 'sudo -S' instead of 'sudo' only!`), logPrefix, responseErrorSendToSentry); return undefined; } else { response = await connection.execCommand(cmd); } if (!response.stderr) { this.debugHandling(`${logPrefix} response stdout: ${response.stdout}`); await this.reportResponse(responseId, 'successful'); // remove system stdout if (!response.stderr) { response.stdout = response.stdout .replace(/^.*\[sudo\] password for.*$/mg, "") .replace(/^.*\[sudo\] Passwort für.*$/mg, "") .replace(/^.*sudo\: setrlimit\(RLIMIT_CORE\)\: Operation not permitted.*$/mg, "") .replace(/^\s*$(?:\r\n?|\n)/gm, ""); // remove all empty lines // .replace(`[sudo] password for ${host.user}: \r`, "") // .replace('sudo: setrlimit(RLIMIT_CORE): Operation not permitted', "").replace("\n\n", "") // .replace('[sudo] Passwort für pi: \r', "") // .replace('[sudo] Passwort für pi: \n', ""); } // catch errors that have no .stderr let errorResponse = ['is not in the sudoers file', 'nicht in der sudoers-Datei'] if (errorResponse.some(word => response.stdout.includes(word))) { this.errorHandling(new ResponseError(`${logPrefix} ${response.stdout}`), logPrefix, responseErrorSendToSentry); return undefined; } return response.stdout; } else { if (response.stderr.includes('Shutdown scheduled for')) { if (cmd.includes('-r')) { this.log.info(`${logPrefix} restart`); } else { this.log.info(`${logPrefix} shutdown`); } await this.reportResponse(responseId, 'successful'); } else { this.errorHandling(new ResponseError(`${logPrefix} ${response.stderr}`), logPrefix, responseErrorSendToSentry); await this.reportResponse(responseId, response.stderr); } return undefined; } } } catch (err) { this.errorHandling(err, logPrefix); await this.reportResponse(responseId, err.message); return undefined; } } //#endregion //#region Functions /** * @param {string | undefined} responseId * @param {string } msg */ async reportResponse(responseId, msg) { if (responseId) { await this.setStateAsync(responseId, msg, true); } } /** * @param {object} host * @returns {Promise<NodeSSH | undefined>} */ async getConnection(host) { try { let pingResult = await ping.promise.probe(host.ip, { timeout: parseInt(host.timeout) || 5 }); if (pingResult.alive) { let password = await this.getPassword(host); let ssh = new NodeSSH(); let options = { host: host.ip, port: host.port, username: host.user, password: password, readyTimeout: parseInt(host.timeout) * 1000 || 5000 } if (host.rsakey && host.rsakey.length > 0) { this.debugHandling(`[getConnection] Host '${host.name}' (${host.ip}:${host.port}): using rsa key for authentification`); options.passphrase = password; options.privateKey = host.rsakey; } else if (host.useSudo) { this.debugHandling(`[getConnection] Host '${host.name}' (${host.ip}:${host.port}): using sudo for authentification`); } return await ssh.connect(options); } else { this.log.info(`[getConnection] Host '${host.name}' (${host.ip}:${host.port}) seems not to be online`); this.debugHandling(`[getConnection] Host '${host.name}' (${host.ip}:${host.port}) ping result: ${JSON.stringify(pingResult)}`) return undefined; } } catch (err) { this.log.error(`[getConnection] Could not establish a connection to '${host.name}' (${host.ip}:${host.port})!`); this.errorHandling(err, '[getConnection]', false); return undefined; } } async getPassword(host) { let obj = await this.getForeignObjectAsync('system.config'); if (obj && obj.native && obj.native.secret) { //noinspection JSUnresolvedVariable return this.decryptPassword(obj.native.secret, host.password); } else { //noinspection JSUnresolvedVariable return this.decryptPassword("Zgfr56gFe87jJOM", host.password); } } async setSelectableHosts() { let hostObj = await this.getObjectAsync(`command.host`); if (hostObj && hostObj.common) { let hostStates = '' // @ts-ignore for (const host of this.config.hosts) { if (host) { // @ts-ignore hostStates = hostStates + `${host.name}:${host.name};` } } hostObj.common.states = hostStates; await this.setObjectAsync(`command.host`, hostObj); if (hostStates) { this.subscribeStates(`command.execute`) } } } /** * Function to decrypt passwords * @param {string | { charCodeAt: (arg0: number) => number; }[]} key * @param {string} value */ decryptPassword(key, value) { let result = ""; for (let i = 0; i < value.length; ++i) { result += String.fromCharCode(key[i % key.length].charCodeAt(0) ^ value.charCodeAt(i)); } return result; } async prepareTranslation() { // language for Tranlation var sysConfig = await this.getForeignObjectAsync('system.config'); if (sysConfig && sysConfig.common && sysConfig.common['language']) { language = sysConfig.common['language'] } // language Function /** * @param {string | number} string */ _ = function (string) { if (words[string]) { return words[string][language] } else { return string; } } } /** * @param {string} id */ getHostById(id) { // @ts-ignore return this.config.hosts.find(x => x.name.replace(/\s/g, "_") === id.replace(/\s/g, "_")); } //#endregion /** * Is called when adapter shuts down - callback has to be called under any circumstances! * @param {() => void} callback */ onUnload(callback) { try { clearTimeout(requestInterval); clearTimeout(requestIntervalUserCommand); this.log.info('cleaned everything up...'); callback(); } catch (e) { callback(); } } /** * Is called if a subscribed object changes * @param {string} id * @param {ioBroker.Object | null | undefined} obj */ onObjectChange(id, obj) { if (obj) { // The object was changed this.log.info(`object ${id} changed: ${JSON.stringify(obj)}`); } else { // The object was deleted this.log.info(`object ${id} deleted`); } } /** * Is called if a subscribed state changes * @param {string} id * @param {ioBroker.State | null | undefined} state */ async onStateChange(id, state) { if (state) { this.log.info(`state ${id} changed: ${state.val} (ack = ${state.ack})`); if (id.includes('.control.')) { let hostIdSplitted = id.replace(`${this.namespace}.`, '').split('.'); /** @type {object} */ let host = this.getHostById(hostIdSplitted[0]); if (host) { let responseId = id.replace(hostIdSplitted[hostIdSplitted.length - 1], 'response'); if (hostIdSplitted[hostIdSplitted.length - 1] === 'aptUpdate') { let connection = await this.getConnection(host); if (connection) { await this.reportResponse(responseId, ''); await this.cmdAptUpdate(connection, host, responseId); connection.dispose(); } } else if (hostIdSplitted[hostIdSplitted.length - 1] === 'aptUpgrade') { let connection = await this.getConnection(host); if (connection) { await this.reportResponse(responseId, ''); await this.cmdAptUpgrade(connection, host, responseId); connection.dispose(); } } else if (hostIdSplitted[hostIdSplitted.length - 1] === 'shutdown') { let connection = await this.getConnection(host); if (connection) { await this.reportResponse(responseId, ''); await this.cmdShutdown(connection, host, false, responseId); connection.dispose(); } } else if (hostIdSplitted[hostIdSplitted.length - 1] === 'restart') { let connection = await this.getConnection(host); if (connection) { await this.reportResponse(responseId, ''); await this.cmdShutdown(connection, host, true, responseId); connection.dispose(); } } } } else if (id.includes('.services.')) { let hostIdSplitted = id.replace(`${this.namespace}.`, '').split('.'); let serviceName = hostIdSplitted[hostIdSplitted.length - 2] + ".service"; /** @type {object} */ let host = this.getHostById(hostIdSplitted[0]); if (host) { if (hostIdSplitted[hostIdSplitted.length - 1] === 'restart') { let connection = await this.getConnection(host); let logPrefix = `[sendCommand restart] ${host.name} (${host.ip}:${host.port}):`; if (connection) { await this.sendCommand(connection, host, `${host.useSudo ? 'sudo -S ' : ''}systemctl restart ${serviceName}`, logPrefix); await this.servicesInfo(connection, host, serviceName); await this.servicesInfo(connection, host); connection.dispose(); } } else if (hostIdSplitted[hostIdSplitted.length - 1] === 'start') { let connection = await this.getConnection(host); let logPrefix = `[sendCommand start] ${host.name} (${host.ip}:${host.port}):`; if (connection) { await this.sendCommand(connection, host, `${host.useSudo ? 'sudo -S ' : ''}systemctl start ${serviceName}`, logPrefix); await this.servicesInfo(connection, host, serviceName); await this.servicesInfo(connection, host); connection.dispose(); } } else if (hostIdSplitted[hostIdSplitted.length - 1] === 'stop') { let connection = await this.getConnection(host); let logPrefix = `[sendCommand stop] ${host.name} (${host.ip}:${host.port}):`; if (connection) { await this.sendCommand(connection, host, `${host.useSudo ? 'sudo -S ' : ''}systemctl stop ${serviceName}`, logPrefix); await this.servicesInfo(connection, host, serviceName); await this.servicesInfo(connection, host); connection.dispose(); } } } } else if (id.includes('.command.execute')) { let cmd = await this.getStateAsync(`${this.namespace}.command.command`); let hostId = await this.getStateAsync(`${this.namespace}.command.host`); let responseId = `${this.namespace}.command.response`; if (hostId && hostId.val) { /** @type {object} */ let host = this.getHostById(hostId.val.toString()); if (host) { if (cmd && cmd.val) { let connection = await this.getConnection(host); if (connection) { let logPrefix = `[sendCommand] ${host.name} (${host.ip}:${host.port}):`; await this.reportResponse(responseId, ''); let response = await this.sendCommand(connection, host, cmd.val.toString(), logPrefix, responseId); if (response) { await this.reportResponse(responseId, response); } // TODO: wieder rein machen // await this.setStateAsync(`${this.namespace}.command.command`, '', true); connection.dispose(); } } else { this.log.warn(`execute command: no command to execute is defined!`); await this.reportResponse(responseId, 'no command to execute is defined!'); } } } else { this.log.warn(`execute command: no host is selected!`); await this.reportResponse(responseId, 'no host is selected!'); } } else if (id.includes(`.refresh`)) { let hostIdSplitted = id.replace(`${this.namespace}.`, '').split('.'); /** @type {object} */ let host = this.getHostById(hostIdSplitted[0]); if (host) { if (id.includes(`${host.name}.refresh`)) { await this.refreshHost(host); } } } else { // user buttons let hostIdSplitted = id.replace(`${this.namespace}.`, '').split('.'); /** @type {object} */ let host = this.getHostById(hostIdSplitted[0]); if (host) { let cmdId = id.replace(`${this.namespace}.${host.name}.`, ''); // @ts-ignore let commandsList = this.config.commands; if (commandsList.length > 0) { let command = commandsList.filter(x => { return x.host === host.name && x.name === cmdId; }); if (command && command.length === 1) { command = command[0]; let connection = await this.getConnection(host); let logPrefix = `[send userCommand] ${host.name} (${host.ip}:${host.port}) - ${cmdId}:`; if (connection && command && command.command) { await this.sendCommand(connection, host, command.command, logPrefix); connection.dispose(); } } } } } } } //#region Objects Functions /** * @param {string} id */ async delMyObject(id, logPrefix = undefined) { if (this.isAdapterStart) { if (await this.getObjectAsync(id)) { if (id && logPrefix) { this.debugHandling(`${logPrefix} datapoint '${id}' is not selected -> removing existing datapoint`); } await this.delObjectAsync(id); } } } /** * @param {object} host */ async createControls(host) { let logPrefix = `[createControls] ${host.name} (${host.ip}:${host.port}):`; try { let idPrefix = `${host.name.replace(' ', '_')}.control`; const objects = require('./admin/lib/control.json'); // @ts-ignore if (this.config.whitelist && this.config.whitelist["control"] && this.config.whitelist["control"].length > 0 && !this.config.blacklistDatapoints[host.name].includes('control.all')) { for (const obj of objects) { // @ts-ignore if (this.config.whitelist["control"].includes(obj.id) && !this.config.blacklistDatapoints[host.name].includes(`control.${obj.id}`)) { if (obj.type === 'button') { await this.createObjectButton(`${idPrefix}.${obj.id}`, obj.name); this.subscribeStates(`${idPrefix}.${obj.id}`); } else if (obj.type === 'string') { await this.createObjectString(`${idPrefix}.${obj.id}`, obj.name); } } else { await this.delMyObject(`${idPrefix}.${obj.id}`, logPrefix); } } } else { if (this.isAdapterStart) { this.debugHandling(`${logPrefix} no datapoints selected -> removing existing datapoints`); for (const obj of objects) { await this.delMyObject(`${idPrefix}.${obj.id}`); } } } } catch (err) { this.errorHandling(err, logPrefix); } } /** * @param {string} id * @param {string} name */ async createObjectString(id, name) { if (this.isAdapterStart) { let obj = await this.getObjectAsync(id); if (obj) { if (obj.common.name !== _(name)) { obj.common.name = _(name); await this.setObjectAsync(id, obj); } } else { this.log.debug(`[createObjectString] creating datapoint '${id}'`); await this.setObjectNotExistsAsync(id, { type: 'state', common: { name: _(name), desc: _(name), type: 'string', read: true, write: false, role: 'state' }, native: {} }); } } } /** * @param {string} id * @param {string} name * @param {any} unit */ async createObjectNumber(id, name, unit = undefined) { if (this.isAdapterStart) { let obj = await this.getObjectAsync(id); if (obj) { if (obj.common.name !== _(name) || obj.common['unit'] !== _(unit)) { obj.common.name = _(name); obj.common['unit'] = _(unit); await this.setObjectAsync(id, obj); } } else { this.log.debug(`[createObjectNumber] creating datapoint '${id}'`); await this.setObjectNotExistsAsync(id, { type: 'state', common: { name: _(name), type: 'number', unit: unit, read: true, write: false, role: 'value' }, native: {} }); } } } /** * @param {string} id * @param {string} name */ async createObjectBoolean(id, name) { if (this.isAdapterStart) { let obj = await this.getObjectAsync(id); if (obj) { if (obj.common.name !== _(name)) { obj.common.name = _(name); await this.setObjectAsync(id, obj); } } else { this.log.debug(`[createObjectBoolean] creating datapoint '${id}'`); await this.setObjectNotExistsAsync(id, { type: 'state', common: { name: _(name), type: 'boolean', read: true, write: false, role: 'indicator', def: false }, native: {} }); } } } /** * @param {string} id * @param {string} name */ async createObjectArray(id, name) { if (this.isAdapterStart) { let obj = await this.getObjectAsync(id); if (obj) { if (obj.common.name !== _(name)) { obj.common.name = _(name); await this.setObjectAsync(id, obj); } } else { this.log.debug(`[createObjectArray] creating datapoint '${id}'`); await this.setObjectNotExistsAsync(id, { type: 'state', common: { name: _(name), desc: _(name), type: 'array', read: true, write: false, role: 'list' }, native: {} }); } } } /** * @param {string} id * @param {string} name */ async createObjectButton(id, name) { if (this.isAdapterStart) { let obj = await this.getObjectAsync(id); if (obj) { if (obj.common.name !== _(name)) { obj.common.name = _(name); await this.setObjectAsync(id, obj); } } else { this.log.debug(`[createObjectButton] creating datapoint '${id}'`); await this.setObjectNotExistsAsync(id, { type: 'state', common: { name: _(name), role: 'button', type: 'boolean', read: false, write: true }, native: {} }); } } } /** * @param {string} id * @param {string} name */ async createMyChannel(id, name) { if (this.isAdapterStart) { let obj = await this.getObjectAsync(id); if (obj) { if (obj.common.name !== _(name)) { obj.common.name = _(name); await this.setObjectAsync(id, obj); } } else { this.log.debug(`[createMyChannel] creating channel '${id}'`); await this.setObjectNotExistsAsync(id, { type: 'channel', common: { name: _(name), }, native: {} }); } } } /** * @param {Error} err * @param {string} logPrefix * @param {boolean} sendToSentry */ errorHandling(err, logPrefix, sendToSentry = true) { if (err.name === 'ResponseError') { if (err.message.includes('Permission denied') || err.message.includes('Keine Berechtigung')) { this.log.error(`Permisson denied. Check the permission rights of your user on your linux devices! Perhaps you need to use 'sudo'?`); } this.log.error(`${l