Authored by 沈志敏

新增重启功能

... ... @@ -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';
... ... @@ -15,6 +16,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();
... ... @@ -22,11 +24,12 @@ const OperationLogger = new OperationLoggerModel();
User.init();
export {
Server,
Building,
Project,
DeployInfo,
User,
Hotfix,
OperationLogger
Server,
Building,
Project,
DeployInfo,
RestartInfo,
User,
Hotfix,
OperationLogger
};
\ 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
... ...
... ... @@ -4,13 +4,15 @@ import Router from 'koa-router';
import moment from 'moment';
import Build from '../../ci/build';
import Deploy from '../../ci/deploy';
import Restart from '../../ci/restart';
import Operation from '../../logger/operation';
import {
Building,
Project,
Server,
DeployInfo
DeployInfo,
RestartInfo
} from '../../models';
let r = new Router();
... ... @@ -132,10 +134,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 +191,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 +238,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 +255,67 @@ 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
};
}
};
... ... @@ -248,4 +327,5 @@ 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);
export default r;
\ No newline at end of file
... ...
... ... @@ -47,6 +47,7 @@
<div class="panel-heading">
<h4>服务器信息</h4>
<p>点击状态Label,可以查看实时日志</p>
<button class="btn btn-success btn-xs restart-btn" data-id="{{project._id}}" data-host='all' data-env='{{deploy.env}}'>全部重启</button>
</div>
<div class="panel-body">
<div class="row">
... ... @@ -58,9 +59,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-id="{{../project._id}}" data-host='{{host}}' data-env='{{../deploy.env}}'>重启</button>
</div><!-- media-body -->
<hr class="mt10 mb10">
<div class="clearfix mt5">
... ... @@ -177,6 +179,27 @@
});
$('.restart-btn').click(function(){
var id = $(this).data('id');
var host = $(this).data('host');
var env = $(this).data('env');
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 +249,11 @@
$('#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('/deploy/{{project._id}}/log', function(data){
// if(tag == data.host){
// cm.replaceRange("> " +data.msg+ "\n", {line: Infinity});
... ...
... ... @@ -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": {
... ... @@ -71,4 +71,4 @@
"babel-register": "^6.9.0",
"nodemon": "^1.9.2"
}
}
}
\ No newline at end of file
... ...
... ... @@ -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
... ...