deploy.js 8.93 KB
/**
 * 分发部署
 * upload >> unzip >> startup
 * 
 * @class Deploy
 * @author jf<jeff.jiang@yoho.cn>
 * @date 2016/5/21
 */
'use strict';
const ssh = require('ssh2');
const path = require('path');
const fs = require('fs');
const config = require('../../config/config');
const ws = require('../../lib/ws');
const {
    DeployInfo,
    Server
} = require('../models');

class Deploy {

    constructor(project, building, host) {
        this.project = project;
        this.building = building;

        let info = {
            projectId: project._id,
            host: host,
            env: building.env,
            building: building.buildTime,
            state: 'waiting'
        };
        this.info = info;
        this.host = host;
    }

    async deploy(cb) {
        this.id = await DeployInfo.insertOrUpdate(this.info);
        this.callback = cb;
        // setImmediate(async function(){
        //     this.sshDeploy(this._getSshInfo());
        // }.bind(this));

        this.server = await this._getSshInfo();
        this.sshDeploy(this.server);

        return this.id;
    }

    stop() {
        this.conn.end();
        this.callback();
        this.state = 'closed';
    }

    async _getSshInfo() {
        let server = await Server.findByHost(this.host);

        if (this.project.type === 'php') {
            return {
                host: server.host,
                username: 'om',
                privateKey: fs.readFileSync(path.join(__dirname, './id_rsa_ssh')),
                port: server.port,
                deployDir: '/home/om'
            };
        } else {
            return {
                host: server.host,
                username: server.username,
                password: server.password,
                port: server.port,
                deployDir: server.deployDir
            };
        }
    }

    sshDeploy(serverInfo) {
        console.log('ssh connecting', JSON.stringify(serverInfo));
        let conn = new ssh.Client();
        let self = this;

        this._state('connecting');
        this.conn = conn;
        conn.on('ready', async() => {
            console.log(`connected ${serverInfo.host}`);
            if (this.state === 'close') {
                conn.end();
                return;
            }
            try {
                await self._preDeploy(conn);
                await self._scp(conn);
                await self._zipCheck(conn);
                await self._unzip(conn);
                await self._startup(conn);
                self.callback(null, null);
            } catch (e) {
                console.dir(e, {color: true});
                self._state('fail');
                self._log(e);
                self.callback(e, null);
            } finally {
                conn.end();
            }
        }).on('error', (err) => {
            console.error(err, {color: true});
            self._state('fail');
            self._log(err);
            self.callback(err, null);
        }).connect(serverInfo);
    }

    _preDeploy(conn) {
        let self = this;
        return new Promise((resolve, reject) => {
            let script = `mkdir -p ${self.remoteWorkDir} && mkdir -p ${self.remoteDist}`;
            self._state('preparing');
            self._log(`>>>>>>>>> ${script} >>>>>>>>`);
            conn.exec(script, (err, stream) => {
                if (err) {
                    reject(err);
                } else {
                    stream.on('exit', (code) => {
                        resolve();
                    });
                }

            });
        });
    }

    _scp(conn) {
        let self = this;

        return new Promise((resolve, reject) => {
            self._state('uploading');
            self._log(`>>>> uploading ${self.localFile} ==> ${self.remoteFile}`);
            let t1 = new Date();
            conn.sftp((err, sftp) => {
                if (err) {
                    reject(err);
                } else {
                    sftp.fastPut(self.localFile, self.remoteFile, {
                        // chunkSize: 10240
                    }, (err) => {
                        if (err) {
                            reject(err);
                        } else {
                            let t2 = new Date();
                            self._log(' uploaded success!');
                            self._state('uploaded');
                            console.log(`upload package in [${t2.getTime() - t1.getTime()}] ms`)
                            resolve();
                        }
                    });
                }
            });
        });
    }

    _zipCheck(conn) {
        let self = this;
        return new Promise((resolve, reject) => {
            self._state('checking');
            let check = `md5sum ${self.remoteFile}`;
            conn.exec(check, (err, stream) => {
                if (err) {
                    reject(err);
                } else {
                    let d = '';
                    stream.on('data', (data) => {
                        console.log(data.toString());
                        d += data.toString();
                    });
                    stream.on('close', (code) => {
                        if (code === 0) {
                            self._state('checked');

                            if (d.indexOf(self.building.md5) >= 0) {
                                resolve();
                            } else {
                                reject('check md5 fail: ' + d + '  except: ' + self.building.md5);
                            }
                        } else {
                            reject('check fail: ' + script);
                        }
                    });
                }
            });
        });
    }

    _unzip(conn) {
        let self = this;
        return new Promise((resolve, reject) => {
            self._state('unziping');
            let script = `tar -zxvf ${self.remoteFile} -C ${self.remoteWorkDir} && rm -rf ${self.remoteDist}`;
            self._log(`>>>> unziping ${self.remoteFile} ==> ${self.remoteWorkDir}`);
            conn.exec(script, (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('unziped');
                            resolve();
                        } else {
                            reject('unzip fail: ' + script);
                        }
                    });
                }
            })
        });
    }

    _startup(conn) {
        let self = this;
        let startup = this.project.scripts.start;

        if (startup) {
            return new Promise((resolve, reject) => {
                self._state('starting');
                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('startup fail');
                            }
                        });
                    }
                });
            });
        } else {
            return Promise.resolve();
        }

    }

    async _state(state) {
        ws.broadcast(`/deploy/${this.project._id}`, {
            host: this.host,
            state: state
        });
        await DeployInfo.updateState(this.id, state);
    }

    _log(msg) {
        ws.broadcast(`/deploy/${this.project._id}/log`, {
            host: this.host,
            msg: msg
        });
    }

    get remoteWorkDir() {
        return this.project.deploy[this.building.env].workingDir || path.join(this.server.deployDir, this.project.name, 'current');
    }

    get remoteRunningDir() {
        return this.project.deploy[this.building.env].workingDir || path.join(this.server.deployDir, this.project.name, 'current', this.project.name);
    }

    get remoteDist() {
        return path.join(this.server.deployDir, this.project.name, this.building.buildTime);
    }

    get remoteFile() {
        return path.join(this.server.deployDir, this.building.distFile);
    }

    get localFile() {
        return path.join(config.buildDir, this.building.distFile);
    }
}

module.exports = Deploy;