<repo_name>boer0924/BoerOps<file_sep>/app/services/roles.py
#!/usr/local/env python
# -*- coding: utf-8 -*-

from . import Base
from app.models import Role

class RoleService(Base):
    __model__ = Role

roles = RoleService()<file_sep>/boerops_dev.sql
-- MySQL dump 10.13  Distrib 5.7.18, for Linux (x86_64)
--
-- Host: localhost    Database: boerops_dev
-- ------------------------------------------------------
-- Server version	5.7.18-0ubuntu0.16.04.1

/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;

--
-- Table structure for table `deploys`
--

DROP TABLE IF EXISTS `deploys`;
/*!40101 SET @saved_cs_client     = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `deploys` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `project_id` int(10) unsigned NOT NULL,
  `user_id` int(10) unsigned NOT NULL,
  `version` varchar(64) COLLATE utf8mb4_bin NOT NULL COMMENT 'commit id',
  `mode` tinyint(1) unsigned DEFAULT '0' COMMENT '0-test30; 1-test31; 2-prod; 3-rollback',
  `status` tinyint(1) unsigned DEFAULT '0' COMMENT '0-input; 1-test; 2-test_result; 3-prod; 4-prod_result; 5release',
  `comment` text COLLATE utf8mb4_bin COMMENT 'release note',
  `result` tinyint(1) unsigned DEFAULT NULL COMMENT '0-success; 1-failed',
  `created_at` datetime NOT NULL,
  `updated_at` datetime NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='发布部署';
/*!40101 SET character_set_client = @saved_cs_client */;

--
-- Table structure for table `hosts`
--

DROP TABLE IF EXISTS `hosts`;
/*!40101 SET @saved_cs_client     = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `hosts` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `hostname` varchar(32) COLLATE utf8mb4_bin NOT NULL,
  `ip_address` varchar(32) COLLATE utf8mb4_bin NOT NULL,
  `ssh_port` int(8) unsigned NOT NULL DEFAULT '22',
  `username` varchar(32) COLLATE utf8mb4_bin NOT NULL,
  `password` varchar(64) COLLATE utf8mb4_bin DEFAULT NULL,
  `ssh_method` tinyint(1) DEFAULT '0' COMMENT '0-password;1-public key',
  `environ` tinyint(1) DEFAULT '0' COMMENT '0-30test;1-31test;2-prod',
  `created_at` datetime NOT NULL,
  `updated_at` datetime NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ip_address` (`ip_address`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='主机资产表';
/*!40101 SET character_set_client = @saved_cs_client */;

--
-- Table structure for table `projects`
--

DROP TABLE IF EXISTS `projects`;
/*!40101 SET @saved_cs_client     = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `projects` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(64) COLLATE utf8mb4_bin NOT NULL,
  `repo_url` varchar(128) COLLATE utf8mb4_bin NOT NULL COMMENT 'git repo url',
  `checkout_dir` varchar(128) COLLATE utf8mb4_bin NOT NULL COMMENT '检出目录',
  `compile_dir` varchar(128) COLLATE utf8mb4_bin NOT NULL COMMENT '编译/打包/发布目录',
  `compile_cmd` text COLLATE utf8mb4_bin COMMENT '编译命令',
  `playbook_path` varchar(128) COLLATE utf8mb4_bin NOT NULL COMMENT 'ansible playbook yaml文件',
  `created_at` datetime NOT NULL,
  `updated_at` datetime NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='项目信息表';
/*!40101 SET character_set_client = @saved_cs_client */;

--
-- Table structure for table `rel_host_project`
--

DROP TABLE IF EXISTS `rel_host_project`;
/*!40101 SET @saved_cs_client     = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `rel_host_project` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `host_id` int(10) unsigned NOT NULL,
  `project_id` int(10) unsigned NOT NULL,
  `created_at` datetime NOT NULL COMMENT 'create time',
  `updated_at` datetime NOT NULL COMMENT 'update time',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=32 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='主机、项目关联';
/*!40101 SET character_set_client = @saved_cs_client */;

--
-- Table structure for table `roles`
--

DROP TABLE IF EXISTS `roles`;
/*!40101 SET @saved_cs_client     = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `roles` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(32) COLLATE utf8mb4_bin NOT NULL,
  `created_at` datetime NOT NULL,
  `updated_at` datetime NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='用户角色';
/*!40101 SET character_set_client = @saved_cs_client */;

--
-- Table structure for table `users`
--

DROP TABLE IF EXISTS `users`;
/*!40101 SET @saved_cs_client     = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `users` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `username` varchar(64) COLLATE utf8mb4_bin NOT NULL,
  `password_hash` varchar(128) COLLATE utf8mb4_bin NOT NULL,
  `name` varchar(64) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '姓名',
  `role_id` int(10) unsigned NOT NULL,
  `email` varchar(64) COLLATE utf8mb4_bin DEFAULT NULL,
  `phone` varchar(16) COLLATE utf8mb4_bin DEFAULT NULL,
  `apikey` varchar(64) COLLATE utf8mb4_bin DEFAULT NULL,
  `created_at` datetime NOT NULL COMMENT 'create time',
  `updated_at` datetime NOT NULL COMMENT 'update time',
  PRIMARY KEY (`id`),
  UNIQUE KEY `username` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='用户信息';
/*!40101 SET character_set_client = @saved_cs_client */;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;

