You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by vo...@apache.org on 2016/09/13 09:53:36 UTC

[44/69] [abbrv] ignite git commit: Web Console beta-3.

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);
+        }
+    };
+};