Authored by 姜枫

Merge branch 'master' into feature/page-cache

... ... @@ -160,7 +160,7 @@ class Deploy {
});
});
}
async _state(state) {
ws.broadcast(`/deploy/${this.project._id}`, {
host: this.info.host,
... ... @@ -170,6 +170,7 @@ class Deploy {
}
_log(msg) {
console.log(msg)
ws.broadcast(`/deploy/${this.project._id}/log`, {
host: this.info.host,
msg: msg
... ...
/**
* 分发部署
*
* @class Restart
* @author shenzm<zhimin.shen@yoho.cn>
* @date 2016/10/12
*/
import ssh from 'ssh2';
import path from 'path';
import ws from '../../lib/ws';
import {
RestartInfo,
Server
} from '../models';
class Restart {
constructor(project) {
this.project = project;
}
async restart(info) {
let server = await Server.findByHost(info.host);
this.server = server;
this.info = info;
this.sshRestart({
host: server.host,
username: server.username,
password: server.password,
port: server.port
});
}
sshRestart(serverInfo) {
console.log('ssh connecting', serverInfo);
let conn = new ssh.Client();
let self = this;
conn.on('ready', async() => {
console.log(`connected ${serverInfo.host}`);
try {
await self._restart(conn);
conn.end();
} catch (e) {
self._state('fail');
self._log(e);
}
}).on('error', (err) => {
self._state('fail');
self._log(err);
}).connect(serverInfo);
}
_restart(conn) {
let self = this;
let startup = this.project.scripts.start;
return new Promise((resolve, reject) => {
self._state('restarting');
self._log(`>>>> ${startup}`);
conn.exec(`cd ${self.remoteRunningDir} && ${startup}`, (err, stream) => {
if (err) {
reject(err);
} else {
stream.stdout.on('data', (data) => {
self._log(data.toString());
});
stream.stderr.on('data', (data) => {
self._log(data.toString());
});
stream.on('exit', (code) => {
if (code === 0) {
self._state('running');
resolve();
} else {
reject('restart fail');
}
});
}
});
});
}
async _state(state) {
ws.broadcast(`/restart/${this.project._id}`, {
host: this.info.host,
state: state
});
await RestartInfo.updateState(this.info._id, state);
}
_log(msg) {
console.log(msg);
ws.broadcast(`/restart/${this.project._id}/log`, {
host: this.info.host,
msg: msg
});
}
get remoteRunningDir() {
return path.join(this.server.deployDir, this.project.name, 'current', this.project.name);
}
}
export default Restart;
\ No newline at end of file
... ...
... ... @@ -5,6 +5,7 @@ import ServerModel from './server';
import BuildingModel from './building';
import ProjectModel from './project';
import DeployModel from './deploy';
import RestartModel from './restart';
import UserModel from './user';
import HotfixModel from './hotfix';
import OperationLoggerModel from './operation_logger';
... ... @@ -17,6 +18,7 @@ const Server = new ServerModel();
const Building = new BuildingModel();
const Project = new ProjectModel();
const DeployInfo = new DeployModel();
const RestartInfo = new RestartModel();
const User = new UserModel();
const Hotfix = new HotfixModel();
const OperationLogger = new OperationLoggerModel();
... ... @@ -35,5 +37,6 @@ export {
Hotfix,
OperationLogger,
PageCache,
MemcachedHost
MemcachedHost,
RestartInfo
};
\ No newline at end of file
... ...
'use strict';
import Model from './model';
class Restart extends Model {
constructor() {
super('restart');
}
async updateState(id, state) {
await this.update({
_id: id
}, {
$set: {
state: state
}
});
}
}
export default Restart;
\ No newline at end of file
... ...
... ... @@ -2,15 +2,19 @@
import Router from 'koa-router';
import moment from 'moment';
import Rp from 'request-promise';
import Build from '../../ci/build';
import Deploy from '../../ci/deploy';
import Restart from '../../ci/restart';
import Operation from '../../logger/operation';
import ws from '../../../lib/ws';
import {
Building,
Project,
Server,
DeployInfo
DeployInfo,
RestartInfo
} from '../../models';
let r = new Router();
... ... @@ -132,10 +136,16 @@ const p = {
}, {
$set: project
});
await Operation.action(ctx.session.user, 'EDIT_PROJECT_INFO', '修改项目信息', {_id: id, name: project.name});
await Operation.action(ctx.session.user, 'EDIT_PROJECT_INFO', '修改项目信息', {
_id: id,
name: project.name
});
} else {
await Project.insert(project);
await Operation.action(ctx.session.user, 'NEW_PROJECT_INFO', '新增项目信息', {_id: id, name: project.name});
await Operation.action(ctx.session.user, 'NEW_PROJECT_INFO', '新增项目信息', {
_id: id,
name: project.name
});
}
ctx.redirect('/projects');
ctx.status = 301;
... ... @@ -183,7 +193,12 @@ const p = {
build.run(id);
await Operation.action(ctx.session.user, 'NEW_PROJECT_BUILDING', '新增项目构建', {_id: id, project: p.name, branch: branch, env: env});
await Operation.action(ctx.session.user, 'NEW_PROJECT_BUILDING', '新增项目构建', {
_id: id,
project: p.name,
branch: branch,
env: env
});
ctx.body = {
code: 200,
... ... @@ -225,7 +240,12 @@ const p = {
}
});
await Operation.action(ctx.session.user, 'PROJECT_DEPLOY', '项目分发部署', {_id: buildingId, project: project.name, branch: building.branch, env: building.env});
await Operation.action(ctx.session.user, 'PROJECT_DEPLOY', '项目分发部署', {
_id: buildingId,
project: project.name,
branch: building.branch,
env: building.env
});
ctx.body = {
code: 200,
... ... @@ -237,6 +257,116 @@ const p = {
msg: '该版本未构建成功,暂不能分发'
};
}
},
project_restart: async(ctx) => {
const projectId = ctx.request.body.id;
const host = ctx.request.body.host;
const env = ctx.request.body.env;
const project = await Project.findById(projectId);
if (!project) {
ctx.body = {
code: 201,
msg: '该项目不存在'
};
return;
}
let hosts = [];
if (host === 'all') {
// 全部重启
hosts = project.deploy[env].target;
} else {
// 单台重启
hosts.push(host);
}
hosts.forEach(async(host) => {
let doc = await DeployInfo.findOne({
projectId: projectId,
host: host,
env: env
});
if (!doc) {
return;
}
let info = {
projectId: projectId,
host: host,
env: env,
createdAt: new Date(),
updatedAt: new Date(),
state: 'waiting'
};
let restartDoc = await RestartInfo.insert(info);
let restart = new Restart(project);
info._id = restartDoc[0]._id;
restart.restart(info);
await Operation.action(ctx.session.user, 'PROJECT_RESTART', '项目重启', {
_id: info._id,
project: project.name,
branch: project.deploy[env].branchName,
env: env
});
});
ctx.body = {
code: 200
};
},
project_monit: async(ctx) => {
const projectId = ctx.request.body.id;
const env = ctx.request.body.env;
const project = await Project.findById(projectId);
if (!project) {
ctx.body = {
code: 201,
msg: '该项目不存在'
};
return;
}
const hosts = project.deploy[env].target;
hosts.forEach((host) => {
var obj = {
'total': 0,
'status': {}
};
Rp({
uri: `http://${host}:9615`,
json: true
}).then(function(data) {
var processes = data.processes || [];
processes.forEach(function(p) {
if (p.name === project.name) {
obj.total++;
if (!obj.status[p.pm2_env.status]) {
obj.status[p.pm2_env.status] = 1;
} else {
obj.status[p.pm2_env.status]++;
}
}
});
}).catch(function(err) {
obj.errmsg = '获取监控状态失败'
}).finally(function() {
ws.broadcast(`/monit/${projectId}`, {
host: host,
monitdata: obj
});
});
});
ctx.body = {
code: 200
};
}
};
... ... @@ -248,4 +378,6 @@ r.get('/:id/buildings', p.buildings_table);
r.post('/save', p.save);
r.post('/build/:pid', p.project_build);
r.post('/deploy/:building', p.project_deploy);
r.post('/restart', p.project_restart);
r.post('/monit', p.project_monit);
export default r;
\ No newline at end of file
... ...
... ... @@ -10,6 +10,7 @@
<li>{{project.name}}</li>
</ul>
<h4>{{project.name}} ({{project.subname}})</h4>
<input id="info" type="hidden" data-id='{{project._id}}' data-env='{{deploy.env}}'>
</div>
</div>
<!-- media -->
... ... @@ -47,6 +48,7 @@
<div class="panel-heading">
<h4>服务器信息</h4>
<p>点击状态Label,可以查看实时日志</p>
<button class="btn btn-success btn-xs restart-btn" data-host='all'>全部重启</button>
</div>
<div class="panel-body">
<div class="row">
... ... @@ -58,9 +60,10 @@
</div><!-- panel-btns -->
<div class="panel-icon"><i class="fa fa-cloud" style="padding-left:12px;"></i></div>
<div class="media-body">
<h2 class="nomargin">{{host}}</h2>
<h5 class="md-title mt5">当前运行版本:&nbsp;<code>{{#if info}}{{info.building}}{{^}}
<h2 class="nomargin">{{host}}</h2>
<h5 class="md-title mt5 version">当前运行版本:&nbsp;<code>{{#if info}}{{info.building}}{{^}}
未知部署{{/if}}</code></h5>
<button class="btn btn-success btn-xs restart-btn" data-host='{{host}}'>重启</button>
</div><!-- media-body -->
<hr class="mt10 mb10">
<div class="clearfix mt5">
... ... @@ -69,6 +72,7 @@
<span class="label label-success deploy-log-btn" data-host="{{host}}"><i
class="fa fa-spinner fa-spin fa-fw margin-bottom"></i> <b>{{#if info}}{{info.state}}{{^}}
未知部署{{/if}}</b></span>
<span class="label label-status"></span>
</div>
<div class="col-xs-6">
... ... @@ -177,6 +181,27 @@
});
$('.restart-btn').click(function(){
var id = $('#info').data('id');
var env = $('#info').data('env');
var host = $(this).data('host');
layer.confirm('确定重启吗?', {
btn: ['确定', '取消']
}, function() {
$.post('/projects/restart', {
id: id,
host: host,
env: env
},function(ret) {
if (ret.code == 200) {
layer.msg('正在重起中');
} else {
layer.msg(ret.msg, {icon: 5});
}
});
});
})
$('.rollback-btn').click(function() {
layer.prompt({
... ... @@ -226,6 +251,33 @@
$('#d-' + data.host.replace(/\./g, '-')).find('b').text(data.state);
});
ws.on('/restart/{{project._id}}', function(data) {
console.log(data);
$('#d-' + data.host.replace(/\./g, '-')).find('b').text(data.state);
});
ws.on('/monit/{{project._id}}', function(data) {
console.log(data);
var label = $('#d-' + data.host.replace(/\./g, '-')).find('.label-status');
data = data.monitdata;
if (data.errmsg) {
label.text(data.errmsg).addClass('label-danger');
} else {
var msg = "线程数:" + data.total
for(var s in data.status) {
msg += '; [' + s + ']状态数:' + data.status[s];
}
label.text(msg);
if (data.total != data.status.online) {
label.addClass('label-danger');
} else {
label.addClass('label-success');
}
}
});
// ws.on('/deploy/{{project._id}}/log', function(data){
// if(tag == data.host){
// cm.replaceRange("> " +data.msg+ "\n", {line: Infinity});
... ... @@ -235,5 +287,25 @@
ws.on('error', function() {
console.log('connect fail');
});
var projectid = $('#info').data('id');
var env = $('#info').data('env');
var monit = function() {
$.post('/projects/monit', {
id: projectid,
env: env
},function(ret) {
if (ret.code != 200) {
layer.msg(ret.msg, {icon: 5});
}
});
}
setInterval(function(){
// 监控服务状态(轮训)
monit();
}, 5000);
monit();
});
</script>
\ No newline at end of file
... ...
... ... @@ -6,7 +6,7 @@
"scripts": {
"test": "./node_modules/.bin/ava",
"babel": "./node_modules/.bin/babel-node",
"dev": "./node_modules/.bin/nodemon -e js,hbs -i public/,packages/ --exec npm run babel -- app.js",
"dev": "./node_modules/.bin/nodemon -e js,hbs -i public/ -i packages/ --exec npm run babel -- app.js",
"start": "./node_modules/.bin/babel-node app.js"
},
"repository": {
... ... @@ -57,6 +57,7 @@
"nedb-promise": "^2.0.0",
"qn": "^1.3.0",
"qs": "^6.2.0",
"request-promise": "^4.1.1",
"shelljs": "^0.7.0",
"socket.io": "^1.4.6",
"ssh2": "^0.5.0",
... ...
... ... @@ -934,4 +934,7 @@ h4, .h4 {
border-bottom-right-radius: 3px;
}
.md-title {
display: inline-block;
margin-right: 15px;
}
\ No newline at end of file
... ...