/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;

-- Dump completed on 2017-06-28 18:27:38
<file_sep>/app/static/js/projects/index.js
layui.use(['element', 'layer', 'util', 'form', 'layedit', 'laypage', 'upload'], function () {
    var $ = layui.jquery;
    var element = layui.element();
    var layer = layui.layer;
    var util = layui.util;
    var form = layui.form();
    var layedit = layui.layedit;
    var laypage = layui.laypage;

    // 文件上传
    layui.upload({
        url: '/projects/uploads',
        type: 'file',
        ext: 'yaml|yml|txt',
        success: function (res, input) {
            // console.log(res.filepath);
            $('#playbook').val(res.filepath);
        }
    });

    // 添加项目表单提交
    form.on('submit(addProject)', function(data) {
        // layer.msg(JSON.stringify(data.field));

        $.ajax({
            type: 'POST',
            url: '/projects/',
            data: data.field,
            dataType: 'json',
            // contentType: false,
            processData: true,
            success: function (data, txtStatus, jqXHR) {
                if (data.code === 200) {
                    layer.msg(data.msg, {
                        icon: 1,
                        time: 2000,
                        anim: 0,
                        shade: [0.6, '#c2c2c2']
                    },
                    function () {
                        location.href="/projects/";
                    });

                } else {
                    layer.msg(data.msg, {
                        icon: 2,
                        time: 2000,
                        anim: 6,
                        shade: [0.6, '#c2c2c2']
                    });
                }
            }
        });
        return false;
    });

    // 封装数据
    var hosts = [];
    form.on('checkbox(chk-hosts)', function (data) {
        hosts.push(data.value);
    });
    // 执行绑定
    form.on('submit(bindHost)', function(data) {
        data.field.hosts = hosts;
        // layer.msg(JSON.stringify(data.field));
        $.ajax({
            type: 'POST',
            url: '/projects/bind',
            data: data.field,
            dataType: 'json',
            // contentType: false,
            processData: true,
            success: function (data, txtStatus, jqXHR) {
                if (data.code === 200) {
                    layer.msg(data.msg, {
                        icon: 1,
                        time: 2000,
                        anim: 0,
                        shade: [0.6, '#c2c2c2']
                    },
                    function () {
                        location.href="/projects/";
                    });

                } else {
                    layer.msg(data.msg, {
                        icon: 2,
                        time: 2000,
                        anim: 6,
                        shade: [0.6, '#c2c2c2']
                    });
                }
            }
        });
        return false;
    });
    // 添加项目弹出层
    $('#add-project-btn').on('click', function () {
        layer.open({
            type: 1,
            title: '添加项目',
            skin: 'layui-layer-molv',
            content: $('#add-project-ctx'),
            area: ['680px', '520px'],
            // area: 'auto',
            // maxWidth: '900px',
            closeBtn: 1,
            resize: true
            // shadeClose: true,
        });
    });

    // 修改项目
    $(document).on('click', '#update-project-btn', function () {
        layer.open({
            type: 1,
            title: '编辑项目',
            skin: 'layui-layer-molv',
            content: $('#add-project-ctx'),
            area: ['680px', '520px'],
            // area: 'auto',
            // maxWidth: '900px',
            closeBtn: 1,
            resize: true
            // shadeClose: true,
        });
        return false;
    });
    
    // 绑定主机
    $('table').on('click', '#bind-host-btn', function (e) {
        var e = window.event || e;
        var id = $(e.target).parent().parent().parent().find('td:eq(0)').find('input').val();
        $('#project_id').val(id);
        layer.open({
            type: 1,
            title: '绑定主机',
            skin: 'layui-layer-molv',
            content: $('#bind-host-ctx'),
            area: ['520px', '360px'],
            // area: 'auto',
            // maxWidth: '900px',
            closeBtn: 1,
            resize: true
            // shadeClose: true,
        });
        return false;
    });
});<file_sep>/api.py
#!/usr/bin/env python

import os
import sys
from collections import namedtuple

from ansible.parsing.dataloader import DataLoader
from ansible.vars import VariableManager
from ansible.inventory import Inventory
from ansible.executor.playbook_executor import PlaybookExecutor

variable_manager = VariableManager()
loader = DataLoader()

inventory = Inventory(loader=loader, variable_manager=variable_manager,  host_list='/home/slotlocker/hosts2')
playbook_path = '/home/slotlocker/ls.yml'

if not os.path.exists(playbook_path):
    print '[INFO] The playbook does not exist'
    sys.exit()

Options = namedtuple('Options', ['listtags', 'listtasks', 'listhosts', 'syntax', 'connection','module_path', 'forks', 'remote_user', 'private_key_file', 'ssh_common_args', 'ssh_extra_args', 'sftp_extra_args', 'scp_extra_args', 'become', 'become_method', 'become_user', 'verbosity', 'check'])
options = Options(listtags=False, listtasks=False, listhosts=False, syntax=False, connection='ssh', module_path=None, forks=100, remote_user='slotlocker', private_key_file=None, ssh_common_args=None, ssh_extra_args=None, sftp_extra_args=None, scp_extra_args=None, become=True, become_method=None, become_user='root', verbosity=None, check=False)

variable_manager.extra_vars = {'hosts': 'mywebserver'} # This can accomodate various other command line arguments.`

passwords = {}

pbex = PlaybookExecutor(playbooks=[playbook_path], inventory=inventory, variable_manager=variable_manager, loader=loader, options=options, passwords=passwords)

results = pbex.run()


# ad-hoc
#!/usr/bin/env python

import json
from collections import namedtuple
from ansible.parsing.dataloader import DataLoader
from ansible.vars import VariableManager
from ansible.inventory import Inventory
from ansible.playbook.play import Play
from ansible.executor.task_queue_manager import TaskQueueManager
from ansible.plugins.callback import CallbackBase

class ResultCallback(CallbackBase):
    """A sample callback plugin used for performing an action as results come in

    If you want to collect all results into a single object for processing at
    the end of the execution, look into utilizing the ``json`` callback plugin
    or writing your own custom callback plugin
    """
    def v2_runner_on_ok(self, result, **kwargs):
        """Print a json representation of the result

        This method could store the result in an instance attribute for retrieval later
        """
        host = result._host
        print json.dumps({host.name: result._result}, indent=4)

Options = namedtuple('Options', ['connection', 'module_path', 'forks', 'become', 'become_method', 'become_user', 'check'])
# initialize needed objects
variable_manager = VariableManager()
loader = DataLoader()
options = Options(connection='local', module_path='/path/to/mymodules', forks=100, become=None, become_method=None, become_user=None, check=False)
passwords = dict(vault_pass='<PASSWORD>')

# Instantiate our ResultCallback for handling results as they come in
results_callback = ResultCallback()

# create inventory and pass to var manager
inventory = Inventory(loader=loader, variable_manager=variable_manager, host_list='localhost')
variable_manager.set_inventory(inventory)

# create play with tasks
play_source =  dict(
        name = "Ansible Play",
        hosts = 'localhost',
        gather_facts = 'no',
        tasks = [
            dict(action=dict(module='shell', args='ls'), register='shell_out'),
            dict(action=dict(module='debug', args=dict(msg='{{shell_out.stdout}}')))
         ]
    )
play = Play().load(play_source, variable_manager=variable_manager, loader=loader)

# actually run it
tqm = None
try:
    tqm = TaskQueueManager(
              inventory=inventory,
              variable_manager=variable_manager,
              loader=loader,
              options=options,
              passwords=passwords,
              stdout_callback=results_callback,  # Use our custom callback instead of the ``default`` callback plugin
          )
    result = tqm.run(play)
finally:
    if tqm is not None:
        tqm.cleanup()

# ipython shell
# from app.models import User, Role, db

# admin_role = Role(name='admin')
# admin_user = User(username='boer', password='123', name='海博', job='Ops', role=admin_role)

# from app import create_app

# app = create_app('dev')
# with app.app_context():
#     db.session.add(admin_role)
#     db.session.add(admin_user)
#     db.session.commit()


pydelo

<EMAIL>:meanstrong/pydelo.git

/tmp/pydelo_chk

/tmp/pydelo_pkg

echo "***begin compile***"
touch /tmp/pydelo_pkg/compile_test.txt
echo "***end compile***"
cd /tmp/pydelo_pkg
rm -f /tmp/pydelo.tar.gz
tar -zcf /tmp/pydelo.tar.gz *<file_sep>/app/services/hosts.py
#!/usr/local/env python
# -*- coding: utf-8 -*-

from . import Base
from app.models import Host

class HostService(Base):
    __model__ = Host

hosts = HostService()<file_sep>/runserver.py
from app import create_app

import socket

if __name__ == '__main__':
    app = create_app('dev')
    if socket.gethostname() not in ['Boer-PC', 'boer-PC']:
        app = create_app('prod')
    
    app.run(host='0.0.0.0')
<file_sep>/requirements.txt
Flask
Flask-Login
Flask-Mail
Flask-SQLAlchemy
PyMySQL
requests
ansible<file_sep>/app/projects/__init__.py
from flask import Blueprint

projects = Blueprint('projects', __name__, url_prefix='/projects')

from . import views<file_sep>/app/services/projects.py
#!/usr/local/env python
# -*- coding: utf-8 -*-

from . import Base
from app.models import Project
from .hosts import hosts

class ProjectService(Base):
    __model__ = Project

    

projs = ProjectService()
# C:\Users\Public\Documents\Hyper-V\Virtual Hard Disks\MobyLinuxVM.vhdx   <file_sep>/Dockerfile

FROM python:3.5
ADD ./requirements.txt /tmp/requirements.txt
RUN pip install -r /tmp/requirements.txt && pip install ipython<file_sep>/app/utils/uploads.py
# -*- coding: utf-8 -*-
"""文件上传"""

import os

from flask import request, current_app, jsonify
from werkzeug.utils import secure_filename


# UPLOAD_FOLDER = os.path.join(os.path.dirname(current_app.root_path), 'playbook')
# UPLOAD_FOLDER = os.path.join(os.path.dirname(current_app.root_path), 'playbook')
ALLOWED_EXTENSIONS = set(['yml', 'yaml', 'txt'])

def allowed_file(filename):
    return '.' in filename and filename.rsplit('.', 1)[1] in ALLOWED_EXTENSIONS

