diff --git a/build/src/src/calls/createGetUserActionLogs.js b/build/src/src/calls/createGetUserActionLogs.js new file mode 100644 index 000000000..2e7be37a3 --- /dev/null +++ b/build/src/src/calls/createGetUserActionLogs.js @@ -0,0 +1,47 @@ +const fs = require('fs'); +const {promisify} = require('util'); +const paramsDefault = require('params'); + +// CALL DOCUMENTATION: +// > kwargs: { +// id, +// isCore, +// options +// } +// > result: { +// id, +// logs: (string) +// } + +function createGetUserActionLogs({ + params = paramsDefault, +}) { + const getUserActionLogs = async ({ + options, + }) => { + const readFileAsync = promisify(fs.readFile); + const {userActionLogsFilename} = params; + + if (!fs.existsSync(userActionLogsFilename)) { + return { + message: 'UserActionLogs are still empty, returning black', + result: '', + }; + } + const userActionLogs = await readFileAsync( + userActionLogsFilename, + {encoding: 'utf8'} + ); + + return { + message: 'Got userActionLogs', + result: userActionLogs, + }; + }; + + // Expose main method + return getUserActionLogs; +} + + +module.exports = createGetUserActionLogs; diff --git a/build/src/src/calls/createInstallPackage.js b/build/src/src/calls/createInstallPackage.js index 9bde439f0..13a65cd76 100644 --- a/build/src/src/calls/createInstallPackage.js +++ b/build/src/src/calls/createInstallPackage.js @@ -33,7 +33,8 @@ function createInstallPackage({ return { message: 'Installed ' + packageReq.name + ' version: ' + packageReq.ver, - log: true, + logMessage: true, + userAction: true, }; }; diff --git a/build/src/src/calls/createListPackages.js b/build/src/src/calls/createListPackages.js index ec342cf69..410a5e981 100644 --- a/build/src/src/calls/createListPackages.js +++ b/build/src/src/calls/createListPackages.js @@ -55,7 +55,6 @@ function createListPackages({ return { message: 'Listing ' + dnpList.length + ' packages', result: dnpList, - logMessage: true, }; }; diff --git a/build/src/src/calls/createManagePorts.js b/build/src/src/calls/createManagePorts.js new file mode 100644 index 000000000..a9cff9481 --- /dev/null +++ b/build/src/src/calls/createManagePorts.js @@ -0,0 +1,54 @@ +const dockerDefault = require('modules/docker'); + +// CALL DOCUMENTATION: +// > kwargs: { ports, logId } +// > result: - + +function createManagePorts({ + docker = dockerDefault, +}) { + const managePorts = async ({ + action, + ports, + }) => { + // ports should be an array of numerical ports + // [5000, 5001] + if (!Array.isArray(ports)) { + throw Error('ports variable must be an array: '+JSON.stringify(ports)); + } + + let msg; + for (const port of ports) { + switch (action) { + case 'open': + await docker.openPort(port); + msg = 'Opened'; + break; + case 'close': + await docker.closePort(port); + msg = 'Closed'; + break; + default: + throw Error('Unkown manage ports action: '+action); + } + } + + return { + message: msg+' ports '+ports.join(', '), + logMessage: true, + userAction: true, + }; + }; + + // Expose main method + return managePorts; +} + +// function getPorts(MANIFEST) { +// return (MANIFEST && MANIFEST.image && MANIFEST.image.ports) +// ? MANIFEST.image.ports.map((p) => p.split(':')[0]) +// : []; +// } + + +module.exports = createManagePorts; diff --git a/build/src/src/calls/createManagePorts.test.js b/build/src/src/calls/createManagePorts.test.js new file mode 100644 index 000000000..41a121dbb --- /dev/null +++ b/build/src/src/calls/createManagePorts.test.js @@ -0,0 +1,44 @@ +const chai = require('chai'); +const expect = require('chai').expect; +const createManagePorts = require('calls/createManagePorts'); + +chai.should(); + +describe('Call function: managePorts', function() { + const openedPorts = []; + const dockerMock = { + openPort: async (port) => { + openedPorts.push(port); + }, + }; + + const managePorts = createManagePorts({ + docker: dockerMock, + }); + + it('should open the requested ports', async () => { + const ports = [5000, 5001]; + const res = await managePorts({ + action: 'open', + ports, + }); + // Check opened ports + expect(ports).to.deep.equal(openedPorts); + // Check response message + expect(res).to.be.ok; + expect(res).to.have.property('message'); + }); + + it('should throw an error with wrong ports variable', async () => { + let error = '--- managePorts did not throw ---'; + try { + await managePorts({ + action: 'open', + ports: 'not an array', + }); + } catch (e) { + error = e.message; + } + expect(error).to.include('ports variable must be an array'); + }); +}); diff --git a/build/src/src/calls/createRemovePackage.js b/build/src/src/calls/createRemovePackage.js index af3bb4f82..2ee23040e 100644 --- a/build/src/src/calls/createRemovePackage.js +++ b/build/src/src/calls/createRemovePackage.js @@ -1,7 +1,6 @@ const fs = require('fs'); const getPath = require('utils/getPath'); const shellSync = require('utils/shell'); -const parse = require('utils/parse'); const logUI = require('utils/logUI'); const paramsDefault = require('params'); const dockerDefault = require('modules/docker'); @@ -34,15 +33,6 @@ function createRemovePackage({ throw Error('The installer cannot be restarted'); } - // Close ports - logUI({logId, pkg: 'all', msg: 'closing ports...'}); - try { - await closePorts(dockerComposePath, docker); - } catch (e) { - logUI({logId, pkg: 'all', msg: 'Error closing ports '+(e ? e.message : '')}); - } - - // Remove container (and) volumes logUI({logId, pkg: 'all', msg: 'Shutting down containers...'}); await docker.compose.down(dockerComposePath, {volumes: Boolean(deleteVolumes)}); @@ -52,7 +42,8 @@ function createRemovePackage({ return { message: 'Removed package: ' + id, - log: true, + logMessage: true, + userAction: true, }; }; @@ -60,12 +51,5 @@ function createRemovePackage({ return removePackage; } -async function closePorts(dockerComposePath, docker) { - const ports = parse.dockerComposePorts(dockerComposePath); - for (const port of ports) { - await docker.closePort(port); - } -} - module.exports = createRemovePackage; diff --git a/build/src/src/calls/createRestartPackage.js b/build/src/src/calls/createRestartPackage.js index a2e304b2b..401c4b1e6 100644 --- a/build/src/src/calls/createRestartPackage.js +++ b/build/src/src/calls/createRestartPackage.js @@ -34,6 +34,7 @@ function createRestartPackage({ return { message: 'Restarted package: ' + id, logMessage: true, + userAction: true, }; }; diff --git a/build/src/src/calls/createRestartPackageVolumes.js b/build/src/src/calls/createRestartPackageVolumes.js index fc9a749d5..89aed7590 100644 --- a/build/src/src/calls/createRestartPackageVolumes.js +++ b/build/src/src/calls/createRestartPackageVolumes.js @@ -43,7 +43,8 @@ function createRestartPackageVolumes({ return { message: 'Restarted '+id+' volumes: ' + packageVolumes.join(', '), - log: true, + logMessage: true, + userAction: true, }; }; } diff --git a/build/src/src/calls/createTogglePackage.js b/build/src/src/calls/createTogglePackage.js index 7c4006edf..7c76cc3be 100644 --- a/build/src/src/calls/createTogglePackage.js +++ b/build/src/src/calls/createTogglePackage.js @@ -35,7 +35,8 @@ function createTogglePackage({ return { message: 'successfully toggled package: ' + id, - log: true, + logMessage: true, + userAction: true, }; }; diff --git a/build/src/src/calls/createUpdatePackageEnv.js b/build/src/src/calls/createUpdatePackageEnv.js index 8d021015e..835b550bf 100644 --- a/build/src/src/calls/createUpdatePackageEnv.js +++ b/build/src/src/calls/createUpdatePackageEnv.js @@ -31,7 +31,8 @@ function createUpdatePackageEnv({ if (!restart) { return { message: 'Updated envs of ' + id, - log: true, + logMessage: true, + userAction: true, }; } @@ -49,7 +50,8 @@ function createUpdatePackageEnv({ return { message: 'Updated envs and restarted ' + id, - log: true, + logMessage: true, + userAction: true, }; }; diff --git a/build/src/src/eventBus.js b/build/src/src/eventBus.js index a5a77a70a..55038eff1 100644 --- a/build/src/src/eventBus.js +++ b/build/src/src/eventBus.js @@ -1,5 +1,17 @@ const EventEmitter = require('events'); +/* HOW TO: + +- ON: +eventBus.on(eventBusTag.logUI, (data) => { + doStuff(data); +}); + +- EMIT: +eventBus.emit(eventBusTag.logUI, data); + +*/ + class MyEmitter extends EventEmitter {} const eventBus = new MyEmitter(); @@ -7,6 +19,7 @@ const eventBus = new MyEmitter(); const eventBusTag = { logUI: 'EVENT_BUS_LOGUI', call: 'EVENT_BUS_CALL', + logUserAction: 'EVENT_BUS_LOGUSERACTION', }; module.exports = { diff --git a/build/src/src/index.js b/build/src/src/index.js index 618225ab7..f54d525b5 100644 --- a/build/src/src/index.js +++ b/build/src/src/index.js @@ -3,6 +3,7 @@ const autobahn = require('autobahn'); const {eventBus, eventBusTag} = require('eventBus'); const logs = require('logs.js')(module); +const logUserAction = require('logUserAction.js'); // import calls const createInstallPackage = require('calls/createInstallPackage'); @@ -16,6 +17,8 @@ const createListPackages = require('calls/createListPackages'); const createFetchDirectory = require('calls/createFetchDirectory'); const createFetchPackageVersions = require('calls/createFetchPackageVersions'); const createFetchPackageData = require('calls/createFetchPackageData'); +const createManagePorts = require('calls/createManagePorts'); +const createGetUserActionLogs = require('calls/createGetUserActionLogs'); // import dependencies const params = require('params'); @@ -48,6 +51,8 @@ const fetchDirectory = createFetchDirectory({getDirectory}); const fetchPackageVersions = createFetchPackageVersions({getManifest, apm}); const updatePackageEnv = createUpdatePackageEnv({}); const fetchPackageData = createFetchPackageData({getManifest}); +const managePorts = createManagePorts({}); +const getUserActionLogs = createGetUserActionLogs({}); // ///////////////////////////// // Connection helper functions @@ -61,11 +66,14 @@ const register = (session, event, handler) => { // 2. details: an object which provides call metadata try { const res = await handler(kwargs); + // Log internally + logUserAction.log({level: 'info', event, ...res, kwargs}); const eventShort = event.replace('.dappmanager.dnp.dappnode.eth', ''); - if (res.log && res.result) logs.info('Result of '+eventShort+': '+JSON.stringify(res)); - else if (res.log && !res.result) logs.info('Result of '+eventShort+': '+res.message); - else if (res.logMessage) logs.info('Result of '+eventShort+': '+res.message); + if (res.logMessage) { + logs.info('Call '+eventShort+' success: '+res.message); + } + // Return to crossbar return JSON.stringify({ success: true, @@ -73,7 +81,8 @@ const register = (session, event, handler) => { result: res.result || {}, }); } catch (err) { - logs.error(' Event: '+event+' Stack: '+err.stack); + logUserAction.log({level: 'error', event, ...error2obj(err), kwargs}); + logs.error('Call '+event+' error: '+err.message+'\nStack: '+err.stack); return JSON.stringify({ success: false, message: err.message, @@ -87,6 +96,10 @@ const register = (session, event, handler) => { ); }; +function error2obj(e) { + return {name: e.name, message: e.message, stack: e.stack, userAction: true}; +} + // ///////////////////////////// // Configure connection: @@ -114,6 +127,8 @@ connection.onopen = (session, details) => { register(session, 'fetchDirectory.dappmanager.dnp.dappnode.eth', fetchDirectory); register(session, 'fetchPackageVersions.dappmanager.dnp.dappnode.eth', fetchPackageVersions); register(session, 'fetchPackageData.dappmanager.dnp.dappnode.eth', fetchPackageData); + register(session, 'managePorts.dappmanager.dnp.dappnode.eth', managePorts); + register(session, 'getUserActionLogs.dappmanager.dnp.dappnode.eth', getUserActionLogs); eventBus.on(eventBusTag.call, (call, args, kwargs) => { session.call(call, args, kwargs) @@ -134,6 +149,14 @@ connection.onopen = (session, details) => { session.publish(autobahnTag.DAppManagerLog, [data]); logs.info('\x1b[35m%s\x1b[0m', JSON.stringify(data)); }); + + eventBus.on(eventBusTag.logUserAction, (data) => { + session.publish(autobahnTag.logUserAction, [data]); + }); + + session.subscribe(autobahnTag.logUserActionToDappmanager, (args) => { + logUserAction.log(args[0]); + }); }; diff --git a/build/src/src/logUserAction.js b/build/src/src/logUserAction.js new file mode 100644 index 000000000..487ee1c84 --- /dev/null +++ b/build/src/src/logUserAction.js @@ -0,0 +1,54 @@ +'use strict'; +const winston = require('winston'); +const {createLogger, format, transports} = winston; +const Transport = require('winston-transport'); +const {eventBus, eventBusTag} = require('eventBus'); +const params = require('params'); + +/* +* > LEVELS: +* --------------------- +* logs.info('Something') +* logs.warn('Something') +* logs.error('Something') +*/ + +// Format function to filter out unrelevant logs +const onlyUserAction = format((info, opts) => { + if (!info.userAction) {return false;} + delete info.userAction; + delete info.logMessage; + return info; +}); + +// Custom transport to broadcast new logs to the admin directly +class EmitToAdmin extends Transport { + constructor(opts) { + super(opts); + } + + log(info, callback) { + setImmediate(() => { + eventBus.emit(eventBusTag.logUserAction, info); + }); + callback(); + } +} + +// Actual logger +const logger = createLogger({ + transports: [ + new transports.File({ + filename: params.userActionLogsFilename, + level: 'info', + }), + new EmitToAdmin(), + ], + format: format.combine( + onlyUserAction(), + format.timestamp(), + format.json() + ), +}); + +module.exports = logger; diff --git a/build/src/src/logs.js b/build/src/src/logs.js index 3fe405bf9..caa095eb7 100644 --- a/build/src/src/logs.js +++ b/build/src/src/logs.js @@ -1,5 +1,6 @@ 'use strict'; const winston = require('winston'); +const {createLogger, format, transports} = winston; /* * > LEVELS: @@ -9,7 +10,7 @@ const winston = require('winston'); * logs.error('Something') */ -const scFormat = winston.format.printf((info) => { +const scFormat = format.printf((info) => { let level = info.level.toUpperCase(); let message = info.message; let filteredInfo = Object.assign({}, info, { @@ -23,8 +24,11 @@ const scFormat = winston.format.printf((info) => { if (append != '{}') { message = message + ' ' + append; } + const variables = []; + if (info.admin) variables.push('ADMIN'); + // return `${info.timestamp} ${level} [${info.label}] : ${message}`; - return `${level} [${info.label}] : ${message}`; + return `${level} [${info.label}] [${variables.join('&')}] : ${message}`; }); /** @@ -43,19 +47,29 @@ function _getLabel(mod) { label = mod.id.replace('.js', ''); label = label.replace(/^.*\/src\//, ''); } - return winston.format.label({'label': label}); + return format.label({'label': label}); } module.exports = function(mod) { - const logger = winston.createLogger({ + const logger = createLogger({ level: process.env.LOG_LEVEL || 'info', - format: winston.format.combine( - winston.format.splat(), - winston.format.timestamp(), + format: format.combine( + format.splat(), + format.timestamp({ + format: 'DD-MM-YYYY HH:mm:ss', + }), _getLabel(mod), scFormat ), - transports: [new winston.transports.Console()], + transports: [ + new transports.Console({ + // format: format.combine( + // format.timestamp(), + // format.colorize(), + // format.simple() + // ), + }), + ], }); return logger; }; diff --git a/build/src/src/params.js b/build/src/src/params.js index b60cb593d..1b6ffc6f8 100644 --- a/build/src/src/params.js +++ b/build/src/src/params.js @@ -6,6 +6,8 @@ module.exports = { // autobahnRealm: 'realm1', autobahnRealm: 'dappnode_admin', autobahnTag: { + logUserActionToDappmanager: 'logUserActionToDappmanager', + logUserAction: 'logUserAction.dappmanager.dnp.dappnode.eth', DAppManagerLog: 'log.dappmanager.dnp.dappnode.eth', }, @@ -34,6 +36,9 @@ module.exports = { IPFS: (process.env.IPFS_REDIRECT || 'my.ipfs.dnp.dappnode.eth'), // Wweb3 parameters - WEB3HOSTWS: 'ws://172.33.1.6:8546', + WEB3HOSTWS: 'ws://my.ethchain.dnp.dappnode.eth:8546', + + // User Action Logs filename + userActionLogsFilename: 'DNCORE/userActionLogs.log', }; diff --git a/build/src/src/utils/generate.js b/build/src/src/utils/generate.js index 3bc13d623..eecca683c 100644 --- a/build/src/src/utils/generate.js +++ b/build/src/src/utils/generate.js @@ -66,6 +66,12 @@ function dockerCompose(dpnManifest, params, isCORE = false) { }); } + // Extra features + if (dpnManifest.image.cap_add) service.cap_add = dpnManifest.image.cap_add; + if (dpnManifest.image.cap_drop) service.cap_drop = dpnManifest.image.cap_drop; + if (dpnManifest.image.network_mode) service.network_mode = dpnManifest.image.network_mode; + if (dpnManifest.image.command) service.command = dpnManifest.image.command; + // DOCKER COMPOSE YML - VOLUMES // ============================ diff --git a/build/src/src/utils/generate.test.js b/build/src/src/utils/generate.test.js index 58190a9f1..c82d677e6 100644 --- a/build/src/src/utils/generate.test.js +++ b/build/src/src/utils/generate.test.js @@ -244,6 +244,49 @@ networks: `, }; +const mysterium = { + manifest: { + 'name': 'Mysterium', + 'version': '', + 'description': '', + 'avatar': '', + 'type': '', + 'image': { + 'path': '', + 'hash': '', + 'size': '', + 'cap_add': [ + 'SYS_ADMIN', + ], + 'cap_drop': [ + 'NET_ADMIN', + ], + 'network_mode': 'host', + 'command': '--command', + }, + }, + dc: `version: '3.4' +services: + Mysterium: + container_name: DAppNodePackage-Mysterium + image: 'Mysterium:' + restart: always + volumes: [] + networks: + - dncore_network + dns: 172.33.1.2 + cap_add: + - SYS_ADMIN + cap_drop: + - NET_ADMIN + network_mode: host + command: '--command' +networks: + dncore_network: + external: true +`, +}; + describe('generate, utils', function() { describe('generate docker-compose.yml file', function() { // Non-CORE @@ -267,11 +310,17 @@ describe('generate, utils', function() { }); // CORE package - VPN - it('should generate the expected docker-compose of IPFS', () => { + it('should generate the expected docker-compose of VPN', () => { const isCORE = true; generate.dockerCompose(vpn.manifest, params, isCORE) .should.equal(vpn.dc); }); + + // Mysterium package + it('should generate the expected docker-compose of Mysterium', () => { + generate.dockerCompose(mysterium.manifest, params) + .should.equal(mysterium.dc); + }); }); describe('generate a manifest file', function() { diff --git a/build/src/src/utils/packages.js b/build/src/src/utils/packages.js index 01a67d464..b2b0ebc84 100644 --- a/build/src/src/utils/packages.js +++ b/build/src/src/utils/packages.js @@ -99,16 +99,6 @@ function runFactory({ const DOCKERCOMPOSE_PATH = getPath.dockerCompose(PACKAGE_NAME, params, isCORE); const IMAGE_PATH = getPath.image(PACKAGE_NAME, IMAGE_NAME, params, isCORE); - for (const port of getPorts(MANIFEST)) { - logUI({logId, pkg: PACKAGE_NAME, msg: 'opening port '+port}); - try { - await docker.openPort(port); - } catch (e) { - logUI({logId, pkg: PACKAGE_NAME, msg: 'Error openning port '+port+' ' - +(e ? e.message : '')}); - } - } - logUI({logId, pkg: PACKAGE_NAME, msg: 'loading image'}); await docker.load(IMAGE_PATH); logUI({logId, pkg: PACKAGE_NAME, msg: 'loaded image'}); @@ -124,12 +114,6 @@ function runFactory({ }; } -function getPorts(MANIFEST) { - return (MANIFEST && MANIFEST.image && MANIFEST.image.ports) - ? MANIFEST.image.ports.map((p) => p.split(':')[0]) - : []; -} - module.exports = { downloadFactory, runFactory, diff --git a/build/src/src/utils/shell.js b/build/src/src/utils/shell.js index 492d3822f..5a4760f48 100644 --- a/build/src/src/utils/shell.js +++ b/build/src/src/utils/shell.js @@ -14,8 +14,10 @@ async function shellExecSync(command, silent = false) { const stdout = res.stdout; const stderr = res.stderr; if (code !== 0) { - logs.error('SHELL JS ERROR, on command: ' + command); - throw Error(stderr); + // const err + const err = stderr.length ? stderr : stdout; + logs.error('SHELL JS ERROR, on command: ' + command+' err: '+err); + throw Error(err); } return stdout; } diff --git a/dappnode_package.json b/dappnode_package.json index 93d93bb6a..e30e86dba 100644 --- a/dappnode_package.json +++ b/dappnode_package.json @@ -1,6 +1,6 @@ { "name": "dappmanager.dnp.dappnode.eth", - "version": "0.1.9", + "version": "0.1.10", "description": "Dappnode package responsible for providing the DappNode Package Manager", "avatar": "/ipfs/QmdT2GX9ybaoE25HBk7is8CDnfn2KaFTpZVkLdfkAQs1PN", "type": "dncore", @@ -38,9 +38,5 @@ "bugs": { "url": "https://github.com/dappnode/DNP_DAPPMANAGER/issues" }, - "license": "GPL-3.0", - "dependencies": { - "admin.dnp.dappnode.eth": "0.1.4", - "vpn.dnp.dappnode.eth": "0.1.7" - } + "license": "GPL-3.0" } \ No newline at end of file diff --git a/docker-compose-dappmanager.yml b/docker-compose-dappmanager.yml index 5c7ec14ea..65dfbbe42 100644 --- a/docker-compose-dappmanager.yml +++ b/docker-compose-dappmanager.yml @@ -13,7 +13,7 @@ volumes: services: dappmanager.dnp.dappnode.eth: build: ./build - image: dappmanager.dnp.dappnode.eth:0.1.9 + image: dappmanager.dnp.dappnode.eth:0.1.10 container_name: DAppNodeCore-dappmanager.dnp.dappnode.eth restart: always volumes: