├── .DS_Store ├── .gitignore ├── Dockerfile ├── README.md ├── api ├── __init__.py ├── authentication │ ├── __init__.py │ ├── model.py │ ├── url.py │ └── view.py └── resource │ ├── __init__.py │ ├── model.py │ ├── url.py │ └── view.py ├── config ├── __init__.py ├── settings.py └── supervisor_celery.conf ├── docker-compose.deploy.yaml ├── entrypoint.sh ├── flask.yml ├── main.py ├── manage.py ├── requirements.txt ├── task ├── __init__.py └── tasks.py ├── templates ├── auth.html ├── logout.html └── test.html ├── tests ├── BaseTest.py ├── __init__.py └── example.py ├── utils ├── ErrorCode.py ├── __init__.py ├── ext.py ├── helper.py └── permission.py └── vue-init ├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .postcssrc.js ├── README.md ├── build ├── build.js ├── check-versions.js ├── dev-client.js ├── dev-server.js ├── utils.js ├── vue-loader.conf.js ├── webpack.base.conf.js ├── webpack.dev.conf.js ├── webpack.prod.conf.js └── webpack.test.conf.js ├── config ├── dev.env.js ├── index.js ├── prod.env.js └── test.env.js ├── index.html ├── package.json ├── src ├── App.vue ├── api │ ├── auth.js │ └── config.js ├── assets │ ├── logo.png │ └── 运维平台.png ├── components │ ├── chart │ │ ├── chart.vue │ │ └── chart_highcharts.vue │ ├── group.vue │ ├── layout.vue │ ├── layout2.vue │ ├── login.vue │ ├── resource │ │ ├── server.vue │ │ ├── server_edit.vue │ │ └── server_view.vue │ ├── server_group │ │ ├── server_group.vue │ │ └── server_group_edit.vue │ └── user │ │ ├── user.vue │ │ ├── user_edit.vue │ │ └── user_view.vue ├── main.js ├── router │ └── index.js └── store │ └── index.js ├── static └── .gitkeep └── test ├── e2e ├── custom-assertions │ └── elementCount.js ├── nightwatch.conf.js ├── runner.js └── specs │ └── test.js └── unit ├── .eslintrc ├── index.js ├── karma.conf.js └── specs └── Hello.spec.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei825/devops-flask-vue/1b8457f62184e857dddf05a87e7ef925025ee1ed/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | *.so 3 | *.egg 4 | *.egg-info 5 | *.log 6 | dist 7 | #build 8 | node_modules/ 9 | 10 | site.pkl 11 | test*.py 12 | log/ 13 | logs/ 14 | __pycache__/ 15 | .idea/ 16 | 17 | migrations 18 | tests/* 19 | !tests/BaseTest.py 20 | !tests/example.py 21 | !tests/__init__.py 22 | 23 | celerybeat-schedule.db 24 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # https://hub.docker.com/r/tiangolo/uwsgi-nginx-flask/tags/ 2 | FROM tiangolo/uwsgi-nginx-flask:flask-python3.5-index 3 | 4 | ENV C_FORCE_ROOT true 5 | 6 | # Set the default timezone to Shanghai 7 | RUN cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime; \ 8 | echo "Asia/Shanghai" > /etc/timezone; \ 9 | dpkg-reconfigure -f noninteractive tzdata 10 | 11 | COPY . /app/ 12 | COPY entrypoint.sh /entrypoint.sh 13 | RUN chmod +x /entrypoint.sh 14 | #COPY ./config/supervisor_celery.conf /etc/supervisor/conf.d/ 15 | RUN pip install --upgrade pip ; pip install -r requirements.txt -i https://pypi.doubanio.com/simple 16 | 17 | ENTRYPOINT ["/entrypoint.sh"] 18 | 19 | WORKDIR /app 20 | 21 | CMD ["/usr/bin/supervisord"] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 运维平台 2 | ================== 3 | 前端基于vue iview框架, 后端flask做api
4 | Demo: http://devops.feifeiwd.com:8080 5 | 6 | ## 功能特色 7 | 统一资产信息源, 以此扩展如下功能: 8 | - 支持ldap,邮箱等多种登录验证方式 9 | - 基于ansible密码或秘钥批量管理主机 10 | - 基于websocket的ssh终端 11 | - 项目分组, 对象级角色管理, 大规模运维分组模块化管理 12 | - docker容器镜像管理, 镜像持续构建部署 13 | - 传统代码发布, 定制更新执行步骤, 还原回滚 14 | - 监控及报警规则, 可设置预处理方案 15 | 16 | ## 设计思路 17 | ![](vue-init/src/assets/运维平台.png)
18 | 19 | ## Docker部署 20 | #### 构建镜像 21 | docker build -t ffwd/blog:latest . 22 | #### 启动服务 23 | docker-compose -f docker-compose.deploy.yaml up 24 | 25 | Python Web 开发 26 | ================== 27 | 基于python3.5的flask框架web server, 请按以下规范开发. 28 | 29 | ## 学习资料参考 30 | - [github官网](https://github.com/pallets/flask) 31 | - [flask中文手册 0.10 文档](http://docs.pythontab.com/flask/flask0.10/index.html) 32 | - <<[Python Web开发实战](https://github.com/dongweiming/web_develop)>> 33 | 34 | ## 安装python3.5(建议使用mac或centos7系统,安装第三方模块会更顺利) 35 | ``` 36 | wget https://www.python.org/ftp/python/3.5.3/Python-3.5.3.tar.xz 37 | tar xf Python-3.5.3.tar.xz 38 | cd Python-3.5.3 39 | ./configure --with-pydebug 40 | make 41 | make install 42 | ``` 43 | 44 | ## 需要安装的第三方模块 45 | 46 | * flask 47 | * redis 48 | * Flask-SQLAlchemy 49 | * Flask-Migrate 50 | * Flask-Security 51 | * Flask-Script 52 | * Flask-RESTful 53 | * Flask-KVSession 54 | * flasgger 55 | 56 | ``` 57 | # 安装第三方模块前最好安装virtualenv,可以创建一个单独干净的python虚拟环境 58 | pip install virtualenv 59 | 60 | # 创建虚拟环境命名为venv 61 | virtualenv venv 62 | 63 | # 切换到虚拟环境 64 | source venc/bin/activate 65 | 66 | # 虚拟环境下安装flask 67 | pip install flask 68 | ``` 69 | 70 | 安装过程报错 如下解决 71 | ``` 72 | yum install python-devel mysql-devel zlib-devel openssl-devel 73 | ``` 74 | 75 | ## 目录结构 76 | ``` 77 | 主目录 78 | |-- config/ 79 | | |-- __init__.py 80 | | |-- settings.py 81 | | |-- debug_settings.py 82 | |-- api/ 83 | | |-- auth/ 84 | | | |-__init__.py 85 | | | |-model.py 86 | | | |-url.py 87 | | | |-view.py 88 | | |-- monitor/ 89 | | | |-__init__.py 90 | | | |-model.py 91 | | | |-url.py 92 | | | |-view.py 93 | |-- utils/ 94 | | |-__init__.py 95 | | |-error_code.py 96 | | |-email.py 97 | | |-zabbix_api.py 98 | | |-cacahe.py 99 | | |-session.py 100 | |-- tests/ 101 | | |-- __init__.py 102 | | |-- test*.py 103 | |-- .gitignore 104 | |-- requirements.txt 105 | |-- manage.py 106 | |-- logs/ 107 | |-- templates/ 108 | |-- static/ 109 | ``` 110 | 111 | * manage.py 主运行程序 112 | * api目录下存放各功能集的view视图,model表结构,url路由 113 | * utils工具方法 114 | * tests单元测试, example.py是一个post请求的单元测试例子 115 | * config配置文件 116 | * logs日志每日分割 117 | 118 | -------------------------------------------------------------------------------- /api/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from flask import Flask, flash, request 3 | from .authentication.url import get_auth_resources 4 | from utils.ext import db, cors, mongo 5 | from utils.permission import security, authenticate, identity 6 | from flask_kvsession import KVSessionExtension 7 | import redis 8 | from simplekv.memory.redisstore import RedisStore 9 | from flask_login import user_logged_in, user_logged_out 10 | import datetime 11 | from flasgger import Swagger 12 | from flask_jwt import JWT 13 | 14 | 15 | def create_app(config_name, template_folder=None, static_folder=None): 16 | app = Flask(__name__, static_folder=static_folder, template_folder=template_folder) 17 | app.config.from_object(config_name) 18 | 19 | # jwt初始化 20 | jwt = JWT(app, authenticate, identity) 21 | jwt.init_app(app) 22 | 23 | # session信息save到reidis 24 | # session = KVSessionExtension() 25 | # store = RedisStore(redis.StrictRedis(**app.config['SESSION_STORE'])) 26 | # session.init_app(app, store) 27 | # session.cleanup_sessions(app) 28 | 29 | # 解决跨域 30 | cors.init_app(app, allow_headers='*') 31 | 32 | # mysql init 33 | db.init_app(app) 34 | 35 | # mongodb init 36 | # mongo.init_app(app) 37 | 38 | # 权限管理 39 | security.init_app(app) 40 | 41 | # 信号 42 | add_signals(app) 43 | 44 | # 临时激活一个请求环境。在这个 环境中可以像以视图函数中一样操作 request 、g 和 session 对象 45 | with app.test_request_context(): 46 | db.create_all() 47 | 48 | # 蓝图功能, 注册api url 49 | # app.register_blueprint(get_auth_resources(), url_prefix='/api') 50 | # app.register_blueprint(get_cmdb_resources(), url_prefix='/api/v1') 51 | 52 | # login 管理 53 | from utils.ext import login_manager 54 | import datetime 55 | login_manager.remember_cookie_duration = datetime.timedelta(seconds=60) 56 | login_manager.init_app(app) 57 | 58 | # api文档初始化 59 | Swagger(app) 60 | 61 | return app 62 | 63 | 64 | def add_signals(app): 65 | """Attaches the user_logged_in and user_logged_out signals to app. Here, we 66 | just use it to flash messages. 67 | """ 68 | @user_logged_in.connect_via(app) 69 | def logged_in(sender, user, **extra): 70 | if 'X-Forwarded-For' in request.headers: 71 | remote_addr = request.headers.getlist("X-Forwarded-For")[0].rpartition(' ')[-1] 72 | else: 73 | remote_addr = request.remote_addr or 'untrackable' 74 | 75 | old_current_login, new_current_login = user.current_login_at, datetime.datetime.now() 76 | old_current_ip, new_current_ip = user.current_login_ip, remote_addr 77 | 78 | user.last_login_at = old_current_login or new_current_login 79 | user.current_login_at = new_current_login 80 | user.last_login_ip = old_current_ip or new_current_ip 81 | user.current_login_ip = new_current_ip 82 | user.login_count = user.login_count + 1 if user.login_count else 1 83 | db.session.add(user) 84 | db.session.commit() 85 | flash('You were Login Success. %s ' % datetime.datetime.now()) 86 | 87 | @user_logged_out.connect_via(app) 88 | def logged_out(app, user): 89 | flash('You were logged out. %s ' % datetime.datetime.now()) 90 | return 91 | -------------------------------------------------------------------------------- /api/authentication/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei825/devops-flask-vue/1b8457f62184e857dddf05a87e7ef925025ee1ed/api/authentication/__init__.py -------------------------------------------------------------------------------- /api/authentication/model.py: -------------------------------------------------------------------------------- 1 | from flask_security import RoleMixin, UserMixin 2 | from utils.ext import db 3 | from functools import reduce 4 | from werkzeug.security import check_password_hash, generate_password_hash 5 | from datetime import datetime 6 | 7 | 8 | class Permission(object): 9 | VIEW = 0X01 10 | EDITOR = 0x02 11 | OPERATOR = 0x04 12 | ADMIN = 0xff # hex(255) 13 | SUPER_ADMIN = -0xff 14 | PERMISSION_MAP = { 15 | VIEW: ('view', 'View'), 16 | EDITOR: ('editor', 'Editor'), 17 | OPERATOR: ('op', 'Operator'), 18 | ADMIN: ('admin', 'administrator'), 19 | } 20 | 21 | roles_users = db.Table( 22 | 'roles_users', 23 | db.Column('user_id', db.Integer, db.ForeignKey('user.id')), 24 | db.Column('role_id', db.Integer, db.ForeignKey('role.id'))) 25 | 26 | 27 | class Role(db.Model, RoleMixin): 28 | id = db.Column(db.Integer, primary_key=True) 29 | name = db.Column(db.String(255)) 30 | permissions = db.Column(db.Integer, default=Permission.VIEW) 31 | description = db.Column(db.String(255)) 32 | groups_id = db.Column(db.Integer, db.ForeignKey('groups.id')) 33 | 34 | def to_json(self): 35 | doc = self.__dict__ 36 | if "_sa_instance_state" in doc: 37 | del doc["_sa_instance_state"] 38 | 39 | if doc.get('confirmed_at', None): 40 | doc['confirmed_at'] = doc['confirmed_at'].strftime('%F %T') 41 | 42 | return doc 43 | 44 | 45 | class Groups(db.Model): 46 | id = db.Column(db.Integer, primary_key=True) 47 | name = db.Column(db.String(255), unique=True) 48 | description = db.Column(db.String(255)) 49 | confirmed_at = db.Column(db.DateTime(), default=datetime.now()) 50 | roles = db.relationship('Role', backref='groups', lazy='dynamic') 51 | 52 | def to_json(self): 53 | doc = self.__dict__ 54 | if "_sa_instance_state" in doc: 55 | del doc["_sa_instance_state"] 56 | 57 | if doc.get('confirmed_at', None): 58 | doc['confirmed_at'] = doc['confirmed_at'].strftime('%F %T') 59 | 60 | return doc 61 | 62 | 63 | class User(db.Model, UserMixin): 64 | id = db.Column(db.Integer, primary_key=True) 65 | email = db.Column(db.String(255), unique=True) 66 | username = db.Column(db.String(255), unique=True) 67 | job = db.Column(db.String(255)) 68 | phone = db.Column(db.String(255)) 69 | password_hash = db.Column(db.String(255)) 70 | active = db.Column(db.Boolean()) 71 | confirmed_at = db.Column(db.DateTime(), default=datetime.now()) 72 | roles = db.relationship('Role', secondary=roles_users, backref=db.backref('users', lazy='dynamic')) 73 | last_login_at = db.Column(db.String(255)) 74 | current_login_at = db.Column(db.String(255)) 75 | last_login_ip = db.Column(db.String(255)) 76 | current_login_ip = db.Column(db.String(255)) 77 | login_count = db.Column(db.Integer) 78 | 79 | # 权限验证 80 | def can(self, gid, permissions=Permission.VIEW): 81 | if self.roles is None: 82 | return False 83 | # 判断是否在组中 [ r for r in self.roles if 组 == r.组] 84 | permissions_list = [r.permissions for r in self.roles if r.groups_id == int(gid) or r.groups_id == 2] 85 | print("can:", permissions_list, gid) 86 | if permissions_list: 87 | all_perms = reduce(lambda x, y: x | y, permissions_list) 88 | else: 89 | all_perms = 0 90 | 91 | return all_perms & permissions == permissions 92 | 93 | def can_admin(self): 94 | return self.can(gid=1, permissions=Permission.ADMIN) 95 | 96 | # password不可读 97 | @property 98 | def password(self): 99 | raise AttributeError('`password` is not a readable attribute') 100 | 101 | # password加密 102 | @password.setter 103 | def password(self, password): 104 | self.password_hash = generate_password_hash(password) 105 | 106 | # 验证password 107 | def verify_password(self, password): 108 | return check_password_hash(self.password_hash, password) 109 | 110 | def to_json(self, gid): 111 | doc = self.__dict__ 112 | doc['roles'] = self.roles 113 | # print(self.username, self.roles) 114 | if "_sa_instance_state" in doc: 115 | del doc["_sa_instance_state"] 116 | 117 | if "password_hash" in doc: 118 | del doc["password_hash"] 119 | 120 | if doc.get('confirmed_at', None): 121 | doc['confirmed_at'] = doc['confirmed_at'].strftime('%F %T') 122 | 123 | if doc.get('current_login_at', None): 124 | doc['current_login_at'] = doc['current_login_at'].split('.')[0] 125 | 126 | if doc.get('last_login_at', None): 127 | doc['last_login_at'] = doc['last_login_at'].split('.')[0] 128 | 129 | if doc.get('roles', None): 130 | doc['roles'] = [str(r.permissions) for r in doc['roles'] if r.groups_id == gid] 131 | 132 | return doc 133 | 134 | 135 | -------------------------------------------------------------------------------- /api/authentication/url.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from flask import Blueprint 3 | from flask_restful import Api 4 | from .view import Auth, Users, Group 5 | 6 | 7 | def get_auth_resources(): 8 | auth_bp = Blueprint('auth', __name__, template_folder='../../templates', static_url_path='', static_folder='') 9 | 10 | api = Api(auth_bp) 11 | 12 | api.add_resource(Auth, '/login') 13 | api.add_resource(Users, '/user') 14 | api.add_resource(Group, '/group') 15 | 16 | return auth_bp 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /api/authentication/view.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from flask_restful import Resource, reqparse, request 3 | from flask import flash, redirect, Blueprint, current_app 4 | from flask_security import login_required, login_user, logout_user 5 | from .model import User, Permission, Groups, Role 6 | from utils.permission import permission_required 7 | from utils.ext import db 8 | from flask_login import current_user 9 | import json 10 | import logging 11 | from utils.ErrorCode import * 12 | import jwt 13 | from flask_jwt import jwt_required, current_identity 14 | from utils.helper import Argument 15 | from sqlalchemy import and_, or_ 16 | 17 | 18 | module = Blueprint('logout', __name__) 19 | 20 | 21 | @module.route('/logout') 22 | def logout(): 23 | logout_user() 24 | return "logout ok" 25 | 26 | 27 | class Auth(Resource): 28 | def __init__(self): 29 | super(Auth, self).__init__() 30 | 31 | @jwt_required() 32 | def put(self): 33 | """ 34 | token验证 35 | --- 36 | tags: 37 | - LOGIN 38 | parameters: 39 | - in: header 40 | name: Authorization 41 | type: string 42 | required: true 43 | description: "JWT " 44 | responses: 45 | 200: 46 | description: token验证 47 | schema: 48 | properties: 49 | result: 50 | type: string 51 | default: ok 52 | examples: 53 | { 54 | "result": { 55 | "verify": True, 56 | }, 57 | "state": "ok" 58 | } 59 | """ 60 | 61 | return {'result': {'verify': True}, 'state': 'ok'}, 200 62 | 63 | def post(self): 64 | """ 65 | 用户登录 66 | --- 67 | tags: 68 | - LOGIN 69 | parameters: 70 | - in: formData 71 | name: email 72 | type: string 73 | description: "邮箱" 74 | - in: formData 75 | name: password 76 | type: string 77 | description: "密码" 78 | responses: 79 | 200: 80 | description: 用户认证登录 81 | schema: 82 | properties: 83 | result: 84 | type: string 85 | default: ok 86 | examples: 87 | { 88 | "result": { 89 | "exp": 1498621139, 90 | "token": "eyJhbGciOiJIUzI1NiIsInR5cCI", 91 | "username": "lifei5" 92 | }, 93 | "state": "ok" 94 | } 95 | """ 96 | email = None 97 | token = None 98 | exp = None 99 | state = STATE_OK 100 | try: 101 | email = request.values.get('email', None) 102 | password = request.values.get('password', None) 103 | _secret = current_app.config.get('SECRET_KEY') 104 | print("identfied", email, password) 105 | 106 | with current_app.test_client() as c: 107 | resp = c.post('/auth', headers={'Content-Type': 'application/json'}, 108 | data=json.dumps({"username": email, "password": password})) 109 | data = json.loads(resp.data.decode('utf8')) 110 | print("auth", data) 111 | if data.get('error'): 112 | raise ErrorCode(451, data.get('description', "Bad Request")) 113 | 114 | token = data.get('access_token', None) 115 | exp = jwt.decode(token, key=_secret).get("exp") 116 | 117 | uid = jwt.decode(token, key=_secret).get("identity") 118 | user = User.query.get(int(uid)) 119 | login_user(user) 120 | 121 | except Exception as e: 122 | logging.error("get token error: %s." % str(e)) 123 | state = isinstance(e, ErrorCode) and e or ErrorCode(451, "unknown error:" + str(e)) 124 | 125 | return {'result': {'username': email, 126 | 'token': token, 127 | 'exp': exp, 128 | 'permission': Permission.PERMISSION_MAP}, 'state': state.message}, state.eid 129 | 130 | 131 | class Users(Resource): 132 | def __init__(self): 133 | self.parser = reqparse.RequestParser() 134 | self.parser.add_argument('user', type=str, required=True, location='form') 135 | self.parser.add_argument('passwd', type=str, required=True, location='form') 136 | 137 | self.parser_post = reqparse.RequestParser() 138 | self.parser_post.add_argument('roles', type=list, action='append', location=['form', 'values', 'json']) 139 | 140 | self.parser_get = reqparse.RequestParser() 141 | self.parser_get.add_argument('user', type=str, required=False, location='args') 142 | super(Users, self).__init__() 143 | 144 | @jwt_required() 145 | @permission_required(Permission.VIEW) 146 | def get(self): 147 | """ 148 | 员工列表接口 149 | --- 150 | tags: 151 | - USER 152 | parameters: 153 | - in: header 154 | name: Authorization 155 | type: string 156 | required: true 157 | description: "JWT " 158 | - in: query 159 | name: gid 160 | type: integer 161 | - in: query 162 | name: page 163 | type: integer 164 | description: 当前页 165 | - in: query 166 | name: pageSize 167 | type: integer 168 | description: 每页显示量 169 | responses: 170 | 200: 171 | description: 仅管理员可访问 172 | """ 173 | doc = [] 174 | state = STATE_OK 175 | users_total = 0 176 | 177 | try: 178 | page = int(request.values.get('page', 1)) 179 | page_size = int(request.values.get('pageSize', 10)) 180 | keyword = request.values.get('keyword', "") 181 | 182 | # 如果是超级管理员可获取所有用户信息 183 | if self.gid == 0: 184 | users_class = User.query.filter(or_(User.username.like("%"+keyword+"%"), 185 | User.email.like("%"+keyword+"%"), 186 | User.phone.like("%"+keyword+"%"), 187 | User.job.like("%"+keyword+"%"),) 188 | ).order_by(User.id.desc()).paginate(page, page_size, error_out=True) 189 | 190 | # 否则获取指定项目下的所有用户 191 | else: 192 | users_class = User.query.join(User.roles).filter( 193 | and_(Role.groups_id == self.gid, 194 | or_(User.email.like("%"+keyword+"%"), 195 | User.phone.like("%"+keyword+"%"), 196 | User.username.like("%"+keyword+"%"), 197 | User.job.like("%"+keyword+"%"),)) 198 | ).order_by(User.id.desc()).paginate(page, page_size, error_out=False) 199 | 200 | users = users_class.items 201 | users_total = users_class.total 202 | 203 | doc = [u.to_json(self.gid) for u in users] 204 | 205 | except Exception as e: 206 | logging.error("get user info error: %s." % str(e)) 207 | state = isinstance(e, ErrorCode) and e or ErrorCode(1, "unknown error:" + str(e)) 208 | 209 | return {'result': {'doc': doc, 'total': users_total}, 'state': state.message}, state.eid 210 | 211 | @jwt_required() 212 | @permission_required(Permission.VIEW) 213 | def post(self): 214 | """ 215 | 用户添加修改 216 | --- 217 | tags: 218 | - USER 219 | parameters: 220 | - in: header 221 | name: Authorization 222 | type: string 223 | required: true 224 | description: "JWT " 225 | - in: formData 226 | name: id 227 | type: integer 228 | description: "用户ID" 229 | - in: formData 230 | name: username 231 | type: string 232 | description: "用户名" 233 | - in: formData 234 | name: job 235 | type: string 236 | description: "职位" 237 | - in: formData 238 | name: phone 239 | type: string 240 | description: "手机" 241 | - in: formData 242 | name: email 243 | type: string 244 | description: "邮箱" 245 | - in: formData 246 | name: active 247 | type: string 248 | description: "是否激活" 249 | - in: formData 250 | name: roles 251 | type: array 252 | description: "角色" 253 | responses: 254 | 200: 255 | description: 员工信息修改接口 256 | """ 257 | 258 | state = STATE_OK 259 | rs = False 260 | try: 261 | request_param = dict(request.values.items()) 262 | print("user post:", request_param) 263 | uid = request.values.get("id", None) 264 | username = request.values.get("username", None) 265 | phone = request.values.get("phone", None) 266 | email = request.values.get("email", None) 267 | job = request.values.get("job", None) 268 | roles = request.values.get("roles", "").split(',') 269 | print(roles, type(roles)) 270 | 271 | if not username and not email: 272 | raise STATE_PARAM_ERR 273 | 274 | if not uid: 275 | user = User(username=username, 276 | email=email, 277 | phone=phone, 278 | job=job, 279 | active=True) 280 | 281 | password = current_app.config.get('PASSWORD_KEY') 282 | user.password = password 283 | 284 | else: 285 | user = User.query.get(int(uid)) 286 | user.username = username, 287 | user.email = email, 288 | user.phone = phone, 289 | user.job = job 290 | user.active = True 291 | 292 | if roles: 293 | print("roles", roles) 294 | for r in roles: 295 | role = Role.query.filter_by(groups_id=self.gid, permissions=int(r)).first() 296 | print("role s", role) 297 | if role not in user.roles: 298 | print("not role s") 299 | user.roles.append(role) 300 | 301 | # user.roles 302 | print("db sssss") 303 | db.session.add(user) 304 | db.session.commit() 305 | 306 | rs = True 307 | 308 | except Exception as e: 309 | logging.error("get user info error: %s." % str(e)) 310 | state = isinstance(e, ErrorCode) and e or ErrorCode(1, "unknown error:" + str(e)) 311 | 312 | return {'result': rs, 'state': state.message}, state.eid 313 | 314 | 315 | class Group(Resource): 316 | def __init__(self): 317 | self.parser = reqparse.RequestParser(argument_class=Argument) 318 | self.parser.add_argument('id', type=int, required=False, location=['form', 'values']) 319 | self.parser.add_argument('name', type=str, required=True, location=['form', 'values'], nullable=False, trim=True) 320 | self.parser.add_argument('description', type=str, required=False, location=['form', 'values'], trim=True) 321 | super(Group, self).__init__() 322 | 323 | @jwt_required() 324 | @permission_required(Permission.VIEW, active=False) 325 | def get(self): 326 | """ 327 | 查询项目列表 328 | --- 329 | tags: 330 | - GROUP 331 | parameters: 332 | - in: header 333 | name: Authorization 334 | type: string 335 | required: true 336 | description: "JWT " 337 | - in: query 338 | name: gid 339 | type: string 340 | - in: query 341 | name: page 342 | type: string 343 | description: 当前页 344 | - in: query 345 | name: pageSize 346 | type: string 347 | description: 每页显示量 348 | responses: 349 | 200: 350 | description: 员工信息查询接口 351 | """ 352 | doc = [] 353 | state = STATE_OK 354 | groups_total = 0 355 | 356 | try: 357 | page = int(request.values.get('page', 1)) 358 | page_size = int(request.values.get('pageSize', 10)) 359 | keyword = request.values.get('keyword', "") 360 | 361 | if self.gid == 2 and [r for r in self.user.roles if r.groups_id == 2]: 362 | groups_class = Groups.query.filter(Groups.name.like('%{0}%'.format(keyword))).order_by( 363 | Groups.id.desc()).paginate(page, page_size, error_out=False) 364 | 365 | else: 366 | groups_class = Groups.query.join(Role.groups).filter( 367 | and_(Groups.id.in_((r.groups_id for r in self.user.roles)), 368 | Groups.name.like("%"+keyword+"%"))).order_by( 369 | Groups.id.desc()).paginate(page, page_size, error_out=False) 370 | 371 | groups = groups_class.items 372 | groups_total = groups_class.total 373 | doc = [g.to_json() for g in set(groups)] 374 | 375 | print('send doc', doc) 376 | 377 | except Exception as e: 378 | logging.error("get user info error: %s." % str(e)) 379 | state = isinstance(e, ErrorCode) and e or ErrorCode(451, "unknown error:" + str(e)) 380 | 381 | return {'result': {'doc': doc, 'total': groups_total}, 'state': state.message}, state.eid 382 | 383 | @jwt_required() 384 | @permission_required(Permission.SUPER_ADMIN) 385 | def post(self): 386 | """ 387 | 项目添加 388 | --- 389 | tags: 390 | - GROUP 391 | parameters: 392 | - in: header 393 | name: Authorization 394 | type: string 395 | required: true 396 | description: "JWT " 397 | - in: formData 398 | name: id 399 | type: string 400 | description: "项目id" 401 | - in: formData 402 | name: name 403 | type: string 404 | required: true 405 | description: "项目名称" 406 | - in: formData 407 | name: description 408 | type: string 409 | description: "项目描述" 410 | responses: 411 | 200: 412 | description: 项目添加 413 | """ 414 | state = STATE_OK 415 | rs = True 416 | 417 | try: 418 | print("start post group") 419 | data = self.parser.parse_args() 420 | gid = data.get("id", None) 421 | name = data.get("name", None) 422 | description = data.get("description", None) 423 | if not name: 424 | raise STATE_PreconditionFailed 425 | 426 | print("post all:", request.values.__dict__, name, description, type(gid)) 427 | 428 | if gid: 429 | group = Groups.query.get(gid) 430 | group.name = name 431 | group.description = description 432 | 433 | db.session.add(group) 434 | db.session.commit() 435 | 436 | else: 437 | group = Groups(name=name, description=description) 438 | db.session.add(group) 439 | 440 | for permissions, (role, desc) in Permission.PERMISSION_MAP.items(): 441 | r = Role(name=role, description=desc, permissions=permissions, groups=group) 442 | db.session.add(r) 443 | 444 | db.session.commit() 445 | 446 | except Exception as e: 447 | rs = False 448 | logging.error("get user info error: %s." % str(e)) 449 | state = isinstance(e, ErrorCode) and e or ErrorCode(451, "unknown error:" + str(e)) 450 | 451 | return {'result': rs, 'state': state.message}, state.eid 452 | 453 | @jwt_required() 454 | @permission_required(Permission.SUPER_ADMIN) 455 | def delete(self): 456 | """ 457 | 项目添加 458 | --- 459 | tags: 460 | - GROUP 461 | parameters: 462 | - in: header 463 | name: Authorization 464 | type: string 465 | required: true 466 | description: "JWT " 467 | - in: formData 468 | name: id 469 | type: string 470 | description: "项目id" 471 | responses: 472 | 200: 473 | description: 项目添加 474 | """ 475 | state = STATE_OK 476 | rs = True 477 | 478 | try: 479 | gid = int(request.values.get('id')) 480 | if gid == 2: 481 | raise ErrorCode(501, "无法删除超级管理组") 482 | 483 | group = Groups.query.get(gid) 484 | 485 | db.session.query(Role).filter(or_(Role.groups_id == gid, Role.groups_id == None)).delete() 486 | db.session.delete(group) 487 | db.session.commit() 488 | 489 | except Exception as e: 490 | rs = False 491 | logging.error("get user info error: %s." % str(e)) 492 | state = isinstance(e, ErrorCode) and e or ErrorCode(451, "unknown error:" + str(e)) 493 | 494 | return {'result': rs, 'state': state.message}, state.eid 495 | -------------------------------------------------------------------------------- /api/resource/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei825/devops-flask-vue/1b8457f62184e857dddf05a87e7ef925025ee1ed/api/resource/__init__.py -------------------------------------------------------------------------------- /api/resource/model.py: -------------------------------------------------------------------------------- 1 | from utils.ext import db 2 | 3 | 4 | class Server(db.Model): 5 | id = db.Column(db.Integer, primary_key=True) 6 | hostname = db.Column(db.String(255), unique=True) 7 | outside = db.Column(db.String(255), unique=True) 8 | intranet = db.Column(db.String(255)) 9 | idc = db.Column(db.String(255)) 10 | area = db.Column(db.String(255)) 11 | 12 | def to_json(self): 13 | doc = self.__dict__ 14 | if "_sa_instance_state" in doc: 15 | del doc["_sa_instance_state"] 16 | 17 | if doc.get('confirmed_at', None): 18 | doc['confirmed_at'] = doc['confirmed_at'].strftime('%F %T') 19 | 20 | return doc 21 | -------------------------------------------------------------------------------- /api/resource/url.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei825/devops-flask-vue/1b8457f62184e857dddf05a87e7ef925025ee1ed/api/resource/url.py -------------------------------------------------------------------------------- /api/resource/view.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei825/devops-flask-vue/1b8457f62184e857dddf05a87e7ef925025ee1ed/api/resource/view.py -------------------------------------------------------------------------------- /config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei825/devops-flask-vue/1b8457f62184e857dddf05a87e7ef925025ee1ed/config/__init__.py -------------------------------------------------------------------------------- /config/settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from datetime import timedelta 3 | 4 | 5 | class Config(object): 6 | DEBUG = True 7 | 8 | SECRET_KEY = 'key-110-password' 9 | 10 | PASSWORD_KEY = "password" # 统一用户登录密码 11 | 12 | SESSION_COOKIE_DOMAIN = 'feifeiwd.com' 13 | 14 | # cook保存到redis过期时间 15 | # PERMANENT_SESSION_LIFETIME = timedelta(seconds=600) 16 | JWT_EXPIRATION_DELTA = timedelta(seconds=60 * 60 * 24 * 10) 17 | 18 | # SESSION_STORE = {'host': 'paas-test.asiainfo.com', 'port': 30487, 'db': 2, 'password': '7788'} 19 | 20 | SWAGGER = {'title': 'Devops API', 'uiversion': 2, 'description': ''} 21 | 22 | 23 | class Devops(Config): 24 | 25 | SQLALCHEMY_DATABASE_URI = 'mysql://root:@127.0.0.1:3306/backend?charset=utf8' 26 | SQLALCHEMY_TRACK_MODIFICATIONS = True 27 | 28 | # celery config 29 | BROKER_URL = 'redis://:123@127.0.0.1:6379/7' 30 | CELERY_RESULT_BACKEND = 'redis://:123@127.0.0.1:6379/8' 31 | 32 | CELERY_TASK_SERIALIZER = 'json' 33 | CELERY_RESULT_SERIALIZER = 'json' 34 | CELERY_TASK_RESULT_EXPIRES = 60 * 60 * 24 35 | CELERY_ACCEPT_CONTENT = ['json'] 36 | CELERYD_MAX_TASKS_PER_CHILD = 40 # 每个worker执行了多少任务就会死掉 37 | 38 | CELERYBEAT_SCHEDULE = { 39 | 'add-every-1-minutes-active': { 40 | 'task': 'task.tasks.some', 41 | 'schedule': timedelta(seconds=60), 42 | }, 43 | } 44 | 45 | 46 | class DevopsProduction(Config): 47 | 48 | SQLALCHEMY_DATABASE_URI = 'mysql://root:passwd@mysql:3306/backend?charset=utf8' 49 | SQLALCHEMY_TRACK_MODIFICATIONS = True 50 | 51 | # celery config 52 | BROKER_URL = 'redis://redis:6379/13' 53 | CELERY_RESULT_BACKEND = 'redis://redis:6379/14 ' 54 | CELERY_TASK_SERIALIZER = 'json' 55 | CELERY_RESULT_SERIALIZER = 'json' 56 | CELERY_TASK_RESULT_EXPIRES = 60 * 60 * 24 57 | CELERY_ACCEPT_CONTENT = ['json'] 58 | CELERYD_MAX_TASKS_PER_CHILD = 40 # 每个worker执行了多少任务就会死掉 59 | 60 | CELERYBEAT_SCHEDULE = { 61 | 'add-every-1-minutes-active': { 62 | 'task': 'task.tasks.maintain_monitor_active', 63 | 'schedule': timedelta(seconds=60), 64 | }, 65 | } 66 | 67 | -------------------------------------------------------------------------------- /config/supervisor_celery.conf: -------------------------------------------------------------------------------- 1 | [program:celery] 2 | command=celery -A task.tasks.app worker -B -l warn -c 3 3 | directory=/app 4 | stdout_logfile=/dev/stdout 5 | stdout_logfile_maxbytes=0 6 | stderr_logfile=/dev/stderr 7 | stderr_logfile_maxbytes=0 -------------------------------------------------------------------------------- /docker-compose.deploy.yaml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | devops: 4 | image: devops:latest 5 | depends_on: 6 | - redis 7 | - mysql 8 | ports: 9 | - "8080:80" 10 | environment: 11 | FLASK_CONFIG: "DevopsProduction" 12 | redis: 13 | image: redis:4.0-alpine 14 | mysql: 15 | image: mysql:5.7.19 16 | environment: 17 | MYSQL_ROOT_PASSWORD: "passwd" 18 | MYSQL_DATABASE: "backend" 19 | volumes: 20 | - /data/mysql:/var/lib/mysql 21 | 22 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | cd /app && python manage.py create_user 5 | 6 | # Get the maximum upload file size for Nginx, default to 0: unlimited 7 | USE_NGINX_MAX_UPLOAD=${NGINX_MAX_UPLOAD:-0} 8 | # Generate Nginx config for maximum upload file size 9 | echo "client_max_body_size $USE_NGINX_MAX_UPLOAD;" > /etc/nginx/conf.d/upload.conf 10 | 11 | 12 | # Generate Nginx config first part using the environment variables 13 | echo 'server { 14 | location / { 15 | include uwsgi_params; 16 | uwsgi_pass unix:///tmp/uwsgi.sock; 17 | } 18 | location ~/static/ { 19 | root /app/vue-init/dist; 20 | index index.html index.htm; 21 | } 22 | '> /etc/nginx/conf.d/nginx.conf 23 | 24 | # Finish the Nginx config file 25 | echo "}" >> /etc/nginx/conf.d/nginx.conf 26 | 27 | exec "$@" -------------------------------------------------------------------------------- /flask.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Template 3 | labels: 4 | application: flask 5 | createdBy: lifei5 6 | metadata: 7 | annotations: 8 | description: "flask-api" 9 | iconClass: icon-flask 10 | tags: python 11 | creationTimestamp: null 12 | name: flask 13 | objects: 14 | - apiVersion: v1 15 | kind: BuildConfig 16 | metadata: 17 | name: ${APPLICATION_NAME} 18 | spec: 19 | output: 20 | to: 21 | kind: ImageStreamTag 22 | name: ${APPLICATION_NAME}:latest 23 | source: 24 | git: 25 | ref: ${GIT_REF} 26 | uri: ${GIT_URI} 27 | sourceSecret: 28 | name: gitbasicsecret 29 | type: Git 30 | strategy: 31 | dockerStrategy: 32 | forcePull: true 33 | from: 34 | kind: DockerImage 35 | name: ${IMAGE_NAME} 36 | noCache: true 37 | type: Docker 38 | triggers: 39 | - generic: 40 | secret: ${Generic_TRIGGER_SECRET} 41 | type: Generic 42 | - apiVersion: v1 43 | kind: ImageStream 44 | metadata: 45 | name: ${APPLICATION_NAME} 46 | spec: 47 | dockerImageRepository: "" 48 | tags: 49 | - name: latest 50 | - apiVersion: v1 51 | kind: DeploymentConfig 52 | metadata: 53 | labels: 54 | deploymentConfig: ${APPLICATION_NAME} 55 | name: ${APPLICATION_NAME} 56 | spec: 57 | replicas: 1 58 | selector: 59 | deploymentConfig: ${APPLICATION_NAME} 60 | strategy: 61 | type: Rolling 62 | template: 63 | metadata: 64 | labels: 65 | deploymentConfig: ${APPLICATION_NAME} 66 | spec: 67 | containers: 68 | - env: 69 | - name: APPLICATION_NAME 70 | value: ${APPLICATION_NAME} 71 | image: ${APPLICATION_NAME}:latest 72 | imagePullPolicy: IfNotPresent 73 | name: ${APPLICATION_NAME} 74 | ports: 75 | - containerPort: 80 76 | name: http 77 | protocol: TCP 78 | readinessProbe: 79 | failureThreshold: 3 80 | initialDelaySeconds: 10 81 | periodSeconds: 10 82 | successThreshold: 1 83 | tcpSocket: 84 | port: 80 85 | timeoutSeconds: 60 86 | resources: 87 | limits: 88 | cpu: ${LIMIT_CPU}m 89 | memory: ${LIMIT_MEMORY}Mi 90 | requests: 91 | cpu: ${LIMIT_CPU}m 92 | memory: ${LIMIT_MEMORY}Mi 93 | securityContext: 94 | capabilities: {} 95 | privileged: true 96 | terminationMessagePath: /dev/termination-log 97 | dnsPolicy: ClusterFirst 98 | nodeSelector: 99 | nodetype: ${NODETYPE} 100 | restartPolicy: Always 101 | triggers: 102 | - imageChangeParams: 103 | automatic: true 104 | containerNames: 105 | - ${APPLICATION_NAME} 106 | from: 107 | kind: ImageStreamTag 108 | name: ${APPLICATION_NAME}:latest 109 | type: ImageChange 110 | - type: ConfigChange 111 | - apiVersion: v1 112 | kind: Service 113 | metadata: 114 | annotations: 115 | description: 应用的服务 116 | name: ${APPLICATION_NAME} 117 | spec: 118 | ports: 119 | - name: 80-tcp 120 | port: 80 121 | protocol: TCP 122 | targetPort: 80 123 | selector: 124 | deploymentConfig: ${APPLICATION_NAME} 125 | type: NodePort 126 | parameters: 127 | - description: 应用名称 128 | name: APPLICATION_NAME 129 | value: "flask-api" 130 | - description: "git源码路径" 131 | name: GIT_URI 132 | value: "http://gitlab-console.asiainfo.com/lifei5/paas_platform.git" 133 | - description: 选用的git分支,默认为master 134 | name: GIT_REF 135 | value: master 136 | - description: 选用Docker镜像版本 137 | name: IMAGE_NAME 138 | value: 10.1.245.137/uwsgi-nginx-flask:flask-python3.5 139 | - description: "gitlab webhook授权密码,由8位数字字母组成,若为空则由系统生成" 140 | from: '[a-zA-Z0-9]{8}' 141 | generate: expression 142 | name: Generic_TRIGGER_SECRET 143 | - description: "设置最大cpu资源,单位MHZ." 144 | name: LIMIT_CPU 145 | value: "200" 146 | - description: "设置最大内存资源,单位MiB." 147 | name: LIMIT_MEMORY 148 | value: "500" 149 | - description: 要部署的节点集群. 150 | name: NODETYPE 151 | value: normal 152 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from api import create_app 3 | from utils.ext import login_manager 4 | from api.authentication.model import Permission, User, Role, Groups 5 | from api.authentication.url import get_auth_resources 6 | from flask import Blueprint, jsonify, request, make_response, session, render_template 7 | from api.authentication.view import module 8 | from os import environ 9 | 10 | config_name = environ.get("FLASK_CONFIG", 'Devops') 11 | app = create_app('config.settings.{0}'.format(config_name)) 12 | 13 | # 蓝图功能, 注册api url 14 | app.register_blueprint(get_auth_resources(), url_prefix='/api/v1') 15 | app.register_blueprint(module) 16 | 17 | home_page = Blueprint('home', __name__, template_folder='./vue-init/dist', static_folder='./vue-init/dist/static') 18 | 19 | 20 | @home_page.route('/') 21 | def home(): 22 | return render_template('index.html') 23 | 24 | app.register_blueprint(home_page) 25 | 26 | 27 | @app.route("/test") 28 | def hello(): 29 | from flask_login import current_user 30 | # print(current_user.is_authenticated, current_user.__dict__) 31 | # print(request.url, request.cookies, request.headers.__dict__) 32 | # print('ticket: ', request.args.get("ticket", None)) 33 | # response = make_response("test") 34 | # response.set_cookie("name", "xxx") 35 | # print(response) 36 | # print(session.items(), session.get('user_id')) 37 | # return render_template('index.html') 38 | return render_template('auth.html', name='lf') 39 | # return "Hello World from Flask" 40 | 41 | 42 | # start 43 | 44 | 45 | @login_manager.user_loader 46 | def load_user(user_id): 47 | """@login_required 获取用户信息""" 48 | user = User.query.get(user_id) 49 | # print("user login:", user) 50 | return user 51 | 52 | 53 | @login_manager.unauthorized_handler 54 | def unauthorized(): 55 | return {"err": "not login"} 56 | # return render_template('auth.html', name='lf') 57 | 58 | # end 59 | 60 | 61 | if __name__ == '__main__': 62 | app.run(host='127.0.0.1', port=8082) 63 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ manage.py runserver -h 127.0.0.1 -p 8888 --debug """ 3 | from api import create_app 4 | from flask_migrate import Migrate, MigrateCommand 5 | from flask_script import Manager 6 | from flask import render_template, jsonify, request, make_response, session 7 | from utils.ext import db 8 | from utils.permission import user_datastore 9 | from api.authentication.model import Permission, User, Role, Groups 10 | from utils.ext import login_manager 11 | from api.authentication.url import get_auth_resources 12 | from api.authentication.view import module 13 | from os import environ 14 | import logging 15 | 16 | 17 | config_name = environ.get("FLASK_CONFIG", 'Devops') 18 | app = create_app('config.settings.{0}'.format(config_name)) 19 | 20 | # 蓝图功能, 注册api url 21 | app.register_blueprint(get_auth_resources(), url_prefix='/api/v1') 22 | app.register_blueprint(module) 23 | 24 | manager = Manager(app) 25 | migrate = Migrate(app, db) 26 | 27 | # python manage.py db init 28 | # python manage.py db migrate 29 | # python manage.py db upgrade 30 | manager.add_command('db', MigrateCommand) 31 | 32 | 33 | @manager.command 34 | def create_user(): 35 | 36 | try: 37 | # db.drop_all() 38 | db.create_all() 39 | 40 | g1 = Groups(name="test") 41 | g2 = Groups(name="admin") 42 | db.session.add(g1) 43 | db.session.add(g2) 44 | db.session.commit() 45 | db.session.close() 46 | 47 | for permissions, (name, desc) in Permission.PERMISSION_MAP.items(): 48 | user_datastore.find_or_create_role(name=name, 49 | description=desc, 50 | permissions=permissions, 51 | groups=Groups.query.filter_by(name="admin").first() 52 | ) 53 | 54 | for email, username, passwd, permissions in ( 55 | ('lifei', 'lifei', '123', (Permission.LOGIN, Permission.EDITOR)), 56 | ('admin', 'admin', 'admin123_2017', (Permission.ADMIN,)) 57 | ): 58 | user_datastore.create_user(email=email, username=username, password=passwd) 59 | for permission in permissions: 60 | user_datastore.add_role_to_user(email, Permission.PERMISSION_MAP[permission][0]) 61 | db.session.commit() 62 | 63 | except Exception as e: 64 | logging.error("already init db: %s" % e) 65 | 66 | # start 67 | 68 | 69 | @login_manager.user_loader 70 | def load_user(user_id): 71 | """@login_required 获取用户信息""" 72 | user = User.query.get(user_id) 73 | return user 74 | 75 | 76 | @login_manager.unauthorized_handler 77 | def unauthorized(): 78 | return {"err": "not login"} 79 | # return render_template('auth.html', name='lf') 80 | 81 | # end 82 | 83 | 84 | @manager.app.route('/') 85 | def h(): 86 | print(request.url, request.cookies, request.headers.__dict__) 87 | print('ticket: ', request.args.get("ticket", None)) 88 | response = make_response("test") 89 | response.set_cookie("name", "xxx") 90 | print(response) 91 | print(session.items(), session.get('user_id')) 92 | return render_template('game.html', name='lf') 93 | # return {'say': "hello world"}, 250, [('HHHH', '111100000')] 94 | 95 | 96 | @manager.app.route('/logout') 97 | def logout(): 98 | from flask_security import logout_user 99 | logout_user() 100 | return "logout ok" 101 | 102 | 103 | @manager.app.route('/check') 104 | def check(): 105 | import requests, re 106 | from flask_security import login_user 107 | ticket = request.args.get("ticket", None) 108 | a = requests.get("https://sso.asiainfo.com/serviceValidate", params={ 109 | 'ticket': ticket, 110 | 'service': 'http://localhost:8080'}) 111 | print("go to check", ticket, a.text) 112 | # username = re.findall(a.text, "") 113 | 114 | user = User.query.filter_by(email='admin').first() 115 | a = login_user(user) 116 | return jsonify({"tocken": 123, "user": "admin"}) 117 | 118 | if __name__ == '__main__': 119 | # python manage.py runserver -h 0.0.0.0 -p 8888 120 | manager.run() 121 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | redis 2 | flask 3 | Flask-SQLAlchemy 4 | Flask-Migrate 5 | Flask-Security 6 | Flask-Script 7 | Flask-RESTful 8 | Flask-KVSession 9 | flask-cors 10 | # flask-mongoengine 11 | flask_jwt 12 | flasgger 13 | flask_pymongo 14 | mysqlclient 15 | requests 16 | celery 17 | ldap3 18 | -------------------------------------------------------------------------------- /task/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei825/devops-flask-vue/1b8457f62184e857dddf05a87e7ef925025ee1ed/task/__init__.py -------------------------------------------------------------------------------- /task/tasks.py: -------------------------------------------------------------------------------- 1 | from celery import Celery, platforms 2 | from api.authentication.model import User 3 | from utils.ext import db 4 | from api import create_app 5 | from os import environ 6 | import logging 7 | 8 | config_name = 'config.settings.{0}'.format(environ.get("FLASK_CONFIG", 'Devops')) 9 | flask_app = create_app(config_name) 10 | 11 | app = Celery('devops') 12 | app.config_from_object(config_name) 13 | 14 | platforms.C_FORCE_ROOT = True 15 | 16 | 17 | @app.task(bind=True) 18 | def maintain_monitor_active(self, item_id=None): 19 | try: 20 | with flask_app.app_context(): 21 | pass 22 | 23 | except Exception as e: 24 | if self.request.retries <= 2: 25 | logging.error("error: %s...retry %s " % (e, self.request.retries)) 26 | raise self.retry(exc=e, countdown=5, max_retries=3) 27 | 28 | 29 | if __name__ == '__main__': 30 | app.start() 31 | -------------------------------------------------------------------------------- /templates/auth.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {##} 4 | 5 | {##} 6 | 7 | {##} 8 | 9 | 10 | Title 11 | 12 | 13 |
14 | 15 | 16 |
17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | Checked names: ${ checkedNames } 27 |
28 | 29 | 30 |
31 | First name:
32 | 33 |
34 | Last name:
35 | 36 |