def gen_file_name(filename):
    flag = 1
    while os.path.exists(os.path.join(UPLOAD_FOLDER, filename)):
        name, ext = os.path.splitext(filename)
        filename = '%s_%s%s' % (name, str(flag), ext)
        flag += 1
    return filename

def upload_file(UPLOAD_FOLDER):
    if request.method == 'POST':
        file = request.files.get('playbook')
        if file and allowed_file(file.filename):
            filename = secure_filename(file.filename)
            uploaded_file_path = os.path.join(UPLOAD_FOLDER, filename)
            if not os.path.exists(os.path.dirname(uploaded_file_path)):
                try:
                    os.makedirs(os.path.dirname(uploaded_file_path))
                except OSError as e:
                    return jsonify(rc=0)
            file.save(uploaded_file_path)
            return jsonify(rc=1, filepath=uploaded_file_path)
        else:
            return jsonify(rc=0)
<file_sep>/app/projects/views.py
#!/usr/local/env python
# -*- coding: utf-8 -*-

from . import projects
from app.utils.uploads import upload_file
from app.utils.remoteshell import MyRunner
from app.utils.helper import get_dynamic_inventory, login_required
from app.services.projects import projs
from app.services.hosts import hosts
from app.services.deploys import deploys

from flask import render_template, request, jsonify, current_app, g

import os

@projects.route('/uploads', methods=['GET', 'POST'])
@login_required
def uploads():
    UPLOAD_FOLDER = os.path.join(os.path.dirname(current_app.root_path), 'playbook')
    return upload_file(UPLOAD_FOLDER)

@projects.route('/bind', methods=['POST'])
@login_required
def binds():
    _project_id = request.form.get('project_id')
    _hosts = request.form.getlist('hosts[]')
    proj = projs.get(_project_id)
    for host in set(_hosts):
        proj.hosts.append(hosts.get(int(host)))
    projs.save(proj)
    return jsonify(code=200, msg='绑定成功')

@projects.route('/', methods=['GET', 'POST'])
@login_required
def index():
    if request.method == 'POST':
        _id = request.form.get('id')
        fields = request.form.to_dict()
        fields.pop('playbook')
        # 修改操作
        if _id is not None:
            proj = projs.first(id=_id)
            if not proj:
                return jsonify(code=404, msg='记录不存在')
            try:
                projs.update(proj, **fields)
            except Exception as e:
                return jsonify(code=500, msg='修改失败')
            return jsonify(code=200, msg='修改成功')
        # 添加操作
        try:
            projs.create(**fields)
        except Exception as e:
            print(e)
            return jsonify(code=500, msg='添加失败')
        return jsonify(code=200, msg='添加成功')
    _projects = projs.all()
    _hosts = hosts.all()
    return render_template('projects/index.html', projects=_projects, hosts=_hosts)


@projects.route('/hosts', methods=['GET', 'POST'])
@login_required
def project_hosts():
    if request.method == 'POST':
        _id = request.form.get('id')
        # 修改操作
        if _id is not None:
            host = hosts.first(id=_id)
            if not host:
                return jsonify(code=404, msg='记录不存在')
            try:
                hosts.update(host, **request.form.to_dict())
            except Exception as e:
                return jsonify(code=500, msg='修改失败')
            return jsonify(code=200, msg='修改成功')
        # 添加操作
        try:
            hosts.create(**request.form.to_dict())
        except Exception as e:
            print(e)
            return jsonify(code=500, msg='添加失败')
        return jsonify(code=200, msg='添加成功')
    _hosts = hosts.all()
    return render_template('projects/hosts.html', hosts=_hosts)


@projects.route('/deploy', methods=['GET', 'POST'])
@login_required
def deploy_step_first():
    if request.method == 'POST':
        fields = request.form.to_dict()
        project_name = fields.get('name')
        version = fields.get('version')
        environ = fields.get('environ')
        proj = projs.first(name=project_name.strip())
        if not proj:
            return jsonify(rc=400, msg='项目不存在')
        if int(environ) == 2:
            num_deploy = deploys.count(project_id=proj.id, status=3)            
            if int(num_deploy) >= 1:
                return jsonify(code=500, msg='有其他任务在上线中...')
            deploys.create(
                project_id=proj.id,
                user_id=g.user.id,
                version=version,
                mode=environ,
                status=3)
        else:
            num_deploy = deploys.count(project_id=proj.id, status=0)
            if int(num_deploy) >= 1:
                return jsonify(code=500, msg='有其他任务在提测中...')
            deploys.create(
                project_id=proj.id,
                user_id=g.user.id,
                version=version,
                mode=environ)
        results = deploys.deploy_task(proj.id, int(environ))
        print(results)
        return jsonify(code=200, msg='job done', status=results)
    return render_template('projects/deploy.html')


@projects.route('/deploy/second', methods=['GET', 'POST'])
@login_required
def deploy_step_second():
    if request.method == 'POST':

        results = deploys.deploy_task(proj.id)
        return jsonify(code=200, msg='job done', status=status)


@projects.route('/rollback')
def rollback():
    return render_template('projects/rollback.html')
<file_sep>/app/auth/views.py
# -*- coding: utf-8 -*-

