Authored by 陈峰

merge

  1 +
  2 +import Memcached from 'memcached';
  3 +import ws from '../../lib/ws';
  4 +import _ from 'lodash';
  5 +
  6 +class ApiCache {
  7 +
  8 + constructor(host) {
  9 + this.host = host;
  10 + this.memcached = new Memcached(host);
  11 + }
  12 +
  13 + delKey(key) {
  14 + this._log(`deleting ${key}`)
  15 +
  16 + this.memcached.del(key, (err) => {
  17 + if (err) {
  18 + this._log(`deleted ${key} fail`)
  19 + } else {
  20 + this._log(`deleted ${key} success`)
  21 + }
  22 + });
  23 + }
  24 +
  25 + flushAll() {
  26 + this.memcached.flush(err => {
  27 + if (err) {
  28 + this._log(`flush all fail.` + err.toString())
  29 + } else {
  30 + this._log(`flush all success`)
  31 + }
  32 + this.memcached.end();
  33 + });
  34 + }
  35 +
  36 + clean(key) {
  37 + let begin = new Date();
  38 +
  39 + let key1 = `apiCache:${key}`;
  40 + let key2 = `apiCache2:${key}`;
  41 +
  42 + let count = 0;
  43 +
  44 + this.memcached.items( ( err, result ) => {
  45 + if( err ) console.error( err );
  46 +
  47 + if (result.length === 0) {
  48 + this._log('empty items')
  49 + }
  50 + // for each server...
  51 + result.forEach(itemSet => {
  52 +
  53 + var keys = Object.keys( itemSet );
  54 + keys.pop(); // we don't need the "server" key, but the other indicate the slab id's
  55 +
  56 + var len = keys.length;
  57 +
  58 + if (keys.length === 0) {
  59 + this._log('empty item set');
  60 + }
  61 +
  62 + keys.forEach(stats => {
  63 +
  64 + // get a cachedump for each slabid and slab.number
  65 + this.memcached.cachedump( itemSet.server, parseInt(stats, 10), itemSet[stats].number, ( err, response ) => {
  66 + // dump the shizzle
  67 +
  68 + if (response) {
  69 + if (_.isArray(response)) {
  70 + _.each(response, keyObj => {
  71 + count ++ ;
  72 + if (keyObj.key && (keyObj.key.indexOf(key1) >= 0 || keyObj.key.indexOf(key2) >= 0 || keyObj.key.indexOf(key) >= 0)) {
  73 + this.delKey(keyObj.key);
  74 + } else {
  75 + // this._log(`skip ${keyObj.key}`)
  76 + }
  77 + });
  78 + } else {
  79 + count ++;
  80 + if (response.key && (response.key.indexOf(key1) >= 0 || response.key.indexOf(key2) >= 0 || response.key.indexOf(key) >= 0)) {
  81 + this.delKey(response.key);
  82 + } else {
  83 + // this._log(`skip ${response.key}`)
  84 + }
  85 + }
  86 + }
  87 +
  88 + len --;
  89 +
  90 + if (len === 0) {
  91 + this.memcached.end();
  92 + let end = new Date();
  93 + this._log(`>>>>>> clean success in ${end.getTime() - begin.getTime()} , all keys: ${count}`)
  94 + }
  95 + })
  96 + })
  97 + })
  98 + });
  99 + }
  100 +
  101 + _log(message) {
  102 + ws.broadcast(`/api_cache/log`, {
  103 + host: this.host,
  104 + msg: message
  105 + });
  106 + }
  107 +}
  108 +
  109 +export default ApiCache;
  110 +
  1 +/**
  2 + * 分发部署
  3 + *
  4 + * @class Restart
  5 + * @author shenzm<zhimin.shen@yoho.cn>
  6 + * @date 2016/10/12
  7 + */
  8 +
  9 +import ssh from 'ssh2';
  10 +import path from 'path';
  11 +
  12 +import ws from '../../lib/ws';
  13 +import {
  14 + RestartInfo,
  15 + Server
  16 +} from '../models';
  17 +
  18 +class DeleteRestart {
  19 +
  20 + constructor(project) {
  21 + this.project = project;
  22 + }
  23 +
  24 + async deleteRestart(info) {
  25 + let server = await Server.findByHost(info.host);
  26 + this.server = server;
  27 + this.info = info;
  28 + this.sshDeleteRestart({
  29 + host: server.host,
  30 + username: server.username,
  31 + password: server.password,
  32 + port: server.port
  33 + });
  34 + }
  35 +
  36 + sshDeleteRestart(serverInfo) {
  37 + console.log('ssh connecting', serverInfo);
  38 + let conn = new ssh.Client();
  39 + let self = this;
  40 + conn.on('ready', async() => {
  41 + console.log(`connected ${serverInfo.host}`);
  42 + try {
  43 + await self._delete(conn);
  44 + await self._restart(conn);
  45 + conn.end();
  46 + } catch (e) {
  47 + self._state('fail');
  48 + self._log(e);
  49 + }
  50 + }).on('error', (err) => {
  51 + self._state('fail');
  52 + self._log(err);
  53 + }).connect(serverInfo);
  54 + }
  55 +
  56 + _delete(conn) {
  57 + let self = this;
  58 + let script = `pm2 delete ${self.project.name}`;
  59 + return new Promise((resolve, reject) => {
  60 + self._state('deleteing');
  61 + self._log(`>>>> ${script}`);
  62 + conn.exec(`cd ${self.remoteRunningDir} && ${script}`, (err, stream) => {
  63 + if (err) {
  64 + reject(err);
  65 + } else {
  66 + stream.stdout.on('data', (data) => {
  67 + self._log(data.toString());
  68 + });
  69 + stream.stderr.on('data', (data) => {
  70 + self._log(data.toString());
  71 + });
  72 + stream.on('exit', (code) => {
  73 + if (code === 0) {
  74 + self._state('deleted');
  75 + resolve();
  76 + } else {
  77 + reject('delete fail: ' + script);
  78 + }
  79 + });
  80 + }
  81 + });
  82 + });
  83 + }
  84 +
  85 + _restart(conn) {
  86 + let self = this;
  87 + let startup = this.project.scripts.start;
  88 + return new Promise((resolve, reject) => {
  89 + self._state('restarting');
  90 + self._log(`>>>> ${startup}`);
  91 + conn.exec(`cd ${self.remoteRunningDir} && ${startup}`, (err, stream) => {
  92 + if (err) {
  93 + reject(err);
  94 + } else {
  95 + stream.stdout.on('data', (data) => {
  96 + self._log(data.toString());
  97 + });
  98 + stream.stderr.on('data', (data) => {
  99 + self._log(data.toString());
  100 + });
  101 + stream.on('exit', (code) => {
  102 + if (code === 0) {
  103 + self._state('running');
  104 + resolve();
  105 + } else {
  106 + reject('restart fail');
  107 + }
  108 + });
  109 + }
  110 + });
  111 + });
  112 + }
  113 +
  114 + async _state(state) {
  115 + ws.broadcast(`/restart/${this.project._id}`, {
  116 + host: this.info.host,
  117 + state: state
  118 + });
  119 + await RestartInfo.updateState(this.info._id, state);
  120 + }
  121 +
  122 + _log(msg) {
  123 + console.log("log: ", msg);
  124 + ws.broadcast(`/restart/${this.project._id}/log`, {
  125 + host: this.info.host,
  126 + msg: msg
  127 + });
  128 + }
  129 +
  130 + get remoteRunningDir() {
  131 + return path.join(this.server.deployDir, this.project.name, 'current', this.project.name);
  132 + }
  133 +}
  134 +
  135 +export default DeleteRestart;