37 | 38 |
39 | 40 | {#
#} 41 |
app - ${messages == true ? "dui" : "cuo"}
42 | 43 |
44 | 45 | 鼠标悬停查看绑定提示信息 46 | 47 |
48 | 49 |
50 |

现在你看到我了

51 |
52 | 53 |
54 |
    55 |
  1. 56 | 59 |
  2. {{ name }}
  3. 60 |
61 |
62 |
63 |
64 |

65 | 66 | 逆转消息 enter 67 | // tab键选中后生效 68 |

69 | 70 |
71 | 72 |
73 |

${addStr}

74 |

${message | jias}

75 | 76 |

77 |
78 | 79 |
80 |
    81 | 85 | 86 | 87 |
88 | 89 |
90 |
{{ name }}
91 | 92 |
93 |

94 | ask a yes/no question: 95 | 96 |

97 |

98 |
99 | 100 |
101 |
102 | ${index}. ${key}: ${value} 103 |
104 |
105 | 106 | 107 | 108 | 109 | 267 | 268 | 269 | 270 | 271 | -------------------------------------------------------------------------------- /templates/logout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 |
9 | First name:
10 | 11 |
12 | Last name:
13 | 14 |

15 | 16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /templates/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 |
9 |
10 |
11 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /tests/BaseTest.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import logging 3 | import urllib.parse 4 | import requests 5 | from api import create_app 6 | 7 | 8 | class BaseTest(unittest.TestCase): 9 | def setUp(self): 10 | # app = create_app('config.settings.DevelopmentConfig') 11 | # app.run(host="0.0.0.0", port=8888, threaded=True, use_debugger=True) 12 | self.ok_code = (200, 201, 202, 203, 204) 13 | super(BaseTest, self).setUp() 14 | 15 | def tearDown(self): 16 | super(BaseTest, self).tearDown() 17 | 18 | def fetch_request(self, path, **arvgs): 19 | response = None 20 | params = arvgs.get("params", {}) 21 | # params = urllib.parse.urlencode(arvgs["params"]) 22 | method = arvgs.get("method", "get") 23 | if method.lower() == "post": 24 | response = requests.post(path, data=params) 25 | 26 | elif method.lower() == "get": 27 | response = requests.get(path, params=params) 28 | 29 | elif method.lower() == "put": 30 | response = requests.put(path, data=params) 31 | 32 | elif method.lower() == "delete": 33 | response = requests.delete(path, params=params) 34 | 35 | elif method.lower() == "patch": 36 | response = requests.patch(path, data=params) 37 | 38 | result = response.status_code 39 | print(response.text) 40 | logging.warning(result) 41 | return result 42 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei825/devops-flask-vue/1b8457f62184e857dddf05a87e7ef925025ee1ed/tests/__init__.py -------------------------------------------------------------------------------- /tests/example.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from tests.BaseTest import BaseTest 3 | 4 | 5 | class TestUser(BaseTest): 6 | def test_example(self): 7 | params = {"user": "lifei", "passwd": "123"} 8 | result = self.fetch_request("http://127.0.0.1:8888/api/login", method="post", params=params) 9 | self.assertIn(result, self.ok_code) 10 | 11 | 12 | if __name__ == "__main__": 13 | unittest.main() 14 | 15 | 16 | -------------------------------------------------------------------------------- /utils/ErrorCode.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | class ErrorCode(Exception): 5 | def __init__(self, eid, message): 6 | self.eid = eid 7 | self.message = message 8 | 9 | def __str__(self): 10 | return self.message 11 | 12 | STATE_OK = ErrorCode(200, 'ok') 13 | STATE_CREATE_OK = ErrorCode(201, '创建资源ok') 14 | STATE_UNKNOWN = ErrorCode(451, '未知错误') 15 | STATE_LOGIN_ERR = ErrorCode(401, '登陆验证错误') 16 | STATE_PARAM_ERR = ErrorCode(400, '参数错误') 17 | STATE_DB_UPDATE_ERR = ErrorCode(422, '数据库更新错误') 18 | STATE_EmptyData_ERR = ErrorCode(402, '数据库查询为空数') 19 | STATE_PreconditionFailed = ErrorCode(412, '字段中给出先决条件时,没能满足其中的一个或多个') 20 | -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei825/devops-flask-vue/1b8457f62184e857dddf05a87e7ef925025ee1ed/utils/__init__.py -------------------------------------------------------------------------------- /utils/ext.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from flask_sqlalchemy import SQLAlchemy 3 | from flask_cors import CORS 4 | from flask_login import LoginManager 5 | from flask_pymongo import PyMongo 6 | 7 | 8 | login_manager = LoginManager() 9 | # login_manager.login_view = 'logins.login' 10 | 11 | cors = CORS() 12 | db = SQLAlchemy() 13 | mongo = PyMongo() 14 | -------------------------------------------------------------------------------- /utils/helper.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | import random 3 | from flask_restful import reqparse 4 | 5 | 6 | def identifying(): 7 | part1 = list(str(uuid.uuid1(node=random.randint(100000, 110000), clock_seq=random.randint(20, 100)))) 8 | part2 = [chr(random.randint(65, 122)) for i in range(24)] 9 | part3 = part1 + part2 10 | 11 | return "".join(random.sample(part3, len(part3))) 12 | 13 | 14 | class Argument(reqparse.Argument): 15 | """ 16 | 重写reqparse的nullable参数为False时, 传空字符窜也报错 17 | """ 18 | def convert(self, value, op): 19 | if not self.nullable and not value: 20 | raise ValueError('Must not be null!') 21 | 22 | return super(Argument, self).convert(value, op) 23 | 24 | 25 | if __name__ == "__main__": 26 | for i in range(20): 27 | uid = identifying() 28 | print(str(uid)) 29 | -------------------------------------------------------------------------------- /utils/permission.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from api.authentication.model import Role, User, Permission 3 | from flask_security import SQLAlchemyUserDatastore, Security, login_user 4 | from functools import wraps 5 | from flask_login import current_user 6 | from flask import abort, request, redirect, current_app 7 | from .ext import db 8 | import requests 9 | import re 10 | from flask_security.forms import LoginForm 11 | import logging 12 | from utils.ErrorCode import * 13 | from flask_jwt import current_identity 14 | 15 | user_datastore = SQLAlchemyUserDatastore(db, User, Role) 16 | security = Security(datastore=user_datastore, register_form=LoginForm) 17 | 18 | # 权限装饰器 19 | 20 | 21 | def permission_required(permission, active=True): 22 | def decorator(f): 23 | @wraps(f) 24 | def _deco(self, *args, **kwargs): 25 | print("self post", dict(request.values.items())) 26 | self.gid = request.values.get("gid", 2) 27 | 28 | if permission == Permission.SUPER_ADMIN: 29 | self.gid = 2 30 | 31 | else: 32 | self.gid = int(self.gid) if self.gid else 2 33 | 34 | uid = current_identity.__dict__.get('id') 35 | self.user = User.query.get(int(uid)) 36 | 37 | if not self.user.can(self.gid, abs(permission)) and active: 38 | print("permission 403") 39 | abort(403) 40 | elif request.method != 'GET': 41 | # 审计 42 | print('{0}通过{1}方法,访问{2}, 参数: {3}'.format(self.user.username, 43 | request.method, 44 | request.path, 45 | dict(request.values.items()) 46 | )) 47 | 48 | return f(self, *args, **kwargs) 49 | return _deco 50 | return decorator 51 | 52 | 53 | def sso_required(f): 54 | @wraps(f) 55 | def _deco(*args, **kwargs): 56 | try: 57 | # 若不是登录状态 跳转到http://sso.asiainfo.com/验证 58 | print("user info: ", current_user.__dict__) 59 | ticket = request.args.get("ticket", None) 60 | service = request.args.get("service", request.url.split('?')[0]) 61 | if ticket: 62 | # 有ticket拿着去公司api验证 获取用户名, service的url必须和请求的url一致 63 | 64 | rs = requests.get("https://sso.asiainfo.com/serviceValidate", params={ 65 | 'ticket': ticket, 66 | 'service': service}) 67 | 68 | print("go to check", ticket, rs.text) 69 | username = re.findall("""(.*?)""", rs.text) 70 | if username: 71 | user = User.query.filter_by(username=username[0]).first() 72 | password = current_app.config.get('PASSWORD_KEY') 73 | if not user: 74 | user = User(username=username[0], active=1) 75 | user.password = password 76 | db.session.add(user) 77 | db.session.commit() 78 | 79 | from task.tasks import sync_employee_to_mysql 80 | sync_employee_to_mysql.delay(username[0]) 81 | 82 | elif not user.verify_password(password): 83 | user.password = password 84 | db.session.add(user) 85 | db.session.commit() 86 | 87 | s = login_user(user) 88 | if not s: 89 | raise STATE_LOGIN_ERR 90 | 91 | else: 92 | raise STATE_LOGIN_ERR 93 | 94 | else: 95 | 96 | return redirect("https://sso.asiainfo.com/login?service=%s" % request.url, code=301) 97 | 98 | except Exception as e: 99 | logging.error("sso verify error: %s." % str(e)) 100 | state = isinstance(e, ErrorCode) and e or ErrorCode(1, "unknown error:" + str(e)) 101 | 102 | return {'result': 'sso verify error', 'state': state.message}, state.eid 103 | 104 | return f(*args, **kwargs) 105 | 106 | return _deco 107 | 108 | 109 | # jwt 验证用户名密码 110 | def authenticate(username, password): 111 | user = User.query.filter_by(email=username).first() 112 | if user and user.verify_password(password): 113 | if user.active: 114 | return user 115 | 116 | 117 | # jwt 获取身份 118 | def identity(payload): 119 | user_id = int(payload['identity']) 120 | return User.query.get(user_id) 121 | -------------------------------------------------------------------------------- /vue-init/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false, 5 | "targets": { 6 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] 7 | } 8 | }], 9 | "stage-2" 10 | ], 11 | "plugins": ["transform-runtime"], 12 | "env": { 13 | "test": { 14 | "presets": ["env", "stage-2"], 15 | "plugins": ["istanbul"] 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /vue-init/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /vue-init/.eslintignore: -------------------------------------------------------------------------------- 1 | build/*.js 2 | config/*.js 3 | -------------------------------------------------------------------------------- /vue-init/.eslintrc.js: -------------------------------------------------------------------------------- 1 | // http://eslint.org/docs/user-guide/configuring 2 | 3 | module.exports = { 4 | root: true, 5 | parser: 'babel-eslint', 6 | parserOptions: { 7 | sourceType: 'module' 8 | }, 9 | env: { 10 | browser: true, 11 | }, 12 | // https://github.com/standard/standard/blob/master/docs/RULES-en.md 13 | extends: 'standard', 14 | // required to lint *.vue files 15 | plugins: [ 16 | 'html' 17 | ], 18 | // add your custom rules here 19 | 'rules': { 20 | // allow paren-less arrow functions 21 | 'arrow-parens': 0, 22 | // allow async-await 23 | 'generator-star-spacing': 0, 24 | // allow debugger during development 25 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /vue-init/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | test/unit/coverage 8 | test/e2e/reports 9 | selenium-debug.log 10 | 11 | # Editor directories and files 12 | .idea 13 | *.suo 14 | *.ntvs* 15 | *.njsproj 16 | *.sln 17 | -------------------------------------------------------------------------------- /vue-init/.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | "plugins": { 5 | // to edit target browsers: use "browserslist" field in package.json 6 | "autoprefixer": {} 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /vue-init/README.md: -------------------------------------------------------------------------------- 1 | # vue-init 2 | 3 | > A Vue.js project 4 | 5 | ## Build Setup 6 | 7 | ``` bash 8 | # install dependencies 9 | npm install 10 | 11 | # serve with hot reload at localhost:8080 12 | npm run dev 13 | 14 | # build for production with minification 15 | npm run build 16 | 17 | # build for production and view the bundle analyzer report 18 | npm run build --report 19 | 20 | # run unit tests 21 | npm run unit 22 | 23 | # run e2e tests 24 | npm run e2e 25 | 26 | # run all tests 27 | npm test 28 | ``` 29 | 30 | For detailed explanation on how things work, checkout the [guide](http://vuejs-templates.github.io/webpack/) and [docs for vue-loader](http://vuejs.github.io/vue-loader). 31 | -------------------------------------------------------------------------------- /vue-init/build/build.js: -------------------------------------------------------------------------------- 1 | require('./check-versions')() 2 | 3 | process.env.NODE_ENV = 'production' 4 | 5 | var ora = require('ora') 6 | var rm = require('rimraf') 7 | var path = require('path') 8 | var chalk = require('chalk') 9 | var webpack = require('webpack') 10 | var config = require('../config') 11 | var webpackConfig = require('./webpack.prod.conf') 12 | 13 | var spinner = ora('building for production...') 14 | spinner.start() 15 | 16 | rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => { 17 | if (err) throw err 18 | webpack(webpackConfig, function (err, stats) { 19 | spinner.stop() 20 | if (err) throw err 21 | process.stdout.write(stats.toString({ 22 | colors: true, 23 | modules: false, 24 | children: false, 25 | chunks: false, 26 | chunkModules: false 27 | }) + '\n\n') 28 | 29 | if (stats.hasErrors()) { 30 | console.log(chalk.red(' Build failed with errors.\n')) 31 | process.exit(1) 32 | } 33 | 34 | console.log(chalk.cyan(' Build complete.\n')) 35 | console.log(chalk.yellow( 36 | ' Tip: built files are meant to be served over an HTTP server.\n' + 37 | ' Opening index.html over file:// won\'t work.\n' 38 | )) 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /vue-init/build/check-versions.js: -------------------------------------------------------------------------------- 1 | var chalk = require('chalk') 2 | var semver = require('semver') 3 | var packageConfig = require('../package.json') 4 | var shell = require('shelljs') 5 | function exec (cmd) { 6 | return require('child_process').execSync(cmd).toString().trim() 7 | } 8 | 9 | var versionRequirements = [ 10 | { 11 | name: 'node', 12 | currentVersion: semver.clean(process.version), 13 | versionRequirement: packageConfig.engines.node 14 | } 15 | ] 16 | 17 | if (shell.which('npm')) { 18 | versionRequirements.push({ 19 | name: 'npm', 20 | currentVersion: exec('npm --version'), 21 | versionRequirement: packageConfig.engines.npm 22 | }) 23 | } 24 | 25 | module.exports = function () { 26 | var warnings = [] 27 | for (var i = 0; i < versionRequirements.length; i++) { 28 | var mod = versionRequirements[i] 29 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { 30 | warnings.push(mod.name + ': ' + 31 | chalk.red(mod.currentVersion) + ' should be ' + 32 | chalk.green(mod.versionRequirement) 33 | ) 34 | } 35 | } 36 | 37 | if (warnings.length) { 38 | console.log('') 39 | console.log(chalk.yellow('To use this template, you must update following to modules:')) 40 | console.log() 41 | for (var i = 0; i < warnings.length; i++) { 42 | var warning = warnings[i] 43 | console.log(' ' + warning) 44 | } 45 | console.log() 46 | process.exit(1) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /vue-init/build/dev-client.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | require('eventsource-polyfill') 3 | var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true') 4 | 5 | hotClient.subscribe(function (event) { 6 | if (event.action === 'reload') { 7 | window.location.reload() 8 | } 9 | }) 10 | -------------------------------------------------------------------------------- /vue-init/build/dev-server.js: -------------------------------------------------------------------------------- 1 | require('./check-versions')() 2 | 3 | var config = require('../config') 4 | if (!process.env.NODE_ENV) { 5 | process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV) 6 | } 7 | 8 | var opn = require('opn') 9 | var path = require('path') 10 | var express = require('express') 11 | var webpack = require('webpack') 12 | var proxyMiddleware = require('http-proxy-middleware') 13 | var webpackConfig = (process.env.NODE_ENV === 'testing' || process.env.NODE_ENV === 'production') 14 | ? require('./webpack.prod.conf') 15 | : require('./webpack.dev.conf') 16 | 17 | // default port where dev server listens for incoming traffic 18 | var port = process.env.PORT || config.dev.port 19 | // automatically open browser, if not set will be false 20 | var autoOpenBrowser = !!config.dev.autoOpenBrowser 21 | // Define HTTP proxies to your custom API backend 22 | // https://github.com/chimurai/http-proxy-middleware 23 | var proxyTable = config.dev.proxyTable 24 | 25 | var app = express() 26 | var compiler = webpack(webpackConfig) 27 | 28 | var devMiddleware = require('webpack-dev-middleware')(compiler, { 29 | publicPath: webpackConfig.output.publicPath, 30 | quiet: true 31 | }) 32 | 33 | var hotMiddleware = require('webpack-hot-middleware')(compiler, { 34 | log: false, 35 | heartbeat: 2000 36 | }) 37 | // force page reload when html-webpack-plugin template changes 38 | compiler.plugin('compilation', function (compilation) { 39 | compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) { 40 | hotMiddleware.publish({ action: 'reload' }) 41 | cb() 42 | }) 43 | }) 44 | 45 | // proxy api requests 46 | Object.keys(proxyTable).forEach(function (context) { 47 | var options = proxyTable[context] 48 | if (typeof options === 'string') { 49 | options = { target: options } 50 | } 51 | app.use(proxyMiddleware(options.filter || context, options)) 52 | }) 53 | 54 | // handle fallback for HTML5 history API 55 | app.use(require('connect-history-api-fallback')()) 56 | 57 | // serve webpack bundle output 58 | app.use(devMiddleware) 59 | 60 | // enable hot-reload and state-preserving 61 | // compilation error display 62 | app.use(hotMiddleware) 63 | 64 | // serve pure static assets 65 | var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory) 66 | app.use(staticPath, express.static('./static')) 67 | 68 | var uri = 'http://localhost:' + port 69 | 70 | var _resolve 71 | var readyPromise = new Promise(resolve => { 72 | _resolve = resolve 73 | }) 74 | 75 | console.log('> Starting dev server...') 76 | devMiddleware.waitUntilValid(() => { 77 | console.log('> Listening at ' + uri + '\n') 78 | // when env is testing, don't need open it 79 | if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') { 80 | opn(uri) 81 | } 82 | _resolve() 83 | }) 84 | 85 | var server = app.listen(port) 86 | 87 | module.exports = { 88 | ready: readyPromise, 89 | close: () => { 90 | server.close() 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /vue-init/build/utils.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var config = require('../config') 3 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 4 | 5 | exports.assetsPath = function (_path) { 6 | var assetsSubDirectory = process.env.NODE_ENV === 'production' 7 | ? config.build.assetsSubDirectory 8 | : config.dev.assetsSubDirectory 9 | return path.posix.join(assetsSubDirectory, _path) 10 | } 11 | 12 | exports.cssLoaders = function (options) { 13 | options = options || {} 14 | 15 | var cssLoader = { 16 | loader: 'css-loader', 17 | options: { 18 | minimize: process.env.NODE_ENV === 'production', 19 | sourceMap: options.sourceMap 20 | } 21 | } 22 | 23 | // generate loader string to be used with extract text plugin 24 | function generateLoaders (loader, loaderOptions) { 25 | var loaders = [cssLoader] 26 | if (loader) { 27 | loaders.push({ 28 | loader: loader + '-loader', 29 | options: Object.assign({}, loaderOptions, { 30 | sourceMap: options.sourceMap 31 | }) 32 | }) 33 | } 34 | 35 | // Extract CSS when that option is specified 36 | // (which is the case during production build) 37 | if (options.extract) { 38 | return ExtractTextPlugin.extract({ 39 | use: loaders, 40 | fallback: 'vue-style-loader' 41 | }) 42 | } else { 43 | return ['vue-style-loader'].concat(loaders) 44 | } 45 | } 46 | 47 | // https://vue-loader.vuejs.org/en/configurations/extract-css.html 48 | return { 49 | css: generateLoaders(), 50 | postcss: generateLoaders(), 51 | less: generateLoaders('less'), 52 | sass: generateLoaders('sass', { indentedSyntax: true }), 53 | scss: generateLoaders('sass'), 54 | stylus: generateLoaders('stylus'), 55 | styl: generateLoaders('stylus') 56 | } 57 | } 58 | 59 | // Generate loaders for standalone style files (outside of .vue) 60 | exports.styleLoaders = function (options) { 61 | var output = [] 62 | var loaders = exports.cssLoaders(options) 63 | for (var extension in loaders) { 64 | var loader = loaders[extension] 65 | output.push({ 66 | test: new RegExp('\\.' + extension + '$'), 67 | use: loader 68 | }) 69 | } 70 | return output 71 | } 72 | -------------------------------------------------------------------------------- /vue-init/build/vue-loader.conf.js: -------------------------------------------------------------------------------- 1 | var utils = require('./utils') 2 | var config = require('../config') 3 | var isProduction = process.env.NODE_ENV === 'production' 4 | 5 | module.exports = { 6 | loaders: utils.cssLoaders({ 7 | sourceMap: isProduction 8 | ? config.build.productionSourceMap 9 | : config.dev.cssSourceMap, 10 | extract: isProduction 11 | }), 12 | transformToRequire: { 13 | video: 'src', 14 | source: 'src', 15 | img: 'src', 16 | image: 'xlink:href' 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /vue-init/build/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var utils = require('./utils') 3 | var config = require('../config') 4 | var vueLoaderConfig = require('./vue-loader.conf') 5 | 6 | function resolve (dir) { 7 | return path.join(__dirname, '..', dir) 8 | } 9 | 10 | module.exports = { 11 | entry: { 12 | app: './src/main.js' 13 | }, 14 | output: { 15 | path: config.build.assetsRoot, 16 | filename: '[name].js', 17 | publicPath: process.env.NODE_ENV === 'production' 18 | ? config.build.assetsPublicPath 19 | : config.dev.assetsPublicPath 20 | }, 21 | resolve: { 22 | extensions: ['.js', '.vue', '.json'], 23 | alias: { 24 | 'vue$': 'vue/dist/vue.esm.js', 25 | '@': resolve('src'), 26 | } 27 | }, 28 | module: { 29 | rules: [ 30 | // { 31 | // test: /\.(js|vue)$/, 32 | // loader: 'eslint-loader', 33 | // enforce: 'pre', 34 | // include: [resolve('src'), resolve('test')], 35 | // options: { 36 | // formatter: require('eslint-friendly-formatter') 37 | // } 38 | // }, 39 | { 40 | test: /\.vue$/, 41 | loader: 'vue-loader', 42 | options: vueLoaderConfig 43 | }, 44 | { 45 | test: /\.js$/, 46 | loader: 'babel-loader', 47 | include: [resolve('src'), resolve('test')] 48 | }, 49 | { 50 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 51 | loader: 'url-loader', 52 | options: { 53 | limit: 10000, 54 | name: utils.assetsPath('img/[name].[hash:7].[ext]') 55 | } 56 | }, 57 | { 58 | test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, 59 | loader: 'url-loader', 60 | options: { 61 | limit: 10000, 62 | name: utils.assetsPath('media/[name].[hash:7].[ext]') 63 | } 64 | }, 65 | { 66 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 67 | loader: 'url-loader', 68 | options: { 69 | limit: 10000, 70 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]') 71 | } 72 | } 73 | ] 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /vue-init/build/webpack.dev.conf.js: -------------------------------------------------------------------------------- 1 | var utils = require('./utils') 2 | var webpack = require('webpack') 3 | var config = require('../config') 4 | var merge = require('webpack-merge') 5 | var baseWebpackConfig = require('./webpack.base.conf') 6 | var HtmlWebpackPlugin = require('html-webpack-plugin') 7 | var FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') 8 | 9 | // add hot-reload related code to entry chunks 10 | Object.keys(baseWebpackConfig.entry).forEach(function (name) { 11 | baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name]) 12 | }) 13 | 14 | module.exports = merge(baseWebpackConfig, { 15 | module: { 16 | rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap }) 17 | }, 18 | // cheap-module-eval-source-map is faster for development 19 | devtool: '#cheap-module-eval-source-map', 20 | plugins: [ 21 | new webpack.DefinePlugin({ 22 | 'process.env': config.dev.env 23 | }), 24 | // https://github.com/glenjamin/webpack-hot-middleware#installation--usage 25 | new webpack.HotModuleReplacementPlugin(), 26 | new webpack.NoEmitOnErrorsPlugin(), 27 | // https://github.com/ampedandwired/html-webpack-plugin 28 | new HtmlWebpackPlugin({ 29 | filename: 'index.html', 30 | template: 'index.html', 31 | inject: true 32 | }), 33 | new FriendlyErrorsPlugin() 34 | ] 35 | }) 36 | -------------------------------------------------------------------------------- /vue-init/build/webpack.prod.conf.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var utils = require('./utils') 3 | var webpack = require('webpack') 4 | var config = require('../config') 5 | var merge = require('webpack-merge') 6 | var baseWebpackConfig = require('./webpack.base.conf') 7 | var CopyWebpackPlugin = require('copy-webpack-plugin') 8 | var HtmlWebpackPlugin = require('html-webpack-plugin') 9 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 10 | var OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin') 11 | 12 | var env = process.env.NODE_ENV === 'testing' 13 | ? require('../config/test.env') 14 | : config.build.env 15 | 16 | var webpackConfig = merge(baseWebpackConfig, { 17 | module: { 18 | rules: utils.styleLoaders({ 19 | sourceMap: config.build.productionSourceMap, 20 | extract: true 21 | }) 22 | }, 23 | devtool: config.build.productionSourceMap ? '#source-map' : false, 24 | output: { 25 | path: config.build.assetsRoot, 26 | filename: utils.assetsPath('js/[name].[chunkhash].js'), 27 | chunkFilename: utils.assetsPath('js/[id].[chunkhash].js') 28 | }, 29 | plugins: [ 30 | // http://vuejs.github.io/vue-loader/en/workflow/production.html 31 | new webpack.DefinePlugin({ 32 | 'process.env': env 33 | }), 34 | new webpack.optimize.UglifyJsPlugin({ 35 | compress: { 36 | warnings: false 37 | }, 38 | sourceMap: true 39 | }), 40 | // extract css into its own file 41 | new ExtractTextPlugin({ 42 | filename: utils.assetsPath('css/[name].[contenthash].css') 43 | }), 44 | // Compress extracted CSS. We are using this plugin so that possible 45 | // duplicated CSS from different components can be deduped. 46 | new OptimizeCSSPlugin({ 47 | cssProcessorOptions: { 48 | safe: true 49 | } 50 | }), 51 | // generate dist index.html with correct asset hash for caching. 52 | // you can customize output by editing /index.html 53 | // see https://github.com/ampedandwired/html-webpack-plugin 54 | new HtmlWebpackPlugin({ 55 | filename: process.env.NODE_ENV === 'testing' 56 | ? 'index.html' 57 | : config.build.index, 58 | template: 'index.html', 59 | inject: true, 60 | minify: { 61 | removeComments: true, 62 | collapseWhitespace: true, 63 | removeAttributeQuotes: true 64 | // more options: 65 | // https://github.com/kangax/html-minifier#options-quick-reference 66 | }, 67 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin 68 | chunksSortMode: 'dependency' 69 | }), 70 | // keep module.id stable when vender modules does not change 71 | new webpack.HashedModuleIdsPlugin(), 72 | // split vendor js into its own file 73 | new webpack.optimize.CommonsChunkPlugin({ 74 | name: 'vendor', 75 | minChunks: function (module, count) { 76 | // any required modules inside node_modules are extracted to vendor 77 | return ( 78 | module.resource && 79 | /\.js$/.test(module.resource) && 80 | module.resource.indexOf( 81 | path.join(__dirname, '../node_modules') 82 | ) === 0 83 | ) 84 | } 85 | }), 86 | // extract webpack runtime and module manifest to its own file in order to 87 | // prevent vendor hash from being updated whenever app bundle is updated 88 | new webpack.optimize.CommonsChunkPlugin({ 89 | name: 'manifest', 90 | chunks: ['vendor'] 91 | }), 92 | // copy custom static assets 93 | new CopyWebpackPlugin([ 94 | { 95 | from: path.resolve(__dirname, '../static'), 96 | to: config.build.assetsSubDirectory, 97 | ignore: ['.*'] 98 | } 99 | ]) 100 | ] 101 | }) 102 | 103 | if (config.build.productionGzip) { 104 | var CompressionWebpackPlugin = require('compression-webpack-plugin') 105 | 106 | webpackConfig.plugins.push( 107 | new CompressionWebpackPlugin({ 108 | asset: '[path].gz[query]', 109 | algorithm: 'gzip', 110 | test: new RegExp( 111 | '\\.(' + 112 | config.build.productionGzipExtensions.join('|') + 113 | ')$' 114 | ), 115 | threshold: 10240, 116 | minRatio: 0.8 117 | }) 118 | ) 119 | } 120 | 121 | if (config.build.bundleAnalyzerReport) { 122 | var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 123 | webpackConfig.plugins.push(new BundleAnalyzerPlugin()) 124 | } 125 | 126 | module.exports = webpackConfig 127 | -------------------------------------------------------------------------------- /vue-init/build/webpack.test.conf.js: -------------------------------------------------------------------------------- 1 | // This is the webpack config used for unit tests. 2 | 3 | var utils = require('./utils') 4 | var webpack = require('webpack') 5 | var merge = require('webpack-merge') 6 | var baseConfig = require('./webpack.base.conf') 7 | 8 | var webpackConfig = merge(baseConfig, { 9 | // use inline sourcemap for karma-sourcemap-loader 10 | module: { 11 | rules: utils.styleLoaders() 12 | }, 13 | devtool: '#inline-source-map', 14 | resolveLoader: { 15 | alias: { 16 | // necessary to to make lang="scss" work in test when using vue-loader's ?inject option 17 | // see discussion at https://github.com/vuejs/vue-loader/issues/724 18 | 'scss-loader': 'sass-loader' 19 | } 20 | }, 21 | plugins: [ 22 | new webpack.DefinePlugin({ 23 | 'process.env': require('../config/test.env') 24 | }) 25 | ] 26 | }) 27 | 28 | // no need for app entry during tests 29 | delete webpackConfig.entry 30 | 31 | module.exports = webpackConfig 32 | -------------------------------------------------------------------------------- /vue-init/config/dev.env.js: -------------------------------------------------------------------------------- 1 | var merge = require('webpack-merge') 2 | var prodEnv = require('./prod.env') 3 | 4 | module.exports = merge(prodEnv, { 5 | NODE_ENV: '"development"' 6 | }) 7 | -------------------------------------------------------------------------------- /vue-init/config/index.js: -------------------------------------------------------------------------------- 1 | // see http://vuejs-templates.github.io/webpack for documentation. 2 | var path = require('path') 3 | 4 | module.exports = { 5 | build: { 6 | env: require('./prod.env'), 7 | index: path.resolve(__dirname, '../dist/index.html'), 8 | assetsRoot: path.resolve(__dirname, '../dist'), 9 | assetsSubDirectory: 'static', 10 | assetsPublicPath: '/', 11 | productionSourceMap: true, 12 | // Gzip off by default as many popular static hosts such as 13 | // Surge or Netlify already gzip all static assets for you. 14 | // Before setting to `true`, make sure to: 15 | // npm install --save-dev compression-webpack-plugin 16 | productionGzip: false, 17 | productionGzipExtensions: ['js', 'css'], 18 | // Run the build command with an extra argument to 19 | // View the bundle analyzer report after build finishes: 20 | // `npm run build --report` 21 | // Set to `true` or `false` to always turn it on or off 22 | bundleAnalyzerReport: process.env.npm_config_report 23 | }, 24 | dev: { 25 | env: require('./dev.env'), 26 | port: 8080, 27 | autoOpenBrowser: true, 28 | assetsSubDirectory: 'static', 29 | assetsPublicPath: '/', 30 | proxyTable: {}, 31 | // CSS Sourcemaps off by default because relative paths are "buggy" 32 | // with this option, according to the CSS-Loader README 33 | // (https://github.com/webpack/css-loader#sourcemaps) 34 | // In our experience, they generally work as expected, 35 | // just be aware of this issue when enabling this option. 36 | cssSourceMap: false 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /vue-init/config/prod.env.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | NODE_ENV: '"production"' 3 | } 4 | -------------------------------------------------------------------------------- /vue-init/config/test.env.js: -------------------------------------------------------------------------------- 1 | var merge = require('webpack-merge') 2 | var devEnv = require('./dev.env') 3 | 4 | module.exports = merge(devEnv, { 5 | NODE_ENV: '"testing"' 6 | }) 7 | -------------------------------------------------------------------------------- /vue-init/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | vue-init 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /vue-init/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-init", 3 | "version": "1.0.0", 4 | "description": "A Vue.js project", 5 | "author": "lifei <747553934@qq.com>", 6 | "private": true, 7 | "scripts": { 8 | "dev": "node build/dev-server.js", 9 | "start": "node build/dev-server.js", 10 | "build": "node build/build.js", 11 | "unit": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js --single-run", 12 | "e2e": "node test/e2e/runner.js", 13 | "test": "npm run unit && npm run e2e", 14 | "lint": "eslint --ext .js,.vue src test/unit/specs test/e2e/specs" 15 | }, 16 | "dependencies": { 17 | "axios": "^0.16.2", 18 | "highcharts": "^6.0.1", 19 | "iview": "^2.3.0", 20 | "vue": "^2.4.2", 21 | "vue-router": "^2.7.0", 22 | "vuex": "^2.4.0" 23 | }, 24 | "devDependencies": { 25 | "autoprefixer": "^7.1.2", 26 | "babel-core": "^6.22.1", 27 | "babel-eslint": "^7.1.1", 28 | "babel-loader": "^7.1.1", 29 | "babel-plugin-transform-runtime": "^6.22.0", 30 | "babel-preset-env": "^1.3.2", 31 | "babel-preset-stage-2": "^6.22.0", 32 | "babel-register": "^6.22.0", 33 | "chalk": "^2.0.1", 34 | "connect-history-api-fallback": "^1.3.0", 35 | "copy-webpack-plugin": "^4.0.1", 36 | "css-loader": "^0.28.0", 37 | "cssnano": "^3.10.0", 38 | "eslint": "^3.19.0", 39 | "eslint-friendly-formatter": "^3.0.0", 40 | "eslint-loader": "^1.7.1", 41 | "eslint-plugin-html": "^3.0.0", 42 | "eslint-config-standard": "^6.2.1", 43 | "eslint-plugin-promise": "^3.4.0", 44 | "eslint-plugin-standard": "^2.0.1", 45 | "eventsource-polyfill": "^0.9.6", 46 | "express": "^4.14.1", 47 | "extract-text-webpack-plugin": "^2.0.0", 48 | "file-loader": "^0.11.1", 49 | "friendly-errors-webpack-plugin": "^1.1.3", 50 | "html-webpack-plugin": "^2.28.0", 51 | "http-proxy-middleware": "^0.17.3", 52 | "webpack-bundle-analyzer": "^2.2.1", 53 | "cross-env": "^5.0.1", 54 | "karma": "^1.4.1", 55 | "karma-coverage": "^1.1.1", 56 | "karma-mocha": "^1.3.0", 57 | "karma-phantomjs-launcher": "^1.0.2", 58 | "karma-phantomjs-shim": "^1.4.0", 59 | "karma-sinon-chai": "^1.3.1", 60 | "karma-sourcemap-loader": "^0.3.7", 61 | "karma-spec-reporter": "0.0.31", 62 | "karma-webpack": "^2.0.2", 63 | "mocha": "^3.2.0", 64 | "chai": "^3.5.0", 65 | "sinon": "^2.1.0", 66 | "sinon-chai": "^2.8.0", 67 | "inject-loader": "^3.0.0", 68 | "babel-plugin-istanbul": "^4.1.1", 69 | "phantomjs-prebuilt": "^2.1.14", 70 | "chromedriver": "^2.27.2", 71 | "cross-spawn": "^5.0.1", 72 | "nightwatch": "^0.9.12", 73 | "selenium-server": "^3.0.1", 74 | "semver": "^5.3.0", 75 | "shelljs": "^0.7.6", 76 | "opn": "^5.1.0", 77 | "optimize-css-assets-webpack-plugin": "^2.0.0", 78 | "ora": "^1.2.0", 79 | "rimraf": "^2.6.0", 80 | "url-loader": "^0.5.8", 81 | "vue-loader": "^13.0.4", 82 | "vue-style-loader": "^3.0.1", 83 | "vue-template-compiler": "^2.4.2", 84 | "webpack": "^2.6.1", 85 | "webpack-dev-middleware": "^1.10.0", 86 | "webpack-hot-middleware": "^2.18.0", 87 | "webpack-merge": "^4.1.0" 88 | }, 89 | "engines": { 90 | "node": ">= 4.0.0", 91 | "npm": ">= 3.0.0" 92 | }, 93 | "browserslist": [ 94 | "> 1%", 95 | "last 2 versions", 96 | "not ie <= 8" 97 | ] 98 | } 99 | -------------------------------------------------------------------------------- /vue-init/src/App.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | 14 | 24 | -------------------------------------------------------------------------------- /vue-init/src/api/auth.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import Qs from 'qs' 3 | import {baseUrl} from './config' 4 | // export const baseUrl = ''; 5 | // export const baseUrl = 'http://127.0.0.1:8082'; 6 | 7 | export const getUserInfo = (username) => { 8 | return axios({ 9 | method: 'GET', 10 | url: baseUrl + '/api/v1/user/' + username 11 | }); 12 | }; 13 | 14 | export const userLogin = (email, password) => { 15 | return axios({ 16 | method: 'post', 17 | url: baseUrl + '/api/v1/login', 18 | headers: { 19 | 'Content-Type': 'application/x-www-form-urlencoded' 20 | }, 21 | data: Qs.stringify({ 22 | "email": email, 23 | "password": password 24 | }) 25 | }); 26 | }; 27 | 28 | export const checkToken = (token) => { 29 | return axios({ 30 | method: 'PUT', 31 | url: baseUrl + '/api/v1/login', 32 | headers: { 33 | 'Authorization': 'JWT '+token 34 | } 35 | }) 36 | }; 37 | 38 | export const getGroups = (token, page=1, pageSize=10, keyword=null) => { 39 | return axios({ 40 | method: 'get', 41 | url: baseUrl + '/api/v1/group', 42 | headers: { 43 | 'Authorization': 'JWT '+token 44 | }, 45 | params: { 46 | "page": page, 47 | "pageSize": pageSize, 48 | "keyword": keyword 49 | } 50 | }) 51 | }; 52 | 53 | export const postGroups = (token, data) => { 54 | return axios({ 55 | method: 'post', 56 | url: baseUrl + '/api/v1/group', 57 | headers: { 58 | 'Authorization': 'JWT '+token, 59 | 'Content-Type': 'application/x-www-form-urlencoded' 60 | }, 61 | data: Qs.stringify({ 62 | "id": data.id, 63 | "name": data.name, 64 | "description": data.desc 65 | }) 66 | }) 67 | }; 68 | 69 | export const deleteGroups = (token, gid) => { 70 | return axios({ 71 | method: 'delete', 72 | url: baseUrl + '/api/v1/group', 73 | headers: { 74 | 'Authorization': 'JWT '+token, 75 | 'Content-Type': 'application/x-www-form-urlencoded' 76 | }, 77 | data: Qs.stringify({ 78 | "id": gid, 79 | }) 80 | }) 81 | }; 82 | 83 | export const getUsers = (token, gid=2, page=1, pageSize=10, keyword=null) => { 84 | return axios({ 85 | method: 'get', 86 | url: baseUrl + '/api/v1/user', 87 | headers: { 88 | 'Authorization': 'JWT '+token 89 | }, 90 | params: { 91 | "gid": gid, 92 | "page": page, 93 | "pageSize": pageSize, 94 | "keyword": keyword 95 | } 96 | }) 97 | }; 98 | 99 | export const postUsers = (token, data) => { 100 | let l = new Array() 101 | for (let r of data.roles) { 102 | l.push(r) 103 | } 104 | delete data.roles 105 | data["roles"] = l.join() 106 | 107 | return axios({ 108 | method: 'post', 109 | url: baseUrl + '/api/v1/user', 110 | headers: { 111 | 'Authorization': 'JWT '+token, 112 | 'Content-Type': 'application/x-www-form-urlencoded' 113 | }, 114 | data: Qs.stringify(data) 115 | }) 116 | }; 117 | export default { 118 | getUsers, 119 | deleteGroups, 120 | postGroups, 121 | getGroups, 122 | getUserInfo, 123 | userLogin, 124 | checkToken 125 | } 126 | -------------------------------------------------------------------------------- /vue-init/src/api/config.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | // export const baseUrl = ''; 4 | export const baseUrl = 'http://127.0.0.1:8082'; 5 | -------------------------------------------------------------------------------- /vue-init/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei825/devops-flask-vue/1b8457f62184e857dddf05a87e7ef925025ee1ed/vue-init/src/assets/logo.png -------------------------------------------------------------------------------- /vue-init/src/assets/运维平台.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei825/devops-flask-vue/1b8457f62184e857dddf05a87e7ef925025ee1ed/vue-init/src/assets/运维平台.png -------------------------------------------------------------------------------- /vue-init/src/components/chart/chart.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 21 | 22 | 92 | -------------------------------------------------------------------------------- /vue-init/src/components/chart/chart_highcharts.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /vue-init/src/components/group.vue: -------------------------------------------------------------------------------- 1 | 2 | 10 | 70 | 262 | 263 | -------------------------------------------------------------------------------- /vue-init/src/components/layout.vue: -------------------------------------------------------------------------------- 1 | 65 | 161 | 162 | 236 | -------------------------------------------------------------------------------- /vue-init/src/components/layout2.vue: -------------------------------------------------------------------------------- 1 | 65 | 196 | 197 | 256 | -------------------------------------------------------------------------------- /vue-init/src/components/login.vue: -------------------------------------------------------------------------------- 1 | 60 | 115 | 188 | -------------------------------------------------------------------------------- /vue-init/src/components/resource/server.vue: -------------------------------------------------------------------------------- 1 | 9 | 39 | 152 | 153 | -------------------------------------------------------------------------------- /vue-init/src/components/resource/server_edit.vue: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei825/devops-flask-vue/1b8457f62184e857dddf05a87e7ef925025ee1ed/vue-init/src/components/resource/server_edit.vue -------------------------------------------------------------------------------- /vue-init/src/components/resource/server_view.vue: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei825/devops-flask-vue/1b8457f62184e857dddf05a87e7ef925025ee1ed/vue-init/src/components/resource/server_view.vue -------------------------------------------------------------------------------- /vue-init/src/components/server_group/server_group.vue: -------------------------------------------------------------------------------- 1 | 9 | 42 | 43 | 163 | -------------------------------------------------------------------------------- /vue-init/src/components/server_group/server_group_edit.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 79 | -------------------------------------------------------------------------------- /vue-init/src/components/user/user.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 50 | 51 | 52 | 271 | -------------------------------------------------------------------------------- /vue-init/src/components/user/user_edit.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 57 | -------------------------------------------------------------------------------- /vue-init/src/components/user/user_view.vue: -------------------------------------------------------------------------------- 1 | 74 | 75 | 92 | -------------------------------------------------------------------------------- /vue-init/src/main.js: -------------------------------------------------------------------------------- 1 | // The Vue build version to load with the `import` command 2 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias. 3 | import Vue from 'vue' 4 | import App from './App' 5 | import router from './router' 6 | import iView from 'iview' 7 | import 'iview/dist/styles/iview.css'; 8 | import store from './store' 9 | 10 | Vue.use(iView) 11 | 12 | Vue.config.productionTip = false 13 | 14 | /* eslint-disable no-new */ 15 | new Vue({ 16 | el: '#app', 17 | router, 18 | store, 19 | template: '', 20 | components: { App } 21 | }); 22 | -------------------------------------------------------------------------------- /vue-init/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import Layout from '@/components/layout' 4 | import server from '@/components/resource/server.vue' 5 | import serverGroup from '@/components/server_group/server_group.vue' 6 | import group from '@/components/group' 7 | import user from '@/components/user/user' 8 | import login from '@/components/login.vue' 9 | import chart from '@/components/chart/chart.vue' 10 | import { checkToken } from '../api/auth'; 11 | import store from '@/store' 12 | 13 | 14 | Vue.use(Router); 15 | 16 | const document = global.document; 17 | 18 | 19 | const router = new Router({ 20 | routes: [ 21 | { 22 | path: '/', 23 | component: Layout, 24 | children: [ 25 | { 26 | path: '/', 27 | name: 'Overview', 28 | components: { 29 | default: server 30 | }, 31 | meta: { 32 | title: 'Overview', 33 | auth: true 34 | } 35 | }, 36 | { 37 | path: '/user', 38 | name: '用户管理', 39 | component: user, 40 | meta: { 41 | title: 'User', 42 | auth: true, 43 | parent: '权限管理' 44 | } 45 | }, 46 | { 47 | path: '/group', 48 | name: '项目管理', 49 | component: group, 50 | meta: { 51 | title: 'Group', 52 | auth: true, 53 | parent: '权限管理' 54 | } 55 | }, 56 | // 资源管理 57 | { 58 | path: '/server', 59 | name: '服务器管理', 60 | component: server, 61 | meta: { 62 | title: 'Server', 63 | auth: true, 64 | parent: '资产管理' 65 | } 66 | }, 67 | { 68 | path: '/server_group', 69 | name: '服务器分组', 70 | component: serverGroup, 71 | meta: { 72 | title: 'ServerGroup', 73 | auth: true, 74 | parent: '资产管理' 75 | } 76 | }, 77 | // 图表 78 | { 79 | path: '/chart', 80 | name: '图表2', 81 | component: chart, 82 | meta: { 83 | title: 'chart', 84 | auth: true, 85 | parent: '图表' 86 | } 87 | }, 88 | ] 89 | }, 90 | { 91 | path: '/login', 92 | name: 'login', 93 | component: login, 94 | meta: { 95 | title: '登录' 96 | } 97 | } 98 | ] 99 | }); 100 | 101 | router.beforeEach((to, from, next) => { 102 | let loginInfo = store.getters.loginInfo; 103 | console.log(1111111, loginInfo) 104 | 105 | if (to.meta.auth && loginInfo) { 106 | let token = loginInfo.token; 107 | 108 | checkToken(token).then((res) => { 109 | let verify = res.data.result.verify; 110 | if (verify) { 111 | next() 112 | } else { 113 | next("/login") 114 | } 115 | }).catch(res => { 116 | next('/login') 117 | }); 118 | } else if (!loginInfo && to.meta.auth){ 119 | console.log('2c'); 120 | next('/login') 121 | } else { 122 | console.log('3c', to.path); 123 | next() 124 | 125 | } 126 | }); 127 | 128 | router.afterEach((route) => { 129 | document.title = `${route.meta.title} - Super`; 130 | }); 131 | 132 | export default router 133 | -------------------------------------------------------------------------------- /vue-init/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | Vue.use(Vuex); 5 | 6 | 7 | export default new Vuex.Store({ 8 | state: { 9 | loginInfo: { 10 | token: null, 11 | user: 'Guest', 12 | src: '', 13 | roles: [] 14 | }, 15 | todos: [ 16 | {id:1, text: '1 text', done: true}, 17 | {id:2, text: '2 text', done: false} 18 | ] 19 | 20 | }, 21 | getters: { 22 | doneTodos: state => { 23 | return state.todos.filter(todo => todo.id === 1) 24 | }, 25 | loginInfo: state => { 26 | state.loginInfo = JSON.parse(localStorage.getItem('loginInfo')); 27 | return state.loginInfo 28 | } 29 | }, 30 | // Mutation同步更改 Vuex 的 store 中的状态, 组件中使用 this.$store.commit('xxx') 提交 mutation 31 | mutations: { 32 | save_token (state, loginInfo) { 33 | state.loginInfo = loginInfo; 34 | localStorage.setItem('loginInfo', JSON.stringify(loginInfo)); 35 | }, 36 | remove_token (state) { 37 | state.loginInfo = {}; 38 | localStorage.removeItem('loginInfo') 39 | } 40 | }, 41 | // Action 可以包含任意异步操作, 组件中使用 this.$store.dispatch('xxx') 分发 action 42 | actions: { 43 | save_token ({ commit }, loginInfo) { 44 | commit('save_token', loginInfo) 45 | }, 46 | remove_token ({commit}) { 47 | commit('remove_token') 48 | } 49 | } 50 | }) 51 | -------------------------------------------------------------------------------- /vue-init/static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lifei825/devops-flask-vue/1b8457f62184e857dddf05a87e7ef925025ee1ed/vue-init/static/.gitkeep -------------------------------------------------------------------------------- /vue-init/test/e2e/custom-assertions/elementCount.js: -------------------------------------------------------------------------------- 1 | // A custom Nightwatch assertion. 2 | // the name of the method is the filename. 3 | // can be used in tests like this: 4 | // 5 | // browser.assert.elementCount(selector, count) 6 | // 7 | // for how to write custom assertions see 8 | // http://nightwatchjs.org/guide#writing-custom-assertions 9 | exports.assertion = function (selector, count) { 10 | this.message = 'Testing if element <' + selector + '> has count: ' + count 11 | this.expected = count 12 | this.pass = function (val) { 13 | return val === this.expected 14 | } 15 | this.value = function (res) { 16 | return res.value 17 | } 18 | this.command = function (cb) { 19 | var self = this 20 | return this.api.execute(function (selector) { 21 | return document.querySelectorAll(selector).length 22 | }, [selector], function (res) { 23 | cb.call(self, res) 24 | }) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /vue-init/test/e2e/nightwatch.conf.js: -------------------------------------------------------------------------------- 1 | require('babel-register') 2 | var config = require('../../config') 3 | 4 | // http://nightwatchjs.org/gettingstarted#settings-file 5 | module.exports = { 6 | src_folders: ['test/e2e/specs'], 7 | output_folder: 'test/e2e/reports', 8 | custom_assertions_path: ['test/e2e/custom-assertions'], 9 | 10 | selenium: { 11 | start_process: true, 12 | server_path: require('selenium-server').path, 13 | host: '127.0.0.1', 14 | port: 4444, 15 | cli_args: { 16 | 'webdriver.chrome.driver': require('chromedriver').path 17 | } 18 | }, 19 | 20 | test_settings: { 21 | default: { 22 | selenium_port: 4444, 23 | selenium_host: 'localhost', 24 | silent: true, 25 | globals: { 26 | devServerURL: 'http://localhost:' + (process.env.PORT || config.dev.port) 27 | } 28 | }, 29 | 30 | chrome: { 31 | desiredCapabilities: { 32 | browserName: 'chrome', 33 | javascriptEnabled: true, 34 | acceptSslCerts: true 35 | } 36 | }, 37 | 38 | firefox: { 39 | desiredCapabilities: { 40 | browserName: 'firefox', 41 | javascriptEnabled: true, 42 | acceptSslCerts: true 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /vue-init/test/e2e/runner.js: -------------------------------------------------------------------------------- 1 | // 1. start the dev server using production config 2 | process.env.NODE_ENV = 'testing' 3 | var server = require('../../build/dev-server.js') 4 | 5 | server.ready.then(() => { 6 | // 2. run the nightwatch test suite against it 7 | // to run in additional browsers: 8 | // 1. add an entry in test/e2e/nightwatch.conf.json under "test_settings" 9 | // 2. add it to the --env flag below 10 | // or override the environment flag, for example: `npm run e2e -- --env chrome,firefox` 11 | // For more information on Nightwatch's config file, see 12 | // http://nightwatchjs.org/guide#settings-file 13 | var opts = process.argv.slice(2) 14 | if (opts.indexOf('--config') === -1) { 15 | opts = opts.concat(['--config', 'test/e2e/nightwatch.conf.js']) 16 | } 17 | if (opts.indexOf('--env') === -1) { 18 | opts = opts.concat(['--env', 'chrome']) 19 | } 20 | 21 | var spawn = require('cross-spawn') 22 | var runner = spawn('./node_modules/.bin/nightwatch', opts, { stdio: 'inherit' }) 23 | 24 | runner.on('exit', function (code) { 25 | server.close() 26 | process.exit(code) 27 | }) 28 | 29 | runner.on('error', function (err) { 30 | server.close() 31 | throw err 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /vue-init/test/e2e/specs/test.js: -------------------------------------------------------------------------------- 1 | // For authoring Nightwatch tests, see 2 | // http://nightwatchjs.org/guide#usage 3 | 4 | module.exports = { 5 | 'default e2e tests': function (browser) { 6 | // automatically uses dev Server port from /config.index.js 7 | // default: http://localhost:8080 8 | // see nightwatch.conf.js 9 | const devServer = browser.globals.devServerURL 10 | 11 | browser 12 | .url(devServer) 13 | .waitForElementVisible('#app', 5000) 14 | .assert.elementPresent('.hello') 15 | .assert.containsText('h1', 'Welcome to Your Vue.js App') 16 | .assert.elementCount('img', 1) 17 | .end() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /vue-init/test/unit/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "globals": { 6 | "expect": true, 7 | "sinon": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /vue-init/test/unit/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | Vue.config.productionTip = false 4 | 5 | // require all test files (files that ends with .spec.js) 6 | const testsContext = require.context('./specs', true, /\.spec$/) 7 | testsContext.keys().forEach(testsContext) 8 | 9 | // require all src files except main.js for coverage. 10 | // you can also change this to match only the subset of files that 11 | // you want coverage for. 12 | const srcContext = require.context('../../src', true, /^\.\/(?!main(\.js)?$)/) 13 | srcContext.keys().forEach(srcContext) 14 | -------------------------------------------------------------------------------- /vue-init/test/unit/karma.conf.js: -------------------------------------------------------------------------------- 1 | // This is a karma config file. For more details see 2 | // http://karma-runner.github.io/0.13/config/configuration-file.html 3 | // we are also using it with karma-webpack 4 | // https://github.com/webpack/karma-webpack 5 | 6 | var webpackConfig = require('../../build/webpack.test.conf') 7 | 8 | module.exports = function (config) { 9 | config.set({ 10 | // to run in additional browsers: 11 | // 1. install corresponding karma launcher 12 | // http://karma-runner.github.io/0.13/config/browsers.html 13 | // 2. add it to the `browsers` array below. 14 | browsers: ['PhantomJS'], 15 | frameworks: ['mocha', 'sinon-chai', 'phantomjs-shim'], 16 | reporters: ['spec', 'coverage'], 17 | files: ['./index.js'], 18 | preprocessors: { 19 | './index.js': ['webpack', 'sourcemap'] 20 | }, 21 | webpack: webpackConfig, 22 | webpackMiddleware: { 23 | noInfo: true 24 | }, 25 | coverageReporter: { 26 | dir: './coverage', 27 | reporters: [ 28 | { type: 'lcov', subdir: '.' }, 29 | { type: 'text-summary' } 30 | ] 31 | } 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /vue-init/test/unit/specs/Hello.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Hello from '@/components/Hello' 3 | 4 | describe('Hello.vue', () => { 5 | it('should render correct contents', () => { 6 | const Constructor = Vue.extend(Hello) 7 | const vm = new Constructor().$mount() 8 | expect(vm.$el.querySelector('.hello h1').textContent) 9 | .to.equal('Welcome to Your Vue.js App') 10 | }) 11 | }) 12 | --------------------------------------------------------------------------------