import datetime
import json

from . import auth
from flask import render_template, redirect, url_for, request, jsonify, g, make_response, current_app
import jwt

from app.services.roles import roles
from app.services.users import users
from app.utils.helper import login_required, permission_required, write_required
from app import redis

@auth.route('/login', methods=['GET', 'POST'])
def login():
    redirect_uri = request.args.get('redirect_uri') or '/dashboard'
    if request.method == 'POST':
        username = request.form.get('username')
        password = request.form.get('password')
        redirect_uri = request.form.get('redirect_uri')
        _u = users.first(username=username)
        if _u and _u.verify_password(password):
            # 获取缓存，动态设置config
            role_map = redis.get('roles').decode('utf-8')
            current_app.config['roles'] = json.loads(role_map)
            # 处理登录、生成token
            resp = jsonify(code=200, msg='登录成功', redirect_uri=redirect_uri)
            payload = dict(
                exp = datetime.datetime.utcnow() + datetime.timedelta(hours=8),
                rid = _u.role_id,
                uid = _u.id
            )
            token = jwt.encode(payload, current_app.config['SECRET_KEY'], algorithm='HS256')
            resp.set_cookie('token', token)
            return resp
        else:
            return jsonify(code=500, msg='用户名或密码错误')
    return render_template('auth/login.html', redirect_uri=redirect_uri)


@auth.route('/users', methods=['GET', 'POST'])
@login_required
@permission_required(3)
def user():
    if request.method == 'POST':
        users.create(**request.form.to_dict())
        return jsonify(code=200, msg='添加成功')
    _users = users.all()
    _roles = roles.all()
    return render_template('auth/users.html', users=_users, roles=_roles)


@auth.route('/groups', methods=['GET', 'POST'])
@login_required
@permission_required(3, alls=True)
def group():
    if request.method == 'POST':
        roles.create(**request.form.to_dict())
        # 每次添加角色，更新redis roles
        all_roles = roles.all()
        all_role_ids = map(lambda x: x.id, all_roles)
        all_role_names = map(lambda x: x.name, all_roles)  
        role_map = dict(zip(all_role_names, all_role_ids))
        redis.set('roles', json.dumps(role_map))
        return jsonify(code=200, msg='添加成功')
    _roles = roles.all()
    return render_template('auth/groups.html', roles=_roles)

@auth.route('/logout')
@login_required
def logout():
    resp = make_response(redirect(url_for('.login')))
    resp.set_cookie('token', '', expires=0)
    return resp

@auth.route('/reset', methods=['POST'])
@login_required
def reset_password():
    if request.method == 'POST':
        current_pass = request.form.get('current_pwd')
        new_pass = request.form.get('new_pwd')
        if g.user and g.user.verify_password(current_pass):
            g.user.password = <PASSWORD>
            users.session_commit()
            return jsonify(code=200, msg='修改成功')
        else:
            return jsonify(code=403, msg='权限不足')


@auth.route('/test')
def test_for_someting():
    print(current_app.config['roles'].get('测试'))
    return ''


@auth.errorhandler(403)
def access_forbidden(e):
    return render_template('auth/403.html'), 403<file_sep>/app/main/views.py
# -*- coding: utf-8 -*-
from . import main
from flask import render_template, redirect
from app.utils.helper import login_required
from app.services.deploys import deploys

@main.route('/')
def index():
    return redirect('/dashboard')


@main.route('/dashboard')
@login_required
def dashboard():
    u_ds = deploys.find(status=5)
    return render_template('main/index.html')<file_sep>/app/utils/sendemail.py
# -*- coding: utf-8 -*-
import os
import datetime
import shutil
import subprocess
from subprocess import Popen, PIPE
import json
from threading import Thread

from flask import Flask, request, render_template
from flask_mail import Mail, Message

app = Flask(__name__)

mail = Mail(app)

app.config['MAIL_SERVER'] = 'mail.heclouds.com'
app.config['MAIL_PORT'] = 25
app.config['MAIL_USERNAME'] = 'boer'
app.config['MAIL_PASSWORD'] = '<PASSWORD>'

app.config['CHECKOUT_DIR'] = '/data/deployment/gitrepos'
app.config['DEPLOY_DIR'] = '/data/deployment/deploydir'
app.config['PROJECTS_DIR'] = '/data/deployment/projects'

def update_repo(repo_path, repo_url, commit_id):
    git_path = os.path.join(repo_path, '.git')
    if os.path.exists(git_path) and os.path.isdir(git_path):
        cmd = 'cd %s && git reset --hard origin/master && git pull -q' % repo_path
        rc = subprocess.check_call(cmd, shell=True, cwd=repo_path)
    else:
        if os.path.exists(os.path.dirname(repo_path)) and os.path.isdir(os.path.dirname(repo_path)):
            shutil.rmtree(os.path.dirname(repo_path))
        else:
            os.makedirs(os.path.dirname(repo_path))
            cmd = 'git clone -q %s' % repo_url
            rc = subprocess.check_call(cmd.split(), cwd=os.path.dirname(repo_path))
    # 指定commit_id
    cmd = 'git reset -q --hard %s' % commit_id
    rc = subprocess.check_call(cmd.split(), cwd=repo_path)
    