@@ -160,7 +160,7 @@ class Deploy { @@ -160,7 +160,7 @@ class Deploy {
160 }); 160 });
161 }); 161 });
162 } 162 }
163 - 163 +
164 async _state(state) { 164 async _state(state) {
165 ws.broadcast(`/deploy/${this.project._id}`, { 165 ws.broadcast(`/deploy/${this.project._id}`, {
166 host: this.info.host, 166 host: this.info.host,
@@ -170,6 +170,7 @@ class Deploy { @@ -170,6 +170,7 @@ class Deploy {
170 } 170 }
171 171
172 _log(msg) { 172 _log(msg) {
  173 + console.log(msg)
173 ws.broadcast(`/deploy/${this.project._id}/log`, { 174 ws.broadcast(`/deploy/${this.project._id}/log`, {
174 host: this.info.host, 175 host: this.info.host,
175 msg: msg 176 msg: msg
  1 +/**
  2 + * 分发部署
  3 + *
  4 + * @class Restart
  5 + * @author shenzm<zhimin.shen@yoho.cn>
  6 + * @date 2016/10/12
  7 + */
  8 +
  9 +import ssh from 'ssh2';
  10 +import path from 'path';
  11 +
  12 +import ws from '../../lib/ws';
  13 +import {
  14 + RestartInfo,
  15 + Server
  16 +} from '../models';
  17 +
  18 +class Restart {
  19 +
  20 + constructor(project) {
  21 + this.project = project;
  22 + }
  23 +
  24 + async restart(info) {
  25 + let server = await Server.findByHost(info.host);
  26 + this.server = server;
  27 + this.info = info;
  28 + this.sshRestart({
  29 + host: server.host,
  30 + username: server.username,
  31 + password: server.password,
  32 + port: server.port
  33 + });
  34 + }
  35 +
  36 + sshRestart(serverInfo) {
  37 + console.log('ssh connecting', serverInfo);
  38 + let conn = new ssh.Client();
  39 + let self = this;
  40 + conn.on('ready', async() => {
  41 + console.log(`connected ${serverInfo.host}`);
  42 + try {
  43 + await self._restart(conn);
  44 + conn.end();
  45 + } catch (e) {
  46 + self._state('fail');
  47 + self._log(e);
  48 + }
  49 + }).on('error', (err) => {
  50 + self._state('fail');
  51 + self._log(err);
  52 + }).connect(serverInfo);
  53 + }
  54 +
  55 + _restart(conn) {
  56 + let self = this;
  57 + let startup = this.project.scripts.start;
  58 + return new Promise((resolve, reject) => {
  59 + self._state('restarting');
  60 + self._log(`>>>> ${startup}`);
  61 + conn.exec(`cd ${self.remoteRunningDir} && ${startup}`, (err, stream) => {
  62 + if (err) {
  63 + reject(err);
  64 + } else {
  65 + stream.stdout.on('data', (data) => {
  66 + self._log(data.toString());
  67 + });
  68 + stream.stderr.on('data', (data) => {
  69 + self._log(data.toString());
  70 + });
  71 + stream.on('exit', (code) => {
  72 + if (code === 0) {
  73 + self._state('running');
  74 + resolve();
  75 + } else {
  76 + reject('restart fail');
  77 + }
  78 + });
  79 + }
  80 + });
  81 + });
  82 + }
  83 +
  84 + async _state(state) {
  85 + ws.broadcast(`/restart/${this.project._id}`, {
  86 + host: this.info.host,
  87 + state: state
  88 + });
  89 + await RestartInfo.updateState(this.info._id, state);
  90 + }
  91 +
  92 + _log(msg) {
  93 + console.log(msg);
  94 + ws.broadcast(`/restart/${this.project._id}/log`, {
  95 + host: this.info.host,
  96 + msg: msg
  97 + });
  98 + }
  99 +
  100 + get remoteRunningDir() {
  101 + return path.join(this.server.deployDir, this.project.name, 'current', this.project.name);
  102 + }
  103 +}
  104 +
  105 +export default Restart;
  1 +'use strict';
  2 +
  3 +import Model from './model';
  4 +import _ from 'lodash';
  5 +
  6 +const defaultDegrades = [
  7 + {
  8 + path: '/pc/common/disMyYohoHover',
  9 + name: '【禁止】公共头部>>> MY有货鼠标移入显示账户信息'
  10 + },
  11 + {
  12 + path: '/pc/common/removeCartCount',
  13 + name: '【移除】公共头部>>> 购物车图标显示的购物车商品数目'
  14 + },
  15 + {
  16 + path: '/pc/common/disSearchAssociation',
  17 + name: '【禁止】公共头部>>> 搜索框执行搜索联想'
  18 + },
  19 + {
  20 + path: '/pc/common/disCartHover',
  21 + name: '【禁止】公共头部>>> 购物车图标鼠标移入请求且不显示购物车商品列表'
  22 + },
  23 + {
  24 + path: '/pc/brands/disBrandNameHover',
  25 + name: '【禁止】品牌一览>>> 品牌名移入显示品牌简介Tip'
  26 + },
  27 + {
  28 + path: '/pc/product/removeRecentView',
  29 + name: '【移除】商品列表/商品详情>>> 最近浏览'
  30 + },
  31 + {
  32 + path: '/pc/guang/removeHotTag',
  33 + name: '【移除】逛>>> 热门标签'
  34 + },
  35 + {
  36 + path: '/pc/guang/removeAd',
  37 + name: '【移除】逛>>> 广告banner'
  38 + },
  39 + {
  40 + path: '/pc/guang/removeItemComment',
  41 + name: '【移除】逛>>> 详情页评论'
  42 + },
  43 + //wap
  44 + {
  45 + path: '/wap/plustar/removeCollect',
  46 + name: '【移除】Plustar>>> 品牌收藏'
  47 + },
  48 + {
  49 + path: '/wap/plustar/removeRelatedPost',
  50 + name: '【移除】Plustar>>> 相关资讯'
  51 + },
  52 + {
  53 + path: '/wap/search/removeHotSearch',
  54 + name: '【移除】搜索>>> 热门搜索'
  55 + },
  56 + {
  57 + path: '/wap/ucenter/removePrefer',
  58 + name: '【移除】个人中心>>> 为您优选'
  59 + },
  60 + {
  61 + path: '/wap/common/removeCartCount',
  62 + name: '【移除】公共>>> 购物车icon商品数目'
  63 + },
  64 + {
  65 + path: '/wap/cart/removePrefer',
  66 + name: '【移除】购物车>>> 为您优选新品'
  67 + }
  68 +];
  69 +
  70 +class Degrade extends Model {
  71 +
  72 + constructor() {
  73 + super('degrade');
  74 + }
  75 +
  76 + async init() {
  77 + for (let i of defaultDegrades) {
  78 + let count = await this.count({
  79 + path: i.path
  80 + });
  81 +
  82 + if (count === 0) {
  83 + await this.insert(i);
  84 + }
  85 + }
  86 + }
  87 +}
  88 +
  89 +export default Degrade;
  1 +'use strict';
  2 +
  3 +import Model from './model';
  4 +
  5 +class DegradeServer extends Model {
  6 + constructor() {
  7 + super('degrade_server');
  8 + }
  9 +}
  10 +
  11 +export default DegradeServer;
  1 +'use strict';
  2 +
  3 +import Model from './model';
  4 +
  5 +class DeleteRestart extends Model {
  6 +
  7 + constructor() {
  8 + super('delete_restart');
  9 + }
  10 +
  11 + async updateState(id, state) {
  12 + await this.update({
  13 + _id: id
  14 + }, {
  15 + $set: {
  16 + state: state
  17 + }
  18 + });
  19 + }
  20 +}
  21 +
  22 +export default DeleteRestart;
@@ -5,12 +5,17 @@ import ServerModel from './server'; @@ -5,12 +5,17 @@ import ServerModel from './server';
5 import BuildingModel from './building'; 5 import BuildingModel from './building';
6 import ProjectModel from './project'; 6 import ProjectModel from './project';
7 import DeployModel from './deploy'; 7 import DeployModel from './deploy';
  8 +import RestartModel from './restart';
  9 +import DeleteRestartModel from './delete_restart';
8 import UserModel from './user'; 10 import UserModel from './user';
9 import HotfixModel from './hotfix'; 11 import HotfixModel from './hotfix';
10 import OperationLoggerModel from './operation_logger'; 12 import OperationLoggerModel from './operation_logger';
11 import PageCacheModel from './page_cache'; 13 import PageCacheModel from './page_cache';
12 import CdnCacheModel from './cdn_cache'; 14 import CdnCacheModel from './cdn_cache';
13 import ProductCacheModel from './product_cache'; 15 import ProductCacheModel from './product_cache';
  16 +import MemcachedHostModel from './memcached_host';
  17 +import DegradeModel from './degrade';
  18 +import DegradeServerModel from './degrade_server';
14 19
15 shelljs.mkdir('-p', config.dbDir); 20 shelljs.mkdir('-p', config.dbDir);
16 21
@@ -18,18 +23,25 @@ const Server = new ServerModel(); @@ -18,18 +23,25 @@ const Server = new ServerModel();
18 const Building = new BuildingModel(); 23 const Building = new BuildingModel();
19 const Project = new ProjectModel(); 24 const Project = new ProjectModel();
20 const DeployInfo = new DeployModel(); 25 const DeployInfo = new DeployModel();
  26 +const RestartInfo = new RestartModel();
  27 +const DeleteRestartInfo = new DeleteRestartModel();
21 const User = new UserModel(); 28 const User = new UserModel();
22 const Hotfix = new HotfixModel(); 29 const Hotfix = new HotfixModel();
23 const OperationLogger = new OperationLoggerModel(); 30 const OperationLogger = new OperationLoggerModel();
24 const PageCache = new PageCacheModel(); 31 const PageCache = new PageCacheModel();
25 const CdnCache = new CdnCacheModel(); 32 const CdnCache = new CdnCacheModel();
26 const ProductCache = new ProductCacheModel(); 33 const ProductCache = new ProductCacheModel();
  34 +const MemcachedHost = new MemcachedHostModel();
  35 +const Degrade = new DegradeModel();
  36 +const DegradeServer = new DegradeServerModel();
27 37
28 User.init(); 38 User.init();
29 PageCache.init(); 39 PageCache.init();
30 CdnCache.init(); 40 CdnCache.init();
31 ProductCache.init(); 41 ProductCache.init();
32 42
  43 +Degrade.init();
  44 +
33 export { 45 export {
34 Server, 46 Server,
35 Building, 47 Building,
@@ -40,5 +52,10 @@ export { @@ -40,5 +52,10 @@ export {
40 OperationLogger, 52 OperationLogger,
41 PageCache, 53 PageCache,
42 CdnCache, 54 CdnCache,
43 - ProductCache 55 + ProductCache,
  56 + MemcachedHost,
  57 + RestartInfo,
  58 + DeleteRestartInfo,
  59 + Degrade,
  60 + DegradeServer
44 }; 61 };
  1 +/**
  2 + *
  3 + * @author: jiangfeng<jeff.jiang@yoho.cn>
  4 + * @date: 16/8/22
  5 + */
  6 +
  7 +'use strict';
  8 +
  9 +import Model from './model';
  10 +
  11 +class MemcachedHost extends Model {
  12 +
  13 + constructor() {
  14 + super('memecached_host');
  15 + }
  16 +
  17 +}
  18 +
  19 +export default MemcachedHost;
  1 +'use strict';
  2 +
  3 +import Model from './model';
  4 +
  5 +class Restart extends Model {
  6 +
  7 + constructor() {
  8 + super('restart');
  9 + }
  10 +
  11 + async updateState(id, state) {
  12 + await this.update({
  13 + _id: id
  14 + }, {
  15 + $set: {
  16 + state: state
  17 + }
  18 + });
  19 + }
  20 +}
  21 +
  22 +export default Restart;
  1 +
  2 +
  3 +'use strict';
  4 +
  5 +import Router from 'koa-router';
  6 +import _ from 'lodash';
  7 +
  8 +import {
  9 + MemcachedHost
  10 +} from '../../models';
  11 +
  12 +import ApiCache from '../../ci/api_cache';
  13 +
  14 +let r = new Router();
  15 +
  16 +const api_cache = {
  17 + async index(ctx) {
  18 + let hosts = await MemcachedHost.findAll();
  19 +
  20 + await ctx.render('action/api_cache', {hosts: hosts});
  21 + },
  22 +
  23 + async addHost(ctx) {
  24 + let q = ctx.request.body;
  25 +
  26 + await MemcachedHost.insert({host: q.host});
  27 + return ctx.body = {
  28 + code: 200
  29 + };
  30 + },
  31 +
  32 + async delHost(ctx) {
  33 + let q = ctx.request.body;
  34 +
  35 + await MemcachedHost.removeById(q.id);
  36 + return ctx.body = {
  37 + code: 200
  38 + };
  39 + },
  40 +
  41 + async cleanKey(ctx) {
  42 + let q = ctx.request.body;
  43 + let key = q.key;
  44 +
  45 + let hosts = await MemcachedHost.findAll();
  46 +
  47 + _.each(hosts, (h) => {
  48 + (new ApiCache(h.host)).clean(key);
  49 + });
  50 +
  51 + return ctx.body = {
  52 + code: 200
  53 + };
  54 + },
  55 +
  56 + async flushAll(ctx) {
  57 + let hosts = await MemcachedHost.findAll();
  58 +
  59 + _.each(hosts, (h) => {
  60 + (new ApiCache(h.host)).flushAll();
  61 + });
  62 +
  63 + return ctx.body = {
  64 + code: 200
  65 + };
  66 + }
  67 +};
  68 +
  69 +r.get('/', api_cache.index);
  70 +r.post('/host/add', api_cache.addHost);
  71 +r.post('/host/del', api_cache.delHost);
  72 +r.post('/clean', api_cache.cleanKey);
  73 +r.post('/flush', api_cache.flushAll);
  74 +
  75 +export default r;
  1 +'use strict';
  2 +
  3 +import Router from 'koa-router';
  4 +import moment from 'moment';
  5 +import _ from 'lodash';
  6 +
  7 +import {Degrade, DegradeServer} from '../../models';
  8 +
  9 +import getter from '../../zookeeper/getter';
  10 +import setter from '../../zookeeper/setter';
  11 +
  12 +const router = new Router();
  13 +
  14 +const ctl = {
  15 + async getServer() {
  16 + let server = await DegradeServer.findAll({});
  17 +
  18 + server = _.last(server);
  19 +
  20 + if (server) {
  21 + return `${server.ip}:${server.port}`;
  22 + } else {
  23 + return 'localhost:2181';
  24 + }
  25 + },
  26 + async index (ctx) {
  27 + let count = await DegradeServer.count({});
  28 + let render;
  29 +
  30 + if (count) {
  31 + let serverPath = await ctl.getServer();
  32 + let degrades = await Degrade.findAll();
  33 +
  34 + for (let i of degrades) {
  35 + i.checked = await getter(serverPath, i.path);
  36 + }
  37 +
  38 + let pc = _.filter(degrades, o => _.startsWith(o.path, '/pc'));
  39 + let wap = _.filter(degrades, o => _.startsWith(o.path, '/wap'));
  40 +
  41 + let serverSplit = serverPath.split(':');
  42 + render = {
  43 + ip: serverSplit[0],
  44 + port: serverSplit[1],
  45 + render: {
  46 + pc: pc,
  47 + wap: wap
  48 + }
  49 + }
  50 + }
  51 +
  52 + await ctx.render('action/degrade', render);
  53 + },
  54 + async server(ctx) {
  55 + let ip = ctx.request.body.ip;
  56 + let port = ctx.request.body.port;
  57 +
  58 + let serverCount = await DegradeServer.count({});
  59 +
  60 + // keep one server
  61 + if (serverCount) {
  62 + let serverConfig = await DegradeServer.findAll({});
  63 + let id = _.last(serverConfig)._id; // get the latest item
  64 +
  65 + await DegradeServer.update({
  66 + _id: id
  67 + }, {
  68 + $set: {
  69 + ip: ip,
  70 + port: port
  71 + }
  72 + });
  73 + } else {
  74 + await DegradeServer.insert({
  75 + ip: ip,
  76 + port: port
  77 + });
  78 + }
  79 +
  80 + ctx.body = {
  81 + code: 200,
  82 + message: `${serverCount ? 'update' : 'new'} server success`
  83 + };
  84 + },
  85 + async setter(ctx) {
  86 + let {checked, id} = ctx.query;
  87 +
  88 + let theDegrade = await Degrade.findById(id);
  89 +
  90 + let path = theDegrade.path;
  91 +
  92 + let serverPath = await ctl.getServer();
  93 +
  94 + await setter(serverPath, path, checked.toString());
  95 +
  96 + ctx.body = {
  97 + code: 200,
  98 + message: 'update success'
  99 + };
  100 + }
  101 +};
  102 +
  103 +router.get('/', ctl.index);
  104 +router.post('/server', ctl.server);
  105 +router.get('/setter', ctl.setter);
  106 +
  107 +export default router;
@@ -2,15 +2,21 @@ @@ -2,15 +2,21 @@
2 2
3 import Router from 'koa-router'; 3 import Router from 'koa-router';
4 import moment from 'moment'; 4 import moment from 'moment';
  5 +import Rp from 'request-promise';
5 import Build from '../../ci/build'; 6 import Build from '../../ci/build';
6 import Deploy from '../../ci/deploy'; 7 import Deploy from '../../ci/deploy';
  8 +import Restart from '../../ci/restart';
  9 +import DeleteRestart from '../../ci/delete_restart';
7 import Operation from '../../logger/operation'; 10 import Operation from '../../logger/operation';
  11 +import ws from '../../../lib/ws';
8 12
9 import { 13 import {
10 Building, 14 Building,
11 Project, 15 Project,
12 Server, 16 Server,
13 - DeployInfo 17 + DeployInfo,
  18 + RestartInfo,
  19 + DeleteRestartInfo
14 } from '../../models'; 20 } from '../../models';
15 21
16 let r = new Router(); 22 let r = new Router();
@@ -132,10 +138,16 @@ const p = { @@ -132,10 +138,16 @@ const p = {
132 }, { 138 }, {
133 $set: project 139 $set: project
134 }); 140 });
135 - await Operation.action(ctx.session.user, 'EDIT_PROJECT_INFO', '修改项目信息', {_id: id, name: project.name}); 141 + await Operation.action(ctx.session.user, 'EDIT_PROJECT_INFO', '修改项目信息', {
  142 + _id: id,
  143 + name: project.name
  144 + });
136 } else { 145 } else {
137 await Project.insert(project); 146 await Project.insert(project);
138 - await Operation.action(ctx.session.user, 'NEW_PROJECT_INFO', '新增项目信息', {_id: id, name: project.name}); 147 + await Operation.action(ctx.session.user, 'NEW_PROJECT_INFO', '新增项目信息', {
  148 + _id: id,
  149 + name: project.name
  150 + });
139 } 151 }
140 ctx.redirect('/projects'); 152 ctx.redirect('/projects');
141 ctx.status = 301; 153 ctx.status = 301;
@@ -148,7 +160,7 @@ const p = { @@ -148,7 +160,7 @@ const p = {
148 projectId: pid 160 projectId: pid
149 }).sort({ 161 }).sort({
150 buildTime: -1 162 buildTime: -1
151 - }).exec(); 163 + }).limit(30).exec();
152 buildings.forEach(b => { 164 buildings.forEach(b => {
153 b.updatedAt = moment(b.updatedAt).format('YYYY-MM-DD HH:mm:ss'); 165 b.updatedAt = moment(b.updatedAt).format('YYYY-MM-DD HH:mm:ss');
154 }); 166 });
@@ -183,7 +195,12 @@ const p = { @@ -183,7 +195,12 @@ const p = {
183 195
184 build.run(id); 196 build.run(id);
185 197
186 - await Operation.action(ctx.session.user, 'NEW_PROJECT_BUILDING', '新增项目构建', {_id: id, project: p.name, branch: branch, env: env}); 198 + await Operation.action(ctx.session.user, 'NEW_PROJECT_BUILDING', '新增项目构建', {
  199 + _id: id,
  200 + project: p.name,
  201 + branch: branch,
  202 + env: env
  203 + });
187 204
188 ctx.body = { 205 ctx.body = {
189 code: 200, 206 code: 200,
@@ -225,7 +242,12 @@ const p = { @@ -225,7 +242,12 @@ const p = {
225 } 242 }
226 }); 243 });
227 244
228 - await Operation.action(ctx.session.user, 'PROJECT_DEPLOY', '项目分发部署', {_id: buildingId, project: project.name, branch: building.branch, env: building.env}); 245 + await Operation.action(ctx.session.user, 'PROJECT_DEPLOY', '项目分发部署', {
  246 + _id: buildingId,
  247 + project: project.name,
  248 + branch: building.branch,
  249 + env: building.env
  250 + });
229 251
230 ctx.body = { 252 ctx.body = {
231 code: 200, 253 code: 200,
@@ -237,6 +259,182 @@ const p = { @@ -237,6 +259,182 @@ const p = {
237 msg: '该版本未构建成功,暂不能分发' 259 msg: '该版本未构建成功,暂不能分发'
238 }; 260 };
239 } 261 }
  262 + },
  263 + project_restart: async(ctx) => {
  264 + const projectId = ctx.request.body.id;
  265 + const host = ctx.request.body.host;
  266 + const env = ctx.request.body.env;
  267 + const project = await Project.findById(projectId);
  268 +
  269 + if (!project) {
  270 + ctx.body = {
  271 + code: 201,
  272 + msg: '该项目不存在'
  273 + };
  274 + return;
  275 + }
  276 +
  277 + let hosts = [];
  278 + if (host === 'all') {
  279 + // 全部重启
  280 + hosts = project.deploy[env].target;
  281 + } else {
  282 + // 单台重启
  283 + hosts.push(host);
  284 + }
  285 +
  286 + hosts.forEach(async(host) => {
  287 + let doc = await DeployInfo.findOne({
  288 + projectId: projectId,
  289 + host: host,
  290 + env: env
  291 + });
  292 +
  293 + if (!doc) {
  294 + return;
  295 + }
  296 +
  297 + let info = {
  298 + projectId: projectId,
  299 + host: host,
  300 + env: env,
  301 + createdAt: new Date(),
  302 + updatedAt: new Date(),
  303 + state: 'waiting'
  304 + };
  305 +
  306 + let restartDoc = await RestartInfo.insert(info);
  307 + let restart = new Restart(project);
  308 +
  309 + info._id = restartDoc[0]._id;
  310 + restart.restart(info);
  311 +
  312 + await Operation.action(ctx.session.user, 'PROJECT_RESTART', '项目重启', {
  313 + _id: info._id,
  314 + project: project.name,
  315 + branch: project.deploy[env].branchName,
  316 + env: env
  317 + });
  318 + });
  319 +
  320 + ctx.body = {
  321 + code: 200
  322 + };
  323 + },
  324 + project_deleterestart: async(ctx) => {
  325 + const projectId = ctx.request.body.id;
  326 + const host = ctx.request.body.host;
  327 + const env = ctx.request.body.env;
  328 + const project = await Project.findById(projectId);
  329 +
  330 + if (!project) {
  331 + ctx.body = {
  332 + code: 201,
  333 + msg: '该项目不存在'
  334 + };
  335 + return;
  336 + }
  337 +
  338 + let hosts = [];
  339 + if (host === 'all') {
  340 + // 全部重启
  341 + hosts = project.deploy[env].target;
  342 + } else {
  343 + // 单台重启
  344 + hosts.push(host);
  345 + }
  346 +
  347 + hosts.forEach(async(host) => {
  348 + let doc = await DeployInfo.findOne({
  349 + projectId: projectId,
  350 + host: host,
  351 + env: env
  352 + });
  353 +
  354 + if (!doc) {
  355 + return;
  356 + }
  357 +
  358 + let info = {
  359 + projectId: projectId,
  360 + host: host,
  361 + env: env,
  362 + createdAt: new Date(),
  363 + updatedAt: new Date(),
  364 + state: 'waiting'
  365 + };
  366 +
  367 + let delrestartDoc = await DeleteRestartInfo.insert(info);
  368 + let deleteRestart = new DeleteRestart(project);
  369 +
  370 + info._id = delrestartDoc[0]._id;
  371 + deleteRestart.deleteRestart(info);
  372 +
  373 + await Operation.action(ctx.session.user, 'PROJECT_DELETERESTART', '项目删除重启', {
  374 + _id: info._id,
  375 + project: project.name,
  376 + branch: project.deploy[env].branchName,
  377 + env: env
  378 + });
  379 + });
  380 +
  381 + ctx.body = {
  382 + code: 200
  383 + };
  384 + },
  385 + project_monit: async(ctx) => {
  386 + const projectId = ctx.request.body.id;
  387 + const env = ctx.request.body.env;
  388 + const project = await Project.findById(projectId);
  389 + if (!project) {
  390 + ctx.body = {
  391 + code: 201,
  392 + msg: '该项目不存在'
  393 + };
  394 + return;
  395 + }
  396 +
  397 + const hosts = project.deploy[env].target;
  398 + hosts.forEach((host) => {
  399 + var obj = {
  400 + 'total': 0,
  401 + 'status': {},
  402 + };
  403 +
  404 + Rp({
  405 + uri: `http://${host}:9615`,
  406 + json: true
  407 + }).then(function(data) {
  408 + var processes = data.processes || [];
  409 + processes.forEach(function(p) {
  410 + if (p.name === 'pm2-server-monit') {
  411 + obj.cpuUsg = p.pm2_env.axm_monitor['CPU usage'].value;
  412 + obj.freeMen = p.pm2_env.axm_monitor['Free memory'].value;
  413 + }
  414 +
  415 + if (p.name === project.name) {
  416 + obj.total++;
  417 +
  418 + if (!obj.status[p.pm2_env.status]) {
  419 + obj.status[p.pm2_env.status] = 1;
  420 + } else {
  421 + obj.status[p.pm2_env.status]++;
  422 + }
  423 + }
  424 + });
  425 + }).catch(function(err) {
  426 + obj.errmsg = '获取监控状态失败'
  427 + }).finally(function() {
  428 + ws.broadcast(`/monit/${projectId}`, {
  429 + host: host,
  430 + monitdata: obj
  431 + });
  432 + });
  433 + });
  434 +
  435 + ctx.body = {
  436 + code: 200
  437 + };
240 } 438 }
241 }; 439 };
242 440
@@ -248,4 +446,7 @@ r.get('/:id/buildings', p.buildings_table); @@ -248,4 +446,7 @@ r.get('/:id/buildings', p.buildings_table);
248 r.post('/save', p.save); 446 r.post('/save', p.save);
249 r.post('/build/:pid', p.project_build); 447 r.post('/build/:pid', p.project_build);
250 r.post('/deploy/:building', p.project_deploy); 448 r.post('/deploy/:building', p.project_deploy);
  449 +r.post('/restart', p.project_restart);
  450 +r.post('/deleterestart', p.project_deleterestart);
  451 +r.post('/monit', p.project_monit);
251 export default r; 452 export default r;
@@ -12,6 +12,8 @@ import operationLog from './actions/operation_log'; @@ -12,6 +12,8 @@ import operationLog from './actions/operation_log';
12 import pageCache from './actions/page_cache'; 12 import pageCache from './actions/page_cache';
13 import cdnCache from './actions/cdn_cache'; 13 import cdnCache from './actions/cdn_cache';
14 import productCache from './actions/product_cache'; 14 import productCache from './actions/product_cache';
  15 +import apiCache from './actions/api_cache';
  16 +import degrade from './actions/degrade';
15 17
16 const noAuth = new Router(); 18 const noAuth = new Router();
17 const base = new Router(); 19 const base = new Router();
@@ -40,6 +42,8 @@ export default function (app) { @@ -40,6 +42,8 @@ export default function (app) {
40 base.use('/page_cache', pageCache.routes(), pageCache.allowedMethods()); 42 base.use('/page_cache', pageCache.routes(), pageCache.allowedMethods());
41 base.use('/cdn_cache', cdnCache.routes(), cdnCache.allowedMethods()); 43 base.use('/cdn_cache', cdnCache.routes(), cdnCache.allowedMethods());
42 base.use('/product_cache', productCache.routes(), productCache.allowedMethods()); 44 base.use('/product_cache', productCache.routes(), productCache.allowedMethods());
  45 + base.use('/api_cache', apiCache.routes(), apiCache.allowedMethods());
  46 + base.use('/degrade', degrade.routes(), degrade.allowedMethods());
43 47
44 base.use('', index.routes(), index.allowedMethods()); 48 base.use('', index.routes(), index.allowedMethods());
45 49
  1 +<div class="pageheader">
  2 + <div class="media">
  3 + <div class="pageicon pull-left">
  4 + <i class="fa fa-th-list"></i>
  5 + </div>
  6 + <div class="media-body">
  7 + <ul class="breadcrumb">
  8 + <li><a href=""><i class="glyphicon glyphicon-home"></i></a></li>
  9 + <li>缓存管理</li>
  10 + </ul>
  11 + <h4>Api缓存清理</h4>
  12 + </div>
  13 + </div>
  14 + <!-- media -->
  15 +</div>
  16 +<!-- pageheader -->
  17 +
  18 +<div class="contentpanel">
  19 +
  20 + <div class="row">
  21 + <div class="col-sm-4 col-md-3">
  22 + <div>
  23 + <button class="btn btn-danger" id="flush-btn">Flush All</button>
  24 + </div>
  25 +
  26 + <h4 class="md-title mb5">memcached列表</h4>
  27 + <div class="host-list">
  28 + {{#each hosts}}
  29 + <p><span>{{host}}</span><span class="fa fa-times pull-right host-remove" style="color: red; cursor: pointer;" data-id="{{_id}}" data-host="{{host}}"></span></p>
  30 + {{/each}}
  31 + </div>
  32 +
  33 + <div class="mb20"></div>
  34 +
  35 + <div>
  36 + <button class="btn btn-success" id="add-btn">添加</button>
  37 + </div>
  38 +
  39 + <br>
  40 +
  41 + </div><!-- col-sm-4 -->
  42 + <div class="col-sm-8 col-md-9">
  43 + <div class="panel">
  44 + <div class="panel-heading">
  45 + <div class="pull-right">
  46 + <a class="btn btn-success btn-rounded mr20 api-clean-btn">删除缓存</a>
  47 + </div>
  48 + <div class="panel-title">
  49 + 缓存key前缀:
  50 + <input type="text" style="width: 33%; height: 40px;" id="api-key">
  51 + </div>
  52 +
  53 +
  54 + </div><!-- panel-heading -->
  55 + <div class="panel-body yoho-log-dark">
  56 + <div class="results-list" id="api-cache-log">
  57 + </div><!-- results-list -->
  58 + </div><!-- panel-body -->
  59 + </div><!-- panel -->
  60 + </div><!-- col-sm-8 -->
  61 + </div><!-- row -->
  62 +
  63 +</div>
  64 +
  65 +<script>
  66 + $(document).on('ready pjax:success', function() {
  67 + $('#add-btn').click(function(){
  68 + var i = layer.prompt({
  69 + title: '请输入host,例如 127.0.0.1:11211'
  70 + }, function(host) {
  71 + $.post('/api_cache/host/add', {host: host}, function(ret) {
  72 + if (ret.code == 200) {
  73 + $('.host-list').append('<p>' + host + '</p>');
  74 + layer.close(i);
  75 + }
  76 + });
  77 + });
  78 + });
  79 +
  80 + $('.api-clean-btn').click(function() {
  81 + var key = $('#api-key').val();
  82 +
  83 + if (key) {
  84 + $.post('/api_cache/clean', {key: key}, function(ret) {
  85 + if (ret.code == 200) {
  86 + layer.msg('正在清理中');
  87 + }
  88 + });
  89 + } else {
  90 + layer.msg('请输入key');
  91 + }
  92 + });
  93 +
  94 + $('.host-remove').click(function(){
  95 + var id = $(this).data('id');
  96 + var host = $(this).data('host');
  97 + var parent = $(this).parent();
  98 + var i = layer.confirm('确定删除host:<code>' + host + '</code>吗?', {
  99 + btn: ['确定', '取消']
  100 + }, function() {
  101 + $.post('/api_cache/host/del', {id: id}, function(ret) {
  102 + if (ret.code === 200) {
  103 + $(parent).remove();
  104 + layer.close(i);
  105 + }
  106 + });
  107 + });
  108 + });
  109 +
  110 + $('#flush-btn').click(function(){
  111 + var i = layer.confirm('确定清楚所有缓存吗?', {
  112 + btn: ['确定', '取消']
  113 + }, function() {
  114 + $.post('/api_cache/flush', function(ret) {
  115 + if (ret.code === 200) {
  116 + layer.close(i);
  117 + }
  118 + });
  119 + });
  120 + });
  121 +
  122 + function layoutResize() {
  123 + $('.yoho-log-dark').height($('body').height() - 340);
  124 + }
  125 +
  126 + $(window).resize(function() {
  127 + layoutResize();
  128 + });
  129 +
  130 + layoutResize();
  131 +
  132 + var $logWrap = $('.yoho-log-dark');
  133 + var timer = 0;
  134 +
  135 + var ws = io();
  136 + ws.on('connect', function() {
  137 + ws.on('/api_cache/log', function(data) {
  138 + $('#api-cache-log').append('<p><span class="host">'+data.host+'</span><span class="message">'+data.msg+'</span></p>');
  139 +
  140 + if (timer == 0) {
  141 + timer = setTimeout(function(){
  142 + $logWrap.scrollTop($logWrap[0].scrollHeight);
  143 + timer = 0;
  144 + }, 1000);
  145 + }
  146 + });
  147 + });
  148 + ws.on('error', function() {
  149 + console.log('connect fail');
  150 + });
  151 + });
  152 +</script>
  1 +<style>
  2 + .degrade-page {
  3 + padding: 20px;
  4 + }
  5 +
  6 + .degrade-tab li {
  7 + cursor: pointer;
  8 + }
  9 +
  10 + .pc-degrade,
  11 + .wap-degrade {
  12 + list-style: none;
  13 + padding: 20px;
  14 + }
  15 +
  16 + .err-tip {
  17 + color: #a94442;
  18 + }
  19 +</style>
  20 +<div class="degrade-page">
  21 + <div class="panel panel-info">
  22 + <div class="panel-heading">zookeeper server</div>
  23 + <div class="panel-body">
  24 + <div class="form-inline">
  25 + <div class="form-group">
  26 + <label for="server-ip">Server IP</label>
  27 + <input id="server-ip" class="form-control" type="text" placeholder="input server ip" value="{{ip}}">
  28 + </div>
  29 + <div class="form-group">
  30 + <label for="server-port">Server Port</label>
  31 + <input id="server-port" class="form-control" type="text" placeholder="input server port" value="{{port}}">
  32 + </div>
  33 + <span id="server-sure" class="btn btn-default">确定</span>
  34 + <span id="err-tip" class="err-tip hide">
  35 + <i class="glyphicon glyphicon-remove-sign"></i>
  36 + 小哥,填错了吧
  37 + </span>
  38 + </div>
  39 + </div>
  40 + </div>
  41 + <div class="panel panel-danger">
  42 + <div class="panel-heading">degrade point</div>
  43 + <div id="pjax-container" class="panel-body">
  44 + {{#render}}
  45 + <ul id="degrade-tab" class="nav nav-tabs degrade-tab" role="tablist">
  46 + <li role="presentation" class="active">
  47 + <a>PC</a>
  48 + </li>
  49 + <li role="presentation">
  50 + <a>WAP</a>
  51 + </li>
  52 + </ul>
  53 +
  54 + <ul class="pc-degrade degrade-content">
  55 + {{#each pc}}
  56 + <li data-id="{{_id}}">
  57 + <div class="checkbox">
  58 + <label>
  59 + <input type="checkbox"{{#if checked}} checked{{/if}}>
  60 + {{name}}
  61 + </label>
  62 + </div>
  63 + </li>
  64 + {{/each}}
  65 + </ul>
  66 +
  67 + <ul class="wap-degrade degrade-content hide">
  68 + {{#each wap}}
  69 + <li data-id="{{_id}}">
  70 + <div class="checkbox">
  71 + <label>
  72 + <input type="checkbox"{{#if checked}} checked{{/if}}>
  73 + {{name}}
  74 + </label>
  75 + </div>
  76 + </li>
  77 + {{/each}}
  78 + </ul>
  79 + {{^}}
  80 + Waitting for connecting to server...
  81 + {{/render}}
  82 + </div>
  83 + </div>
  84 +</div>
  85 +<script>
  86 + $(function() {
  87 + var $ip = $('#server-ip');
  88 + var $port = $('#server-port');
  89 + var $tip = $('#err-tip');
  90 +
  91 + function validateIP(ip) {
  92 + if (ip === 'localhost' ||
  93 + /^(\d)+\.(\d)+\.(\d)+\.(\d)+$/.test(ip))
  94 + {
  95 + $ip.closest('.form-group').removeClass('has-error');
  96 + return true;
  97 + }
  98 +
  99 + $ip.closest('.form-group').addClass('has-error');
  100 + return false;
  101 + }
  102 +
  103 + function validatePort(port) {
  104 + if (/^[1-9]\d*$/.test(port) && +port >= 1 && +port <= 65535) {
  105 + $port.closest('.form-group').removeClass('has-error');
  106 + return true;
  107 + }
  108 +
  109 + $port.closest('.form-group').addClass('has-error');
  110 + return false;
  111 + }
  112 +
  113 + // server
  114 + $('#server-sure').click(function() {
  115 + var ip = $.trim($ip.val());
  116 + var port = $.trim($port.val());
  117 +
  118 + if (!validateIP(ip) || !validatePort(port)) {
  119 + $tip.removeClass('hide');
  120 + return;
  121 + }
  122 +
  123 + $tip.addClass('hide');
  124 +
  125 + var serverPrompt = confirm('小伙砸,你确认修改zookeeper的server到:' + ip + ':' + port + '吗?')
  126 +
  127 + if (!serverPrompt) {
  128 + return;
  129 + }
  130 +
  131 + $.ajax({
  132 + type: 'POST',
  133 + url: '/degrade/server',
  134 + data: {
  135 + ip: ip,
  136 + port: port
  137 + }
  138 + }).then(function(data) {
  139 + if (data.code === 200) {
  140 + $.pjax.reload('#pjax-container');
  141 + }
  142 + });
  143 + });
  144 +
  145 + $('#degrade-tab').on('click', 'li', function() {
  146 + var $this = $(this);
  147 +
  148 + if ($this.hasClass('active')) {
  149 + return;
  150 + }
  151 +
  152 + $('li', $('#degrade-tab')).toggleClass('active');
  153 +
  154 + var index = $this.index();
  155 +
  156 + if (index === 0) {
  157 +
  158 + //PC active
  159 + $('.pc-degrade').removeClass('hide');
  160 + $('.wap-degrade').addClass('hide');
  161 + } else {
  162 +
  163 + // wap active
  164 + $('.wap-degrade').removeClass('hide');
  165 + $('.pc-degrade').addClass('hide');
  166 + }
  167 + });
  168 +
  169 + // change
  170 + $('.degrade-content input[type="checkbox"]').change(function() {
  171 + var $checkbox = $(this),
  172 + $li = $checkbox.closest('li');
  173 +
  174 + var checked = $checkbox.prop('checked');
  175 +
  176 + var id = $li.data('id');
  177 +
  178 + $.ajax({
  179 + url: '/degrade/setter',
  180 + data: {
  181 + checked: checked,
  182 + id: id
  183 + }
  184 + });
  185 + });
  186 + })
  187 +</script>
@@ -10,6 +10,7 @@ @@ -10,6 +10,7 @@
10 <li>{{project.name}}</li> 10 <li>{{project.name}}</li>
11 </ul> 11 </ul>
12 <h4>{{project.name}} ({{project.subname}})</h4> 12 <h4>{{project.name}} ({{project.subname}})</h4>
  13 + <input id="info" type="hidden" data-id='{{project._id}}' data-env='{{deploy.env}}'>
13 </div> 14 </div>
14 </div> 15 </div>
15 <!-- media --> 16 <!-- media -->
@@ -47,6 +48,8 @@ @@ -47,6 +48,8 @@
47 <div class="panel-heading"> 48 <div class="panel-heading">
48 <h4>服务器信息</h4> 49 <h4>服务器信息</h4>
49 <p>点击状态Label,可以查看实时日志</p> 50 <p>点击状态Label,可以查看实时日志</p>
  51 + <button class="btn btn-info btn-xs restart-btn" data-host='all'>全部重启</button>
  52 + <button class="btn btn-warning btn-xs deleterestart-btn" data-host='all'>全部删除重启</button>
50 </div> 53 </div>
51 <div class="panel-body"> 54 <div class="panel-body">
52 <div class="row"> 55 <div class="row">
@@ -58,20 +61,35 @@ @@ -58,20 +61,35 @@
58 </div><!-- panel-btns --> 61 </div><!-- panel-btns -->
59 <div class="panel-icon"><i class="fa fa-cloud" style="padding-left:12px;"></i></div> 62 <div class="panel-icon"><i class="fa fa-cloud" style="padding-left:12px;"></i></div>
60 <div class="media-body"> 63 <div class="media-body">
61 - <h2 class="nomargin">{{host}}</h2>  
62 - <h5 class="md-title mt5">当前运行版本:&nbsp;<code>{{#if info}}{{info.building}}{{^}} 64 + <h2 class="nomargin">{{host}}</h2>
  65 + <h5 class="md-title mt5 version">当前运行版本:&nbsp;<code>{{#if info}}{{info.building}}{{^}}
63 未知部署{{/if}}</code></h5> 66 未知部署{{/if}}</code></h5>
64 </div><!-- media-body --> 67 </div><!-- media-body -->
65 <hr class="mt10 mb10"> 68 <hr class="mt10 mb10">
  69 + <div class="clearfix mt5" style="padding-left:10px;">
  70 + <button class="btn btn-info btn-xs restart-btn" data-host='{{host}}'>重启</button>
  71 + <button class="btn btn-warning btn-xs deleterestart-btn" data-host='{{host}}'>删除重启</button>
  72 + </div>
  73 + <div class="clearfix mt5" style="padding-left:10px;">
  74 + <h5 class="md-title mt10">服务器状态</h5>
  75 + <span class="label label-serstatus"></span>
  76 + </div>
66 <div class="clearfix mt5"> 77 <div class="clearfix mt5">
67 - <div class="col-xs-6 project-env" data-env="test"> 78 + <div class="col-xs-4 project-env" data-env="test">
68 <h5 class="md-title mt10">当前状态</h5> 79 <h5 class="md-title mt10">当前状态</h5>
  80 + </div>
  81 + <div class="col-xs-4">
  82 + <h5 class="md-title mt10">进程状态</h5>
  83 + </div>
  84 + </div>
  85 + <div class="clearfix mt5">
  86 + <div class="col-xs-4 project-env" data-env="test">
69 <span class="label label-success deploy-log-btn" data-host="{{host}}"><i 87 <span class="label label-success deploy-log-btn" data-host="{{host}}"><i
70 class="fa fa-spinner fa-spin fa-fw margin-bottom"></i> <b>{{#if info}}{{info.state}}{{^}} 88 class="fa fa-spinner fa-spin fa-fw margin-bottom"></i> <b>{{#if info}}{{info.state}}{{^}}
71 未知部署{{/if}}</b></span> 89 未知部署{{/if}}</b></span>
72 </div> 90 </div>
73 - <div class="col-xs-6">  
74 - 91 + <div class="col-xs-4">
  92 + <span class="label label-status"></span>
75 </div> 93 </div>
76 </div> 94 </div>
77 </div><!-- panel-body --> 95 </div><!-- panel-body -->
@@ -177,6 +195,49 @@ @@ -177,6 +195,49 @@
177 195
178 }); 196 });
179 197
  198 + $('.restart-btn').click(function(){
  199 + var id = $('#info').data('id');
  200 + var env = $('#info').data('env');
  201 + var host = $(this).data('host');
  202 +
  203 + layer.confirm('确定重启吗?', {
  204 + btn: ['确定', '取消']
  205 + }, function() {
  206 + $.post('/projects/restart', {
  207 + id: id,
  208 + host: host,
  209 + env: env
  210 + },function(ret) {
  211 + if (ret.code == 200) {
  212 + layer.msg('正在重启中');
  213 + } else {
  214 + layer.msg(ret.msg, {icon: 5});
  215 + }
  216 + });
  217 + });
  218 + });
  219 +
  220 + $('.deleterestart-btn').click(function(){
  221 + var id = $('#info').data('id');
  222 + var env = $('#info').data('env');
  223 + var host = $(this).data('host');
  224 +
  225 + layer.confirm('确定删除后重启吗?', {
  226 + btn: ['确定', '取消']
  227 + }, function() {
  228 + $.post('/projects/deleterestart', {
  229 + id: id,
  230 + host: host,
  231 + env: env
  232 + },function(ret) {
  233 + if (ret.code == 200) {
  234 + layer.msg('正在删除重启中');
  235 + } else {
  236 + layer.msg(ret.msg, {icon: 5});
  237 + }
  238 + });
  239 + });
  240 + })
180 241
181 $('.rollback-btn').click(function() { 242 $('.rollback-btn').click(function() {
182 layer.prompt({ 243 layer.prompt({
@@ -226,6 +287,45 @@ @@ -226,6 +287,45 @@
226 $('#d-' + data.host.replace(/\./g, '-')).find('b').text(data.state); 287 $('#d-' + data.host.replace(/\./g, '-')).find('b').text(data.state);
227 }); 288 });
228 289
  290 + ws.on('/restart/{{project._id}}', function(data) {
  291 + console.log(data);
  292 + $('#d-' + data.host.replace(/\./g, '-')).find('b').text(data.state);
  293 + });
  294 +
  295 + ws.on('/deleterestart/{{project._id}}', function(data) {
  296 + console.log(data);
  297 + $('#d-' + data.host.replace(/\./g, '-')).find('b').text(data.state);
  298 + });
  299 +
  300 + ws.on('/monit/{{project._id}}', function(data) {
  301 + console.log(data);
  302 + var label = $('#d-' + data.host.replace(/\./g, '-')).find('.label-status');
  303 + var servers = $('#d-' + data.host.replace(/\./g, '-')).find('.label-serstatus');
  304 +
  305 + data = data.monitdata;
  306 + if (data.errmsg) {
  307 + label.text(data.errmsg).removeClass('label-success').addClass('label-danger');
  308 + servers.text("获取失败").removeClass('label-success').addClass('label-danger');
  309 + } else {
  310 + var msg = "线程:" + data.total
  311 + for(var s in data.status) {
  312 + msg += '; [' + s + ']:' + data.status[s];
  313 + }
  314 + label.text(msg);
  315 +
  316 + if (data.total != data.status.online) {
  317 + label.removeClass('label-success').addClass('label-danger');
  318 + } else {
  319 + label.removeClass('label-danger').addClass('label-success');
  320 + }
  321 +
  322 + msg = "";
  323 + if (data.cpuUsg) msg += "CPU:" + data.cpuUsg + ";";
  324 + if (data.freeMen) msg += "可用内存:" + data.freeMen;
  325 + servers.text(msg).removeClass('label-danger').addClass('label-success');
  326 + }
  327 + });
  328 +
229 // ws.on('/deploy/{{project._id}}/log', function(data){ 329 // ws.on('/deploy/{{project._id}}/log', function(data){
230 // if(tag == data.host){ 330 // if(tag == data.host){
231 // cm.replaceRange("> " +data.msg+ "\n", {line: Infinity}); 331 // cm.replaceRange("> " +data.msg+ "\n", {line: Infinity});
@@ -235,5 +335,25 @@ @@ -235,5 +335,25 @@
235 ws.on('error', function() { 335 ws.on('error', function() {
236 console.log('connect fail'); 336 console.log('connect fail');
237 }); 337 });
  338 +
  339 + var projectid = $('#info').data('id');
  340 + var env = $('#info').data('env');
  341 + var monit = function() {
  342 + $.post('/projects/monit', {
  343 + id: projectid,
  344 + env: env
  345 + },function(ret) {
  346 + if (ret.code != 200) {
  347 + layer.msg(ret.msg, {icon: 5});
  348 + }
  349 + });
  350 + }
  351 + setInterval(function(){
  352 + // 监控服务状态(轮训)
  353 + monit();
  354 + }, 5000);
  355 +
  356 + monit();
  357 +
238 }); 358 });
239 </script> 359 </script>
@@ -28,8 +28,9 @@ @@ -28,8 +28,9 @@
28 <li class="parent"><a href=""><i class="fa fa-history"></i> <span>缓存管理</span></a> 28 <li class="parent"><a href=""><i class="fa fa-history"></i> <span>缓存管理</span></a>
29 <ul class="children"> 29 <ul class="children">
30 <li><a href="/page_cache/query">PageCahe清理</a></li> 30 <li><a href="/page_cache/query">PageCahe清理</a></li>
31 - <li><a href="/cdn_cache/query">CDN清理</a></li>  
32 - <li><a href="/product_cache/query">商品清理</a></li> 31 + {{!-- <li><a href="/cdn_cache/query">CDN清理</a></li>
  32 + <li><a href="/product_cache/query">商品清理</a></li> --}}
  33 + <li><a href="/api_cache">ApiCahe清理</a></li>
33 </ul> 34 </ul>
34 </li> 35 </li>
35 {{#if is_master}} 36 {{#if is_master}}
@@ -41,6 +42,7 @@ @@ -41,6 +42,7 @@
41 </ul> 42 </ul>
42 </li> 43 </li>
43 {{/if}} 44 {{/if}}
  45 + <li><a href="/degrade"><i class="fa fa-hand-o-down"></i> <span>降级配置</span></a></li>
44 </ul> 46 </ul>
45 47
46 </div> 48 </div>
  1 +'use strict';
  2 +
  3 +import zookeeper from 'node-zookeeper-client';
  4 +
  5 +module.exports = (server, path) => {
  6 + const client = zookeeper.createClient(server);
  7 +
  8 + client.once('connected', () => {
  9 + client.mkdirp(path, new Buffer('false'), (err, path) => {
  10 + if (err) {
  11 + console.log('Node %s create err', path, err.stack);
  12 + } else {
  13 + console.log('Node %s is created', path);
  14 + }
  15 +
  16 + client.close();
  17 + });
  18 + });
  19 +
  20 + client.connect();
  21 +};
  1 +'usu strict';
  2 +
  3 +import _ from 'lodash';
  4 +import zookeeper from 'node-zookeeper-client';
  5 +
  6 +import creator from './creator';
  7 +
  8 +const getter = (server, client, path, resolve, reject) => {
  9 + client.exists(path, (err, stat) => {
  10 + if (err) {
  11 + console.log('path %s exits error', path, err.stack);
  12 + resolve(false);
  13 + return;
  14 + }
  15 +
  16 + if (stat) {
  17 + client.getData(
  18 + path,
  19 + (err, data, stat) => {
  20 + if (err) {
  21 + console.log('Got path %s data error', path, err.stack);
  22 + }
  23 + client.close();
  24 +
  25 + resolve((data && data.toString('utf8') === 'true') ? true : false);
  26 + }
  27 + )
  28 + } else {
  29 + // 不存在的路径
  30 + console.log('no path %s, we will create it with value "false" automatic', path);
  31 + client.close();
  32 +
  33 + // create path
  34 + creator(server, path);
  35 +
  36 + resolve(false);
  37 + }
  38 + });
  39 +
  40 +};
  41 +
  42 +module.exports = (server, path) => new Promise((resolve, reject) => {
  43 + const client = zookeeper.createClient(server);
  44 +
  45 + client.once('connected', () => {
  46 + getter(server, client, path, resolve, reject);
  47 + });
  48 +
  49 + client.connect();
  50 +});
  1 +'usu strict';
  2 +
  3 +import _ from 'lodash';
  4 +import zookeeper from 'node-zookeeper-client';
  5 +
  6 +module.exports = (server, path, val) => new Promise((resolve, reject) => {
  7 + const client = zookeeper.createClient(server);
  8 +
  9 + client.once('connected', function () {
  10 + client.setData(path, new Buffer(val.toString()), function(err, data, stat) {
  11 + console.log('path %s data change to', path, val);
  12 + resolve();
  13 + client.close();
  14 + });
  15 + });
  16 +
  17 + client.connect();
  18 +});
1 -0 info it worked if it ends with ok  
2 -1 verbose cli [ '/usr/local/bin/node',  
3 -1 verbose cli '/usr/local/bin/npm',  
4 -1 verbose cli 'run',  
5 -1 verbose cli 'babel',  
6 -1 verbose cli 'app.js' ]  
7 -2 info using npm@3.8.9  
8 -3 info using node@v6.2.0  
9 -4 verbose run-script [ 'prebabel', 'babel', 'postbabel' ]  
10 -5 info lifecycle yoho-node-ci@0.0.1~prebabel: yoho-node-ci@0.0.1  
11 -6 silly lifecycle yoho-node-ci@0.0.1~prebabel: no script for prebabel, continuing  
12 -7 info lifecycle yoho-node-ci@0.0.1~babel: yoho-node-ci@0.0.1  
13 -8 verbose lifecycle yoho-node-ci@0.0.1~babel: unsafe-perm in lifecycle true  
14 -9 verbose lifecycle yoho-node-ci@0.0.1~babel: PATH: /usr/local/lib/node_modules/npm/bin/node-gyp-bin:/Users/chenfeng/Documents/source/yoho/yoho-node-ci/node_modules/.bin:/usr/local/bin:/usr/local/lib/node_modules/npm/bin/node-gyp-bin:/Users/chenfeng/Documents/source/yoho/yoho-node-ci/node_modules/.bin:/usr/local/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/share/dotnet:/Users/chenfeng/Documents/tools/android-sdk-macosx/platform-tools:/Users/chenfeng/Documents/tools/android-sdk-macosx/tools:/usr/local/nginx/sbin  
15 -10 verbose lifecycle yoho-node-ci@0.0.1~babel: CWD: /Users/chenfeng/Documents/source/yoho/yoho-node-ci  
16 -11 silly lifecycle yoho-node-ci@0.0.1~babel: Args: [ '-c', 'babel-node "app.js"' ]  
17 -12 silly lifecycle yoho-node-ci@0.0.1~babel: Returned: code: 1 signal: null  
18 -13 info lifecycle yoho-node-ci@0.0.1~babel: Failed to exec babel script  
19 -14 verbose stack Error: yoho-node-ci@0.0.1 babel: `babel-node "app.js"`  
20 -14 verbose stack Exit status 1  
21 -14 verbose stack at EventEmitter.<anonymous> (/usr/local/lib/node_modules/npm/lib/utils/lifecycle.js:245:16)  
22 -14 verbose stack at emitTwo (events.js:106:13)  
23 -14 verbose stack at EventEmitter.emit (events.js:191:7)  
24 -14 verbose stack at ChildProcess.<anonymous> (/usr/local/lib/node_modules/npm/lib/utils/spawn.js:24:14)  
25 -14 verbose stack at emitTwo (events.js:106:13)  
26 -14 verbose stack at ChildProcess.emit (events.js:191:7)  
27 -14 verbose stack at maybeClose (internal/child_process.js:850:16)  
28 -14 verbose stack at Process.ChildProcess._handle.onexit (internal/child_process.js:215:5)  
29 -15 verbose pkgid yoho-node-ci@0.0.1  
30 -16 verbose cwd /Users/chenfeng/Documents/source/yoho/yoho-node-ci  
31 -17 error Darwin 16.0.0  
32 -18 error argv "/usr/local/bin/node" "/usr/local/bin/npm" "run" "babel" "app.js"  
33 -19 error node v6.2.0  
34 -20 error npm v3.8.9  
35 -21 error code ELIFECYCLE  
36 -22 error yoho-node-ci@0.0.1 babel: `babel-node "app.js"`  
37 -22 error Exit status 1  
38 -23 error Failed at the yoho-node-ci@0.0.1 babel script 'babel-node "app.js"'.  
39 -23 error Make sure you have the latest version of node.js and npm installed.  
40 -23 error If you do, this is most likely a problem with the yoho-node-ci package,  
41 -23 error not with npm itself.  
42 -23 error Tell the author that this fails on your system:  
43 -23 error babel-node "app.js"  
44 -23 error You can get information on how to open an issue for this project with:  
45 -23 error npm bugs yoho-node-ci  
46 -23 error Or if that isn't available, you can get their info via:  
47 -23 error npm owner ls yoho-node-ci  
48 -23 error There is likely additional logging output above.  
49 -24 verbose exit [ 1, true ]  
@@ -6,7 +6,7 @@ @@ -6,7 +6,7 @@
6 "scripts": { 6 "scripts": {
7 "test": "./node_modules/.bin/ava", 7 "test": "./node_modules/.bin/ava",
8 "babel": "./node_modules/.bin/babel-node", 8 "babel": "./node_modules/.bin/babel-node",
9 - "dev": "./node_modules/.bin/nodemon -e js,hbs -i public/,packages/ --exec npm run babel -- app.js", 9 + "dev": "./node_modules/.bin/nodemon -e js,hbs -i public/ -i packages/ --exec npm run babel -- app.js",
10 "start": "./node_modules/.bin/babel-node app.js" 10 "start": "./node_modules/.bin/babel-node app.js"
11 }, 11 },
12 "repository": { 12 "repository": {
@@ -51,9 +51,11 @@ @@ -51,9 +51,11 @@
51 "lodash": "^4.13.1", 51 "lodash": "^4.13.1",
52 "md5": "^2.1.0", 52 "md5": "^2.1.0",
53 "md5-file": "^3.1.1", 53 "md5-file": "^3.1.1",
  54 + "memcached": "^2.2.2",
54 "moment": "^2.13.0", 55 "moment": "^2.13.0",
55 "nedb": "^1.8.0", 56 "nedb": "^1.8.0",
56 "nedb-promise": "^2.0.0", 57 "nedb-promise": "^2.0.0",
  58 + "node-zookeeper-client": "^0.2.2",
57 "qn": "^1.3.0", 59 "qn": "^1.3.0",
58 "qs": "^6.2.0", 60 "qs": "^6.2.0",
59 "request-promise": "^4.1.1", 61 "request-promise": "^4.1.1",
@@ -934,4 +934,7 @@ h4, .h4 { @@ -934,4 +934,7 @@ h4, .h4 {
934 border-bottom-right-radius: 3px; 934 border-bottom-right-radius: 3px;
935 } 935 }
936 936
937 - 937 +.md-title {
  938 + display: inline-block;
  939 + margin-right: 15px;
  940 +}