http://git-wip-us.apache.org/repos/asf/ignite/blob/6af6560a/modules/web-console/backend/app/agent.js ---------------------------------------------------------------------- diff --git a/modules/web-console/backend/app/agent.js b/modules/web-console/backend/app/agent.js new file mode 100644 index 0000000..a1858fd --- /dev/null +++ b/modules/web-console/backend/app/agent.js @@ -0,0 +1,753 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +// Fire me up! + +/** + * Module interaction with agents. + */ +module.exports = { + implements: 'agent-manager', + inject: ['require(lodash)', 'require(ws)', 'require(fs)', 'require(path)', 'require(jszip)', 'require(socket.io)', 'settings', 'mongo'] +}; + +/** + * @param _ + * @param fs + * @param ws + * @param path + * @param JSZip + * @param socketio + * @param settings + * @param mongo + * @returns {AgentManager} + */ +module.exports.factory = function(_, ws, fs, path, JSZip, socketio, settings, mongo) { + /** + * + */ + class Command { + /** + * @param {Boolean} demo Is need run command on demo node. + * @param {String} name Command name. + */ + constructor(demo, name) { + this._demo = demo; + + /** + * Command name. + * @type {String} + */ + this._name = name; + + /** + * Command parameters. + * @type {Array.<String>} + */ + this._params = []; + } + + /** + * Add parameter to command. + * @param {string} key Parameter key. + * @param {Object} value Parameter value. + * @returns {Command} + */ + addParam(key, value) { + this._params.push({key, value}); + + return this; + } + } + + /** + * Connected agent descriptor. + */ + class Agent { + /** + * @param {socketIo.Socket} socket - Agent socket for interaction. + */ + constructor(socket) { + /** + * Agent socket for interaction. + * + * @type {socketIo.Socket} + * @private + */ + this._socket = socket; + } + + /** + * Send message to agent. + * + * @this {Agent} + * @param {String} event Command name. + * @param {Object} data Command params. + * @param {Function} [callback] on finish + */ + _emit(event, data, callback) { + if (!this._socket.connected) { + if (callback) + callback('org.apache.ignite.agent.AgentException: Connection is closed'); + + return; + } + + this._socket.emit(event, data, callback); + } + + /** + * Send message to agent. + * + * @param {String} event - Event name. + * @param {Object?} data - Transmitted data. + * @returns {Promise} + */ + executeAgent(event, data) { + return new Promise((resolve, reject) => + this._emit(event, data, (error, res) => { + if (error) + return reject(error); + + resolve(res); + }) + ); + } + + /** + * Execute rest request on node. + * + * @param {Command} cmd - REST command. + * @return {Promise} + */ + executeRest(cmd) { + const params = {cmd: cmd._name}; + + for (const param of cmd._params) + params[param.key] = param.value; + + return new Promise((resolve, reject) => { + this._emit('node:rest', {uri: 'ignite', params, demo: cmd._demo, method: 'GET'}, (error, res) => { + if (error) + return reject(new Error(error)); + + error = res.error; + + const code = res.code; + + if (code === 401) + return reject(new Error('Agent failed to authenticate in grid. Please check agent\'s login and password or node port.')); + + if (code !== 200) + return reject(new Error(error || 'Failed connect to node and execute REST command.')); + + try { + const msg = JSON.parse(res.data); + + if (msg.successStatus === 0) + return resolve(msg.response); + + if (msg.successStatus === 2) + return reject(new Error('Agent failed to authenticate in grid. Please check agent\'s login and password or node port.')); + + reject(new Error(msg.error)); + } + catch (e) { + return reject(e); + } + }); + }); + } + + /** + * @param {String} driverPath + * @param {String} driverClass + * @param {String} url + * @param {Object} info + * @returns {Promise} Promise on list of tables (see org.apache.ignite.schema.parser.DbTable java class) + */ + metadataSchemas(driverPath, driverClass, url, info) { + return this.executeAgent('schemaImport:schemas', {driverPath, driverClass, url, info}); + } + + /** + * @param {String} driverPath + * @param {String} driverClass + * @param {String} url + * @param {Object} info + * @param {Array} schemas + * @param {Boolean} tablesOnly + * @returns {Promise} Promise on list of tables (see org.apache.ignite.schema.parser.DbTable java class) + */ + metadataTables(driverPath, driverClass, url, info, schemas, tablesOnly) { + return this.executeAgent('schemaImport:metadata', {driverPath, driverClass, url, info, schemas, tablesOnly}); + } + + /** + * @returns {Promise} Promise on list of jars from driver folder. + */ + availableDrivers() { + return this.executeAgent('schemaImport:drivers'); + } + + /** + * + * @param {Boolean} demo Is need run command on demo node. + * @param {Boolean} attr Get attributes, if this parameter has value true. Default value: true. + * @param {Boolean} mtr Get metrics, if this parameter has value true. Default value: false. + * @returns {Promise} + */ + topology(demo, attr, mtr) { + const cmd = new Command(demo, 'top') + .addParam('attr', attr !== false) + .addParam('mtr', !!mtr); + + return this.executeRest(cmd); + } + + /** + * @param {Boolean} demo Is need run command on demo node. + * @param {String} nid Node id. + * @param {String} cacheName Cache name. + * @param {String} query Query. + * @param {Boolean} local Flag whether to execute query locally. + * @param {int} pageSize Page size. + * @returns {Promise} + */ + fieldsQuery(demo, nid, cacheName, query, local, pageSize) { + const cmd = new Command(demo, 'exe') + .addParam('name', 'org.apache.ignite.internal.visor.compute.VisorGatewayTask') + .addParam('p1', nid) + .addParam('p2', 'org.apache.ignite.internal.visor.query.VisorQueryTask') + .addParam('p3', 'org.apache.ignite.internal.visor.query.VisorQueryArg') + .addParam('p4', cacheName) + .addParam('p5', query) + .addParam('p6', local) + .addParam('p7', pageSize); + + return this.executeRest(cmd); + } + + /** + * @param {Boolean} demo Is need run command on demo node. + * @param {String} nid Node id. + * @param {int} queryId Query Id. + * @param {int} pageSize Page size. + * @returns {Promise} + */ + queryFetch(demo, nid, queryId, pageSize) { + const cmd = new Command(demo, 'exe') + .addParam('name', 'org.apache.ignite.internal.visor.compute.VisorGatewayTask') + .addParam('p1', nid) + .addParam('p2', 'org.apache.ignite.internal.visor.query.VisorQueryNextPageTask') + .addParam('p3', 'org.apache.ignite.lang.IgniteBiTuple') + .addParam('p4', 'java.lang.String') + .addParam('p5', 'java.lang.Integer') + .addParam('p6', queryId) + .addParam('p7', pageSize); + + return this.executeRest(cmd); + } + + /** + * @param {Boolean} demo Is need run command on demo node. + * @param {String} nid Node id. + * @param {int} queryId Query Id. + * @returns {Promise} + */ + queryClose(demo, nid, queryId) { + const cmd = new Command(demo, 'exe') + .addParam('name', 'org.apache.ignite.internal.visor.compute.VisorGatewayTask') + .addParam('p1', '') + .addParam('p2', 'org.apache.ignite.internal.visor.query.VisorQueryCleanupTask') + .addParam('p3', 'java.util.Map') + .addParam('p4', 'java.util.UUID') + .addParam('p5', 'java.util.Set') + .addParam('p6', `${nid}=${queryId}`); + + return this.executeRest(cmd); + } + + /** + * @param {Boolean} demo Is need run command on demo node. + * @param {String} cacheName Cache name. + * @returns {Promise} + */ + metadata(demo, cacheName) { + const cmd = new Command(demo, 'metadata') + .addParam('cacheName', cacheName); + + return this.executeRest(cmd); + } + + /** + * @param {Boolean} demo Is need run command on demo node. + * @param {String} evtOrderKey Event order key, unique for tab instance. + * @param {String} evtThrottleCntrKey Event throttle counter key, unique for tab instance. + * @returns {Promise} + */ + collect(demo, evtOrderKey, evtThrottleCntrKey) { + const cmd = new Command(demo, 'exe') + .addParam('name', 'org.apache.ignite.internal.visor.compute.VisorGatewayTask') + .addParam('p1', '') + .addParam('p2', 'org.apache.ignite.internal.visor.node.VisorNodeDataCollectorTask') + .addParam('p3', 'org.apache.ignite.internal.visor.node.VisorNodeDataCollectorTaskArg') + .addParam('p4', true) + .addParam('p5', 'CONSOLE_' + evtOrderKey) + .addParam('p6', evtThrottleCntrKey) + .addParam('p7', 10) + .addParam('p8', false); + + return this.executeRest(cmd); + } + + /** + * @param {Boolean} demo Is need run command on demo node. + * @param {String} nid Node id. + * @returns {Promise} + */ + collectNodeConfiguration(demo, nid) { + const cmd = new Command(demo, 'exe') + .addParam('name', 'org.apache.ignite.internal.visor.compute.VisorGatewayTask') + .addParam('p1', nid) + .addParam('p2', 'org.apache.ignite.internal.visor.node.VisorNodeConfigurationCollectorTask') + .addParam('p3', 'java.lang.Void') + .addParam('p4', null); + + return this.executeRest(cmd); + } + + /** + * @param {Boolean} demo Is need run command on demo node. + * @param {String} nid Node id. + * @param {Array.<String>} caches Caches deployment IDs to collect configuration. + * @returns {Promise} + */ + collectCacheConfigurations(demo, nid, caches) { + const cmd = new Command(demo, 'exe') + .addParam('name', 'org.apache.ignite.internal.visor.compute.VisorGatewayTask') + .addParam('p1', nid) + .addParam('p2', 'org.apache.ignite.internal.visor.cache.VisorCacheConfigurationCollectorTask') + .addParam('p3', 'java.util.Collection') + .addParam('p4', 'org.apache.ignite.lang.IgniteUuid') + .addParam('p5', caches); + + return this.executeRest(cmd); + } + + /** + * @param {Boolean} demo Is need run command on demo node. + * @param {String} nid Node id. + * @param {String} cacheName Cache name. + * @returns {Promise} + */ + cacheClear(demo, nid, cacheName) { + const cmd = new Command(demo, 'exe') + .addParam('name', 'org.apache.ignite.internal.visor.compute.VisorGatewayTask') + .addParam('p1', nid) + .addParam('p2', 'org.apache.ignite.internal.visor.cache.VisorCacheClearTask') + .addParam('p3', 'java.lang.String') + .addParam('p4', cacheName); + + return this.executeRest(cmd); + } + + /** + * @param {Boolean} demo Is need run command on demo node. + * @param {Array.<String>} nids Node ids. + * @param {Boolean} near true if near cache should be started. + * @param {String} cacheName Name for near cache. + * @param {String} cfg Cache XML configuration. + * @returns {Promise} + */ + cacheStart(demo, nids, near, cacheName, cfg) { + const cmd = new Command(demo, 'exe') + .addParam('name', 'org.apache.ignite.internal.visor.compute.VisorGatewayTask') + .addParam('p1', nids) + .addParam('p2', 'org.apache.ignite.internal.visor.cache.VisorCacheStartTask') + .addParam('p3', 'org.apache.ignite.internal.visor.cache.VisorCacheStartTask$VisorCacheStartArg') + .addParam('p4', near) + .addParam('p5', cacheName) + .addParam('p6', cfg); + + return this.executeRest(cmd); + } + + /** + * @param {Boolean} demo Is need run command on demo node. + * @param {String} nid Node id. + * @param {String} cacheName Cache name. + * @returns {Promise} + */ + cacheStop(demo, nid, cacheName) { + const cmd = new Command(demo, 'exe') + .addParam('name', 'org.apache.ignite.internal.visor.compute.VisorGatewayTask') + .addParam('p1', nid) + .addParam('p2', 'org.apache.ignite.internal.visor.cache.VisorCacheStopTask') + .addParam('p3', 'java.lang.String') + .addParam('p4', cacheName); + + return this.executeRest(cmd); + } + + /** + * @param {Boolean} demo Is need run command on demo node. + * @param {String} nid Node id. + * @param {String} cacheName Cache name. + * @returns {Promise} + */ + cacheResetMetrics(demo, nid, cacheName) { + const cmd = new Command(demo, 'exe') + .addParam('name', 'org.apache.ignite.internal.visor.compute.VisorGatewayTask') + .addParam('p1', nid) + .addParam('p2', 'org.apache.ignite.internal.visor.cache.VisorCacheResetMetricsTask') + .addParam('p3', 'java.lang.String') + .addParam('p4', cacheName); + + return this.executeRest(cmd); + } + + /** + * @param {Boolean} demo Is need run command on demo node. + * @param {String} nid Node id. + * @param {String} cacheNames Cache names separated by comma. + * @returns {Promise} + */ + cacheSwapBackups(demo, nid, cacheNames) { + const cmd = new Command(demo, 'exe') + .addParam('name', 'org.apache.ignite.internal.visor.compute.VisorGatewayTask') + .addParam('p1', nid) + .addParam('p2', 'org.apache.ignite.internal.visor.cache.VisorCacheSwapBackupsTask') + .addParam('p3', 'java.util.Set') + .addParam('p4', 'java.lang.String') + .addParam('p5', cacheNames); + + return this.executeRest(cmd); + } + + /** + * @param {Boolean} demo Is need run command on demo node. + * @param {String} nids Node ids. + * @returns {Promise} + */ + gc(demo, nids) { + const cmd = new Command(demo, 'exe') + .addParam('name', 'org.apache.ignite.internal.visor.compute.VisorGatewayTask') + .addParam('p1', nids) + .addParam('p2', 'org.apache.ignite.internal.visor.node.VisorNodeGcTask') + .addParam('p3', 'java.lang.Void'); + + return this.executeRest(cmd); + } + + /** + * @param {Boolean} demo Is need run command on demo node. + * @param {String} taskNid node that is not node we want to ping. + * @param {String} nid Id of the node to ping. + * @returns {Promise} + */ + ping(demo, taskNid, nid) { + const cmd = new Command(demo, 'exe') + .addParam('name', 'org.apache.ignite.internal.visor.compute.VisorGatewayTask') + .addParam('p1', taskNid) + .addParam('p2', 'org.apache.ignite.internal.visor.node.VisorNodePingTask') + .addParam('p3', 'java.util.UUID') + .addParam('p4', nid); + + return this.executeRest(cmd); + } + + /** + * @param {Boolean} demo Is need run command on demo node. + * @param {String} nid Id of the node to get thread dump. + * @returns {Promise} + */ + threadDump(demo, nid) { + const cmd = new Command(demo, 'exe') + .addParam('name', 'org.apache.ignite.internal.visor.compute.VisorGatewayTask') + .addParam('p1', nid) + .addParam('p2', 'org.apache.ignite.internal.visor.debug.VisorThreadDumpTask') + .addParam('p3', 'java.lang.Void'); + + return this.executeRest(cmd); + } + } + + /** + * Connected agents manager. + */ + class AgentManager { + /** + * @constructor + */ + constructor() { + /** + * Connected agents by user id. + * @type {Object.<ObjectId, Array.<Agent>>} + */ + this._agents = {}; + + /** + * Connected browsers by user id. + * @type {Object.<ObjectId, Array.<Socket>>} + */ + this._browsers = {}; + + const agentArchives = fs.readdirSync(settings.agent.dists) + .filter((file) => path.extname(file) === '.zip'); + + /** + * Supported agents distribution. + * @type {Object.<String, String>} + */ + this.supportedAgents = {}; + + const jarFilter = (file) => path.extname(file) === '.jar'; + + const agentsPromises = _.map(agentArchives, (fileName) => { + const filePath = path.join(settings.agent.dists, fileName); + + return JSZip.loadAsync(fs.readFileSync(filePath)) + .then((zip) => { + const jarPath = _.find(_.keys(zip.files), jarFilter); + + return JSZip.loadAsync(zip.files[jarPath].async('nodebuffer')) + .then((jar) => jar.files['META-INF/MANIFEST.MF'].async('string')) + .then((lines) => lines.trim() + .split(/\s*\n+\s*/) + .map((line, r) => { + r = line.split(/\s*:\s*/); + + this[r[0]] = r[1]; + + return this; + }, {})[0]) + .then((manifest) => { + const ver = manifest['Implementation-Version']; + const buildTime = manifest['Build-Time']; + + if (ver && buildTime) + return { fileName, filePath, ver, buildTime }; + }); + }); + }); + + Promise.all(agentsPromises) + .then((agents) => { + this.supportedAgents = _.keyBy(_.remove(agents, null), 'ver'); + + const latest = _.head(Object.keys(this.supportedAgents).sort((a, b) => { + const aParts = a.split('.'); + const bParts = b.split('.'); + + for (let i = 0; i < aParts.length; ++i) { + if (bParts.length === i) + return 1; + + if (aParts[i] === aParts[i]) + continue; + + return aParts[i] > bParts[i] ? 1 : -1; + } + })); + + // Latest version of agent distribution. + if (latest) + this.supportedAgents.latest = this.supportedAgents[latest]; + }); + } + + attachLegacy(server) { + const wsSrv = new ws.Server({server}); + + wsSrv.on('connection', (_wsClient) => { + _wsClient.send(JSON.stringify({ + method: 'authResult', + args: ['You are using an older version of the agent. Please reload agent archive'] + })); + }); + } + + /** + * @param {http.Server|https.Server} srv Server instance that we want to attach agent handler. + */ + attach(srv) { + if (this._server) + throw 'Agent server already started!'; + + this._server = srv; + + /** + * @type {socketIo.Server} + */ + this._socket = socketio(this._server); + + this._socket.on('connection', (socket) => { + socket.on('agent:auth', (data, cb) => { + if (!_.isEmpty(this.supportedAgents)) { + const ver = data.ver; + const bt = data.bt; + + if (_.isEmpty(ver) || _.isEmpty(bt) || _.isEmpty(this.supportedAgents[ver]) || + this.supportedAgents[ver].buildTime > bt) + return cb('You are using an older version of the agent. Please reload agent archive'); + } + + const tokens = data.tokens; + + mongo.Account.find({token: {$in: tokens}}, '_id token').lean().exec() + .then((accounts) => { + if (!accounts.length) + return cb('Agent is failed to authenticate. Please check agent\'s token(s)'); + + const agent = new Agent(socket); + + const accountIds = _.map(accounts, (account) => account._id); + + socket.on('disconnect', () => this._agentDisconnected(accountIds, agent)); + + this._agentConnected(accountIds, agent); + + const missedTokens = _.difference(tokens, _.map(accounts, (account) => account.token)); + + if (missedTokens.length) { + agent._emit('agent:warning', + `Failed to authenticate with token(s): ${missedTokens.join(', ')}.`); + } + + cb(); + }) + // TODO IGNITE-1379 send error to web master. + .catch(() => cb('Agent is failed to authenticate. Please check agent\'s tokens')); + }); + }); + } + + /** + * @param {ObjectId} accountId + * @param {Socket} socket + * @returns {int} Connected agent count. + */ + addAgentListener(accountId, socket) { + let sockets = this._browsers[accountId]; + + if (!sockets) + this._browsers[accountId] = sockets = []; + + sockets.push(socket); + + const agents = this._agents[accountId]; + + return agents ? agents.length : 0; + } + + /** + * @param {ObjectId} accountId. + * @param {Socket} socket. + * @returns {int} connected agent count. + */ + removeAgentListener(accountId, socket) { + const sockets = this._browsers[accountId]; + + _.pull(sockets, socket); + } + + /** + * @param {ObjectId} accountId + * @returns {Promise.<Agent>} + */ + findAgent(accountId) { + if (!this._server) + return Promise.reject(new Error('Agent server not started yet!')); + + const agents = this._agents[accountId]; + + if (!agents || agents.length === 0) + return Promise.reject(new Error('Failed to connect to agent')); + + return Promise.resolve(agents[0]); + } + + /** + * Close connections for all user agents. + * @param {ObjectId} accountId + * @param {String} oldToken + */ + close(accountId, oldToken) { + if (!this._server) + return; + + const agentsForClose = this._agents[accountId]; + + const agentsForWarning = _.clone(agentsForClose); + + this._agents[accountId] = []; + + _.forEach(this._agents, (sockets) => _.pullAll(agentsForClose, sockets)); + + _.pullAll(agentsForWarning, agentsForClose); + + const msg = `Security token has been reset: ${oldToken}`; + + _.forEach(agentsForWarning, (socket) => socket._emit('agent:warning', msg)); + + _.forEach(agentsForClose, (socket) => socket._emit('agent:close', msg)); + + _.forEach(this._browsers[accountId], (socket) => socket.emit('agent:count', {count: 0})); + } + + /** + * @param {ObjectId} accountIds + * @param {Agent} agent + */ + _agentConnected(accountIds, agent) { + _.forEach(accountIds, (accountId) => { + let agents = this._agents[accountId]; + + if (!agents) + this._agents[accountId] = agents = []; + + agents.push(agent); + + const sockets = this._browsers[accountId]; + + _.forEach(sockets, (socket) => socket.emit('agent:count', {count: agents.length})); + }); + } + + /** + * @param {ObjectId} accountIds + * @param {Agent} agent + */ + _agentDisconnected(accountIds, agent) { + _.forEach(accountIds, (accountId) => { + const agents = this._agents[accountId]; + + if (agents && agents.length) + _.pull(agents, agent); + + const sockets = this._browsers[accountId]; + + _.forEach(sockets, (socket) => socket.emit('agent:count', {count: agents.length})); + }); + } + } + + return new AgentManager(); +};
http://git-wip-us.apache.org/repos/asf/ignite/blob/6af6560a/modules/web-console/backend/app/app.js ---------------------------------------------------------------------- diff --git a/modules/web-console/backend/app/app.js b/modules/web-console/backend/app/app.js new file mode 100644 index 0000000..1bbfd2c --- /dev/null +++ b/modules/web-console/backend/app/app.js @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +// Fire me up! + +module.exports = { + implements: 'app', + inject: ['require(express)', 'configure', 'routes'] +}; + +module.exports.factory = function(Express, configure, routes) { + return { + /** + * @param {Server} srv + */ + listen: (srv) => { + const app = new Express(); + + configure.express(app); + + routes.register(app); + + // Catch 404 and forward to error handler. + app.use((req, res, next) => { + const err = new Error('Not Found: ' + req.originalUrl); + + err.status = 404; + + next(err); + }); + + // Production error handler: no stacktraces leaked to user. + app.use((err, req, res) => { + res.status(err.status || 500); + + res.render('error', { + message: err.message, + error: {} + }); + }); + + srv.addListener('request', app); + } + }; +}; http://git-wip-us.apache.org/repos/asf/ignite/blob/6af6560a/modules/web-console/backend/app/browser.js ---------------------------------------------------------------------- diff --git a/modules/web-console/backend/app/browser.js b/modules/web-console/backend/app/browser.js new file mode 100644 index 0000000..3256b6a --- /dev/null +++ b/modules/web-console/backend/app/browser.js @@ -0,0 +1,404 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +// Fire me up! + +/** + * Module interaction with browsers. + */ +module.exports = { + implements: 'browser-manager', + inject: ['require(lodash)', 'require(socket.io)', 'agent-manager', 'configure'] +}; + +module.exports.factory = (_, socketio, agentMgr, configure) => { + const _errorToJson = (err) => { + return { + message: err.message || err, + code: err.code || 1 + }; + }; + + return { + attach: (server) => { + const io = socketio(server); + + configure.socketio(io); + + io.sockets.on('connection', (socket) => { + const user = socket.request.user; + + const demo = socket.request._query.IgniteDemoMode === 'true'; + + const accountId = () => user._id; + + // Return available drivers to browser. + socket.on('schemaImport:drivers', (cb) => { + agentMgr.findAgent(accountId()) + .then((agent) => agent.availableDrivers()) + .then((drivers) => cb(null, drivers)) + .catch((err) => cb(_errorToJson(err))); + }); + + // Return schemas from database to browser. + socket.on('schemaImport:schemas', (preset, cb) => { + agentMgr.findAgent(accountId()) + .then((agent) => { + const jdbcInfo = {user: preset.user, password: preset.password}; + + return agent.metadataSchemas(preset.jdbcDriverJar, preset.jdbcDriverClass, preset.jdbcUrl, jdbcInfo); + }) + .then((schemas) => cb(null, schemas)) + .catch((err) => cb(_errorToJson(err))); + }); + + // Return tables from database to browser. + socket.on('schemaImport:tables', (preset, cb) => { + agentMgr.findAgent(accountId()) + .then((agent) => { + const jdbcInfo = {user: preset.user, password: preset.password}; + + return agent.metadataTables(preset.jdbcDriverJar, preset.jdbcDriverClass, preset.jdbcUrl, jdbcInfo, + preset.schemas, preset.tablesOnly); + }) + .then((tables) => cb(null, tables)) + .catch((err) => cb(_errorToJson(err))); + }); + + // Return topology command result from grid to browser. + socket.on('node:topology', (attr, mtr, cb) => { + agentMgr.findAgent(accountId()) + .then((agent) => agent.topology(demo, attr, mtr)) + .then((clusters) => cb(null, clusters)) + .catch((err) => cb(_errorToJson(err))); + }); + + // Close query on node. + socket.on('node:query:close', (nid, queryId, cb) => { + agentMgr.findAgent(accountId()) + .then((agent) => agent.queryClose(demo, nid, queryId)) + .then(() => cb()) + .catch((err) => cb(_errorToJson(err))); + }); + + // Execute query on node and return first page to browser. + socket.on('node:query', (nid, cacheName, query, local, pageSize, cb) => { + agentMgr.findAgent(accountId()) + .then((agent) => agent.fieldsQuery(demo, nid, cacheName, query, local, pageSize)) + .then((res) => cb(null, res)) + .catch((err) => cb(_errorToJson(err))); + }); + + // Fetch next page for query and return result to browser. + socket.on('node:query:fetch', (nid, queryId, pageSize, cb) => { + agentMgr.findAgent(accountId()) + .then((agent) => agent.queryFetch(demo, nid, queryId, pageSize)) + .then((res) => cb(null, res)) + .catch((err) => cb(_errorToJson(err))); + }); + + // Execute query on node and return full result to browser. + socket.on('node:query:getAll', (nid, cacheName, query, local, cb) => { + // Set page size for query. + const pageSize = 1024; + + agentMgr.findAgent(accountId()) + .then((agent) => { + const firstPage = agent.fieldsQuery(demo, nid, cacheName, query, local, pageSize) + .then(({result}) => { + if (result.key) + return Promise.reject(result.key); + + return result.value; + }); + + const fetchResult = (acc) => { + if (!acc.hasMore) + return acc; + + return agent.queryFetch(demo, acc.responseNodeId, acc.queryId, pageSize) + .then((res) => { + acc.rows = acc.rows.concat(res.rows); + + acc.hasMore = res.hasMore; + + return fetchResult(acc); + }); + }; + + return firstPage + .then(fetchResult); + }) + .then((res) => cb(null, res)) + .catch((err) => cb(_errorToJson(err))); + }); + + // Return cache metadata from all nodes in grid. + socket.on('node:cache:metadata', (cacheName, cb) => { + agentMgr.findAgent(accountId()) + .then((agent) => agent.metadata(demo, cacheName)) + .then((caches) => { + let types = []; + + const _compact = (className) => { + return className.replace('java.lang.', '').replace('java.util.', '').replace('java.sql.', ''); + }; + + const _typeMapper = (meta, typeName) => { + const maskedName = _.isEmpty(meta.cacheName) ? '<default>' : meta.cacheName; + + let fields = meta.fields[typeName]; + + let columns = []; + + for (const fieldName in fields) { + if (fields.hasOwnProperty(fieldName)) { + const fieldClass = _compact(fields[fieldName]); + + columns.push({ + type: 'field', + name: fieldName, + clazz: fieldClass, + system: fieldName === '_KEY' || fieldName === '_VAL', + cacheName: meta.cacheName, + typeName, + maskedName + }); + } + } + + const indexes = []; + + for (const index of meta.indexes[typeName]) { + fields = []; + + for (const field of index.fields) { + fields.push({ + type: 'index-field', + name: field, + order: index.descendings.indexOf(field) < 0, + unique: index.unique, + cacheName: meta.cacheName, + typeName, + maskedName + }); + } + + if (fields.length > 0) { + indexes.push({ + type: 'index', + name: index.name, + children: fields, + cacheName: meta.cacheName, + typeName, + maskedName + }); + } + } + + columns = _.sortBy(columns, 'name'); + + if (!_.isEmpty(indexes)) { + columns = columns.concat({ + type: 'indexes', + name: 'Indexes', + cacheName: meta.cacheName, + typeName, + maskedName, + children: indexes + }); + } + + return { + type: 'type', + cacheName: meta.cacheName || '', + typeName, + maskedName, + children: columns + }; + }; + + for (const meta of caches) { + const cacheTypes = meta.types.map(_typeMapper.bind(null, meta)); + + if (!_.isEmpty(cacheTypes)) + types = types.concat(cacheTypes); + } + + return cb(null, types); + }) + .catch((err) => cb(_errorToJson(err))); + }); + + // Fetch next page for query and return result to browser. + socket.on('node:visor:collect', (evtOrderKey, evtThrottleCntrKey, cb) => { + agentMgr.findAgent(accountId()) + .then((agent) => agent.collect(demo, evtOrderKey, evtThrottleCntrKey)) + .then((data) => { + if (data.finished) + return cb(null, data.result); + + cb(_errorToJson(data.error)); + }) + .catch((err) => cb(_errorToJson(err))); + }); + + // Gets node configuration for specified node. + socket.on('node:configuration', (nid, cb) => { + agentMgr.findAgent(accountId()) + .then((agent) => agent.collectNodeConfiguration(demo, nid)) + .then((data) => { + if (data.finished) + return cb(null, data.result); + + cb(_errorToJson(data.error)); + }) + .catch((err) => cb(_errorToJson(err))); + }); + + // Gets cache configurations for specified node and caches deployment IDs. + socket.on('cache:configuration', (nid, caches, cb) => { + agentMgr.findAgent(accountId()) + .then((agent) => agent.collectCacheConfigurations(demo, nid, caches)) + .then((data) => { + if (data.finished) + return cb(null, data.result); + + cb(_errorToJson(data.error)); + }) + .catch((err) => cb(_errorToJson(err))); + }); + + // Swap backups specified caches on specified node and return result to browser. + socket.on('node:cache:swap:backups', (nid, cacheNames, cb) => { + agentMgr.findAgent(accountId()) + .then((agent) => agent.cacheSwapBackups(demo, nid, cacheNames)) + .then((data) => { + if (data.finished) + return cb(null, data.result); + + cb(_errorToJson(data.error)); + }) + .catch((err) => cb(_errorToJson(err))); + }); + + // Reset metrics specified cache on specified node and return result to browser. + socket.on('node:cache:reset:metrics', (nid, cacheName, cb) => { + agentMgr.findAgent(accountId()) + .then((agent) => agent.cacheResetMetrics(demo, nid, cacheName)) + .then((data) => { + if (data.finished) + return cb(null, data.result); + + cb(_errorToJson(data.error)); + }) + .catch((err) => cb(_errorToJson(err))); + }); + + // Clear specified cache on specified node and return result to browser. + socket.on('node:cache:clear', (nid, cacheName, cb) => { + agentMgr.findAgent(accountId()) + .then((agent) => agent.cacheClear(demo, nid, cacheName)) + .then((data) => { + if (data.finished) + return cb(null, data.result); + + cb(_errorToJson(data.error)); + }) + .catch((err) => cb(_errorToJson(err))); + }); + + // Start specified cache and return result to browser. + socket.on('node:cache:start', (nids, near, cacheName, cfg, cb) => { + agentMgr.findAgent(accountId()) + .then((agent) => agent.cacheStart(demo, nids, near, cacheName, cfg)) + .then((data) => { + if (data.finished) + return cb(null, data.result); + + cb(_errorToJson(data.error)); + }) + .catch((err) => cb(_errorToJson(err))); + }); + + // Stop specified cache on specified node and return result to browser. + socket.on('node:cache:stop', (nid, cacheName, cb) => { + agentMgr.findAgent(accountId()) + .then((agent) => agent.cacheStop(demo, nid, cacheName)) + .then((data) => { + if (data.finished) + return cb(null, data.result); + + cb(_errorToJson(data.error)); + }) + .catch((err) => cb(_errorToJson(err))); + }); + + + // Ping node and return result to browser. + socket.on('node:ping', (taskNid, nid, cb) => { + agentMgr.findAgent(accountId()) + .then((agent) => agent.ping(demo, taskNid, nid)) + .then((data) => { + if (data.finished) + return cb(null, data.result); + + cb(_errorToJson(data.error)); + }) + .catch((err) => cb(_errorToJson(err))); + }); + + // GC node and return result to browser. + socket.on('node:gc', (nids, cb) => { + agentMgr.findAgent(accountId()) + .then((agent) => agent.gc(demo, nids)) + .then((data) => { + if (data.finished) + return cb(null, data.result); + + cb(_errorToJson(data.error)); + }) + .catch((err) => cb(_errorToJson(err))); + }); + + // GC node and return result to browser. + socket.on('node:thread:dump', (nid, cb) => { + agentMgr.findAgent(accountId()) + .then((agent) => agent.threadDump(demo, nid)) + .then((data) => { + if (data.finished) + return cb(null, data.result); + + cb(_errorToJson(data.error)); + }) + .catch((err) => cb(_errorToJson(err))); + }); + + const count = agentMgr.addAgentListener(user._id, socket); + + socket.emit('agent:count', {count}); + }); + + // Handle browser disconnect event. + io.sockets.on('disconnect', (socket) => + agentMgr.removeAgentListener(socket.client.request.user._id, socket) + ); + } + }; +}; http://git-wip-us.apache.org/repos/asf/ignite/blob/6af6560a/modules/web-console/backend/app/configure.js ---------------------------------------------------------------------- diff --git a/modules/web-console/backend/app/configure.js b/modules/web-console/backend/app/configure.js new file mode 100644 index 0000000..7624bdd --- /dev/null +++ b/modules/web-console/backend/app/configure.js @@ -0,0 +1,86 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +// Fire me up! + +/** + * Module for configuration express and websocket server. + */ +module.exports = { + implements: 'configure', + inject: ['require(lodash)', 'require(morgan)', 'require(cookie-parser)', 'require(body-parser)', + 'require(express-session)', 'require(connect-mongo)', 'require(passport)', 'require(passport.socketio)', 'settings', 'mongo', 'middlewares:*'] +}; + +module.exports.factory = function(_, logger, cookieParser, bodyParser, session, connectMongo, passport, passportSocketIo, settings, mongo, apis) { + const _sessionStore = new (connectMongo(session))({mongooseConnection: mongo.connection}); + + return { + express: (app) => { + app.use(logger('dev', { + skip: (req, res) => res.statusCode < 400 + })); + + _.forEach(apis, (api) => app.use(api)); + + app.use(cookieParser(settings.sessionSecret)); + + app.use(bodyParser.json({limit: '50mb'})); + app.use(bodyParser.urlencoded({limit: '50mb', extended: true})); + + app.use(session({ + secret: settings.sessionSecret, + resave: false, + saveUninitialized: true, + unset: 'destroy', + cookie: { + expires: new Date(Date.now() + settings.cookieTTL), + maxAge: settings.cookieTTL + }, + store: _sessionStore + })); + + app.use(passport.initialize()); + app.use(passport.session()); + + passport.serializeUser(mongo.Account.serializeUser()); + passport.deserializeUser(mongo.Account.deserializeUser()); + + passport.use(mongo.Account.createStrategy()); + }, + socketio: (io) => { + const _onAuthorizeSuccess = (data, accept) => { + accept(null, true); + }; + + const _onAuthorizeFail = (data, message, error, accept) => { + accept(null, false); + }; + + io.use(passportSocketIo.authorize({ + cookieParser, + key: 'connect.sid', // the name of the cookie where express/connect stores its session_id + secret: settings.sessionSecret, // the session_secret to parse the cookie + store: _sessionStore, // we NEED to use a sessionstore. no memorystore please + success: _onAuthorizeSuccess, // *optional* callback on success - read more below + fail: _onAuthorizeFail // *optional* callback on fail/error - read more below + })); + } + }; +}; http://git-wip-us.apache.org/repos/asf/ignite/blob/6af6560a/modules/web-console/backend/app/index.js ---------------------------------------------------------------------- diff --git a/modules/web-console/backend/app/index.js b/modules/web-console/backend/app/index.js new file mode 100644 index 0000000..5796318 --- /dev/null +++ b/modules/web-console/backend/app/index.js @@ -0,0 +1,116 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +import fs from 'fs'; +import path from 'path'; +import http from 'http'; +import https from 'https'; + +const igniteModules = process.env.IGNITE_MODULES || './ignite_modules'; + +let injector; + +try { + const igniteModulesInjector = path.resolve(path.join(igniteModules, 'backend', 'injector.js')); + + fs.accessSync(igniteModulesInjector, fs.F_OK); + + injector = require(igniteModulesInjector); +} catch (ignore) { + injector = require(path.join(__dirname, '../injector')); +} + +/** + * Event listener for HTTP server "error" event. + */ +const _onError = (port, error) => { + if (error.syscall !== 'listen') + throw error; + + const bind = typeof port === 'string' ? 'Pipe ' + port : 'Port ' + port; + + // Handle specific listen errors with friendly messages. + switch (error.code) { + case 'EACCES': + console.error(bind + ' requires elevated privileges'); + process.exit(1); + + break; + case 'EADDRINUSE': + console.error(bind + ' is already in use'); + process.exit(1); + + break; + default: + throw error; + } +}; + +/** + * Event listener for HTTP server "listening" event. + */ +const _onListening = (addr) => { + const bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port; + + console.log('Start listening on ' + bind); +}; + +Promise.all([injector('settings'), injector('app'), injector('agent-manager'), injector('browser-manager')]) + .then(([settings, app, agentMgr, browserMgr]) => { + // Start rest server. + const server = settings.server.SSLOptions + ? https.createServer(settings.server.SSLOptions) : http.createServer(); + + server.listen(settings.server.port); + server.on('error', _onError.bind(null, settings.server.port)); + server.on('listening', _onListening.bind(null, server.address())); + + app.listen(server); + browserMgr.attach(server); + + // Start legacy agent server for reject connection with message. + if (settings.agent.legacyPort) { + const agentLegacySrv = settings.agent.SSLOptions + ? https.createServer(settings.agent.SSLOptions) : http.createServer(); + + agentLegacySrv.listen(settings.agent.legacyPort); + agentLegacySrv.on('error', _onError.bind(null, settings.agent.legacyPort)); + agentLegacySrv.on('listening', _onListening.bind(null, agentLegacySrv.address())); + + agentMgr.attachLegacy(agentLegacySrv); + } + + // Start agent server. + const agentServer = settings.agent.SSLOptions + ? https.createServer(settings.agent.SSLOptions) : http.createServer(); + + agentServer.listen(settings.agent.port); + agentServer.on('error', _onError.bind(null, settings.agent.port)); + agentServer.on('listening', _onListening.bind(null, agentServer.address())); + + agentMgr.attach(agentServer); + + // Used for automated test. + if (process.send) + process.send('running'); + }).catch((err) => { + console.error(err); + + process.exit(1); + }); http://git-wip-us.apache.org/repos/asf/ignite/blob/6af6560a/modules/web-console/backend/app/mongo.js ---------------------------------------------------------------------- diff --git a/modules/web-console/backend/app/mongo.js b/modules/web-console/backend/app/mongo.js new file mode 100644 index 0000000..7fe39f0 --- /dev/null +++ b/modules/web-console/backend/app/mongo.js @@ -0,0 +1,673 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +// Fire me up! + +/** + * Module mongo schema. + */ +module.exports = { + implements: 'mongo', + inject: ['require(passport-local-mongoose)', 'settings', 'ignite_modules/mongo:*', 'require(mongoose)'] +}; + +module.exports.factory = function(passportMongo, settings, pluginMongo, mongoose) { + // Use native promises + mongoose.Promise = global.Promise; + + // Connect to mongoDB database. + mongoose.connect(settings.mongoUrl, {server: {poolSize: 4}}); + + const Schema = mongoose.Schema; + const ObjectId = mongoose.Schema.Types.ObjectId; + const result = { connection: mongoose.connection }; + + result.ObjectId = ObjectId; + + // Define Account schema. + const AccountSchema = new Schema({ + firstName: String, + lastName: String, + email: String, + company: String, + country: String, + lastLogin: Date, + admin: Boolean, + token: String, + resetPasswordToken: String + }); + + // Install passport plugin. + AccountSchema.plugin(passportMongo, { + usernameField: 'email', limitAttempts: true, lastLoginField: 'lastLogin', + usernameLowerCase: true + }); + + // Configure transformation to JSON. + AccountSchema.set('toJSON', { + transform: (doc, ret) => { + return { + _id: ret._id, + email: ret.email, + firstName: ret.firstName, + lastName: ret.lastName, + company: ret.company, + country: ret.country, + admin: ret.admin, + token: ret.token, + lastLogin: ret.lastLogin + }; + } + }); + + result.errCodes = { + DUPLICATE_KEY_ERROR: 11000, + DUPLICATE_KEY_UPDATE_ERROR: 11001 + }; + // Define Account model. + result.Account = mongoose.model('Account', AccountSchema); + + // Define Space model. + result.Space = mongoose.model('Space', new Schema({ + name: String, + owner: {type: ObjectId, ref: 'Account'}, + demo: {type: Boolean, default: false}, + usedBy: [{ + permission: {type: String, enum: ['VIEW', 'FULL']}, + account: {type: ObjectId, ref: 'Account'} + }] + })); + + // Define Domain model schema. + const DomainModelSchema = new Schema({ + space: {type: ObjectId, ref: 'Space', index: true}, + caches: [{type: ObjectId, ref: 'Cache'}], + queryMetadata: {type: String, enum: ['Annotations', 'Configuration']}, + kind: {type: String, enum: ['query', 'store', 'both']}, + databaseSchema: String, + databaseTable: String, + keyType: String, + valueType: {type: String}, + keyFields: [{ + databaseFieldName: String, + databaseFieldType: String, + javaFieldName: String, + javaFieldType: String + }], + valueFields: [{ + databaseFieldName: String, + databaseFieldType: String, + javaFieldName: String, + javaFieldType: String + }], + fields: [{name: String, className: String}], + aliases: [{field: String, alias: String}], + indexes: [{ + name: String, + indexType: {type: String, enum: ['SORTED', 'FULLTEXT', 'GEOSPATIAL']}, + fields: [{name: String, direction: Boolean}] + }], + demo: Boolean + }); + + DomainModelSchema.index({valueType: 1, space: 1}, {unique: true}); + + // Define model of Domain models. + result.DomainModel = mongoose.model('DomainModel', DomainModelSchema); + + // Define Cache schema. + const CacheSchema = new Schema({ + space: {type: ObjectId, ref: 'Space', index: true}, + name: {type: String}, + clusters: [{type: ObjectId, ref: 'Cluster'}], + domains: [{type: ObjectId, ref: 'DomainModel'}], + cacheMode: {type: String, enum: ['PARTITIONED', 'REPLICATED', 'LOCAL']}, + atomicityMode: {type: String, enum: ['ATOMIC', 'TRANSACTIONAL']}, + + nodeFilter: { + kind: {type: String, enum: ['Default', 'Exclude', 'IGFS', 'OnNodes', 'Custom']}, + Exclude: { + nodeId: String + }, + IGFS: { + igfs: {type: ObjectId, ref: 'Igfs'} + }, + OnNodes: { + nodeIds: [String] + }, + Custom: { + className: String + } + }, + + backups: Number, + memoryMode: {type: String, enum: ['ONHEAP_TIERED', 'OFFHEAP_TIERED', 'OFFHEAP_VALUES']}, + offHeapMaxMemory: Number, + startSize: Number, + swapEnabled: Boolean, + + evictionPolicy: { + kind: {type: String, enum: ['LRU', 'FIFO', 'SORTED']}, + LRU: { + batchSize: Number, + maxMemorySize: Number, + maxSize: Number + }, + FIFO: { + batchSize: Number, + maxMemorySize: Number, + maxSize: Number + }, + SORTED: { + batchSize: Number, + maxMemorySize: Number, + maxSize: Number + } + }, + + rebalanceMode: {type: String, enum: ['SYNC', 'ASYNC', 'NONE']}, + rebalanceBatchSize: Number, + rebalanceBatchesPrefetchCount: Number, + rebalanceOrder: Number, + rebalanceDelay: Number, + rebalanceTimeout: Number, + rebalanceThrottle: Number, + + cacheStoreFactory: { + kind: { + type: String, + enum: ['CacheJdbcPojoStoreFactory', 'CacheJdbcBlobStoreFactory', 'CacheHibernateBlobStoreFactory'] + }, + CacheJdbcPojoStoreFactory: { + dataSourceBean: String, + dialect: { + type: String, + enum: ['Generic', 'Oracle', 'DB2', 'SQLServer', 'MySQL', 'PostgreSQL', 'H2'] + } + }, + CacheJdbcBlobStoreFactory: { + connectVia: {type: String, enum: ['URL', 'DataSource']}, + connectionUrl: String, + user: String, + dataSourceBean: String, + dialect: { + type: String, + enum: ['Generic', 'Oracle', 'DB2', 'SQLServer', 'MySQL', 'PostgreSQL', 'H2'] + }, + initSchema: Boolean, + createTableQuery: String, + loadQuery: String, + insertQuery: String, + updateQuery: String, + deleteQuery: String + }, + CacheHibernateBlobStoreFactory: { + hibernateProperties: [String] + } + }, + storeKeepBinary: Boolean, + loadPreviousValue: Boolean, + readThrough: Boolean, + writeThrough: Boolean, + + writeBehindEnabled: Boolean, + writeBehindBatchSize: Number, + writeBehindFlushSize: Number, + writeBehindFlushFrequency: Number, + writeBehindFlushThreadCount: Number, + + invalidate: Boolean, + defaultLockTimeout: Number, + atomicWriteOrderMode: {type: String, enum: ['CLOCK', 'PRIMARY']}, + writeSynchronizationMode: {type: String, enum: ['FULL_SYNC', 'FULL_ASYNC', 'PRIMARY_SYNC']}, + + sqlEscapeAll: Boolean, + sqlSchema: String, + sqlOnheapRowCacheSize: Number, + longQueryWarningTimeout: Number, + sqlFunctionClasses: [String], + snapshotableIndex: Boolean, + statisticsEnabled: Boolean, + managementEnabled: Boolean, + readFromBackup: Boolean, + copyOnRead: Boolean, + maxConcurrentAsyncOperations: Number, + nearCacheEnabled: Boolean, + nearConfiguration: { + nearStartSize: Number, + nearEvictionPolicy: { + kind: {type: String, enum: ['LRU', 'FIFO', 'SORTED']}, + LRU: { + batchSize: Number, + maxMemorySize: Number, + maxSize: Number + }, + FIFO: { + batchSize: Number, + maxMemorySize: Number, + maxSize: Number + }, + SORTED: { + batchSize: Number, + maxMemorySize: Number, + maxSize: Number + } + } + }, + demo: Boolean + }); + + CacheSchema.index({name: 1, space: 1}, {unique: true}); + + // Define Cache model. + result.Cache = mongoose.model('Cache', CacheSchema); + + const IgfsSchema = new Schema({ + space: {type: ObjectId, ref: 'Space', index: true}, + name: {type: String}, + clusters: [{type: ObjectId, ref: 'Cluster'}], + affinnityGroupSize: Number, + blockSize: Number, + streamBufferSize: Number, + dataCacheName: String, + metaCacheName: String, + defaultMode: {type: String, enum: ['PRIMARY', 'PROXY', 'DUAL_SYNC', 'DUAL_ASYNC']}, + dualModeMaxPendingPutsSize: Number, + dualModePutExecutorService: String, + dualModePutExecutorServiceShutdown: Boolean, + fragmentizerConcurrentFiles: Number, + fragmentizerEnabled: Boolean, + fragmentizerThrottlingBlockLength: Number, + fragmentizerThrottlingDelay: Number, + ipcEndpointConfiguration: { + type: {type: String, enum: ['SHMEM', 'TCP']}, + host: String, + port: Number, + memorySize: Number, + tokenDirectoryPath: String, + threadCount: Number + }, + ipcEndpointEnabled: Boolean, + maxSpaceSize: Number, + maximumTaskRangeLength: Number, + managementPort: Number, + pathModes: [{path: String, mode: {type: String, enum: ['PRIMARY', 'PROXY', 'DUAL_SYNC', 'DUAL_ASYNC']}}], + perNodeBatchSize: Number, + perNodeParallelBatchCount: Number, + prefetchBlocks: Number, + sequentialReadsBeforePrefetch: Number, + trashPurgeTimeout: Number, + secondaryFileSystemEnabled: Boolean, + secondaryFileSystem: { + uri: String, + cfgPath: String, + userName: String + }, + colocateMetadata: Boolean, + relaxedConsistency: Boolean + }); + + IgfsSchema.index({name: 1, space: 1}, {unique: true}); + + // Define IGFS model. + result.Igfs = mongoose.model('Igfs', IgfsSchema); + + // Define Cluster schema. + const ClusterSchema = new Schema({ + space: {type: ObjectId, ref: 'Space', index: true}, + name: {type: String}, + localHost: String, + discovery: { + localAddress: String, + localPort: Number, + localPortRange: Number, + addressResolver: String, + socketTimeout: Number, + ackTimeout: Number, + maxAckTimeout: Number, + networkTimeout: Number, + joinTimeout: Number, + threadPriority: Number, + heartbeatFrequency: Number, + maxMissedHeartbeats: Number, + maxMissedClientHeartbeats: Number, + topHistorySize: Number, + listener: String, + dataExchange: String, + metricsProvider: String, + reconnectCount: Number, + statisticsPrintFrequency: Number, + ipFinderCleanFrequency: Number, + authenticator: String, + forceServerMode: Boolean, + clientReconnectDisabled: Boolean, + kind: {type: String, enum: ['Vm', 'Multicast', 'S3', 'Cloud', 'GoogleStorage', 'Jdbc', 'SharedFs', 'ZooKeeper']}, + Vm: { + addresses: [String] + }, + Multicast: { + multicastGroup: String, + multicastPort: Number, + responseWaitTime: Number, + addressRequestAttempts: Number, + localAddress: String, + addresses: [String] + }, + S3: { + bucketName: String + }, + Cloud: { + credential: String, + credentialPath: String, + identity: String, + provider: String, + regions: [String], + zones: [String] + }, + GoogleStorage: { + projectName: String, + bucketName: String, + serviceAccountP12FilePath: String, + serviceAccountId: String, + addrReqAttempts: String + }, + Jdbc: { + initSchema: Boolean, + dataSourceBean: String, + dialect: { + type: String, + enum: ['Generic', 'Oracle', 'DB2', 'SQLServer', 'MySQL', 'PostgreSQL', 'H2'] + } + }, + SharedFs: { + path: String + }, + ZooKeeper: { + curator: String, + zkConnectionString: String, + retryPolicy: { + kind: {type: String, enum: ['ExponentialBackoff', 'BoundedExponentialBackoff', 'UntilElapsed', + 'NTimes', 'OneTime', 'Forever', 'Custom']}, + ExponentialBackoff: { + baseSleepTimeMs: Number, + maxRetries: Number, + maxSleepMs: Number + }, + BoundedExponentialBackoff: { + baseSleepTimeMs: Number, + maxSleepTimeMs: Number, + maxRetries: Number + }, + UntilElapsed: { + maxElapsedTimeMs: Number, + sleepMsBetweenRetries: Number + }, + NTimes: { + n: Number, + sleepMsBetweenRetries: Number + }, + OneTime: { + sleepMsBetweenRetry: Number + }, + Forever: { + retryIntervalMs: Number + }, + Custom: { + className: String + } + }, + basePath: String, + serviceName: String, + allowDuplicateRegistrations: Boolean + } + }, + atomicConfiguration: { + backups: Number, + cacheMode: {type: String, enum: ['LOCAL', 'REPLICATED', 'PARTITIONED']}, + atomicSequenceReserveSize: Number + }, + binaryConfiguration: { + idMapper: String, + nameMapper: String, + serializer: String, + typeConfigurations: [{ + typeName: String, + idMapper: String, + nameMapper: String, + serializer: String, + enum: Boolean + }], + compactFooter: Boolean + }, + caches: [{type: ObjectId, ref: 'Cache'}], + clockSyncSamples: Number, + clockSyncFrequency: Number, + deploymentMode: {type: String, enum: ['PRIVATE', 'ISOLATED', 'SHARED', 'CONTINUOUS']}, + discoveryStartupDelay: Number, + igfsThreadPoolSize: Number, + igfss: [{type: ObjectId, ref: 'Igfs'}], + includeEventTypes: [String], + managementThreadPoolSize: Number, + marshaller: { + kind: {type: String, enum: ['OptimizedMarshaller', 'JdkMarshaller']}, + OptimizedMarshaller: { + poolSize: Number, + requireSerializable: Boolean + } + }, + marshalLocalJobs: Boolean, + marshallerCacheKeepAliveTime: Number, + marshallerCacheThreadPoolSize: Number, + metricsExpireTime: Number, + metricsHistorySize: Number, + metricsLogFrequency: Number, + metricsUpdateFrequency: Number, + networkTimeout: Number, + networkSendRetryDelay: Number, + networkSendRetryCount: Number, + communication: { + listener: String, + localAddress: String, + localPort: Number, + localPortRange: Number, + sharedMemoryPort: Number, + directBuffer: Boolean, + directSendBuffer: Boolean, + idleConnectionTimeout: Number, + connectTimeout: Number, + maxConnectTimeout: Number, + reconnectCount: Number, + socketSendBuffer: Number, + socketReceiveBuffer: Number, + messageQueueLimit: Number, + slowClientQueueLimit: Number, + tcpNoDelay: Boolean, + ackSendThreshold: Number, + unacknowledgedMessagesBufferSize: Number, + socketWriteTimeout: Number, + selectorsCount: Number, + addressResolver: String + }, + connector: { + enabled: Boolean, + jettyPath: String, + host: String, + port: Number, + portRange: Number, + idleTimeout: Number, + idleQueryCursorTimeout: Number, + idleQueryCursorCheckFrequency: Number, + receiveBufferSize: Number, + sendBufferSize: Number, + sendQueueLimit: Number, + directBuffer: Boolean, + noDelay: Boolean, + selectorCount: Number, + threadPoolSize: Number, + messageInterceptor: String, + secretKey: String, + sslEnabled: Boolean, + sslClientAuth: Boolean, + sslFactory: String + }, + peerClassLoadingEnabled: Boolean, + peerClassLoadingLocalClassPathExclude: [String], + peerClassLoadingMissedResourcesCacheSize: Number, + peerClassLoadingThreadPoolSize: Number, + publicThreadPoolSize: Number, + swapSpaceSpi: { + kind: {type: String, enum: ['FileSwapSpaceSpi']}, + FileSwapSpaceSpi: { + baseDirectory: String, + readStripesNumber: Number, + maximumSparsity: Number, + maxWriteQueueSize: Number, + writeBufferSize: Number + } + }, + systemThreadPoolSize: Number, + timeServerPortBase: Number, + timeServerPortRange: Number, + transactionConfiguration: { + defaultTxConcurrency: {type: String, enum: ['OPTIMISTIC', 'PESSIMISTIC']}, + defaultTxIsolation: {type: String, enum: ['READ_COMMITTED', 'REPEATABLE_READ', 'SERIALIZABLE']}, + defaultTxTimeout: Number, + pessimisticTxLogLinger: Number, + pessimisticTxLogSize: Number, + txSerializableEnabled: Boolean, + txManagerFactory: String + }, + sslEnabled: Boolean, + sslContextFactory: { + keyAlgorithm: String, + keyStoreFilePath: String, + keyStoreType: String, + protocol: String, + trustStoreFilePath: String, + trustStoreType: String, + trustManagers: [String] + }, + rebalanceThreadPoolSize: Number, + attributes: [{name: String, value: String}], + collision: { + kind: {type: String, enum: ['Noop', 'PriorityQueue', 'FifoQueue', 'JobStealing', 'Custom']}, + PriorityQueue: { + parallelJobsNumber: Number, + waitingJobsNumber: Number, + priorityAttributeKey: String, + jobPriorityAttributeKey: String, + defaultPriority: Number, + starvationIncrement: Number, + starvationPreventionEnabled: Boolean + }, + FifoQueue: { + parallelJobsNumber: Number, + waitingJobsNumber: Number + }, + JobStealing: { + activeJobsThreshold: Number, + waitJobsThreshold: Number, + messageExpireTime: Number, + maximumStealingAttempts: Number, + stealingEnabled: Boolean, + stealingAttributes: [{name: String, value: String}], + externalCollisionListener: String + }, + Custom: { + class: String + } + }, + failoverSpi: [{ + kind: {type: String, enum: ['JobStealing', 'Never', 'Always', 'Custom']}, + JobStealing: { + maximumFailoverAttempts: Number + }, + Always: { + maximumFailoverAttempts: Number + }, + Custom: { + class: String + } + }], + logger: { + kind: {type: 'String', enum: ['Log4j2', 'Null', 'Java', 'JCL', 'SLF4J', 'Log4j', 'Custom']}, + Log4j2: { + level: {type: String, enum: ['OFF', 'FATAL', 'ERROR', 'WARN', 'INFO', 'DEBUG', 'TRACE', 'ALL']}, + path: String + }, + Log4j: { + mode: {type: String, enum: ['Default', 'Path']}, + level: {type: String, enum: ['OFF', 'FATAL', 'ERROR', 'WARN', 'INFO', 'DEBUG', 'TRACE', 'ALL']}, + path: String + }, + Custom: { + class: String + } + }, + cacheKeyConfiguration: [{ + typeName: String, + affinityKeyFieldName: String + }] + }); + + ClusterSchema.index({name: 1, space: 1}, {unique: true}); + + // Define Cluster model. + result.Cluster = mongoose.model('Cluster', ClusterSchema); + + // Define Notebook schema. + const NotebookSchema = new Schema({ + space: {type: ObjectId, ref: 'Space', index: true}, + name: String, + expandedParagraphs: [Number], + paragraphs: [{ + name: String, + query: String, + editor: Boolean, + result: {type: String, enum: ['none', 'table', 'bar', 'pie', 'line', 'area']}, + pageSize: Number, + timeLineSpan: String, + hideSystemColumns: Boolean, + cacheName: String, + chartsOptions: {barChart: {stacked: Boolean}, areaChart: {style: String}}, + rate: { + value: Number, + unit: Number + } + }] + }); + + NotebookSchema.index({name: 1, space: 1}, {unique: true}); + + // Define Notebook model. + result.Notebook = mongoose.model('Notebook', NotebookSchema); + + result.handleError = function(res, err) { + // TODO IGNITE-843 Send error to admin + res.status(err.code || 500).send(err.message); + }; + + // Registering the routes of all plugin modules + for (const name in pluginMongo) { + if (pluginMongo.hasOwnProperty(name)) + pluginMongo[name].register(mongoose, result); + } + + return result; +}; http://git-wip-us.apache.org/repos/asf/ignite/blob/6af6560a/modules/web-console/backend/app/nconf.js ---------------------------------------------------------------------- diff --git a/modules/web-console/backend/app/nconf.js b/modules/web-console/backend/app/nconf.js new file mode 100644 index 0000000..c585ac6 --- /dev/null +++ b/modules/web-console/backend/app/nconf.js @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +// Fire me up! + +/** + * Module with server-side configuration. + */ +module.exports = { + implements: 'nconf', + inject: ['require(nconf)', 'require(fs)'] +}; + +module.exports.factory = function(nconf, fs) { + const default_config = './config/settings.json'; + const file = process.env.SETTINGS || default_config; + + nconf.env({separator: '_'}); + + try { + fs.accessSync(file, fs.F_OK); + + nconf.file({file}); + } catch (ignore) { + nconf.file({file: default_config}); + } + + if (process.env.CONFIG_PATH && fs.existsSync(process.env.CONFIG_PATH)) + nconf.file({file: process.env.CONFIG_PATH}); + + return nconf; +}; http://git-wip-us.apache.org/repos/asf/ignite/blob/6af6560a/modules/web-console/backend/app/routes.js ---------------------------------------------------------------------- diff --git a/modules/web-console/backend/app/routes.js b/modules/web-console/backend/app/routes.js new file mode 100644 index 0000000..6961173 --- /dev/null +++ b/modules/web-console/backend/app/routes.js @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +// Fire me up! + +module.exports = { + implements: 'routes', + inject: ['routes/public', 'routes/admin', 'routes/profiles', 'routes/demo', 'routes/clusters', 'routes/domains', + 'routes/caches', 'routes/igfss', 'routes/notebooks', 'routes/agents', 'routes/configurations'] +}; + +module.exports.factory = function(publicRoute, adminRoute, profilesRoute, demoRoute, + clustersRoute, domainsRoute, cachesRoute, igfssRoute, notebooksRoute, agentsRoute, configurationsRoute) { + return { + register: (app) => { + const _mustAuthenticated = (req, res, next) => { + if (req.isAuthenticated()) + return next(); + + res.status(401).send('Access denied. You are not authorized to access this page.'); + }; + + const _adminOnly = (req, res, next) => { + if (req.isAuthenticated() && req.user.admin) + return next(); + + res.status(401).send('Access denied. You are not authorized to access this page.'); + }; + + // Registering the standard routes + app.use('/', publicRoute); + app.use('/admin', _mustAuthenticated, _adminOnly, adminRoute); + app.use('/profile', _mustAuthenticated, profilesRoute); + app.use('/demo', _mustAuthenticated, demoRoute); + + app.all('/configuration/*', _mustAuthenticated); + + app.use('/configuration', configurationsRoute); + app.use('/configuration/clusters', clustersRoute); + app.use('/configuration/domains', domainsRoute); + app.use('/configuration/caches', cachesRoute); + app.use('/configuration/igfs', igfssRoute); + + app.use('/notebooks', _mustAuthenticated, notebooksRoute); + app.use('/agent', _mustAuthenticated, agentsRoute); + } + }; +};