def rsync_local(src, dest, excludes=[]):
    excludes.append('.git')
    exclude_args = ''
    for e in excludes:
        exclude_args = exclude_args + ' --exclude %s' % e
    cmd = 'rsync -qa --delete %s %s%s %s%s' % (exclude_args, src, os.sep, dest, os.sep)
    rc = subprocess.check_call(cmd.split())

def chk_and_set_exe(src_path):
    if not os.access(src_path, os.X_OK):
        os.chmod(src_path, 755)

def exec_script(script_file):
    if os.path.exists(script_file) and os.path.isfile(script_file):
       chk_and_set_exe(script_file)
       outputs = subprocess.check_output(script_file, shell=True)
       # p = Popen(script_file, stdout=PIPE)
       # c_pid = p.pid
       # outputs = p.communicate()[0]
       # rc = p.poll()
       #if rc == 0:
       #    p.terminate()
       #else:
       #    p.kill()
       return outputs
    

# 同步邮件
def send_sync_email(sender, to, cc, subject, template, **kwargs):
    msg = Message(subject, sender=sender, recipients=to, cc=cc)
    # msg.body = render_template(template + '.txt', **kwargs)
    msg.html = render_template(template + '.html', **kwargs)
    mail.send(msg)

# 异步发邮件
def send_async_email(app, msg):
    with app.app_context():
        mail.send(msg)

def send_email(sender, to, cc, subject, template, **kwargs):
    msg = Message(subject, sender=sender, recipients=to, cc=cc)
    # msg.body = render_template(template + '.txt', **kwargs)
    msg.html = render_template(template + '.html', **kwargs)
    thr = Thread(target=send_async_email, args=[app, msg])
    thr.start()
    return thr   

@app.route('/sendmail')
def sendmail():
    user_email_map = {
        'zhanghaibo': '<EMAIL>',
        'mazhijie': '<EMAIL>',
        'jieyuanfei': '<EMAIL>',
        'weizhengdong': '<EMAIL>',
        'tanjiang': '<EMAIL>',
        'chenbo': '<EMAIL>'
    }
    send_email('<EMAIL>', ['<EMAIL>', '<EMAIL>', '<EMAIL>'], ['<EMAIL>', '<EMAIL>'], 'Just a test', 'deploy')
    # msg = Message('Hello', sender="<EMAIL>", recipients=['<EMAIL>'])
    # msg.body = "testing"
    # mail.send(msg)
    return 'done'


@app.route('/', methods=['GET', 'POST'])
def index():
    if request.method == 'POST':
        results = json.loads(request.data)
        noteable_type = results['object_attributes']['noteable_type']
        # Triggered when a new comment is made on commits, merge requests, issues, and code snippets. 
        if noteable_type != 'Commit':
            return '', 404
        # user 相关
        name = results['user']['name']
        username = results['user']['username']
        if username != 'zhanghaibo':
            return '', 403
        # repository 相关
        repo_name = results['repository']['name']
        repo_url = results['repository']['url']
        repo_path = git_path = os.path.join(app.config['CHECKOUT_DIR'], repo_name, repo_name)
        # commit 相关
        commit_id = results['commit']['id']
        print('---commit_id---', commit_id)
        commit_msg = results['commit']['message'].strip('\n')
        # object_attributes 相关
        try:
            notes = results['object_attributes']['note']
            if not notes.startswith('```json') and not notes.endswith('```'):
                return ''
            notes = notes.lstrip('```json').rstrip('```').replace('\r\n', '') 
            notes = json.loads(notes)
        except Exception as e:
            print('<-debug->', e)
            # send_email()
            return ''
        deploy_type = notes.get('deploy_type')
        recipients = notes.get('recipients')
        carbon_copy = notes.get('carbon_copy')
        functions = notes.get('functions')

        update_repo(repo_path, repo_url, commit_id)
        rsync_local(repo_path, os.path.join(app.config['DEPLOY_DIR'], repo_name, repo_name))

        script_file = ''
        subject = ''
        if deploy_type == 'weekfix':
            script_file = os.path.join(app.config['PROJECTS_DIR'], repo_name + '_weekfix', 'script/local_after.sh')
            subject = datetime.datetime.strftime(datetime.date.today(), '%Y/%m/%d') + 'weekfix测试'
        elif deploy_type == 'hotfix':
            pass
        elif deploy_type == 'feature':
            pass
        elif deploy_type == 'prod':
            pass
        else:
            print('滚蛋吧')
        outputs = exec_script(script_file)
        print(outputs)
        print(type(outputs))
        send_sync_email('<EMAIL>', recipients, carbon_copy, subject, 'deploy', functions=functions, outputs=outputs)
        return ''
    return '', 405

if __name__ == '__main__':
    app.run(host='172.19.3.23', port=8080)
# {
#     "deploy_type": "weekfix/feature/hotfix/prod",
#     "recipients": ["mazhijie", "limao", "dengdeng"],
#     "carbon_copy": ["bmwlee", "limao"],
#     "functions": [
#         {
#             "name": "功能点一",
#             "content": "balabala~"
#         },
#         {
#             "name": "功能点二",
#             "content": "balabala~"
#         }
#     ]
# }

# onenet_v3
# onenet_ee
# forum_v2
# passport
# phpcorelib
# groupservice
# admin_onenetv3
# campaignmap<file_sep>/app/services/users.py
#!/usr/local/env python
# -*- coding: utf-8 -*-

from . import Base
from app.models import User

class UserService(Base):
    __model__ = User

users = UserService()<file_sep>/app/models.py
from app import db, login_manager
from werkzeug.security import generate_password_hash, check_password_hash
from flask_login import UserMixin

class Permission(db.Model):
    __tablename__ = 'permissions'

    id = db.Column(db.Integer, primary_key=True)    
    created_at = db.Column(db.DateTime, default=db.func.now())
    updated_at = db.Column(db.DateTime, default=db.func.now(), onupdate=db.func.now())

class Role(db.Model):
    __tablename__ = 'roles'

    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(64), unique=True)
    created_at = db.Column(db.DateTime, default=db.func.now())
    updated_at = db.Column(db.DateTime, default=db.func.now(), onupdate=db.func.now())

    users = db.relationship('User', backref='role', lazy="dynamic")

    def __repr__(self):
        return '<%s %r>' % (self.__class__.__name__, self.name)


class User(UserMixin, db.Model):
    __tablename__ = 'users'

    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(64), unique=True)
    password_hash = db.Column(db.String(64))
    name = db.Column(db.String(64))
    # job = db.Column(db.String(64))
    email = db.Column(db.String(64))
    phone = db.Column(db.String(64))
    apikey = db.Column(db.String(64))
    created_at = db.Column(db.DateTime, default=db.func.now())
    updated_at = db.Column(db.DateTime, default=db.func.now(), onupdate=db.func.now())
    
    role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))

    @property
    def password(self):
        raise AttributeError('password is not a readable attribute')
    
    @password.setter
    def password(self, pwd):
        self.password_hash = generate_password_hash(pwd, salt_length=16)

    def verify_password(self, pwd):
        return check_password_hash(self.password_hash, pwd)

    def __repr__(self):
        return '<%s %r>' % (self.__class__.__name__, self.username)


@login_manager.user_loader
def load_user(user_id):
    return User.query.get(int(user_id))

rel_host_project = db.Table(
    "rel_host_project",
    db.Column("id", db.Integer, primary_key=True),
    db.Column("host_id", db.Integer, db.ForeignKey("hosts.id")),
    db.Column("project_id", db.Integer, db.ForeignKey("projects.id")),
    db.Column("created_at", db.DateTime, default=db.func.now()),
    db.Column("updated_at", db.DateTime, default=db.func.now(), onupdate=db.func.now()),
)


class Project(db.Model):
    __tablename__ = 'projects'

    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(64), unique=True)
    repo_url = db.Column(db.String(128))
    checkout_dir = db.Column(db.String(128))
    compile_dir = db.Column(db.String(128))
    compile_cmd = db.Column(db.String(512))
    playbook_path = db.Column(db.String(128))
    created_at = db.Column(db.DateTime, default=db.func.now())
    updated_at = db.Column(db.DateTime, default=db.func.now(), onupdate=db.func.now())
    
    hosts = db.relationship("Host", secondary=rel_host_project, 
                            backref=db.backref("projects", lazy="dynamic"))

    def __repr__(self):
        return '<%s %r>' % (self.__class__.__name__, self.name)


class Host(db.Model):
    __tablename__ = 'hosts'

    id = db.Column(db.Integer, primary_key=True)
    hostname = db.Column(db.String(64))
    ip_address = db.Column(db.String(64), unique=True)
    ssh_port = db.Column(db.Integer)
    username = db.Column(db.String(32))
    password = db.Column(db.String(128))
    ssh_method = db.Column(db.Integer)
    environ = db.Column(db.Integer)
    created_at = db.Column(db.DateTime, default=db.func.now())
    updated_at = db.Column(db.DateTime, default=db.func.now(), onupdate=db.func.now())

    def __repr__(self):
        return '<%s %r>' % (self.__class__.__name__, self.ip_address)

class Deploy(db.Model):
    __tablename__ = 'deploys'

    id = db.Column(db.Integer, primary_key=True)
    project_id = db.Column(db.Integer, db.ForeignKey('projects.id'))
    user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
    version = db.Column(db.String(64))
    mode = db.Column(db.Integer)
    status = db.Column(db.Integer, default=0)
    comment = db.Column(db.Text)
    result = db.Column(db.Integer)
    created_at = db.Column(db.DateTime, default=db.func.now())
    updated_at = db.Column(db.DateTime, default=db.func.now(), onupdate=db.func.now())

    user = db.relationship('User',
                           backref=db.backref("deploys", lazy="dynamic"))
    project = db.relationship('Project',
                              backref=db.backref("deploys", lazy="dynamic"))

    def __repr__(self):
        return '<%s %r>' % (self.__class__.__name__, self.version)<file_sep>/app/utils/helper.py
# -*- coding: utf-8 -*-

from functools import wraps
from flask import g, request, redirect, url_for, json, abort, jsonify, current_app
import jwt

from app.services.users import users
from app.services.roles import roles

__all__ = [
    'get_dynamic_inventory', 'login_required', 'permission_required',
    'write_required'
]

def get_dynamic_inventory(proj, environ):
    return {
        proj.name: {
            'hosts': [{
                'hostname': h.ip_address,
                'port': h.ssh_port,
                'username': h.username,
                'password': h.<PASSWORD>
            } for h in proj.hosts if h.environ == int(environ)],
            'vars': {
                'ansible_user': 'boer',
                'ansible_become': True,
                'ansible_become_method': 'sudo',
                'ansible_become_user': 'root',
                'ansible_become_pass': '<PASSWORD>'
            }
        }
    }


def login_required(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        token = request.cookies.get('token')
        if not token:
            return redirect(url_for('auth.login', redirect_uri=request.path))
        try:
            token = jwt.decode(token, current_app.config['SECRET_KEY'])
            g.user = users.get(token['uid'])
            g.role = roles.get(token['rid'])
        except jwt.InvalidTokenError as e:
            return redirect(url_for('auth.login', redirect_uri=request.path))
        return f(*args, **kwargs)
    return decorated_function


def permission_required(*roles, alls=False):
    # http://flask.pocoo.org/snippets/98/
    def wrapper(f):
        @wraps(f)
        def wrapped(*args, **kwargs):
            if g.role.id in roles:
                if alls:
                    abort(403)
                if request.method == 'POST':
                    return jsonify(code=405, msg='权限不足')                    
            return f(*args, **kwargs)
        return wrapped
    return wrapper


def write_required(f):
    @wraps(f)
    def wrapped(*args, **kwargs):
        if request.method == 'POST':
            return jsonify(code=405, msg='权限不足')
        return f(*args, **kwargs)
    return wrapped<file_sep>/app/services/deploys.py
#!/usr/local/env python
# -*- coding: utf-8 -*-

import os
import subprocess

from flask import g

from . import Base
from app.models import Deploy
from app.utils.remoteshell import MyRunner
from app.utils.helper import get_dynamic_inventory

class DeployService(Base):
    __model__ = Deploy

    def deploy_task(self, project_id, environ):
        c_d = self.first(project_id=project_id, status=0) if int(environ) != 2 else self.first(project_id=project_id, status=3)
        if not c_d:
            return
        # 检出代码库
        cmd = 'git clone -q %s %s' % (c_d.project.repo_url, c_d.project.checkout_dir)
        git_path = os.path.join(c_d.project.checkout_dir, '.git')
        if os.path.exists(git_path) and os.path.isdir(git_path):
            cmd = 'cd %s && git fetch --all -fq' % c_d.project.checkout_dir
        rc = subprocess.call(cmd, shell=True)
        # 指定版本
        cmd = 'cd %s && git reset -q --hard %s' % (c_d.project.checkout_dir, c_d.version.strip())
        rc = subprocess.call(cmd, shell=True)
        # 同步到编译/打包目录
        cmd = 'rsync -qa --delete --exclude .git %s%s %s%s' % (c_d.project.checkout_dir, os.sep, c_d.project.compile_dir, os.sep)
        rc = subprocess.call(cmd, shell=True)
        # 执行用户自定义命令（权限，fis编译，打包）
        cmds = c_d.project.compile_cmd.split('\n')
        cmds = ' && '.join(cmds)
        rc = subprocess.call(cmds, shell=True)
        # 获取ansible动态Inventory
        resource = get_dynamic_inventory(c_d.project, environ)
        host_lists = [h.ip_address for h in c_d.project.hosts if h.environ == int(environ)]
        # 执行ansible playbook
        runner = MyRunner(resource)
        runner.run_playbook(host_lists, c_d.project.playbook_path)
        if int(environ) == 2:
            self.update(c_d, status=4)
        else:
            self.update(c_d, status=1)
        # return runner.get_playbook_result()
        return self.get(id=c_d.id).status

    
    def rollback(self, deploy):
        pass

deploys = DeployService()<file_sep>/app/static/js/auth/users.js
layui.use(['element', 'layer', 'util', 'form', 'layedit', 'laypage'], function () {
    var $ = layui.jquery;
    var element = layui.element();
    var layer = layui.layer;
    var util = layui.util;
    var form = layui.form();
    var layedit = layui.layedit;
    var laypage = layui.laypage;

    // 表单提交
    form.on('submit(addUser)', function(data) {
        // layer.msg(JSON.stringify(data.field));
        $.ajax({
            type: 'POST',
            url: '/users',
            data: data.field,
            dataType: 'json',
            processData: true,
            success: function (data, txtStatus, jqXHR) {
                if (data.code === 200) {
                    layer.msg(data.msg, {
                        icon: 1,
                        time: 2000,
                        anim: 0,
                        shade: [0.6, '#c2c2c2']
                    },
                    function () {
                        location.href="/users";
                    });
                } else {
                    layer.msg(data.msg, {
                        icon: 2,
                        time: 2000,
                        anim: 6,
                        shade: [0.6, '#c2c2c2']
                    });
                }
            }
        });
        return false;
    });

    // 添加主机弹出层
    $('#add-user-btn').on('click', function () {
        layer.open({
            type: 1,
            title: '添加用户',
            skin: 'layui-layer-molv',
            content: $('#add-user-ctx'),
            area: ['600px', '480px'],
            // area: 'auto',
            // maxWidth: '900px',
            closeBtn: 1,
            resize: true
            // shadeClose: true,
        });
    });
});