├── README.md ├── app.py ├── config ├── config.py ├── dns_templates.tpl └── peb_dns.cfg.sample ├── docker_file ├── Dockerfile ├── README.md ├── bind_dockerfile ├── docker-compose.yml ├── hfdns_client_etcd_config.sh ├── python3_dockerfile └── requirements.txt ├── docker_start.sh ├── docs ├── images │ ├── dns.jpg │ ├── manage_view.png │ ├── manage_zone.png │ ├── peb-dashbard.png │ └── qq-group.png ├── install │ ├── checkdb.py │ ├── dns_api.md │ ├── dns_install.md │ ├── peb_dns_nginx.conf │ └── requirements.txt ├── sql │ └── peb_dns.sql └── zabbix │ ├── zabbix_dns_README.md │ ├── zabbix_monitor_dns.sh │ └── zbx_export_templates.xml ├── etcd_client └── hfdns_client_etcd_config.sh ├── peb_dns ├── __init__.py ├── common │ ├── __init__.py │ ├── decorators.py │ ├── request_code.py │ └── util.py ├── extensions.py ├── models │ ├── __init__.py │ ├── account.py │ ├── dns.py │ └── mappings.py └── resourses │ ├── __init__.py │ ├── account │ ├── __init__.py │ └── auth.py │ ├── admin │ ├── __init__.py │ ├── privilege.py │ ├── role.py │ └── user.py │ ├── dns │ ├── __init__.py │ ├── operation_log.py │ ├── record.py │ ├── server.py │ ├── view.py │ └── zone.py │ └── page │ ├── __init__.py │ ├── dashboard.py │ └── menu.py └── static ├── index.html └── static ├── css ├── app.5c17d435ea22558d83f9485f4f0c876d.css └── app.5c17d435ea22558d83f9485f4f0c876d.css.map ├── fonts ├── fontawesome-webfont.674f50d.eot ├── fontawesome-webfont.af7ae50.woff2 ├── fontawesome-webfont.b06871f.ttf ├── fontawesome-webfont.fee66e7.woff ├── glyphicons-halflings-regular.448c34a.woff2 ├── glyphicons-halflings-regular.e18bbf6.ttf ├── glyphicons-halflings-regular.f4769f9.eot ├── glyphicons-halflings-regular.fa27723.woff ├── iconfont.af12dbc.ttf ├── iconfont.f47fda4.woff ├── ionicons.05acfdb.woff ├── ionicons.24712f6.ttf └── ionicons.2c2ae06.eot ├── img ├── boxed-bg.7799dec.jpg ├── error.75f4c76.png ├── success.f09cb57.png └── warn.99e51ab.png └── js ├── app.3c272e9bf5082eea090f.js ├── app.3c272e9bf5082eea090f.js.map ├── manifest.47c127f6253f528e05ba.js ├── manifest.47c127f6253f528e05ba.js.map ├── vendor.234aa88aab8f29cc10e1.js └── vendor.234aa88aab8f29cc10e1.js.map /README.md: -------------------------------------------------------------------------------- 1 | DNS-Manager 2 | =========================== 3 | 该项目可用于多 IDC 多 View 的 dns 管理。 4 | **** 5 | 可提issue进行交流 6 | 7 | **** 8 | 9 | # 平台架构图如下: 10 | 11 | ![dns](/docs/images/dns.jpg "DNS 平台架构图") 12 | 13 | 功能介绍 14 | ------ 15 | 16 | #### 1. 平台管理方式: 17 | 18 | 使用 BIND9 作为 DNS 服务器。 19 | 20 | 使用 ETCD 来管理 DNS 服务器的 BIND 配置文件,包括 VIEW,ZONE,RECORD 的配置文件。 21 | 22 | 所有BIND DNS 服务器角色均为 `Master`,不存在 `Slave`,服务器配置文件数据都是统一从 ETCD 获取。 23 | 24 | 利用ETCD本身的订阅发布机制,当ETCD上数据发生变更后,所有 DNS 服务器都会实时获取到变更信息,并将本地配置文件同步到最新,跟ETCD上数据保持一致。 25 | 26 | 公网域名可托管在DNSPod,通过操作DNSPod Api对公网域名进行管理。 27 | 28 | #### 2. 使用技术栈: 29 | 30 | 后端: Python3.5 + Flask + Mysql + Etcd 31 | 32 | 前端: Vue.js 33 | 34 | 架构: 前后端分离,纯 restful 架构,后端只提供 restful api,前端用 vue 框架。 35 | 36 | 37 | #### 3. 功能简介 38 | 39 | * DNS 服务器管理 40 | 41 | * BIND 主配置文件管理 42 | 43 | * View 管理 (区域) 44 | 45 | * Zone 管理 (域名) 46 | 47 | * Record 管理 (子域名) 48 | 49 | * 内网域名 Record 管理 50 | 51 | * 劫持域名 Record 管理 52 | 53 | * 公网域名 Record 管理 54 | 55 | * 平台权限管理 56 | 57 | * 用户管理 58 | 59 | * 角色管理 60 | 61 | * 权限管理 62 | 63 | * 操作记录 64 | 65 | 66 | #### 4. 安装部署 67 | 68 | * 可查看 [项目部署文档](docs/install/dns_install.md) 69 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | from peb_dns import create_app 2 | from peb_dns.extensions import mail, db 3 | from peb_dns.models.mappings import Operation, ResourceType, OPERATION_STR_MAPPING, ROLE_MAPPINGS, DefaultPrivilege 4 | from peb_dns.models.account import DBUser, DBUserRole, DBRole, DBLocalAuth, \ 5 | DBPrivilege, DBRolePrivilege 6 | from peb_dns.models.dns import DBView 7 | from peb_dns.common.util import getETCDclient 8 | import etcd 9 | import time 10 | 11 | app = create_app() 12 | 13 | 14 | def init_privilege(): 15 | """init the default privilege data when you first time start the app.""" 16 | privilege_count = db.session.query(DBPrivilege).count() 17 | if privilege_count < 1: 18 | print('initing the default privileges...') 19 | default_privileges = [ 20 | DefaultPrivilege.SERVER_ADD, 21 | DefaultPrivilege.ZONE_ADD, 22 | DefaultPrivilege.VIEW_ADD, 23 | DefaultPrivilege.BIND_CONF_EDIT 24 | ] 25 | for p in default_privileges: 26 | new_p = DBPrivilege(name=p) 27 | db.session.add(new_p) 28 | db.session.flush() 29 | admin_rp = DBRolePrivilege( 30 | role_id=ROLE_MAPPINGS['admin'], 31 | privilege_id=new_p.id 32 | ) 33 | db.session.add(admin_rp) 34 | if p == DefaultPrivilege.SERVER_ADD: 35 | server_admim_rp = DBRolePrivilege( 36 | role_id=ROLE_MAPPINGS['server_admin'], 37 | privilege_id=new_p.id 38 | ) 39 | db.session.add(server_admim_rp) 40 | if p == DefaultPrivilege.ZONE_ADD: 41 | zone_admin_rp = DBRolePrivilege( 42 | role_id=ROLE_MAPPINGS['zone_admin'], 43 | privilege_id=new_p.id 44 | ) 45 | db.session.add(zone_admin_rp) 46 | if p == DefaultPrivilege.VIEW_ADD: 47 | view_admin_rp = DBRolePrivilege( 48 | role_id=ROLE_MAPPINGS['view_admin'], 49 | privilege_id=new_p.id 50 | ) 51 | db.session.add(view_admin_rp) 52 | 53 | def init_user_role(app): 54 | """init the default user and role data when you first time start the app.""" 55 | auth_user_count = db.session.query(DBLocalAuth).count() 56 | local_user_count = db.session.query(DBUser).count() 57 | if auth_user_count < 1 and local_user_count < 1: 58 | print('initing the default users...') 59 | default_admin = DBLocalAuth( 60 | id=ROLE_MAPPINGS['admin'], 61 | username=app.config.get("DEFAULT_ADMIN_USERNAME"), 62 | email=app.config.get("DEFAULT_ADMIN_EMAIL") 63 | ) 64 | default_admin.password = app.config.get("DEFAULT_ADMIN_PASSWD") 65 | default_admin_local = DBUser( 66 | id=ROLE_MAPPINGS['admin'], 67 | username=app.config.get("DEFAULT_ADMIN_USERNAME"), 68 | email=app.config.get("DEFAULT_ADMIN_EMAIL") 69 | ) 70 | db.session.add(default_admin) 71 | db.session.add(default_admin_local) 72 | role_count = db.session.query(DBRole).count() 73 | if role_count < 1: 74 | print('initing the default roles...') 75 | for k,v in ROLE_MAPPINGS.items(): 76 | new_role = DBRole(id=v, name=k) 77 | db.session.add(new_role) 78 | user_role_count = db.session.query(DBUserRole).count() 79 | if role_count < 1: 80 | admin_user_role = DBUserRole( 81 | id=ROLE_MAPPINGS['admin'], 82 | user_id=ROLE_MAPPINGS['admin'], 83 | role_id=ROLE_MAPPINGS['admin'], 84 | ) 85 | db.session.add(admin_user_role) 86 | 87 | 88 | def add_privilege_for_view(new_view): 89 | """Add privilege for the new view.""" 90 | access_privilege_name = 'VIEW#' + new_view.name + \ 91 | '#' + OPERATION_STR_MAPPING[Operation.ACCESS] 92 | update_privilege_name = 'VIEW#' + new_view.name + \ 93 | '#' + OPERATION_STR_MAPPING[Operation.UPDATE] 94 | delete_privilege_name = 'VIEW#' + new_view.name + \ 95 | '#' + OPERATION_STR_MAPPING[Operation.DELETE] 96 | access_privilege = DBPrivilege( 97 | name=access_privilege_name, 98 | resource_type=ResourceType.VIEW, 99 | operation=Operation.ACCESS, 100 | resource_id=new_view.id 101 | ) 102 | update_privilege = DBPrivilege( 103 | name=update_privilege_name, 104 | resource_type=ResourceType.VIEW, 105 | operation=Operation.UPDATE, 106 | resource_id=new_view.id 107 | ) 108 | delete_privilege = DBPrivilege( 109 | name=delete_privilege_name, 110 | resource_type=ResourceType.VIEW, 111 | operation=Operation.DELETE, 112 | resource_id=new_view.id 113 | ) 114 | db.session.add(access_privilege) 115 | db.session.add(update_privilege) 116 | db.session.add(delete_privilege) 117 | db.session.flush() 118 | for role in ['admin', 'view_admin', 'view_guest']: 119 | role_access = DBRolePrivilege( 120 | role_id=ROLE_MAPPINGS[role], 121 | privilege_id=access_privilege.id) 122 | db.session.add(role_access) 123 | if role not in ['view_guest']: 124 | role_update = DBRolePrivilege( 125 | role_id=ROLE_MAPPINGS[role], 126 | privilege_id=update_privilege.id) 127 | role_delete = DBRolePrivilege( 128 | role_id=ROLE_MAPPINGS[role], 129 | privilege_id=delete_privilege.id) 130 | db.session.add(role_update) 131 | db.session.add(role_delete) 132 | 133 | def init_view(app): 134 | view_count = db.session.query(DBView).count() 135 | if view_count < 1: 136 | print('initing the default views...') 137 | default_view = DBView( 138 | id=1, 139 | name='default_view', 140 | acl='0.0.0.0/0' 141 | ) 142 | db.session.add(default_view) 143 | add_privilege_for_view(default_view) 144 | view_list = db.session.query(DBView).all() 145 | default_view.make_view('create', view_list) 146 | 147 | def init_bind_config(app): 148 | client = getETCDclient() 149 | print('initing etcd data...') 150 | try: 151 | client.read(app.config.get('BIND_CONF')) 152 | except etcd.EtcdKeyNotFound: 153 | client.write(app.config.get('BIND_CONF'), 154 | app.config.get('DEFAULT_BIND_CONF_CONTENT'), 155 | prevExist=False) 156 | time.sleep(1) 157 | try: 158 | client.read(app.config.get('VIEW_DEFINE_CONF')) 159 | except etcd.EtcdKeyNotFound: 160 | client.write(app.config.get('VIEW_DEFINE_CONF'), 161 | '', 162 | prevExist=False) 163 | time.sleep(1) 164 | 165 | @app.cli.command('initdb') 166 | def initdb_command(): 167 | """init the default data in database when you first time start the app.""" 168 | with app.app_context(): 169 | init_user_role(app) 170 | init_privilege() 171 | db.session.flush() 172 | init_bind_config(app) 173 | init_view(app) 174 | db.session.commit() 175 | print('done.') 176 | 177 | 178 | @app.cli.command('init_etcd') 179 | def init_etcd_command(): 180 | """init the default data in etcd when you first time start the app.""" 181 | with app.app_context(): 182 | init_bind_config(app) 183 | print('done.') -------------------------------------------------------------------------------- /config/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | root_dir = os.path.abspath(os.path.dirname(__file__)) 3 | 4 | class Config: 5 | SECRET_KEY = os.environ.get('SECRET_KEY') or 'peb_dns_author_lijiajia' 6 | ROOT_DIR = root_dir 7 | 8 | @staticmethod 9 | def init_app(app): 10 | pass 11 | 12 | 13 | class ProductionConfig(Config): 14 | DEBUG = False 15 | 16 | @classmethod 17 | def init_app(cls, app): 18 | import logging 19 | app.logger.setLevel(logging.INFO) 20 | 21 | # log to syslog 22 | from logging.handlers import SysLogHandler 23 | syslog_handler = SysLogHandler() 24 | syslog_handler.setLevel(logging.WARNING) 25 | app.logger.addHandler(syslog_handler) 26 | 27 | # log to stderr 28 | from logging import StreamHandler 29 | st_handler = StreamHandler() 30 | st_handler.setLevel(logging.WARNING) 31 | app.logger.addHandler(st_handler) 32 | 33 | # log to file 34 | info_log = os.path.join(app.root_path, "logs", "peb_dns_info.log") 35 | if not os.path.exists(os.path.dirname(info_log)): 36 | os.makedirs(os.path.dirname(info_log)) 37 | if not os.path.exists(info_log): 38 | open(info_log,"w+").close() 39 | info_file_handler = logging.handlers.RotatingFileHandler( 40 | info_log, maxBytes=1048576, backupCount=20) 41 | info_file_handler.setLevel(logging.INFO) 42 | info_file_handler.setFormatter(logging.Formatter( 43 | '%(asctime)s %(levelname)s: %(message)s ' 44 | '[in %(pathname)s:%(lineno)d]') 45 | ) 46 | app.logger.addHandler(info_file_handler) 47 | 48 | config = { 49 | 'prod': ProductionConfig, 50 | 'default': ProductionConfig 51 | } 52 | 53 | config_pyfiles = { 54 | 'prod': 'config/peb_dns.cfg', 55 | 'default': 'config/peb_dns.cfg' 56 | } 57 | 58 | -------------------------------------------------------------------------------- /config/dns_templates.tpl: -------------------------------------------------------------------------------- 1 | #以下均为 DNS 配置文件,使用 jinja2 来渲染配置文件,然后推送到etcd服务器上 2 | 3 | VIEW_DEFINE_TEMPLATE = ''' 4 | {% for view in view_list -%} 5 | include "/etc/named/{{ view.name }}/view.conf"; 6 | include "/etc/named/{{ view.name }}/acl.conf"; 7 | {% endfor %} 8 | ''' 9 | 10 | 11 | ACL_TEMPLATE = ''' 12 | acl "{{ view_name }}" { 13 | {% for ip in ip_list -%} 14 | {{ ip }}; 15 | {% endfor %} 16 | }; 17 | ''' 18 | 19 | 20 | VIEW_TEMPLATE = ''' 21 | view "{{ view_name }}" { 22 | match-clients { key default; "{{ view_name }}"; }; 23 | 24 | zone "." IN { 25 | type hint; 26 | file "named.ca"; 27 | }; 28 | }; 29 | ''' 30 | 31 | 32 | ZONE_TEMPLATE = ''' 33 | view "{{ view_name }}" { 34 | match-clients { key default; "{{ view_name }}"; }; 35 | 36 | zone "." IN { 37 | type hint; 38 | file "named.ca"; 39 | }; 40 | 41 | {% for zone in zone_list %} 42 | zone "{{ zone.name }}" IN { 43 | {% if zone.zone_type == 'forward only' %} 44 | type forward; 45 | forward only; 46 | forwarders { {{ zone.forwarders }} }; 47 | {% else %} 48 | type {{ zone.zone_type }}; 49 | file "zone/{{ view_name }}/zone.{{ zone.name }}"; 50 | notify yes; 51 | {% endif %} 52 | }; 53 | {% endfor %} 54 | }; 55 | ''' 56 | 57 | 58 | RECORD_TEMPLATE = ''' 59 | $ORIGIN . 60 | $TTL 600 ; 10 minutes 61 | {{ zone_name }} IN SOA master.{{ zone_name }}. root.{{ zone_name }}. ( 62 | 2015081304 ; serial 63 | 10800 ; refresh (3 hours) 64 | 900 ; retry (15 minutes) 65 | 604800 ; expire (1 week) 66 | 86400 ; minimum (1 day) 67 | ) 68 | MX 10 master.{{ zone_name }}. 69 | $ORIGIN {{ zone_name }}. 70 | 71 | @ 86400 IN NS master.{{ zone_name }}. 72 | {% for record in record_list -%} 73 | {{ record.host }} {{ record.ttl }} IN {{ record.record_type }} {{ record.value }} 74 | {% endfor %} 75 | ''' 76 | 77 | 78 | -------------------------------------------------------------------------------- /config/peb_dns.cfg.sample: -------------------------------------------------------------------------------- 1 | # 认证, 通过LDAP进行认证登陆 2 | LDAP_SERVER = 'ldap://ldap.xxx.com' # LDAP服务器地址 3 | LDAP_SERVER_PORT = '389' # LDAP服务器端口 4 | LDAP_CONFIG = ',ou=users,dc=ipo,dc=com' # LDAP 认证配置 5 | 6 | 7 | # 设置默认用户账号,密码,邮箱。 8 | # 默认系统管理员用户,应用启动后可直接用管理员用户登录, 9 | DEFAULT_ADMIN_USERNAME = 'admin' 10 | DEFAULT_ADMIN_PASSWD = 'admin123' 11 | DEFAULT_ADMIN_EMAIL = 'xxxx@gmail.com' 12 | 13 | 14 | # 数据库配置 15 | ############## 如用Docker方式部署,此处可忽略 ################ 16 | DB_HOST = '1.1.1.1' # Mysql IP地址 17 | DB_USERNAME = 'mysql_user' # Mysql 用户名 18 | DB_PASSWORD = 'mysql_passwd' # Mysql 密码 19 | DB_NAME = 'peb_dns_test_db' # Mysql 数据库实例名 20 | ######################################################## 21 | 22 | 23 | # sqlalchemy ORM框架默认参数配置,可保持以下默认值即可 24 | SQLALCHEMY_ECHO = False 25 | SQLALCHEMY_POOL_SIZE = 10 26 | SQLALCHEMY_POOL_RECYCLE = 300 27 | SQLALCHEMY_COMMIT_ON_TEARDOWN = False 28 | SQLALCHEMY_TRACK_MODIFICATIONS = False 29 | SQLALCHEMY_RECORD_QUERIES = True 30 | 31 | # ETCD 配置文件 32 | ############## 如用Docker方式部署,此处可忽略 ################ 33 | ETCD_SERVER_HOST = '2.2.2.2' # ETCD Server IP 地址 34 | ETCD_SERVER_PORT = 2379 # ETCD Server 端口号 35 | ######################################################## 36 | 37 | ETCD_BASE_DIR = '/opscmdb/dns/etc/named/' # 配置文件根目录 38 | ZONE_BASE_DIR = '/opscmdb/dns/var/named/zone/' # zone 文件跟目录 39 | BIND_CONF = '/opscmdb/dns/etc/named.conf' # ETCD上保存 BIND 主配置文件 40 | VIEW_DEFINE_CONF = ETCD_BASE_DIR + 'view_define.conf' # ETCD上保存 VIEW 声明配置文件目录 41 | # BIND 主配置文件默认配置内容 42 | DEFAULT_BIND_CONF_CONTENT = '''// Global config 43 | 44 | options { 45 | #listen-on port 53 { 127.0.0.1; }; 46 | #listen-on-v6 port 53 { ::1; }; 47 | directory "/var/named/"; 48 | dump-file "/var/named/data/cache_dump.db"; 49 | statistics-file "/var/named/data/named_stats.txt"; 50 | memstatistics-file "/var/named/data/named_mem_stats.txt"; 51 | allow-query { any; }; 52 | allow-query-cache { any; }; 53 | allow-transfer { }; 54 | also-notify { }; 55 | 56 | #shanghai dns 57 | forwarders { 202.96.199.133; 202.106.0.20; 202.96.209.5; 202.96.209.133; }; 58 | allow-recursion { any; }; 59 | recursion yes; 60 | 61 | dnssec-enable yes; 62 | dnssec-validation no; 63 | dnssec-lookaside auto; 64 | 65 | /* Path to ISC DLV key */ 66 | bindkeys-file "/etc/named.iscdlv.key"; 67 | 68 | managed-keys-directory "/var/named/dynamic"; 69 | }; 70 | 71 | // 日志定义 72 | logging { 73 | channel custom_log { 74 | file "data/named.log" versions 5 size 100m; 75 | print-time yes; 76 | severity dynamic; 77 | }; 78 | 79 | channel query_log { 80 | file "data/query.log" versions 5 size 100m; 81 | print-time yes; 82 | severity info; 83 | }; 84 | 85 | channel general_log { 86 | file "data/general.log" versions 5 size 100m; 87 | print-time yes; 88 | severity info; 89 | }; 90 | 91 | category queries { query_log; }; 92 | category default { custom_log; }; 93 | category client { custom_log; }; 94 | category xfer-in { custom_log; }; 95 | category general { general_log; }; 96 | }; 97 | 98 | 99 | //view define 100 | include "/etc/named/view_define.conf"; 101 | 102 | // 103 | ''' 104 | 105 | 106 | # 服务器创建表单,<环境> 选择框的选项 107 | SERVER_ENVS = ['qa', 'smoke', 'prod'] 108 | 109 | 110 | # DNSPOD 配置文件,只需配置TOKEN,未说明的变量,保持默认值即可 111 | DNSPOD_TOKEN = "29999,qqqqqqqqecba2a214894a74ca4ba9b9d" # DNSPOD token 112 | DNSPOD_DATA_FORMAT = "json" # DNSPOD 数据格式 113 | DNSPOD_RECORD_BASE_URL = "https://dnsapi.cn/Record." 114 | DNSPOD_DOMAIN_BASE_URL = "https://dnsapi.cn/Domain." 115 | DNSPOD_LINE_URL = "https://dnsapi.cn/Record.Line" 116 | DNSPOD_TYPE_URL = "https://dnsapi.cn/Record.Type" 117 | 118 | 119 | # 使用 Zabbix 监控DNS服务器,主要获取DNS服务器状态,获取解析量等 120 | ZABBIX_URL = "http://zabbix.proxy.test.com/api_jsonrpc.php" #ZABBIX API 地址 (根据自己的zabbix api来设置URL) 121 | ZABBIX_USERNAME = "test_admin" #ZABBIX 用户名 122 | ZABBIX_PASSWORD = "test_admin" # 用户密码 123 | 124 | #Swagger配置 125 | SWAGGER_TEMPLATE = { 126 | "swagger": "2.0", 127 | "info": { 128 | "title": "PEB EVENT API", 129 | "description": "API for peb_event", 130 | "contact": { 131 | "responsibleOrganization": "ME", 132 | "responsibleDeveloper": "Me", 133 | "email": "me@me.com", 134 | "url": "www.me.com", 135 | }, 136 | "termsOfService": "http://me.com/terms", 137 | "version": "1.0" 138 | }, 139 | "securityDefinitions": { 140 | "UserSecurity": { 141 | "type": "apiKey", 142 | "in": "header", 143 | "name": "Authorization", 144 | } 145 | }, 146 | "basePath": "/", # base bash for blueprint registration 147 | "schemes": [ 148 | "http", 149 | "https" 150 | ], 151 | "operationId": "getmyData" 152 | } 153 | 154 | 155 | -------------------------------------------------------------------------------- /docker_file/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.6-alpine 2 | WORKDIR /code 3 | VOLUME /code 4 | COPY peb-dns/docs/install/requirements.txt /code 5 | RUN pip install -r requirements.txt 6 | ADD https://cdn.mysql.com//Downloads/Connector-Python/mysql-connector-python-2.1.7.tar.gz /tmp/ 7 | RUN tar -xvf /tmp/mysql-connector-python-2.1.7.tar.gz -C /tmp/ 8 | WORKDIR /tmp/mysql-connector-python-2.1.7/ 9 | RUN python setup.py install 10 | ENV FLASK_APP=/peb-dns/app.py 11 | EXPOSE 8080 12 | -------------------------------------------------------------------------------- /docker_file/README.md: -------------------------------------------------------------------------------- 1 | # 使用 Docker 部署项目 2 | 3 | ## Docker-compose Install 4 | 5 | ## Docker-compose File 6 | -------------------------------------------------------------------------------- /docker_file/bind_dockerfile: -------------------------------------------------------------------------------- 1 | FROM centos:6.9 2 | WORKDIR /root/ 3 | RUN yum install -y initscripts;yum install -y bind;yum -y install epel-release;yum install -y jq;yum install -y lsof 4 | COPY hfdns_client_etcd_config.sh /root/ 5 | RUN /etc/init.d/named start;chmod 777 /root/hfdns_client_etcd_config.sh 6 | CMD ["/bin/bash", "/root/hfdns_client_etcd_config.sh single >/dev/null 2>&1"] 7 | EXPOSE 53 8 | -------------------------------------------------------------------------------- /docker_file/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | db: 4 | image: mysql:5.7 5 | volumes: 6 | - "./.data/db:/var/lib/mysql" 7 | restart: always 8 | ports: 9 | - "33060:3306" 10 | environment: 11 | MYSQL_ROOT_PASSWORD: root 12 | MYSQL_DATABASE: peb_dns_db 13 | MYSQL_USER: peb_admin 14 | MYSQL_PASSWORD: passwd123 15 | command: ['mysqld', '--character-set-server=utf8mb4', '--collation-server=utf8mb4_unicode_ci'] 16 | 17 | etcd: 18 | image: appcelerator/etcd 19 | restart: always 20 | ports: 21 | - "23790:2379" 22 | - "23800:2380" 23 | 24 | peb_dns: 25 | build: 26 | context: ./peb-dns/docker_file 27 | dockerfile: python3_dockerfile 28 | working_dir: /code 29 | depends_on: 30 | - db 31 | - etcd 32 | - bind 33 | - nginx 34 | network_mode: "service:nginx" 35 | volumes: 36 | - ./peb-dns:/code 37 | restart: always 38 | environment: 39 | DB_USERNAME: peb_admin 40 | DB_PASSWORD: passwd123 41 | DB_HOST: db:3306 42 | DB_NAME: peb_dns_db 43 | ETCD_SERVER_HOST: etcd 44 | ETCD_SERVER_PORT: 2379 45 | command: ./docker_start.sh 8080 db:3306 peb_admin passwd123 peb_dns_db 46 | 47 | nginx: 48 | image: nginx 49 | volumes: 50 | - ./peb-dns/docs/install/peb_dns_nginx.conf:/etc/nginx/conf.d/mysite.template 51 | - ./peb-dns:/code 52 | ports: 53 | - "8967:80" 54 | - "8966:8080" 55 | environment: 56 | - NGINX_PORT=80 57 | command: /bin/bash -c "envsubst < /etc/nginx/conf.d/mysite.template > /etc/nginx/conf.d/default.conf && nginx -g 'daemon off;'" 58 | 59 | bind: 60 | build: 61 | context: ./peb-dns/docker_file 62 | dockerfile: bind_dockerfile 63 | depends_on: 64 | - etcd 65 | ports: 66 | - "53:53" 67 | environment: 68 | DOCKER_ETCD_ADDRESS: etcd:2379 69 | command: sh -c '/etc/init.d/named stop && /etc/init.d/named start && /bin/bash /root/hfdns_client_etcd_config.sh single' 70 | -------------------------------------------------------------------------------- /docker_file/hfdns_client_etcd_config.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | #Auth: 平安好房 4 | #URL: http://www.pinganfang.com 5 | #USE: 结合ETCD配置本地文件 6 | # 7 | # 脚本使用说明如下: 8 | :</dev/null 13 | 5. 脚本stop方式: /bin/bash hfdns_client_etcd_config.sh dev stop 14 | 6. 脚本依赖linux jq命令进行解析json文件, 请使用前安装 yum install -y jq 15 | 7. 问题定位 查看日志如: tail -f /data1/logs/etcd_config/etcd_config_2017-09-06_access.log 16 | 8. 此脚本可以放到crontab 中每分钟运行以防止脚本挂掉 17 | ! 18 | 19 | #ETCD 环境 20 | ETCD_ENV=${1} 21 | 22 | if [ ${#ETCD_ENV} -eq 0 ];then 23 | echo "请提供环境参数...如{dev|anhouse|ga}" 24 | exit 25 | fi 26 | 27 | #ETCD Log目录 28 | ETCD_LOG_DIR="/data1/logs/etcd_config" 29 | 30 | #脚本锁 31 | ETCD_LOCK="/tmp/.${ETCD_ENV}_etcd_config.lock" 32 | 33 | #服务本身名称 34 | ETCD_JOBS_NAME=$(/bin/basename $BASH_SOURCE) 35 | 36 | #打印信息 37 | PRINT_INFO(){ 38 | /bin/echo "$(/bin/date +%F' '%T) ${1}" >> ${ETCD_LOG_DIR}/etcd_config_$(/bin/date +%F)_access.log 39 | } 40 | 41 | #关闭进程 42 | ETCD_STOP=${2} 43 | if [ "x${ETCD_STOP}" == "xstop" ];then 44 | 45 | #打印信息 46 | PRINT_INFO "${ETCD_JOBS_NAME} 服务已关闭..." 47 | 48 | #关闭监听ETCD 49 | /bin/ps -ef |/bin/egrep "(opscmdb|${ETCD_JOBS_NAME})" |/bin/grep -v grep | /bin/awk '{print $2}' | /usr/bin/xargs kill -9 50 | 51 | #退出 52 | exit 53 | fi 54 | 55 | #默认ETCD配置 56 | ETCD_SERVER_NAME="http://10.59.87.121:2379/v2/keys/opscmdb" 57 | 58 | echo "-------------------${DOCKER_ETCD_ADDRESS}--------------------" 59 | #ETCD集群服务器 60 | if [ -z "${DOCKER_ETCD_ADDRESS}" ];then 61 | [ "x${ETCD_ENV}" == "xdev" ] && ETCD_SERVER_NAME="http://dev-etcd01:2379/v2/keys/opscmdb" 62 | [ "x${ETCD_ENV}" == "xanhouse" ] && ETCD_SERVER_NAME="http://shzr-etcd01:2379/v2/keys/opscmdb" 63 | [ "x${ETCD_ENV}" == "xga" ] && ETCD_SERVER_NAME="http://shbx-etcd01:2379/v2/keys/opscmdb" 64 | else 65 | ETCD_SERVER_NAME="http://${DOCKER_ETCD_ADDRESS}/v2/keys/opscmdb" 66 | fi 67 | 68 | #日志路径创建 69 | if [ ! -d ${ETCD_LOG_DIR} ];then 70 | 71 | #创建日志路径 72 | /bin/mkdir -p ${ETCD_LOG_DIR} >/dev/null 2>&1 73 | 74 | #返回状态 75 | if [ $? -ne 0 ];then 76 | 77 | #打印信息 78 | PRINT_INFO "${ETCD_LOG_DIR} 创建日志路径失败,查看是否权限有问题" 79 | exit 80 | fi 81 | fi 82 | 83 | #安装依赖包 84 | if [ ! -x /usr/bin/jq ];then 85 | 86 | #打印信息 87 | PRINT_INFO "${ETCD_JOBS_NAME} 依赖jq.x86_64安装包" 88 | exit 89 | fi 90 | 91 | #默认ID号,用于临时文件 92 | ETCD_ID=0 93 | 94 | #获取ETCD配置文件列表 95 | _ETCD_ENV_DOWNLOAD_CONFIG() { 96 | 97 | #ETCD 配置文件临时 98 | ETCD_ENV_CONFIG_FILE="/tmp/.${ETCD_ENV}_${ETCD_ID}_dns_etcd_config.json" 99 | 100 | #初始化数据 无需区分环境 101 | #/usr/bin/curl -s "${ETCD_SERVER_NAME}/${ETCD_ENV}?recursive=true&wait=true" > ${ETCD_ENV_CONFIG_FILE} 102 | 103 | /usr/bin/curl -s "${ETCD_SERVER_NAME}/?recursive=true&wait=true" > ${ETCD_ENV_CONFIG_FILE} 104 | } 105 | 106 | #获取ETCD内容 107 | _ETCD_ENV_VALUE_INFO(){ 108 | 109 | #获取INDEX DIR 110 | _ETCD_ENV_FILE=$(/bin/cat ${ETCD_ENV_CONFIG_FILE} | /usr/bin/jq .node.key |/bin/sed -e 's/\"//g' -e "s#/opscmdb/dns##g") 111 | 112 | #获取INDEX VALUE #去除文件开始结尾引号,回车,双引号(默认双引号ETCD会加\,) 113 | #_ETCD_ENV_VALUE=$(/bin/cat ${ETCD_ENV_CONFIG_FILE} | /usr/bin/jq .node.value |/bin/sed -e 's/^"//' -e 's/"$//' -e 's/\\n/\n/g' -e 's/\\//g') 114 | _ETCD_ENV_VALUE=$(/bin/cat ${ETCD_ENV_CONFIG_FILE} | /usr/bin/jq .node.value |/bin/sed -e 's/^"//' -e 's/"$//' -e 's/\\n/\n/g' -e 's/\\"/"/g') 115 | 116 | ## 117 | echo "$_ETCD_ENV_VALUE" 118 | 119 | #获取空内容 120 | if [ ${#_ETCD_ENV_FILE} -eq 0 ];then 121 | 122 | #打印信息 123 | PRINT_INFO "${ETCD_SERVER_NAME} 获取了内容为空..." 124 | 125 | #初始删除文件 126 | #/bin/rm -f ${ETCD_ENV_CONFIG_FILE} >/dev/null 2>&1 127 | 128 | #退出 129 | exit 130 | fi 131 | 132 | #第一次创建文件 133 | if [ ! -f ${_ETCD_ENV_FILE} ];then 134 | 135 | #获取配置文件目录 去除前缀 136 | ETCD_ENV_DIR=$(/usr/bin/dirname ${_ETCD_ENV_FILE}) 137 | #ETCD_ENV_DIR=$(/usr/bin/dirname ${_ETCD_ENV_FILE}) | sed 's#/opscmdb/dns##g' 138 | 139 | #创建目录 140 | [ ! -d ${ETCD_ENV_DIR} ] && /bin/mkdir -p ${ETCD_ENV_DIR} >/dev/null 2>&1 141 | fi 142 | 143 | #导入配置文件 144 | echo "$_ETCD_ENV_VALUE" > ${_ETCD_ENV_FILE} 145 | 146 | #打印信息 147 | PRINT_INFO "更新配置文件 ${_ETCD_ENV_FILE}" 148 | 149 | #初始删除文件 150 | #/bin/rm -f ${ETCD_ENV_CONFIG_FILE} >/dev/null 2>&1 151 | } 152 | 153 | 154 | ## DNS 配置文件reload 155 | _DNS_RELOAD() { 156 | ## 修改配置文件权限 157 | chown root.named /etc/named.conf 158 | chown named.named -R /etc/named 159 | chown named.named -R /var/named 160 | 161 | /usr/sbin/named-checkconf 162 | [ $? -ne 0 ] && PRINT_INFO "DNS named-checkconf 配置文件检测失败" 163 | PRINT_INFO "DNS named-checkconf 配置文件检测成功" 164 | ## 自带检测功能 配置文件检测失败 无法reload成功 但不会停止DNS解析服务 165 | /usr/sbin/rndc reload 166 | [ $? -ne 0 ] && PRINT_INFO "DNS reload 失败" 167 | [ $? -ne 0 ] && PRINT_INFO "DNS reload 成功" 168 | } 169 | 170 | #判断锁文件 171 | if [ -f ${ETCD_LOCK} ];then 172 | [ ! -z "$(/usr/sbin/lsof -p $(/bin/cat ${ETCD_LOCK}))" ] && exit 173 | fi 174 | 175 | #创建锁文件 176 | echo $$ > ${ETCD_LOCK} 177 | 178 | #创建锁文件出错 179 | if [ $? -ne 0 ];then 180 | exit 181 | fi 182 | 183 | 184 | #打印信息 185 | PRINT_INFO "${ETCD_JOBS_NAME} 服务已启动..." 186 | 187 | #获取任务列表 188 | while true 189 | do 190 | #监听是否有配置文件更新 191 | _ETCD_ENV_DOWNLOAD_CONFIG 192 | 193 | #解析配置文件 194 | _ETCD_ENV_VALUE_INFO & 195 | 196 | ## DNS配置文件权限修改和检查 reload 197 | _DNS_RELOAD 198 | 199 | #ETCD自增ID 200 | ETCD_ID=$((${ETCD_ID}+1)) 201 | 202 | done 203 | -------------------------------------------------------------------------------- /docker_file/python3_dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.6-alpine 2 | WORKDIR /code 3 | VOLUME /code 4 | COPY requirements.txt /code 5 | RUN pip install -r requirements.txt 6 | ADD https://cdn.mysql.com//Downloads/Connector-Python/mysql-connector-python-2.1.7.tar.gz /tmp/ 7 | RUN tar -xvf /tmp/mysql-connector-python-2.1.7.tar.gz -C /tmp/ 8 | WORKDIR /tmp/mysql-connector-python-2.1.7/ 9 | RUN python setup.py install 10 | ENV FLASK_APP=/peb-dns/app.py 11 | EXPOSE 8080 12 | -------------------------------------------------------------------------------- /docker_file/requirements.txt: -------------------------------------------------------------------------------- 1 | alembic==0.9.6 2 | aniso8601==1.3.0 3 | blinker==1.4 4 | certifi==2017.11.5 5 | chardet==3.0.4 6 | click==6.7 7 | dnspython==1.15.0 8 | flasgger==0.8.0 9 | Flask==0.12.2 10 | Flask-Cors==3.0.3 11 | Flask-Mail==0.9.1 12 | Flask-Migrate==2.1.1 13 | Flask-RESTful==0.3.6 14 | Flask-SQLAlchemy==2.3.2 15 | gunicorn==19.7.1 16 | idna==2.6 17 | itsdangerous==0.24 18 | Jinja2==2.10 19 | jsonschema==2.6.0 20 | ldap3==2.4 21 | Mako==1.0.7 22 | MarkupSafe==1.0 23 | mistune==0.8.3 24 | pyasn1==0.4.2 25 | PyJWT==1.5.3 26 | python-dateutil==2.6.1 27 | python-editor==1.0.3 28 | python-etcd==0.4.5 29 | pytz==2017.3 30 | PyYAML==3.12 31 | requests==2.18.4 32 | six==1.11.0 33 | SQLAlchemy==1.1.15 34 | urllib3==1.22 35 | Werkzeug==0.13 -------------------------------------------------------------------------------- /docker_start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ###################### 此脚本docker部署自动调用,请勿手动运行! ######################## 4 | sleep 5 5 | export FLASK_APP=${PWD}/app.py 6 | export FLASK_DEBUG=0 7 | PEB_PORT=$1 8 | MYSQL_HOST_PORT=$2 9 | USER=$3 10 | PASSWORD=$4 11 | DB_NAME=$5 12 | 13 | while true;do 14 | echo $i 15 | echo "################ mysql initing... #######################" 16 | python /code/docs/install/checkdb.py ${MYSQL_HOST_PORT} ${USER} ${PASSWORD} ${DB_NAME} > /dev/null 2>&1 17 | ready=$? 18 | if [ $ready -eq 0 ];then 19 | break 20 | fi 21 | sleep 1 22 | done 23 | 24 | echo "################# mysql init done!!! #######################" 25 | 26 | if [ ! -d migrations ];then 27 | echo "migration not exists!!!!!" 28 | flask db init 29 | flask db migrate 30 | flask db upgrade 31 | flask initdb 32 | fi 33 | 34 | echo "################# init db data done!!! #######################" 35 | 36 | gunicorn -w 4 app:app -b 0.0.0.0:${PEB_PORT} \ 37 | --log-level=debug \ 38 | --access-logfile logs/peb_dns_access.log \ 39 | --error-logfile logs/peb_dns_error.log \ 40 | --log-file logs/peb_dns.log 41 | 42 | echo "################# peb_dns started !!! #######################" 43 | -------------------------------------------------------------------------------- /docs/images/dns.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackjiali/dns-manager/c7e983f17b79a1f0d8e71c789320d0b83dfcb6e9/docs/images/dns.jpg -------------------------------------------------------------------------------- /docs/images/manage_view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackjiali/dns-manager/c7e983f17b79a1f0d8e71c789320d0b83dfcb6e9/docs/images/manage_view.png -------------------------------------------------------------------------------- /docs/images/manage_zone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackjiali/dns-manager/c7e983f17b79a1f0d8e71c789320d0b83dfcb6e9/docs/images/manage_zone.png -------------------------------------------------------------------------------- /docs/images/peb-dashbard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackjiali/dns-manager/c7e983f17b79a1f0d8e71c789320d0b83dfcb6e9/docs/images/peb-dashbard.png -------------------------------------------------------------------------------- /docs/images/qq-group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackjiali/dns-manager/c7e983f17b79a1f0d8e71c789320d0b83dfcb6e9/docs/images/qq-group.png -------------------------------------------------------------------------------- /docs/install/checkdb.py: -------------------------------------------------------------------------------- 1 | import mysql.connector 2 | import sys 3 | 4 | if __name__ == '__main__': 5 | host_port = sys.argv[1].split(':') 6 | host = host_port[0] 7 | port = host_port[1] 8 | user = sys.argv[2] 9 | password = sys.argv[3] 10 | database = sys.argv[4] 11 | conn = mysql.connector.connect(host=host, port=port, user=user, password=password, database=database) 12 | 13 | -------------------------------------------------------------------------------- /docs/install/dns_install.md: -------------------------------------------------------------------------------- 1 | # 部署 2 | ## 一,本地部署 3 | ### 后端Python工程部署 4 | 5 | * #### 本教程部署基于 Ubuntu/Debian 平台,其他平台亦可作为参考。 6 | 7 | * #### Python3.5 安装(已安装 Python3.5 环境的请跳过) 8 | ```bash 9 | # 安装python3.5 10 | sudo add-apt-repository ppa:fkrull/deadsnakes 11 | sudo apt-get update 12 | sudo apt-get install python3.5 13 | 14 | # 安装pip 15 | sudo apt-get install python3-pip python-pip 16 | 17 | # 设置python3.5为默认python程序 18 | sudo mv /usr/bin/python /usr/bin/python.bak 19 | sudo ln -s python3.5 python 20 | 21 | ``` 22 | 23 | * #### 工具安装 24 | ```bash 25 | # 安装 mysql 略 (请安装mysql5.7版本) 26 | sudo apt-get install mysql-server 27 | # 安装 etcd 略 (请参考官方文档) 28 | sudo apt-get install etcd 29 | ``` 30 | 31 | 32 | * #### 克隆项目代码到本地 33 | ```bash 34 | # 将本仓库clone到本地 35 | git clone git@github.com:pahf-ops/peb-dns.git 36 | ``` 37 | 38 | 39 | * #### 安装依赖 40 | 41 | ```bash 42 | # 首先进入当前目录下 43 | # 使用pip3为python3.5安装依赖包 44 | sudo pip3 install -r requirements.txt 45 | # 若安装过程中出现区域报错,locale.Error: unsupported locale setting 46 | # export LC_ALL=C 47 | 48 | # 安装 mysql 驱动 49 | wget https://cdn.mysql.com//Downloads/Connector-Python/mysql-connector-python-2.1.7.tar.gz 50 | tar -zxf mysql-connector-python-2.1.7.tar.gz 51 | cd mysql-connector-python-2.1.7/ 52 | sudo python3.5 setup.py install 53 | 54 | ``` 55 | 56 | 57 | * #### 初始化数据库 58 | 59 | 1,将Mysql数据库编码设置为UTF-8 60 | ```bash 61 | # 登入mysql 62 | ❯ mysql -u root -p 63 | # 查看编码 64 | ❯ show variables like '%character%'; 65 | 66 | | Variable_name | Value | 67 | +--------------------------+----------------------------+ 68 | | character_set_client | utf8 | 69 | | character_set_connection | utf8 | 70 | | character_set_database | utf8 | 71 | | character_set_filesystem | binary | 72 | | character_set_results | utf8 | 73 | | character_set_server | utf8 | 74 | | character_set_system | utf8 | 75 | | character_sets_dir | /usr/share/mysql/charsets/ | 76 | +--------------------------+----------------------------+ 77 | 78 | 若编码不是utf8 79 | 在/etc/mysql/mysql.conf.d/mysqld.cnf文件中[mysqld]加入变量,如: 80 | character_set_database = utf8 81 | character-set-server = utf8 82 | 83 | 修改完之后, 通过service mysql restart重启 mysql 84 | ``` 85 | 86 | 2,创建数据库 87 | ```bash 88 | # 创建数据库实例 89 | ❯ mysql -u root -p 90 | 91 | mysql> create database ; 92 | Query OK, 1 row affected (0.01 sec) 93 | 94 | mysql> ^DBye 95 | ``` 96 | 97 | 3,修改项目配置文件 98 | ```bash 99 | # 配置文件路径如下: 100 | peb-dns/config/peb_dns.cfg.sample 101 | # 先拷贝一份配置文件并重命名,去掉 .sample 后缀: 102 | cp peb-dns/config/peb_dns.cfg.sample peb-dns/config/peb_dns.cfg 103 | # 然后所有字段都有详细说明,请严格按照说明一一配好。 104 | ``` 105 | 106 | 4,配置应用环境变量 107 | ```bash 108 | #设置 FLASK_APP 环境变量,值为本项目根目录下 app.py 的绝对路径 109 | #首先进入当前项目根目录下,即 app.py 同级目录,然后执行以下语句 110 | export FLASK_APP=${PWD}/app.py 111 | ``` 112 | 113 | 5,初始化数据库 114 | ```bash 115 | flask db init 116 | flask db migrate 117 | flask db upgrade 118 | flask initdb 119 | ``` 120 | 121 | * #### 简单快速部署方式 122 | 123 | ```bash 124 | # 进入当前项目根目录下,运行以下命令部署 125 | nohup gunicorn -w 4 app:app -b 0.0.0.0:8080 --log-level=debug & 126 | # PS: 上面 -w 为 开启workers数,公式:(系统内核数*2 + 1), 8080 为端口号 127 | ``` 128 | 129 | * #### 配置您的Zabbix 130 | Zabbix 配置请参考 [这里](../zabbix/zabbix_dns_README.md) 131 | 132 | 133 | ### 前端vue工程部署 134 | 135 | * #### 前端配置文件 136 | 137 | ```javascript 138 | 前端工程需要配置后端项目的base url 139 | 文件路径:static/static/js/app.*.js 140 | 141 | 找到以下内容,并配置baseURL 142 | var instance = __WEBPACK_IMPORTED_MODULE_2_axios___default.a.create({ 143 | baseURL: '<后端URL>/api/', 144 | timeout: 10000, 145 | withCredentials: true 146 | }); 147 | ``` 148 | 149 | * #### 安装和配置nginx 150 | ```json 151 | 首先安装 nginx 152 | sudo apt-get install nginx 153 | 154 | 然后添加 155 | /etc/nginx/conf.d/peb_dns.conf 156 | 配置文件 157 | 158 | 内容如下: 159 | 160 | server { 161 | listen 80 default; 162 | server_name _; 163 | location / { 164 | index /index.html; 165 | root /home/ubuntu/peb-dns/static; //peb-dns项目下static文件夹的绝对路径 166 | } 167 | location /static { 168 | index index.html; 169 | root /home/ubuntu/peb-dns/static; //peb-dns项目下static文件夹的绝对路径 170 | } 171 | 172 | location /api { 173 | proxy_pass http://<后端URL>; //这里配置为 后端项目部署成功之后的访问地址 174 | } 175 | } 176 | 177 | 重新加载nginx配置 178 | sudo service nginx reload 179 | ``` 180 | 181 | * #### 部署bind服务器端脚本 182 | 183 | ```bash 184 | 启动etcd客户端 185 | 拷贝代码目录下脚本到服务器root目录下: 186 | etcd_client/hfdns_client_etcd_config.sh 187 | 188 | 配置默认ETCD地址: 189 | ETCD_SERVER_NAME="http://1.1.1.1:2379/v2/keys/opscmdb" 190 | 191 | 如公司有多套环境,根据自身情况,配置每个环境对应的ETCD地址: 192 | #ETCD集群服务器 193 | #[ "x${ETCD_ENV}" == "xdev" ] && ETCD_SERVER_NAME="http://dev-etcd01:2379/v2/keys/opscmdb" 194 | #[ "x${ETCD_ENV}" == "xanhouse" ] && ETCD_SERVER_NAME="http://shzr-etcd01:2379/v2/keys/opscmdb" 195 | #[ "x${ETCD_ENV}" == "xga" ] && ETCD_SERVER_NAME="http://shbx-etcd01:2379/v2/keys/opscmdb" 196 | [ "x${ETCD_ENV}" == "xsingle" ] && ETCD_SERVER_NAME="2.2.2.2:2379/v2/keys/opscmdb" 197 | 198 | root用户下添加计划任务: 199 | */1 * * * * /bin/bash /root/hfdns_client_etcd_config.sh single >/dev/null 2>&1 200 | ``` 201 | 202 | 203 | ## 二,docker一键部署 204 | 205 | * 本教程基于已经安装docker和docker-compose的用户,两者安装教程,请参考官方文档。 206 | 207 | * 克隆项目代码到本地 208 | ```bash 209 | # 将本仓库clone到本地 210 | git clone git@github.com:pahf-ops/peb-dns.git 211 | ``` 212 | 213 | * 部署 214 | ```bash 215 | # 1,修改项目配置文件 216 | 配置文件路径如下: 217 | peb-dns/config/peb_dns.cfg.sample 218 | 先将将配置文件重命名,去掉.sample后缀: 219 | mv peb-dns/config/peb_dns.cfg.sample peb-dns/config/peb_dns.cfg 220 | 然后所有字段都有详细说明,请严格按照说明一一配好。 221 | 222 | # 2, 切换目录至 peb-dns 文件夹 平级目录,然后执行: 223 | cp peb-dns/docker_file/docker-compose.yml . 224 | 225 | # 3,初始化操作 226 | docker-compose down 227 | rm -rf .data 228 | rm -rf peb-dns/migrations/ 229 | chmod 755 peb-dns/docker_start.sh 230 | 231 | # 4,部署项目 232 | docker-compose up 233 | 234 | ``` 235 | 236 | 237 | 238 | 239 | -------------------------------------------------------------------------------- /docs/install/peb_dns_nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen ${NGINX_PORT} default; 3 | server_name _; 4 | location / { 5 | index /index.html; 6 | root /code/static/; 7 | } 8 | location /static { 9 | index index.html; 10 | root /code/static/; 11 | } 12 | 13 | location /api { 14 | proxy_pass http://127.0.0.1:8080; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /docs/install/requirements.txt: -------------------------------------------------------------------------------- 1 | alembic==0.9.6 2 | aniso8601==1.3.0 3 | blinker==1.4 4 | certifi==2017.11.5 5 | chardet==3.0.4 6 | click==6.7 7 | dnspython==1.15.0 8 | flasgger==0.8.0 9 | Flask==0.12.2 10 | Flask-Cors==3.0.3 11 | Flask-Mail==0.9.1 12 | Flask-Migrate==2.1.1 13 | Flask-RESTful==0.3.6 14 | Flask-SQLAlchemy==2.3.2 15 | gunicorn==19.7.1 16 | idna==2.6 17 | itsdangerous==0.24 18 | Jinja2==2.10 19 | jsonschema==2.6.0 20 | ldap3==2.4 21 | Mako==1.0.7 22 | MarkupSafe==1.0 23 | mistune==0.8.3 24 | pyasn1==0.4.2 25 | PyJWT==1.5.3 26 | python-dateutil==2.6.1 27 | python-editor==1.0.3 28 | python-etcd==0.4.5 29 | pytz==2017.3 30 | PyYAML==3.12 31 | requests==2.18.4 32 | six==1.11.0 33 | SQLAlchemy==1.1.15 34 | urllib3==1.22 35 | Werkzeug==0.13 -------------------------------------------------------------------------------- /docs/sql/peb_dns.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Date: 2018-01-08 11:54:35 3 | */ 4 | 5 | SET FOREIGN_KEY_CHECKS=0; 6 | 7 | -- ---------------------------- 8 | -- Table structure for `account_local_auth` 9 | -- ---------------------------- 10 | DROP TABLE IF EXISTS `account_local_auth`; 11 | CREATE TABLE `account_local_auth` ( 12 | `id` int(11) NOT NULL AUTO_INCREMENT, 13 | `username` varchar(64) DEFAULT NULL, 14 | `password_hash` varchar(128) DEFAULT NULL, 15 | `email` varchar(128) DEFAULT NULL, 16 | PRIMARY KEY (`id`), 17 | UNIQUE KEY `ix_account_local_auth_username` (`username`) 18 | ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; 19 | 20 | -- ---------------------------- 21 | -- Records of account_local_auth 22 | -- ---------------------------- 23 | INSERT INTO `account_local_auth` VALUES ('1', 'admin', 'pbkdf2:sha256:50000$B8xHsDQ7$6c01906213113c1fc3e94e897f2128a407da5e9b41dd18bb9799f080230bd95e', 'xxxx@gmail.com'); 24 | 25 | -- ---------------------------- 26 | -- Table structure for `account_privilege` 27 | -- ---------------------------- 28 | DROP TABLE IF EXISTS `account_privilege`; 29 | CREATE TABLE `account_privilege` ( 30 | `id` int(11) NOT NULL AUTO_INCREMENT, 31 | `name` varchar(128) DEFAULT NULL, 32 | `operation` int(11) DEFAULT NULL, 33 | `resource_type` varchar(64) DEFAULT NULL, 34 | `resource_id` int(11) DEFAULT NULL, 35 | `comment` varchar(128) DEFAULT NULL, 36 | PRIMARY KEY (`id`), 37 | KEY `ix_account_privilege_resource_id` (`resource_id`) 38 | ) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8; 39 | 40 | -- ---------------------------- 41 | -- Records of account_privilege 42 | -- ---------------------------- 43 | INSERT INTO `account_privilege` VALUES ('1', 'SERVER_ADD', null, null, null, null); 44 | INSERT INTO `account_privilege` VALUES ('2', 'ZONE_ADD', null, null, null, null); 45 | INSERT INTO `account_privilege` VALUES ('3', 'VIEW_ADD', null, null, null, null); 46 | INSERT INTO `account_privilege` VALUES ('4', 'BIND_CONF_EDIT', null, null, null, null); 47 | INSERT INTO `account_privilege` VALUES ('5', 'VIEW#default_view#ACCESS', '0', '1', '1', null); 48 | INSERT INTO `account_privilege` VALUES ('6', 'VIEW#default_view#UPDATE', '1', '1', '1', null); 49 | INSERT INTO `account_privilege` VALUES ('7', 'VIEW#default_view#DELETE', '2', '1', '1', null); 50 | INSERT INTO `account_privilege` VALUES ('8', 'ZONE#z1.com#ACCESS', '0', '2', '1', null); 51 | INSERT INTO `account_privilege` VALUES ('9', 'ZONE#z1.com#UPDATE', '1', '2', '1', null); 52 | INSERT INTO `account_privilege` VALUES ('10', 'ZONE#z1.com#DELETE', '2', '2', '1', null); 53 | 54 | -- ---------------------------- 55 | -- Table structure for `account_role` 56 | -- ---------------------------- 57 | DROP TABLE IF EXISTS `account_role`; 58 | CREATE TABLE `account_role` ( 59 | `id` int(11) NOT NULL AUTO_INCREMENT, 60 | `name` varchar(64) DEFAULT NULL, 61 | PRIMARY KEY (`id`), 62 | UNIQUE KEY `name` (`name`) 63 | ) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8; 64 | 65 | -- ---------------------------- 66 | -- Records of account_role 67 | -- ---------------------------- 68 | INSERT INTO `account_role` VALUES ('1', 'admin'); 69 | INSERT INTO `account_role` VALUES ('2', 'server_admin'); 70 | INSERT INTO `account_role` VALUES ('3', 'server_guest'); 71 | INSERT INTO `account_role` VALUES ('4', 'view_admin'); 72 | INSERT INTO `account_role` VALUES ('5', 'view_guest'); 73 | INSERT INTO `account_role` VALUES ('6', 'zone_admin'); 74 | INSERT INTO `account_role` VALUES ('7', 'zone_guest'); 75 | 76 | -- ---------------------------- 77 | -- Table structure for `account_role_privilege` 78 | -- ---------------------------- 79 | DROP TABLE IF EXISTS `account_role_privilege`; 80 | CREATE TABLE `account_role_privilege` ( 81 | `id` int(11) NOT NULL AUTO_INCREMENT, 82 | `role_id` int(11) DEFAULT NULL, 83 | `privilege_id` int(11) DEFAULT NULL, 84 | PRIMARY KEY (`id`), 85 | KEY `ix_account_role_privilege_privilege_id` (`privilege_id`), 86 | KEY `ix_account_role_privilege_role_id` (`role_id`) 87 | ) ENGINE=InnoDB AUTO_INCREMENT=22 DEFAULT CHARSET=utf8; 88 | 89 | -- ---------------------------- 90 | -- Records of account_role_privilege 91 | -- ---------------------------- 92 | INSERT INTO `account_role_privilege` VALUES ('1', '1', '1'); 93 | INSERT INTO `account_role_privilege` VALUES ('2', '2', '1'); 94 | INSERT INTO `account_role_privilege` VALUES ('3', '1', '2'); 95 | INSERT INTO `account_role_privilege` VALUES ('4', '6', '2'); 96 | INSERT INTO `account_role_privilege` VALUES ('5', '1', '3'); 97 | INSERT INTO `account_role_privilege` VALUES ('6', '4', '3'); 98 | INSERT INTO `account_role_privilege` VALUES ('7', '1', '4'); 99 | INSERT INTO `account_role_privilege` VALUES ('8', '1', '5'); 100 | INSERT INTO `account_role_privilege` VALUES ('9', '1', '6'); 101 | INSERT INTO `account_role_privilege` VALUES ('10', '1', '7'); 102 | INSERT INTO `account_role_privilege` VALUES ('11', '4', '5'); 103 | INSERT INTO `account_role_privilege` VALUES ('12', '4', '6'); 104 | INSERT INTO `account_role_privilege` VALUES ('13', '4', '7'); 105 | INSERT INTO `account_role_privilege` VALUES ('14', '5', '5'); 106 | INSERT INTO `account_role_privilege` VALUES ('15', '1', '8'); 107 | INSERT INTO `account_role_privilege` VALUES ('16', '1', '9'); 108 | INSERT INTO `account_role_privilege` VALUES ('17', '1', '10'); 109 | INSERT INTO `account_role_privilege` VALUES ('18', '6', '8'); 110 | INSERT INTO `account_role_privilege` VALUES ('19', '6', '9'); 111 | INSERT INTO `account_role_privilege` VALUES ('20', '6', '10'); 112 | INSERT INTO `account_role_privilege` VALUES ('21', '7', '8'); 113 | 114 | -- ---------------------------- 115 | -- Table structure for `account_user` 116 | -- ---------------------------- 117 | DROP TABLE IF EXISTS `account_user`; 118 | CREATE TABLE `account_user` ( 119 | `id` int(11) NOT NULL AUTO_INCREMENT, 120 | `email` varchar(64) DEFAULT NULL, 121 | `username` varchar(64) DEFAULT NULL, 122 | `chinese_name` varchar(64) DEFAULT NULL, 123 | `cellphone` varchar(64) DEFAULT NULL, 124 | `actived` int(11) DEFAULT NULL, 125 | `position` varchar(64) DEFAULT NULL, 126 | `location` varchar(64) DEFAULT NULL, 127 | `member_since` datetime DEFAULT NULL, 128 | `last_seen` datetime DEFAULT NULL, 129 | PRIMARY KEY (`id`), 130 | UNIQUE KEY `ix_account_user_username` (`username`) 131 | ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; 132 | 133 | -- ---------------------------- 134 | -- Records of account_user 135 | -- ---------------------------- 136 | INSERT INTO `account_user` VALUES ('1', 'xxxx@gmail.com', 'admin', '', '', '1', '', '', '2018-01-08 11:47:22', '2018-01-08 11:47:22'); 137 | 138 | -- ---------------------------- 139 | -- Table structure for `account_user_role` 140 | -- ---------------------------- 141 | DROP TABLE IF EXISTS `account_user_role`; 142 | CREATE TABLE `account_user_role` ( 143 | `id` int(11) NOT NULL AUTO_INCREMENT, 144 | `user_id` int(11) DEFAULT NULL, 145 | `role_id` int(11) DEFAULT NULL, 146 | PRIMARY KEY (`id`), 147 | KEY `ix_account_user_role_role_id` (`role_id`), 148 | KEY `ix_account_user_role_user_id` (`user_id`) 149 | ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; 150 | 151 | -- ---------------------------- 152 | -- Records of account_user_role 153 | -- ---------------------------- 154 | INSERT INTO `account_user_role` VALUES ('1', '1', '1'); 155 | 156 | -- ---------------------------- 157 | -- Table structure for `alembic_version` 158 | -- ---------------------------- 159 | DROP TABLE IF EXISTS `alembic_version`; 160 | CREATE TABLE `alembic_version` ( 161 | `version_num` varchar(32) NOT NULL, 162 | PRIMARY KEY (`version_num`) 163 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 164 | 165 | -- ---------------------------- 166 | -- Records of alembic_version 167 | -- ---------------------------- 168 | INSERT INTO `alembic_version` VALUES ('73e7a686b691'); 169 | 170 | -- ---------------------------- 171 | -- Table structure for `dns_operation_log` 172 | -- ---------------------------- 173 | DROP TABLE IF EXISTS `dns_operation_log`; 174 | CREATE TABLE `dns_operation_log` ( 175 | `id` int(11) NOT NULL AUTO_INCREMENT, 176 | `operation_time` datetime DEFAULT NULL, 177 | `operation_type` varchar(64) DEFAULT NULL, 178 | `operator` varchar(64) DEFAULT NULL, 179 | `target_type` varchar(64) DEFAULT NULL, 180 | `target_name` varchar(64) DEFAULT NULL, 181 | `target_id` varchar(64) DEFAULT NULL, 182 | `target_detail` text, 183 | `gmt_create` datetime DEFAULT NULL, 184 | `gmt_modified` datetime DEFAULT NULL, 185 | PRIMARY KEY (`id`) 186 | ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8; 187 | 188 | -- ---------------------------- 189 | -- Records of dns_operation_log 190 | -- ---------------------------- 191 | INSERT INTO `dns_operation_log` VALUES ('1', '2018-01-08 11:47:51', '添加', 'admin', 'Zone', 'z1.com', '1', 'id: 1\nZone名称: z1.com\nZone归属: 内部域名\nZone类型: master\n关联View: [\'default_view\']\n', '2018-01-08 11:47:51', '2018-01-08 11:47:51'); 192 | INSERT INTO `dns_operation_log` VALUES ('2', '2018-01-08 11:48:16', '添加', 'admin', 'Record', 'r1111', '2', 'id: 2\n记录主机: r1111\n记录类型: A\n记录值: 0.0.0.\nTTL: 600\n线路类型: default_view\n备注: asdf\n创建人: None\n创建时间: 2018-01-08 11:48:15.547158', '2018-01-08 11:48:16', '2018-01-08 11:48:16'); 193 | 194 | -- ---------------------------- 195 | -- Table structure for `dns_record` 196 | -- ---------------------------- 197 | DROP TABLE IF EXISTS `dns_record`; 198 | CREATE TABLE `dns_record` ( 199 | `id` int(11) NOT NULL AUTO_INCREMENT, 200 | `host` varchar(64) DEFAULT NULL, 201 | `record_type` varchar(64) DEFAULT NULL, 202 | `ttl` varchar(64) DEFAULT NULL, 203 | `value` varchar(64) DEFAULT NULL, 204 | `view_name` varchar(64) DEFAULT NULL, 205 | `comment` varchar(64) DEFAULT NULL, 206 | `creator` varchar(64) DEFAULT NULL, 207 | `status` varchar(64) DEFAULT NULL, 208 | `enabled` varchar(64) DEFAULT NULL, 209 | `alive` varchar(64) DEFAULT NULL, 210 | `outter_record_id` varchar(64) DEFAULT NULL, 211 | `zone_id` int(11) DEFAULT NULL, 212 | `gmt_create` datetime DEFAULT NULL, 213 | `gmt_modified` datetime DEFAULT NULL, 214 | PRIMARY KEY (`id`), 215 | KEY `ix_dns_record_host` (`host`), 216 | KEY `ix_dns_record_zone_id` (`zone_id`) 217 | ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8; 218 | 219 | -- ---------------------------- 220 | -- Records of dns_record 221 | -- ---------------------------- 222 | INSERT INTO `dns_record` VALUES ('1', '@', 'NS', '86400', 'master.z1.com.', 'default_view', null, 'admin', 'enabled', '1', 'ON', '', '1', '2018-01-08 11:47:51', '2018-01-08 11:47:51'); 223 | INSERT INTO `dns_record` VALUES ('2', 'r1111', 'A', '600', '0.0.0.', 'default_view', 'asdf', null, 'enabled', '1', 'ON', '', '1', '2018-01-08 11:48:16', '2018-01-08 11:48:16'); 224 | 225 | -- ---------------------------- 226 | -- Table structure for `dns_server` 227 | -- ---------------------------- 228 | DROP TABLE IF EXISTS `dns_server`; 229 | CREATE TABLE `dns_server` ( 230 | `id` int(11) NOT NULL AUTO_INCREMENT, 231 | `host` varchar(64) DEFAULT NULL, 232 | `ip` varchar(64) DEFAULT NULL, 233 | `env` varchar(64) DEFAULT NULL, 234 | `dns_server_type` varchar(64) DEFAULT NULL, 235 | `status` varchar(64) DEFAULT NULL, 236 | `zb_process_itemid` varchar(64) DEFAULT NULL, 237 | `zb_port_itemid` varchar(64) DEFAULT NULL, 238 | `zb_resolve_itemid` varchar(64) DEFAULT NULL, 239 | `zb_resolve_rate_itemid` varchar(64) DEFAULT NULL, 240 | `server_log` text, 241 | `gmt_create` datetime DEFAULT NULL, 242 | `gmt_modified` datetime DEFAULT NULL, 243 | PRIMARY KEY (`id`), 244 | KEY `ix_dns_server_host` (`host`) 245 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 246 | 247 | -- ---------------------------- 248 | -- Records of dns_server 249 | -- ---------------------------- 250 | 251 | -- ---------------------------- 252 | -- Table structure for `dns_view` 253 | -- ---------------------------- 254 | DROP TABLE IF EXISTS `dns_view`; 255 | CREATE TABLE `dns_view` ( 256 | `id` int(11) NOT NULL AUTO_INCREMENT, 257 | `name` varchar(64) DEFAULT NULL, 258 | `acl` text, 259 | `gmt_create` datetime DEFAULT NULL, 260 | `gmt_modified` datetime DEFAULT NULL, 261 | PRIMARY KEY (`id`), 262 | KEY `ix_dns_view_name` (`name`) 263 | ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; 264 | 265 | -- ---------------------------- 266 | -- Records of dns_view 267 | -- ---------------------------- 268 | INSERT INTO `dns_view` VALUES ('1', 'default_view', '0.0.0.0/0', '2018-01-08 11:47:22', '2018-01-08 11:47:22'); 269 | 270 | -- ---------------------------- 271 | -- Table structure for `dns_view_zone` 272 | -- ---------------------------- 273 | DROP TABLE IF EXISTS `dns_view_zone`; 274 | CREATE TABLE `dns_view_zone` ( 275 | `id` int(11) NOT NULL AUTO_INCREMENT, 276 | `view_id` int(11) DEFAULT NULL, 277 | `zone_id` int(11) DEFAULT NULL, 278 | `gmt_create` datetime DEFAULT NULL, 279 | `gmt_modified` datetime DEFAULT NULL, 280 | PRIMARY KEY (`id`), 281 | KEY `ix_dns_view_zone_view_id` (`view_id`), 282 | KEY `ix_dns_view_zone_zone_id` (`zone_id`) 283 | ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; 284 | 285 | -- ---------------------------- 286 | -- Records of dns_view_zone 287 | -- ---------------------------- 288 | INSERT INTO `dns_view_zone` VALUES ('1', '1', '1', '2018-01-08 11:47:51', '2018-01-08 11:47:51'); 289 | 290 | -- ---------------------------- 291 | -- Table structure for `dns_zone` 292 | -- ---------------------------- 293 | DROP TABLE IF EXISTS `dns_zone`; 294 | CREATE TABLE `dns_zone` ( 295 | `id` int(11) NOT NULL AUTO_INCREMENT, 296 | `name` varchar(64) DEFAULT NULL, 297 | `zone_group` int(11) DEFAULT NULL, 298 | `zone_type` varchar(64) DEFAULT NULL, 299 | `forwarders` varchar(64) DEFAULT NULL, 300 | `gmt_create` datetime DEFAULT NULL, 301 | `gmt_modified` datetime DEFAULT NULL, 302 | PRIMARY KEY (`id`), 303 | KEY `ix_dns_zone_name` (`name`) 304 | ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; 305 | 306 | -- ---------------------------- 307 | -- Records of dns_zone 308 | -- ---------------------------- 309 | INSERT INTO `dns_zone` VALUES ('1', 'z1.com', '1', 'master', '', '2018-01-08 11:47:51', '2018-01-08 11:47:51'); 310 | -------------------------------------------------------------------------------- /docs/zabbix/zabbix_dns_README.md: -------------------------------------------------------------------------------- 1 | Zabbix 配置说明 2 | =========================== 3 | #### 文件说明 4 | 5 | 文件 |说明 6 | ------------|----------- 7 | [zbx_export_templates.xml](zbx_export_templates.xml) |zabbix DNS服务监控模板 8 | [zabbix_monitor_dns.sh](zabbix_monitor_dns.sh) |zabbix DNS模板使用到的key脚本 9 | 10 | 11 | #### 配置说明 12 | * ### 本公司使用的zabbix是3.4 版本,导入DNS模板的时候请注意!! 13 | 14 | * DNS bind 模板 自定义的需要两个KEY: 15 | ```bash 16 | UserParameter=check_dns[*],/bin/bash /data1/env/zabbix/etc/custon_scripts/zabbix_monitor_dns.sh $1 17 | 18 | UserParameter=net.tcp.listen.grep[*],if grep -q $$(printf '%04X.00000000:0000.0A' $1) /proc/net/tcp || grep -q $$(printf '00000000000000000000000000000000:%04X' $1) /proc/net/tcp6;then echo 1;else echo 0;fi 19 | ``` 20 | 21 | * 获取bind DNS值请参考以下脚本: 22 | 23 | * [docs/zabbix/zabbix_monitor_dns.sh](zabbix_monitor_dns.sh) 24 | 25 | 26 | #### 注意 27 | 28 | * 1,需要源文件 /var/named/data/named_stats.txt 此数据的来源是通过bind的命令获取的 /usr/sbin/rndc stats 获取,自行通过在DNS主机上通过crontab每分钟执行生成此文件。 或者通过其他脚本方式 29 | 30 | * 2,注意zabbix用户需要有读取 /var/named/data/named_stats.txt的权限,若没有请通过修改权限或者使用其他方式赋权。 31 | 32 | -------------------------------------------------------------------------------- /docs/zabbix/zabbix_monitor_dns.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | named_stats='/tmp/named_stats.txt' 3 | ###++ Incoming Requests ++ 4 | Incoming_QUERY=`awk '/QUERY/{print $1}' $named_stats` 5 | Incoming_RESERVED9=`awk '/RESERVED9/{print $1}' $named_stats` 6 | ###++ Incoming Queries ++ 7 | Incoming_A=`grep A $named_stats |awk 'NR==1{print $1}'` 8 | Incoming_SOA=`grep SOA $named_stats |awk 'NR==1{print $1}'` 9 | Incoming_PTR=`grep PTR $named_stats |awk 'NR==1{print $1}'` 10 | Incoming_MX=`grep MX $named_stats |awk 'NR==1{print $1}'` 11 | Incoming_TXT=`grep TXT $named_stats |awk 'NR==1{print $1}'` 12 | Incoming_AAAA=`grep AAAA $named_stats |awk 'NR==1{print $1}'` 13 | Incoming_A6=`grep A6 $named_stats |awk 'NR==1{print $1}'` 14 | Incoming_IXFR=`grep IXFR $named_stats |awk 'NR==1{print $1}'` 15 | Incoming_ANY=`grep ANY $named_stats |awk 'NR==1{print $1}'` 16 | ###++ Outgoing Queries ++ 17 | Outgoing_A=`grep "\" $named_stats |awk 'NR==2{print $1}'` 18 | Outgoing_NS=`grep NS $named_stats |awk 'NR==1{print $1}'` 19 | Outgoing_PTR=`grep PTR $named_stats |awk 'NR==2{print $1}'` 20 | #Outgoing_AAAA=`grep NS $named_stats |awk 'NR==2{print $1}'` 21 | Outgoing_DNSKEY=`grep DNSKEY $named_stats |awk 'NR==1{print $1}'` 22 | Outgoing_ANY=`grep ANY $named_stats |awk 'NR==2{print $1}'` 23 | Outgoing_DLV=`grep DLV $named_stats |awk 'NR==2{print $1}'` 24 | ###++ Name Server Statistics ++ 25 | Statistics_IPv4_requests=`grep "IPv4 requests received" $named_stats |awk 'NR==1{print $1}'` 26 | Statistics_requests_received=`grep "requests with EDNS(0) received" $named_stats |awk 'NR==1{print $1}'` 27 | Statistics_TCP_requests=`grep "TCP requests received" $named_stats |awk 'NR==1{print $1}'` 28 | Statistics_queries_rejected=`grep "recursive queries rejected" $named_stats |awk 'NR==1{print $1}'` 29 | Statistics_responses_sent=`grep "responses sent" $named_stats |awk 'NR==1{print $1}'` 30 | Statistics_EDNS_sent=`grep "responses with EDNS(0) sent" $named_stats |awk 'NR==1{print $1}'` 31 | Statistics_successful_answer=`grep "queries resulted in successful answer" $named_stats |awk 'NR==1{print $1}'` 32 | Statistics_authoritative_answer=`grep "queries resulted in authoritative answer" $named_stats |awk 'NR==1{print $1}'` 33 | Statistics_non_authoritative_answer=`grep "queries resulted in non authoritative answer" $named_stats |awk 'NR==1{print $1}'` 34 | Statistics_nxrrset=`grep "queries resulted in nxrrset" $named_stats |awk 'NR==1{print $1}'` 35 | Statistics_SERVFAIL=`grep "queries resulted in SERVFAIL" $named_stats |awk 'NR==1{print $1}'` 36 | Statistics_NXDOMAIN=`grep "queries resulted in NXDOMAIN" $named_stats |awk 'NR==1{print $1}'` 37 | Statistics_recursion=`grep "queries resulted in recursion" $named_stats |awk 'NR==1{print $1}'` 38 | Statistics_received=`grep "queries resulted in received" $named_stats |awk 'NR==1{print $1}'` 39 | Statistics_dropped=`grep "queries resulted in dropped" $named_stats |awk 'NR==1{print $1}'` 40 | ###++ Resolver Statistics ++ 41 | Resolver_sent=`grep "IPv4 queries sent" $named_stats |awk 'NR==1{print $1}'` 42 | Resolver_received=`grep "IPv4 responses received" $named_stats |awk 'NR==1{print $1}'` 43 | #Resolver_NXDOMAIN_received=`grep "" $named_stats |awk 'NR==1{print $1}'` 44 | #Resolver_responses_received=`sed -n '49p' $named_stats |sed 's/^[ \t]*//g'|cut -d ' ' -f 1` 45 | #Resolver_delegations_received=`sed -n '50p' $named_stats |sed 's/^[ \t]*//g'|cut -d ' ' -f 1` 46 | Resolver_query_retries=`grep "query retries" $named_stats |awk 'NR==1{print $1}'` 47 | Resolver_query_timeouts=`grep "query timeouts" $named_stats |awk 'NR==1{print $1}'` 48 | Resolver_fetches=`grep "IPv4 NS address fetches" $named_stats |awk 'NR==1{print $1}'` 49 | #Resolver_fetch_failed=`sed -n '54p' $named_stats |sed 's/^[ \t]*//g'|cut -d ' ' -f 1` 50 | Resolver_validation_attempted=`grep "DNSSEC validation attempted" $named_stats |awk 'NR==1{print $1}'` 51 | Resolver_validation_succeeded=`grep "DNSSEC validation succeeded" $named_stats |awk 'NR==1{print $1}'` 52 | Resolver_NX_validation_succeeded=`grep "DNSSEC NX validation succeeded" $named_stats |awk 'NR==1{print $1}'` 53 | Resolver_RTT_10ms=`grep "queries with RTT < 10ms" $named_stats |awk 'NR==1{print $1}'` 54 | Resolver_RTT_100ms=`grep "queries with RTT 10-100ms" $named_stats |awk 'NR==1{print $1}'` 55 | Resolver_RTT_500ms=`grep "queries with RTT 100-500ms" $named_stats |awk 'NR==1{print $1}'` 56 | Resolver_RTT_800ms=`grep "queries with RTT 500-800ms" $named_stats |awk 'NR==1{print $1}'` 57 | Resolver_RTT_1600ms=`grep "queries with RTT 800-1600ms" $named_stats |awk 'NR==1{print $1}'` 58 | #Resolver_RTT_gt_1600ms=`sed -n '63p' $named_stats |sed 's/^[ \t]*//g'|cut -d ' ' -f 1` 59 | ###++ Cache DB RRsets ++ 60 | Cache_A=`grep "\" $named_stats |awk 'NR==3{print $1}'` 61 | Cache_NS=`grep "\" $named_stats |awk 'NR==3{print $1}'` 62 | #Cache_CNAME=`sed -n '69p' $named_stats |sed 's/^[ \t]*//g'|cut -d ' ' -f 1` 63 | #Cache_SOA=`sed -n '70p' $named_stats |sed 's/^[ \t]*//g'|cut -d ' ' -f 1` 64 | #Cache_PTR=`sed -n '71p' $named_stats |sed 's/^[ \t]*//g'|cut -d ' ' -f 1` 65 | Cache_AAAA=`grep "\" $named_stats |awk 'NR==2{print $1}'` 66 | Cache_DS=`grep "DS" $named_stats |awk 'NR==1{print $1}'` 67 | Cache_RRSIG=`grep "RRSIG" $named_stats |awk 'NR==1{print $1}'` 68 | Cache_NSEC=`grep "NSEC" $named_stats |awk 'NR==1{print $1}'` 69 | Cache_DNSKEY=`grep "DNSKEY" $named_stats |awk 'NR==2{print $1}'` 70 | #Cache_AAA=`sed -n '77p' $named_stats |sed 's/^[ \t]*//g'|cut -d ' ' -f 1` 71 | Cache_cDLV=`grep "DLV" $named_stats |awk 'NR==2{print $1}'` 72 | #Cache_NXDOMAIN=`sed -n '79p' $named_stats |sed 's/^[ \t]*//g'|cut -d ' ' -f 1` 73 | ###++ Socket I/O Statistics ++ 74 | Socket_UDP_opened=`grep "UDP/IPv4 sockets opened" $named_stats |awk 'NR==1{print $1}'` 75 | Socket_TCP_opened=`grep "TCP/IPv4 sockets opened" $named_stats |awk 'NR==1{print $1}'` 76 | Socket_UDP_closed=`grep "UDP/IPv4 sockets closed" $named_stats |awk 'NR==1{print $1}'` 77 | Socket_TCP_closed=`grep " TCP/IPv4 sockets closed" $named_stats |awk 'NR==1{print $1}'` 78 | Socket_UDP_established=`grep "UDP/IPv4 connections established" $named_stats |awk 'NR==1{print $1}'` 79 | Socket_TCP_established=`grep "TCP/IPv4 connections accepted" $named_stats |awk 'NR==1{print $1}'` 80 | Socket_TCP_accepted=`grep "TCP/IPv4 recv errors" $named_stats |awk 'NR==1{print $1}'` 81 | eval echo \$$1 82 | -------------------------------------------------------------------------------- /etcd_client/hfdns_client_etcd_config.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | #Auth: 平安好房 4 | #URL: http://www.pinganfang.com 5 | #USE: 结合ETCD配置本地文件 6 | # 7 | # 脚本使用说明如下: 8 | :</dev/null 13 | 5. 脚本stop方式: /bin/bash hfdns_client_etcd_config.sh dev stop 14 | 6. 脚本依赖linux jq命令进行解析json文件, 请使用前安装 yum install -y jq 15 | 7. 问题定位 查看日志如: tail -f /data1/logs/etcd_config/etcd_config_2017-09-06_access.log 16 | 8. 此脚本可以放到crontab 中每分钟运行以防止脚本挂掉 17 | ! 18 | 19 | #ETCD 环境 20 | ETCD_ENV=${1} 21 | 22 | if [ ${#ETCD_ENV} -eq 0 ];then 23 | echo "请提供环境参数...如{dev|anhouse|ga}" 24 | exit 25 | fi 26 | 27 | #ETCD Log目录 28 | ETCD_LOG_DIR="/data1/logs/etcd_config" 29 | 30 | #脚本锁 31 | ETCD_LOCK="/tmp/.${ETCD_ENV}_etcd_config.lock" 32 | 33 | #服务本身名称 34 | ETCD_JOBS_NAME=$(/bin/basename $BASH_SOURCE) 35 | 36 | #打印信息 37 | PRINT_INFO(){ 38 | /bin/echo "$(/bin/date +%F' '%T) ${1}" >> ${ETCD_LOG_DIR}/etcd_config_$(/bin/date +%F)_access.log 39 | } 40 | 41 | #关闭进程 42 | ETCD_STOP=${2} 43 | if [ "x${ETCD_STOP}" == "xstop" ];then 44 | 45 | #打印信息 46 | PRINT_INFO "${ETCD_JOBS_NAME} 服务已关闭..." 47 | 48 | #关闭监听ETCD 49 | /bin/ps -ef |/bin/egrep "(opscmdb|${ETCD_JOBS_NAME})" |/bin/grep -v grep | /bin/awk '{print $2}' | /usr/bin/xargs kill -9 50 | 51 | #退出 52 | exit 53 | fi 54 | 55 | #默认ETCD配置 56 | ETCD_SERVER_NAME="http://dev-etcd01:2379/v2/keys/opscmdb" 57 | 58 | #ETCD集群服务器 59 | [ "x${ETCD_ENV}" == "xdev" ] && ETCD_SERVER_NAME="http://dev-etcd01:2379/v2/keys/opscmdb" 60 | [ "x${ETCD_ENV}" == "xanhouse" ] && ETCD_SERVER_NAME="http://shzr-etcd01:2379/v2/keys/opscmdb" 61 | [ "x${ETCD_ENV}" == "xga" ] && ETCD_SERVER_NAME="http://shbx-etcd01:2379/v2/keys/opscmdb" 62 | 63 | 64 | 65 | #日志路径创建 66 | if [ ! -d ${ETCD_LOG_DIR} ];then 67 | 68 | #创建日志路径 69 | /bin/mkdir -p ${ETCD_LOG_DIR} >/dev/null 2>&1 70 | 71 | #返回状态 72 | if [ $? -ne 0 ];then 73 | 74 | #打印信息 75 | PRINT_INFO "${ETCD_LOG_DIR} 创建日志路径失败,查看是否权限有问题" 76 | exit 77 | fi 78 | fi 79 | 80 | #安装依赖包 81 | if [ ! -x /usr/bin/jq ];then 82 | 83 | #打印信息 84 | PRINT_INFO "${ETCD_JOBS_NAME} 依赖jq.x86_64安装包" 85 | exit 86 | fi 87 | 88 | #默认ID号,用于临时文件 89 | ETCD_ID=0 90 | 91 | #获取ETCD配置文件列表 92 | _ETCD_ENV_DOWNLOAD_CONFIG() { 93 | 94 | #ETCD 配置文件临时 95 | ETCD_ENV_CONFIG_FILE="/tmp/.${ETCD_ENV}_${ETCD_ID}_dns_etcd_config.json" 96 | 97 | #初始化数据 无需区分环境 98 | #/usr/bin/curl -s "${ETCD_SERVER_NAME}/${ETCD_ENV}?recursive=true&wait=true" > ${ETCD_ENV_CONFIG_FILE} 99 | 100 | /usr/bin/curl -s "${ETCD_SERVER_NAME}/?recursive=true&wait=true" > ${ETCD_ENV_CONFIG_FILE} 101 | } 102 | 103 | #获取ETCD内容 104 | _ETCD_ENV_VALUE_INFO(){ 105 | 106 | #获取INDEX DIR 107 | _ETCD_ENV_FILE=$(/bin/cat ${ETCD_ENV_CONFIG_FILE} | /usr/bin/jq .node.key |/bin/sed -e 's/\"//g' -e "s#/opscmdb/dns##g") 108 | 109 | #获取INDEX VALUE #去除文件开始结尾引号,回车,双引号(默认双引号ETCD会加\,) 110 | #_ETCD_ENV_VALUE=$(/bin/cat ${ETCD_ENV_CONFIG_FILE} | /usr/bin/jq .node.value |/bin/sed -e 's/^"//' -e 's/"$//' -e 's/\\n/\n/g' -e 's/\\//g') 111 | _ETCD_ENV_VALUE=$(/bin/cat ${ETCD_ENV_CONFIG_FILE} | /usr/bin/jq .node.value |/bin/sed -e 's/^"//' -e 's/"$//' -e 's/\\n/\n/g' -e 's/\\"/"/g') 112 | 113 | ## 114 | echo "$_ETCD_ENV_VALUE" 115 | 116 | #获取空内容 117 | if [ ${#_ETCD_ENV_FILE} -eq 0 ];then 118 | 119 | #打印信息 120 | PRINT_INFO "${ETCD_SERVER_NAME} 获取了内容为空..." 121 | 122 | #初始删除文件 123 | #/bin/rm -f ${ETCD_ENV_CONFIG_FILE} >/dev/null 2>&1 124 | 125 | #退出 126 | exit 127 | fi 128 | 129 | #第一次创建文件 130 | if [ ! -f ${_ETCD_ENV_FILE} ];then 131 | 132 | #获取配置文件目录 去除前缀 133 | ETCD_ENV_DIR=$(/usr/bin/dirname ${_ETCD_ENV_FILE}) 134 | #ETCD_ENV_DIR=$(/usr/bin/dirname ${_ETCD_ENV_FILE}) | sed 's#/opscmdb/dns##g' 135 | 136 | #创建目录 137 | [ ! -d ${ETCD_ENV_DIR} ] && /bin/mkdir -p ${ETCD_ENV_DIR} >/dev/null 2>&1 138 | fi 139 | 140 | #导入配置文件 141 | echo "$_ETCD_ENV_VALUE" > ${_ETCD_ENV_FILE} 142 | 143 | #打印信息 144 | PRINT_INFO "更新配置文件 ${_ETCD_ENV_FILE}" 145 | 146 | #初始删除文件 147 | #/bin/rm -f ${ETCD_ENV_CONFIG_FILE} >/dev/null 2>&1 148 | } 149 | 150 | 151 | ## DNS 配置文件reload 152 | _DNS_RELOAD() { 153 | ## 修改配置文件权限 154 | chown root.named /etc/named.conf 155 | chown named.named -R /etc/named 156 | chown named.named -R /var/named 157 | 158 | /usr/sbin/named-checkconf 159 | [ $? -ne 0 ] && PRINT_INFO "DNS named-checkconf 配置文件检测失败" 160 | PRINT_INFO "DNS named-checkconf 配置文件检测成功" 161 | ## 自带检测功能 配置文件检测失败 无法reload成功 但不会停止DNS解析服务 162 | /usr/sbin/rndc reload 163 | [ $? -ne 0 ] && PRINT_INFO "DNS reload 失败" 164 | [ $? -ne 0 ] && PRINT_INFO "DNS reload 成功" 165 | } 166 | 167 | #判断锁文件 168 | if [ -f ${ETCD_LOCK} ];then 169 | [ ! -z "$(/usr/sbin/lsof -p $(/bin/cat ${ETCD_LOCK}))" ] && exit 170 | fi 171 | 172 | #创建锁文件 173 | echo $$ > ${ETCD_LOCK} 174 | 175 | #创建锁文件出错 176 | if [ $? -ne 0 ];then 177 | exit 178 | fi 179 | 180 | 181 | #打印信息 182 | PRINT_INFO "${ETCD_JOBS_NAME} 服务已启动..." 183 | 184 | #获取任务列表 185 | while true 186 | do 187 | #监听是否有配置文件更新 188 | _ETCD_ENV_DOWNLOAD_CONFIG 189 | 190 | #解析配置文件 191 | _ETCD_ENV_VALUE_INFO & 192 | 193 | ## DNS配置文件权限修改和检查 reload 194 | _DNS_RELOAD 195 | 196 | #ETCD自增ID 197 | ETCD_ID=$((${ETCD_ID}+1)) 198 | 199 | done 200 | -------------------------------------------------------------------------------- /peb_dns/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, Blueprint, request, jsonify 2 | from config.config import config, config_pyfiles 3 | from flask_migrate import Migrate, MigrateCommand 4 | from .extensions import mail, db 5 | from flask_cors import CORS 6 | from .resourses.account import auth_bp 7 | from .resourses.admin import admin 8 | from .resourses.dns import dns_bp 9 | from .resourses.page import page_bp 10 | import os 11 | import click 12 | from .common.util import get_response 13 | from flasgger import Swagger 14 | 15 | APP_NAME = 'DNS-Manager' 16 | 17 | def configure_extensions(app): 18 | mail.init_app(app) 19 | db.init_app(app) 20 | migrate = Migrate(app, db) 21 | Swagger(app, template=app.config['SWAGGER_TEMPLATE']) 22 | 23 | def configure_blueprints(app, blueprints): 24 | for blueprint in blueprints: 25 | app.register_blueprint(blueprint) 26 | 27 | def configure_error_handlers(app): 28 | @app.errorhandler(400) 29 | def valid_request_args(error): 30 | return jsonify(message='服务器无法理解此请求!'), 400 31 | 32 | @app.errorhandler(401) 33 | def login_required(error): 34 | return jsonify(message="请先进行认证!"), 401 35 | 36 | @app.errorhandler(403) 37 | def forbidden(error): 38 | return jsonify(message="无权限!"), 403 39 | 40 | @app.errorhandler(404) 41 | def page_not_found(error): 42 | return jsonify(message="您访问的资源/页面不存在!"), 404 43 | 44 | @app.errorhandler(500) 45 | def server_error_page(error): 46 | return jsonify(message="非常抱歉,服务器内部出错!"), 500 47 | 48 | def configure_hooks(app): 49 | pass 50 | 51 | def configure_crossdomain(app): 52 | CORS(app, supports_credentials=True) 53 | 54 | def configure_data(app): 55 | env = os.environ 56 | app_config = app.config 57 | ZABBIX_POST_DATA = { 58 | "jsonrpc": "2.0", 59 | "method": "history.get", 60 | "params": { 61 | "output": "extend", 62 | "history": 3, 63 | "itemids": "", 64 | "sortfield": "clock", 65 | "sortorder": "DESC", 66 | "limit": 1 67 | }, 68 | "auth": "", 69 | "id": 1 70 | } 71 | SQLALCHEMY_DATABASE_URI = \ 72 | "mysql+mysqlconnector://{db_username}:{db_passwd}@{db_host}/{db_name}?charset=utf8".format( 73 | db_username = env.get('DB_USERNAME', app_config.get('DB_USERNAME')), 74 | db_passwd = env.get('DB_PASSWORD', app_config.get('DB_PASSWORD')), 75 | db_host = env.get('DB_HOST', app_config.get('DB_HOST')), 76 | db_name = env.get('DB_NAME', app_config.get('DB_NAME')), 77 | ) 78 | app.config['ZABBIX_POST_DATA'] = ZABBIX_POST_DATA 79 | app.config['SQLALCHEMY_DATABASE_URI'] = SQLALCHEMY_DATABASE_URI 80 | 81 | ETCD_SERVER_HOST = env.get('ETCD_SERVER_HOST') 82 | ETCD_SERVER_PORT = env.get('ETCD_SERVER_PORT') 83 | if ETCD_SERVER_HOST: 84 | app.config['ETCD_SERVER_HOST'] = ETCD_SERVER_HOST 85 | if ETCD_SERVER_PORT: 86 | app.config['ETCD_SERVER_PORT'] = ETCD_SERVER_PORT 87 | 88 | def create_app(config_name='default'): 89 | app = Flask(APP_NAME) 90 | app.config.from_object(config[config_name]) 91 | config[config_name].init_app(app) 92 | app.config.from_pyfile(config_pyfiles[config_name]) 93 | app.config.from_pyfile('config/dns_templates.tpl') 94 | configure_data(app) 95 | configure_extensions(app) 96 | configure_blueprints(app, [auth_bp, dns_bp, admin, page_bp]) 97 | configure_error_handlers(app) 98 | # configure_hooks(app) 99 | configure_crossdomain(app) 100 | return app 101 | 102 | -------------------------------------------------------------------------------- /peb_dns/common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackjiali/dns-manager/c7e983f17b79a1f0d8e71c789320d0b83dfcb6e9/peb_dns/common/__init__.py -------------------------------------------------------------------------------- /peb_dns/common/decorators.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | from flask_restful import Api, Resource, url_for, reqparse, abort 3 | from flask import current_app, g, request 4 | import jwt 5 | from peb_dns.models.dns import DBView, DBViewZone, DBZone, DBOperationLog, DBRecord, DBDNSServer, dns_models 6 | from peb_dns.models.account import DBUser, DBUserRole, DBRole, DBRolePrivilege, DBPrivilege, DBLocalAuth, account_models 7 | from peb_dns.models.mappings import Operation, ResourceType, OPERATION_STR_MAPPING, DefaultPrivilege 8 | from sqlalchemy import and_, or_ 9 | from peb_dns import db 10 | from .util import get_response 11 | from .request_code import RequestCode 12 | 13 | dns_models.update(account_models) 14 | all_resources_models = dns_models 15 | 16 | 17 | def permission_required(resource_type, operation_type): 18 | def decorator(f): 19 | @wraps(f) 20 | def wrapper(*args, **kwargs): 21 | resource_id = list(kwargs.values())[0] 22 | resource = all_resources_models[resource_type].query.get(resource_id) 23 | if all_resources_models[resource_type] == DBRecord: 24 | if not g.current_user.can_do(operation_type, ResourceType.ZONE, resource.zone.id): 25 | return get_response(RequestCode.OTHER_FAILED, '拒绝访问!您无权访问当前资源,如有问题请联系管理员。') 26 | return f(*args, **kwargs) 27 | if not g.current_user.can_do( 28 | operation_type, 29 | resource_type, 30 | resource_id): 31 | return get_response(RequestCode.OTHER_FAILED, '拒绝访问!您无权访问当前资源,如有问题请联系管理员。') 32 | return f(*args, **kwargs) 33 | return wrapper 34 | return decorator 35 | 36 | def resource_exists_required(resource_type): 37 | def decorator(f): 38 | @wraps(f) 39 | def wrapper(*args, **kwargs): 40 | resource_id = list(kwargs.values())[0] 41 | resource = all_resources_models[resource_type].query.get(resource_id) 42 | if not resource: 43 | return get_response(RequestCode.OTHER_FAILED, '你请求的资源不存在!') 44 | return f(*args, **kwargs) 45 | return wrapper 46 | return decorator 47 | 48 | def indicated_privilege_required(privilege_name): 49 | def decorator(f): 50 | @wraps(f) 51 | def wrapper(*args, **kwargs): 52 | if not g.current_user.can(privilege_name): 53 | return get_response(RequestCode.OTHER_FAILED, '拒绝访问!您无权访问当前资源,如有问题请联系管理员。') 54 | return f(*args, **kwargs) 55 | return wrapper 56 | return decorator 57 | 58 | def admin_required(f): 59 | @wraps(f) 60 | def decorated_function(*args, **kwargs): 61 | if not g.current_user.is_admin(): 62 | return get_response(RequestCode.OTHER_FAILED, '拒绝访问!您无权访问当前资源,如有问题请联系管理员。') 63 | return f(*args, **kwargs) 64 | return decorated_function 65 | 66 | 67 | def access_permission_required(f): 68 | @wraps(f) 69 | def decorated_function(*args, **kwargs): 70 | if not g.current_user.can_access_zone(*args, **kwargs): 71 | return get_response(RequestCode.OTHER_FAILED, '拒绝访问!您无权访问当前资源,如有问题请联系管理员。') 72 | return f(*args, **kwargs) 73 | return decorated_function 74 | 75 | 76 | def token_required(f): 77 | @wraps(f) 78 | def decorated(*args, **kwargs): 79 | token = request.headers.get('Authorization') 80 | if not token: 81 | return get_response(RequestCode.AUTH_FAILED, '认证失败!') 82 | try: 83 | data = jwt.decode(token, current_app.config['SECRET_KEY']) 84 | except: 85 | return get_response(RequestCode.AUTH_FAILED, '认证失败!') 86 | g.current_user = DBUser.query.filter_by(username=data.get('user')).first() 87 | if g.current_user is None: 88 | return get_response(RequestCode.AUTH_FAILED, '认证失败!') 89 | if g.current_user.actived == 0: 90 | return get_response(RequestCode.AUTH_FAILED, '对不起,您已经被管理员禁止登陆!') 91 | return f(*args, **kwargs) 92 | return decorated 93 | 94 | 95 | def owner_or_admin_required(f): 96 | @wraps(f) 97 | def decorated(*args, **kwargs): 98 | print(kwargs) 99 | if not (g.current_user.is_admin() or g.current_user.id == kwargs.get('user_id')): 100 | return get_response(RequestCode.OTHER_FAILED, '拒绝访问!您无权访问当前资源,如有问题请联系管理员。') 101 | return f(*args, **kwargs) 102 | return decorated 103 | -------------------------------------------------------------------------------- /peb_dns/common/request_code.py: -------------------------------------------------------------------------------- 1 | 2 | # 标准的RESTFul风格,服务器要返回原生的HTTP状态码,客户端根据HTTP状态码来判断业务状态。 3 | # 由于 URL 会直接定位到资源,访问 URL 如果资源不存在会大量抛 4xx,会引发监控报警 4 | # 为了跟其他项目监控保持一致,所以放弃使用HTTP状态码来判断业务正确和失败。 5 | # 因此本项目严格意义上不属于 RESTFul 风格,在此说明。 6 | 7 | class RequestCode(object): 8 | SUCCESS = 100000 #请求成功 9 | AUTH_FAILED = 100001 #token认证失败 10 | LOGIN_FAILED = 100002 #登录失败 11 | OTHER_FAILED = 105000 #其他原因请求失败 12 | 13 | -------------------------------------------------------------------------------- /peb_dns/common/util.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | import subprocess 4 | import os 5 | import signal 6 | import sys 7 | import etcd 8 | from flask import current_app 9 | import requests 10 | import json 11 | import copy 12 | from datetime import datetime 13 | from collections import OrderedDict 14 | from flask_restful import Api, Resource, url_for, reqparse, abort, marshal_with, fields, marshal 15 | from .request_code import RequestCode 16 | 17 | 18 | ZONE_GROUP_MAPPING = { 19 | 0:"外部域名", 20 | 1:"内部域名", 21 | 2:"劫持域名" 22 | } 23 | 24 | 25 | #获取ETCD客户端 26 | def getETCDclient(): 27 | client = etcd.Client( 28 | host=current_app.config.get('ETCD_SERVER_HOST'), 29 | port=int(current_app.config.get('ETCD_SERVER_PORT')) 30 | ) 31 | return client 32 | 33 | 34 | def getLogger(log_path): 35 | # logger初始化 36 | logger = logging.getLogger('DNS') 37 | logger.setLevel(logging.DEBUG) 38 | fh = logging.FileHandler(log_path) 39 | formatter = logging.Formatter( 40 | '[%(asctime)s] [%(name)s] [%(levelname)s] %(message)s') 41 | fh.setFormatter(formatter) 42 | logger.addHandler(fh) 43 | return logger 44 | 45 | 46 | def killProcesses(ppid=None): 47 | ppid = str(ppid) 48 | pidgrp = [] 49 | def GetChildPids(ppid): 50 | command = "ps -ef | awk '{if ($3 ==%s) print $2;}'" % str(ppid) 51 | pids = os.popen(command).read() 52 | pids = pids.split() 53 | return pids 54 | pidgrp.extend(GetChildPids(ppid)) 55 | for pid in pidgrp: 56 | pidgrp.extend(GetChildPids(pid)) 57 | 58 | pidgrp.insert(0, ppid) 59 | while len(pidgrp) > 0: 60 | pid = pidgrp.pop() 61 | try: 62 | os.kill(int(pid), signal.SIGKILL) 63 | return True 64 | except OSError: 65 | try: 66 | os.popen("kill -9 %d" % int(pid)) 67 | return True 68 | except Exception: 69 | return False 70 | 71 | 72 | DEFAULT_CMD_TIMEOUT = 1200 73 | def doCMDWithOutput(cmd, time_out = None): 74 | if time_out is None: 75 | time_out = DEFAULT_CMD_TIMEOUT 76 | # LOG.info("Doing CMD: [ %s ]" % cmd) 77 | pre_time = time.time() 78 | output = [] 79 | cmd_return_code = 1 80 | cmd_proc = subprocess.Popen( 81 | cmd, stdout=subprocess.PIPE, 82 | stderr=subprocess.STDOUT, shell=True) 83 | while True: 84 | output_line = cmd_proc.stdout.readline().decode().strip("\r\n") 85 | cmd_return_code = cmd_proc.poll() 86 | elapsed_time = time.time() - pre_time 87 | if cmd_return_code is None: 88 | if elapsed_time >= time_out: 89 | killProcesses(ppid=cmd_proc.pid) 90 | return False 91 | elif output_line == '' and cmd_return_code is not None: 92 | break 93 | sys.stdout.flush() 94 | if output_line.strip() != '': 95 | output.append(output_line) 96 | return (cmd_return_code, output) 97 | 98 | 99 | def get_response(code, msg, data=None): 100 | return { 101 | 'code': code, 102 | 'msg': msg, 103 | 'data': data 104 | } 105 | 106 | 107 | def get_response_wrapper_fields(f): 108 | return { 109 | 'code': fields.Integer, 110 | 'data': f, 111 | 'msg': fields.String 112 | } 113 | 114 | 115 | # def initServer(cmd, app_object, server_id): 116 | # with app_object.app_context(): 117 | # # print(server_id) 118 | # current_server = DBDNSServer.query.get(int(server_id)) 119 | # res = doCMDWithOutput(cmd) 120 | # if not res: 121 | # current_server.status = '初始化失败' 122 | # current_server.logs = '超时!!初始化时间已超过20分钟' 123 | # db.session.add(current_server) 124 | # db.session.commit() 125 | # return False, ['超时!!初始化时间已超过20分钟!'] 126 | # cmd_return_code, output = res 127 | # if cmd_return_code != 0: 128 | # print('\n'.join(output)) 129 | # current_server.status = '初始化失败' 130 | # current_server.logs = '\n'.join(output) 131 | # db.session.add(current_server) 132 | # db.session.commit() 133 | # return False, output 134 | # else: 135 | # print('\n'.join(output)) 136 | # current_server.status = 'ONLINE' 137 | # current_server.logs = '\n'.join(output) 138 | # db.session.add(current_server) 139 | # db.session.commit() 140 | # return True, output 141 | 142 | 143 | class DNSPod(object): 144 | @staticmethod 145 | def getDNSPodLines(domain): 146 | body_info = { 147 | "login_token": current_app.config.get('DNSPOD_TOKEN'), 148 | "format": current_app.config.get('DNSPOD_DATA_FORMAT'), 149 | "domain": domain 150 | } 151 | try: 152 | res = requests.post( 153 | current_app.config.get('DNSPOD_LINE_URL'), 154 | data=body_info 155 | ) 156 | except Exception as e: 157 | return [] 158 | if res.status_code >= 200 and res.status_code <= 220: 159 | return res.json()['lines'] 160 | return [] 161 | 162 | @staticmethod 163 | def getDNSPodTypes(domain): 164 | body_info = { 165 | "login_token": current_app.config.get('DNSPOD_TOKEN'), 166 | "format": current_app.config.get('DNSPOD_DATA_FORMAT'), 167 | "domain": domain 168 | } 169 | try: 170 | res = requests.post( 171 | current_app.config.get('DNSPOD_TYPE_URL'), 172 | data=body_info 173 | ) 174 | except Exception as e: 175 | return [] 176 | if res.status_code >= 200 and res.status_code <= 220: 177 | return res.json()['lines'] 178 | return [] 179 | 180 | 181 | class ZBapi(object): 182 | def __init__(self, server): 183 | self._url = current_app.config.get('ZABBIX_URL') 184 | self._header = {"Content-Type":"application/json"} 185 | self._server = server 186 | self._num = 30 187 | 188 | def _get_authid(self): 189 | data = { 190 | "jsonrpc": "2.0", 191 | "method": "user.login", 192 | "params": { 193 | "user": current_app.config.get('ZABBIX_USERNAME'), 194 | "password": current_app.config.get('ZABBIX_PASSWORD') 195 | }, 196 | "id": 1, 197 | "auth":None 198 | } 199 | try: 200 | r = requests.post(self._url, 201 | data=json.dumps(data), headers=self._header, timeout=10) 202 | except Exception as e: 203 | raise e 204 | authid = json.loads(r.text).get("result") 205 | return authid 206 | 207 | def _configure_post_data(self, zb_post_data, itemid, history): 208 | zb_post_data['auth'] = self._get_authid() 209 | zb_post_data['params']['itemids'] = itemid 210 | zb_post_data['params']['history'] = history 211 | return zb_post_data 212 | 213 | def _get_server_status_by_itemid(self, itemid): 214 | zb_data_default = copy.deepcopy( 215 | current_app.config.get('ZABBIX_POST_DATA')) 216 | zb_post_data = self._configure_post_data(zb_data_default, itemid, 3) 217 | try: 218 | r = requests.post(self._url, 219 | data=json.dumps(zb_post_data), 220 | headers=self._header, timeout=10) 221 | except Exception as e: 222 | raise e 223 | result = json.loads(r.text).get("result") 224 | if result: 225 | return result[0].get('value') 226 | return '0' 227 | 228 | def _get_resolve_rate_by_itemid(self, itemid, limit_num): 229 | time_slot_minutes = int(limit_num/self._num) 230 | # all_num = time_slot_minutes * (self._num + 1) 231 | zb_data_default = copy.deepcopy(current_app.config.get('ZABBIX_POST_DATA')) 232 | zb_post_data = self._configure_post_data(zb_data_default, itemid, 3) 233 | zb_post_data['params']['limit'] = limit_num 234 | try: 235 | r = requests.post(self._url, 236 | data=json.dumps(zb_post_data), 237 | headers=self._header, timeout=10) 238 | except Exception as e: 239 | raise e 240 | results = json.loads(r.text).get("result") 241 | results_dct = OrderedDict() 242 | for i in range(self._num): 243 | end = time_slot_minutes*(i+1) 244 | resolving_slot = results[time_slot_minutes*i : end] 245 | time_flag = results[time_slot_minutes*i]['clock'] 246 | time_flag_str = datetime.fromtimestamp(int(time_flag)).strftime("%m-%d %H:%M") 247 | resolving_slot_amount = 0 248 | for ss in resolving_slot: 249 | resolving_slot_amount += int(ss['value']) 250 | results_dct[time_flag_str] = resolving_slot_amount 251 | return {'name':self._server.host, 'data':results_dct} 252 | 253 | def get_server_status(self): 254 | return {'process':self._get_server_status_by_itemid(self._server.zb_process_itemid), 255 | 'port':self._get_server_status_by_itemid(self._server.zb_port_itemid), 256 | 'resolve':self._get_server_status_by_itemid(self._server.zb_resolve_itemid)} 257 | 258 | def get_resolve_rate(self, start_time, end_time): 259 | # time_slot = (end_time - start_time)/11 260 | time_slot = end_time - start_time 261 | total_minutes = time_slot.total_seconds()/60 262 | time_slot_minutes = int(total_minutes/self._num) 263 | # dns_servers = Server.query.all() 264 | return self._get_resolve_rate_by_itemid( 265 | self._server.zb_resolve_rate_itemid, total_minutes) 266 | 267 | 268 | -------------------------------------------------------------------------------- /peb_dns/extensions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from flask_sqlalchemy import SQLAlchemy 4 | db = SQLAlchemy() 5 | 6 | from flask_migrate import Migrate 7 | 8 | from flask_mail import Mail 9 | mail = Mail() 10 | -------------------------------------------------------------------------------- /peb_dns/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackjiali/dns-manager/c7e983f17b79a1f0d8e71c789320d0b83dfcb6e9/peb_dns/models/__init__.py -------------------------------------------------------------------------------- /peb_dns/models/account.py: -------------------------------------------------------------------------------- 1 | from flask import current_app, request 2 | from peb_dns.extensions import db 3 | from sqlalchemy import and_, or_ 4 | from werkzeug.security import generate_password_hash, check_password_hash 5 | from datetime import datetime 6 | from peb_dns.models.mappings import Operation, ResourceType, OPERATION_STR_MAPPING, DefaultPrivilege 7 | from .dns import DBZone, DBView, DBRecord, DBDNSServer 8 | 9 | RESOURCE_TYPE_MAPPING = { 10 | ResourceType.ZONE: DBZone, 11 | ResourceType.VIEW: DBView, 12 | ResourceType.RECORD: DBRecord, 13 | ResourceType.SERVER: DBDNSServer 14 | } 15 | 16 | class DBUser(db.Model): 17 | __tablename__ = 'account_user' 18 | id = db.Column(db.Integer, primary_key=True) 19 | email = db.Column(db.String(64), default='') 20 | username = db.Column(db.String(64), unique=True, index=True) 21 | chinese_name = db.Column(db.String(64), default='') 22 | cellphone = db.Column(db.String(64), default='') 23 | actived = db.Column(db.Integer, default=1) 24 | position = db.Column(db.String(64), default='') 25 | location = db.Column(db.String(64), default='') 26 | member_since = db.Column(db.DateTime(), default=datetime.now) 27 | last_seen = db.Column(db.DateTime(), default=datetime.now) 28 | 29 | def to_json(self): 30 | json_user = { 31 | 'id': self.id, 32 | 'username': self.username, 33 | 'email': self.email, 34 | 'chinese_name': self.chinese_name, 35 | 'cellphone': self.cellphone, 36 | 'position': self.position, 37 | 'location': self.location, 38 | 'can_add_server': self.can_add_server, 39 | 'can_add_view': self.can_add_view, 40 | 'can_add_zone': self.can_add_zone, 41 | 'member_since': str(self.member_since) 42 | } 43 | return json_user 44 | 45 | def can(self, privilege): 46 | current_user_privileges = db.session.query(DBPrivilege) \ 47 | .join(DBRolePrivilege, and_( 48 | DBPrivilege.id == DBRolePrivilege.privilege_id, 49 | DBPrivilege.name == privilege)) \ 50 | .join(DBRole, and_(DBRole.id == DBRolePrivilege.role_id)) \ 51 | .join(DBUserRole, and_(DBUserRole.role_id == DBRole.id)) \ 52 | .join(DBUser, and_(DBUser.id == DBUserRole.user_id)) \ 53 | .filter(DBUser.id == self.id).all() 54 | for current_user_privilege in current_user_privileges: 55 | if privilege == current_user_privilege.name: 56 | return True 57 | return False 58 | 59 | def is_admin(self): 60 | admins = db.session.query(DBRole).join( 61 | DBUserRole, and_(DBUserRole.role_id == DBRole.id, DBRole.name == "admin")) \ 62 | .join(DBUser, and_(DBUser.id == DBUserRole.user_id)) \ 63 | .filter(DBUser.id == self.id).all() 64 | if admins: 65 | return True 66 | return False 67 | 68 | @property 69 | def can_add_server(self): 70 | return self.can(DefaultPrivilege.SERVER_ADD) 71 | 72 | @property 73 | def can_add_view(self): 74 | return self.can(DefaultPrivilege.VIEW_ADD) 75 | 76 | @property 77 | def can_add_zone(self): 78 | return self.can(DefaultPrivilege.ZONE_ADD) 79 | 80 | @property 81 | def can_edit_bind_conf(self): 82 | return self.can(DefaultPrivilege.BIND_CONF_EDIT) 83 | 84 | def can_access_log(self): 85 | return self.can(DefaultPrivilege.LOG_PAGE_ACCESS) 86 | 87 | @property 88 | def roles(self): 89 | return db.session.query(DBRole).join( 90 | DBUserRole, and_(DBUserRole.role_id == DBRole.id)) \ 91 | .join(DBUser, and_(DBUser.id == DBUserRole.user_id)) \ 92 | .filter(DBUser.id == self.id).all() 93 | 94 | @property 95 | def role_ids(self): 96 | return [r.id for r in db.session.query(DBRole).join( 97 | DBUserRole, and_(DBUserRole.role_id == DBRole.id)) \ 98 | .join(DBUser, and_(DBUser.id == DBUserRole.user_id)) \ 99 | .filter(DBUser.id == self.id).all()] 100 | 101 | @roles.setter 102 | def roles(self, role_ids): 103 | for role_id in role_ids: 104 | current_user_new_role = DBUserRole(user_id=self.id, role_id=role_id) 105 | db.session.add(current_user_new_role) 106 | 107 | def can_do(self, operation, resource_type, resource_id): 108 | r = RESOURCE_TYPE_MAPPING.get(resource_type) 109 | current_user_resources = db.session.query(r) \ 110 | .join(DBPrivilege, and_( 111 | r.id == resource_id, 112 | r.id == DBPrivilege.resource_id, 113 | DBPrivilege.resource_type == resource_type, 114 | DBPrivilege.operation == operation)) \ 115 | .join(DBRolePrivilege, and_(DBPrivilege.id == DBRolePrivilege.privilege_id)) \ 116 | .join(DBRole, and_(DBRole.id == DBRolePrivilege.role_id)) \ 117 | .join(DBUserRole, and_(DBUserRole.role_id == DBRole.id)) \ 118 | .join(DBUser, and_(DBUser.id == DBUserRole.user_id)) \ 119 | .filter(DBUser.id == self.id).all() 120 | if current_user_resources: 121 | return True 122 | return False 123 | 124 | 125 | class DBLocalAuth(db.Model): 126 | __tablename__ = 'account_local_auth' 127 | id = db.Column(db.Integer, primary_key=True) 128 | username = db.Column(db.String(64), unique=True, index=True) 129 | password_hash = db.Column(db.String(128)) 130 | email = db.Column(db.String(128)) 131 | 132 | @property 133 | def password(self): 134 | raise AttributeError('password is not a readable attribute') 135 | 136 | @password.setter 137 | def password(self, password): 138 | self.password_hash = generate_password_hash(password) 139 | 140 | def verify_password(self, password): 141 | return check_password_hash(self.password_hash, password) 142 | 143 | 144 | class DBUserRole(db.Model): 145 | __tablename__ = 'account_user_role' 146 | id = db.Column(db.Integer, primary_key=True) 147 | user_id = db.Column(db.Integer, index=True) 148 | role_id = db.Column(db.Integer, index=True) 149 | 150 | 151 | class DBRole(db.Model): 152 | __tablename__ = 'account_role' 153 | id = db.Column(db.Integer, primary_key=True) 154 | name = db.Column(db.String(64), unique=True) 155 | 156 | def __repr__(self): 157 | return '' % self.name 158 | 159 | @property 160 | def users(self): 161 | return db.session.query(DBUser).join( 162 | DBUserRole, and_(DBUserRole.user_id == DBUser.id)) \ 163 | .join(DBRole, and_(DBRole.id == DBUserRole.role_id)) \ 164 | .filter(DBRole.id == self.id).all() 165 | 166 | @property 167 | def privileges(self): 168 | return db.session.query(DBPrivilege).join( 169 | DBRolePrivilege, and_(DBRolePrivilege.privilege_id == DBPrivilege.id)) \ 170 | .join(DBRole, and_(DBRole.id == DBRolePrivilege.role_id)) \ 171 | .filter(DBRole.id == self.id).all() 172 | 173 | @property 174 | def privilege_ids(self): 175 | return [p.id for p in db.session.query(DBPrivilege).join( 176 | DBRolePrivilege, and_(DBRolePrivilege.privilege_id == DBPrivilege.id)) \ 177 | .join(DBRole, and_(DBRole.id == DBRolePrivilege.role_id)) \ 178 | .filter(DBRole.id == self.id).all()] 179 | 180 | @privileges.setter 181 | def privileges(self, privilege_ids): 182 | for privilege_id in privilege_ids: 183 | current_role_new_privilege = DBRolePrivilege( 184 | role_id=self.id, privilege_id=privilege_id) 185 | db.session.add(current_role_new_privilege) 186 | 187 | 188 | class DBRolePrivilege(db.Model): 189 | __tablename__ = 'account_role_privilege' 190 | id = db.Column(db.Integer, primary_key=True) 191 | role_id = db.Column(db.Integer, index=True) 192 | privilege_id = db.Column(db.Integer, index=True) 193 | 194 | 195 | #权限表 196 | class DBPrivilege(db.Model): 197 | __tablename__ = 'account_privilege' 198 | id = db.Column(db.Integer, primary_key=True) 199 | name = db.Column(db.String(128)) 200 | operation = db.Column(db.Integer, default=100) 201 | resource_type = db.Column(db.Integer, default=100) 202 | resource_id = db.Column(db.Integer, default=0, index=True) 203 | comment = db.Column(db.String(128)) 204 | 205 | 206 | account_models = { 207 | ResourceType.USER: DBUser, 208 | ResourceType.ROLE: DBRole, 209 | ResourceType.PRIVILEGE: DBPrivilege, 210 | } -------------------------------------------------------------------------------- /peb_dns/models/mappings.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | 3 | class Operation(object): 4 | ACCESS = 0 5 | UPDATE = 1 6 | DELETE = 2 7 | 8 | 9 | class ResourceType(object): 10 | SERVER = 0 11 | VIEW = 1 12 | ZONE = 2 13 | RECORD = 3 14 | USER = 4 15 | ROLE = 5 16 | PRIVILEGE = 6 17 | PAGE = 7 18 | 19 | 20 | class DefaultPrivilege(object): 21 | SERVER_ADD = 'SERVER_ADD' 22 | ZONE_ADD = 'ZONE_ADD' 23 | VIEW_ADD = 'VIEW_ADD' 24 | BIND_CONF_EDIT = 'BIND_CONF_EDIT' 25 | LOG_PAGE_ACCESS = 'LOG_PAGE_ACCESS' 26 | 27 | 28 | OPERATION_STR_MAPPING = { 29 | Operation.ACCESS:'ACCESS', 30 | Operation.UPDATE:'UPDATE', 31 | Operation.DELETE:'DELETE' 32 | } 33 | 34 | 35 | 36 | ROLE_MAPPINGS = OrderedDict({ 37 | 'admin': 1, 38 | 'server_admin': 2, 39 | 'server_guest': 3, 40 | 'view_admin': 4, 41 | 'view_guest': 5, 42 | 'zone_admin': 6, 43 | 'zone_guest': 7 44 | }) 45 | 46 | 47 | -------------------------------------------------------------------------------- /peb_dns/resourses/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackjiali/dns-manager/c7e983f17b79a1f0d8e71c789320d0b83dfcb6e9/peb_dns/resourses/__init__.py -------------------------------------------------------------------------------- /peb_dns/resourses/account/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, Blueprint 2 | from flask_restful import Api 3 | 4 | auth_bp = Blueprint('auth', __name__, url_prefix='/api/auth') 5 | 6 | auth_api = Api(auth_bp) 7 | 8 | 9 | from .auth import AuthLDAP, AuthLocal, RegisterLocal 10 | auth_api.add_resource(AuthLDAP, '/login_ldap') 11 | auth_api.add_resource(AuthLocal, '/login_local') 12 | auth_api.add_resource(RegisterLocal, '/register_local') 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /peb_dns/resourses/account/auth.py: -------------------------------------------------------------------------------- 1 | from flask_restful import Api, Resource, url_for, reqparse, abort 2 | from flask import Blueprint, request, jsonify, current_app 3 | from ldap3 import Connection, ALL 4 | from ldap3 import Server as LDAPServer 5 | import datetime 6 | import jwt 7 | from peb_dns.models.account import DBUser, DBLocalAuth 8 | from peb_dns.models.mappings import Operation, ResourceType, OPERATION_STR_MAPPING, ROLE_MAPPINGS, DefaultPrivilege 9 | from peb_dns.common.util import getETCDclient, get_response, get_response_wrapper_fields 10 | from peb_dns import db 11 | from peb_dns.common.request_code import RequestCode 12 | 13 | 14 | class AuthLDAP(Resource): 15 | def __init__(self): 16 | self.reqparse = reqparse.RequestParser() 17 | self.reqparse.add_argument('username', type = str, location = 'json') 18 | self.reqparse.add_argument('password', type = str, location = 'json') 19 | super(AuthLDAP, self).__init__() 20 | 21 | def post(self): 22 | """ 23 | 功能: LDAP认证接口 24 | --- 25 | security: 26 | - UserSecurity: [] 27 | tags: 28 | - Auth 29 | parameters: 30 | - in: body 31 | name: body 32 | schema: 33 | id: AuthLDAP_post 34 | properties: 35 | username: 36 | type: string 37 | default: user123 38 | description: 用户名 39 | password: 40 | type: string 41 | default: passwd123 42 | description: 密码 43 | responses: 44 | 200: 45 | description: 请求结果 46 | schema: 47 | properties: 48 | code: 49 | type: integer 50 | description: response code 51 | msg: 52 | type: string 53 | description: response message 54 | data: 55 | type: string 56 | examples: 57 | { 58 | "code": 100000, 59 | "msg": "认证失败!", 60 | "data": null 61 | } 62 | 63 | """ 64 | args = self.reqparse.parse_args() 65 | username, password = args['username'], args['password'] 66 | if self._auth_via_ldap(username, password): 67 | user = DBUser.query.filter_by(username=username).first() 68 | if user is not None : 69 | if user.actived == 0: 70 | return get_response(RequestCode.LOGIN_FAILED, '对不起,您已经被管理员禁止登陆!') 71 | token = jwt.encode({ 72 | 'user' : user.username, 73 | 'exp' : datetime.datetime.now() + datetime.timedelta(hours=24) 74 | }, current_app.config['SECRET_KEY']) 75 | response_data = { 76 | 'token' : token.decode('UTF-8'), 77 | 'user_info': user.to_json() 78 | } 79 | return get_response(RequestCode.SUCCESS, '认证成功!', response_data) 80 | new_user = DBUser(username=username) 81 | db.session.add(new_user) 82 | db.session.commit() 83 | token = jwt.encode( 84 | { 85 | 'user' : new_user.username, 86 | 'exp' : datetime.datetime.now() + datetime.timedelta(hours=24) 87 | }, current_app.config['SECRET_KEY']) 88 | response_data = { 89 | 'token' : token.decode('UTF-8'), 90 | 'user_info': new_user.to_json() 91 | } 92 | return get_response(RequestCode.SUCCESS, '认证成功!', response_data) 93 | return get_response(RequestCode.LOGIN_FAILED, '登录失败!账号密码错误!') 94 | 95 | def _auth_via_ldap(self, username, passwd): 96 | try: 97 | server = LDAPServer( 98 | current_app.config.get('LDAP_SERVER'), 99 | port=int(current_app.config.get('LDAP_SERVER_PORT') 100 | ), use_ssl=True, get_info=ALL) 101 | _connection = Connection(server, 102 | 'cn=' + username + current_app.config.get('LDAP_CONFIG'), 103 | passwd, 104 | auto_bind=True 105 | ) 106 | except Exception as e: 107 | return False 108 | return True 109 | 110 | 111 | class AuthLocal(Resource): 112 | def __init__(self): 113 | self.reqparse = reqparse.RequestParser() 114 | self.reqparse.add_argument('username', type = str, location = 'json') 115 | self.reqparse.add_argument('password', type = str, location = 'json') 116 | super(AuthLocal, self).__init__() 117 | 118 | def post(self): 119 | """ 120 | 功能: 本地注册用户认证接口 121 | --- 122 | security: 123 | - UserSecurity: [] 124 | tags: 125 | - Auth 126 | parameters: 127 | - in: body 128 | name: body 129 | schema: 130 | id: AuthLocal_post 131 | properties: 132 | username: 133 | type: string 134 | default: user123 135 | description: 用户名 136 | password: 137 | type: string 138 | default: passwd123 139 | description: 密码 140 | responses: 141 | 200: 142 | description: 请求结果 143 | schema: 144 | properties: 145 | code: 146 | type: integer 147 | description: response code 148 | msg: 149 | type: string 150 | description: response message 151 | data: 152 | type: string 153 | examples: 154 | { 155 | "code": 105000, 156 | "msg": "认证失败!", 157 | "data": null 158 | } 159 | """ 160 | args = self.reqparse.parse_args() 161 | username, password = args['username'], args['password'] 162 | auth_user = DBLocalAuth.query.filter_by( 163 | username = args['username']).first() 164 | if auth_user is None: 165 | return get_response(RequestCode.LOGIN_FAILED, '认证失败!用户不存在!') 166 | if not auth_user.verify_password(args['password']) : 167 | return get_response(RequestCode.LOGIN_FAILED, '认证失败!账号或密码错误!') 168 | local_user = DBUser.query.filter_by(username=args['username']).first() 169 | if local_user.actived == 0: 170 | return get_response(RequestCode.LOGIN_FAILED, '对不起,您已经被管理员禁止登陆!') 171 | token = jwt.encode( 172 | { 173 | 'user' : local_user.username, 174 | 'exp' : datetime.datetime.now() + datetime.timedelta(hours=24) 175 | }, current_app.config['SECRET_KEY']) 176 | response_data = { 177 | 'token' : token.decode('UTF-8'), 178 | 'user_info': local_user.to_json() 179 | } 180 | return get_response(RequestCode.SUCCESS, '认证成功!', response_data) 181 | 182 | 183 | class RegisterLocal(Resource): 184 | def __init__(self): 185 | super(RegisterLocal, self).__init__() 186 | self.reqparse = reqparse.RequestParser() 187 | self.reqparse.add_argument('username', type = str, 188 | location = 'json', required=True) 189 | self.reqparse.add_argument('password', type = str, 190 | location = 'json', required=True) 191 | self.reqparse.add_argument('password2', type = str, 192 | location = 'json', required=True) 193 | self.reqparse.add_argument('email', type = str, 194 | location = 'json', required=True) 195 | 196 | def post(self): 197 | """ 198 | 功能:本地用户注册接口 199 | --- 200 | security: 201 | - UserSecurity: [] 202 | tags: 203 | - Auth 204 | parameters: 205 | - in: body 206 | name: body 207 | schema: 208 | id: RegisterLocal_post 209 | properties: 210 | username: 211 | type: string 212 | default: user123 213 | description: 用户名 214 | password: 215 | type: string 216 | default: passwd123 217 | description: 密码 218 | password2: 219 | type: string 220 | default: passwd123 221 | description: 两次密码输入要一致 222 | responses: 223 | 200: 224 | description: 请求结果 225 | schema: 226 | properties: 227 | code: 228 | type: integer 229 | description: response code 230 | msg: 231 | type: string 232 | description: response message 233 | data: 234 | type: string 235 | examples: 236 | { 237 | "code": 100000, 238 | "msg": "注册成功!", 239 | "data": null 240 | } 241 | """ 242 | args = self.reqparse.parse_args() 243 | auth_user = DBLocalAuth.query.filter_by( 244 | username = args['username']).first() 245 | local_user = DBUser.query.filter_by( 246 | username = args['username']).first() 247 | if auth_user or local_user: 248 | return get_response(RequestCode.OTHER_FAILED, '用户已存在!') 249 | new_auth_user = DBLocalAuth( 250 | username=args['username'], email=args['email']) 251 | new_auth_user.password = args['password'] 252 | new_local_user = DBUser(username=args['username'], email=args['email']) 253 | db.session.add(new_local_user) 254 | db.session.add(new_auth_user) 255 | db.session.commit() 256 | return get_response(RequestCode.SUCCESS, '注册成功!') 257 | 258 | 259 | -------------------------------------------------------------------------------- /peb_dns/resourses/admin/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, request, jsonify, current_app 2 | from flask_restful import Api, Resource, url_for 3 | 4 | admin = Blueprint('admin', __name__, url_prefix='/api/admin') 5 | 6 | admin_api = Api(admin) 7 | 8 | from .user import UserList, User 9 | admin_api.add_resource(UserList, '/users') 10 | admin_api.add_resource(User, '/users/') 11 | 12 | from .role import RoleList, Role 13 | admin_api.add_resource(RoleList, '/roles') 14 | admin_api.add_resource(Role, '/roles/') 15 | 16 | from .privilege import PrivilegeList, Privilege 17 | admin_api.add_resource(PrivilegeList, '/privileges') 18 | admin_api.add_resource(Privilege, '/privileges/') -------------------------------------------------------------------------------- /peb_dns/resourses/admin/privilege.py: -------------------------------------------------------------------------------- 1 | from flask_restful import Resource, marshal_with, fields, marshal, reqparse, abort 2 | from flask import Blueprint, request, jsonify, current_app, g 3 | 4 | from peb_dns.models.dns import DBView, DBViewZone, DBZone, DBOperationLog, DBRecord 5 | from peb_dns.models.account import DBUser, DBUserRole, DBRole, DBRolePrivilege, DBPrivilege 6 | from peb_dns.common.util import getETCDclient, get_response, get_response_wrapper_fields 7 | from peb_dns.models.mappings import Operation, ResourceType, OPERATION_STR_MAPPING, ROLE_MAPPINGS, DefaultPrivilege 8 | from peb_dns.common.decorators import token_required, admin_required, resource_exists_required 9 | from peb_dns import db 10 | from sqlalchemy import and_, or_ 11 | from datetime import datetime 12 | from peb_dns.common.request_code import RequestCode 13 | 14 | privilege_fields = { 15 | 'id': fields.Integer, 16 | 'name': fields.String, 17 | 'operation': fields.Integer, 18 | 'resource_type': fields.Integer, 19 | 'resource_id': fields.Integer, 20 | 'comment': fields.String, 21 | } 22 | 23 | 24 | paginated_privilege_fields = { 25 | 'total': fields.Integer, 26 | 'privileges': fields.List(fields.Nested(privilege_fields)), 27 | 'current_page': fields.Integer 28 | } 29 | 30 | 31 | class PrivilegeList(Resource): 32 | method_decorators = [admin_required, token_required] 33 | 34 | def get(self): 35 | """ 36 | 功能: 获取权限列表资源 37 | --- 38 | security: 39 | - UserSecurity: [] 40 | tags: 41 | - Privilege 42 | parameters: 43 | - name: currentPage 44 | in: query 45 | description: the page of Privilege 46 | type: integer 47 | default: 1 48 | - name: pageSize 49 | in: query 50 | description: the max records of page 51 | type: integer 52 | default: 10 53 | - name: id 54 | in: query 55 | description: Privilege id 56 | type: integer 57 | default: 1 58 | - name: name 59 | type: string 60 | in: query 61 | description: the name of Privilege 62 | default: PRIVILEGE_MODIFY 63 | - name: operation 64 | in: query 65 | type: integer 66 | description: the value of Privilege 67 | default: 1 68 | enum: [0, 1, 2] 69 | - name: role_id 70 | in: query 71 | type: integer 72 | description: the id of role 73 | default: 1 74 | - name: resource_type 75 | in: query 76 | type: integer 77 | description: the id of resource_type 78 | default: 1 79 | enum: [0, 1, 2, 3] 80 | - name: resource_id 81 | in: query 82 | type: integer 83 | description: the id of resource 84 | default: 1 85 | definitions: 86 | Privileges: 87 | properties: 88 | total: 89 | type: integer 90 | description: the count of records 91 | current_page: 92 | type: integer 93 | description: the current page 94 | privileges: 95 | type: array 96 | items: 97 | $ref: "#/definitions/Privilege" 98 | responses: 99 | 200: 100 | description: 请求结果 101 | schema: 102 | properties: 103 | code: 104 | type: integer 105 | description: response code 106 | msg: 107 | type: string 108 | description: response message 109 | data: 110 | $ref: "#/definitions/Privileges" 111 | examples: 112 | { 113 | "code": 100000, 114 | "data": { 115 | "total": 37, 116 | "privileges": [ 117 | { 118 | "id": 58, 119 | "name": "VIEW#v555#DELETE", 120 | "operation": 2, 121 | "resource_type": 1, 122 | "resource_id": 6, 123 | "comment": null 124 | }, 125 | { 126 | "id": 57, 127 | "name": "VIEW#v555#UPDATE", 128 | "operation": 1, 129 | "resource_type": 1, 130 | "resource_id": 6, 131 | "comment": null 132 | } 133 | ], 134 | "current_page": 1 135 | }, 136 | "msg": "获取成功!" 137 | } 138 | """ 139 | args = request.args 140 | current_page = args.get('currentPage', 1, type=int) 141 | page_size = args.get('pageSize', 10, type=int) 142 | role_id = args.get('role_id', type=int) 143 | 144 | id = args.get('id', type=int) 145 | name = args.get('name', type=str) 146 | operation = args.get('operation', type=int) 147 | resource_type = args.get('resource_type', type=int) 148 | resource_id = args.get('resource_id', type=int) 149 | privilege_query = DBPrivilege.query 150 | if id is not None: 151 | privilege_query = privilege_query.filter_by(id=id) 152 | if name is not None: 153 | privilege_query = privilege_query.filter(DBPrivilege.name.like('%'+name+'%')) 154 | if operation is not None: 155 | privilege_query = privilege_query.filter_by(operation=operation) 156 | if resource_type is not None: 157 | privilege_query = privilege_query.filter_by(resource_type=resource_type) 158 | if resource_id is not None: 159 | privilege_query = privilege_query.filter_by(resource_id=resource_id) 160 | if role_id is not None: 161 | privilege_query = privilege_query.join( 162 | DBRolePrivilege, and_(DBRolePrivilege.privilege_id == DBPrivilege.id)) \ 163 | .join(DBRole, and_(DBRole.id == DBRolePrivilege.role_id)) \ 164 | .filter(DBRole.id == role_id) 165 | 166 | marshal_records = marshal( 167 | privilege_query.order_by(DBPrivilege.id.desc()).paginate( 168 | current_page, 169 | page_size, 170 | error_out=False 171 | ).items, privilege_fields) 172 | results_wrapper = { 173 | 'total': privilege_query.count(), 174 | 'privileges': marshal_records, 175 | 'current_page': current_page 176 | } 177 | response_wrapper_fields = get_response_wrapper_fields(fields.Nested(paginated_privilege_fields)) 178 | response_wrapper = get_response(RequestCode.SUCCESS, '获取成功!', results_wrapper) 179 | return marshal(response_wrapper, response_wrapper_fields) 180 | 181 | def post(self): 182 | """ 183 | 功能: 创建新的权限 184 | --- 185 | security: 186 | - UserSecurity: [] 187 | tags: 188 | - Privilege 189 | definitions: 190 | Privilege_Parm: 191 | properties: 192 | name: 193 | type: string 194 | default: p123 195 | description: privilege name 196 | operation: 197 | type: integer 198 | default: 100 199 | description: the value of operation 200 | resource_type: 201 | type: integer 202 | default: 100 203 | description: the type of resource 204 | resource_id: 205 | type: integer 206 | default: 0 207 | description: the id of resource 208 | comment: 209 | type: string 210 | default: 权限修改 211 | description: the comment of privilege 212 | parameters: 213 | - in: body 214 | name: body 215 | schema: 216 | id: Add_Privilege 217 | required: 218 | - name 219 | $ref: "#/definitions/Privilege_Parm" 220 | responses: 221 | 200: 222 | description: 请求结果 223 | schema: 224 | properties: 225 | code: 226 | type: integer 227 | description: response code 228 | msg: 229 | type: string 230 | description: response message 231 | data: 232 | type: string 233 | examples: 234 | { 235 | "code": 100000, 236 | "msg": "添加成功", 237 | "data": null 238 | } 239 | """ 240 | args = request.json 241 | privilege_name = args['name'] 242 | operation = args.get('operation') if args.get('operation') != '' else 100 243 | resource_type = args.get('resource_type') if args.get('resource_type') != '' else 100 244 | resource_id = args.get('resource_id') if args.get('resource_id') != '' else 0 245 | comment = args.get('comment') if args.get('comment') else '' 246 | # print(privilege_name, operation, resource_type, resource_id, comment) 247 | uniq_privilege = DBPrivilege.query.filter_by(name=privilege_name).first() 248 | if uniq_privilege: 249 | return get_response(RequestCode.OTHER_FAILED, "{e} 权限名已存在!".format(e=str(uniq_privilege.name))) 250 | try: 251 | new_privilege = DBPrivilege( 252 | name=privilege_name, 253 | operation=operation, 254 | resource_type=resource_type, 255 | resource_id=resource_id, 256 | comment=comment 257 | ) 258 | db.session.add(new_privilege) 259 | db.session.flush() 260 | new_rp = DBRolePrivilege( 261 | role_id=1, 262 | privilege_id=new_privilege.id 263 | ) 264 | db.session.add(new_rp) 265 | db.session.commit() 266 | except Exception as e: 267 | db.session.rollback() 268 | return get_response(RequestCode.OTHER_FAILED, '创建失败!') 269 | return get_response(RequestCode.SUCCESS, '创建成功!') 270 | 271 | 272 | class Privilege(Resource): 273 | method_decorators = [admin_required, token_required] 274 | 275 | # def __init__(self): 276 | # self.role_common_parser = reqparse.RequestParser() 277 | # self.role_common_parser.add_argument( 278 | # 'privilege_ids', 279 | # type = int, 280 | # location = 'json', 281 | # action='append', 282 | # required=True 283 | # ) 284 | # super(Privilege, self).__init__() 285 | 286 | @resource_exists_required(ResourceType.PRIVILEGE) 287 | def get(self, privilege_id): 288 | """ 289 | 功能: 获取指定ID的权限详情 290 | --- 291 | security: 292 | - UserSecurity: [] 293 | tags: 294 | - Privilege 295 | parameters: 296 | - name: privilege_id 297 | in: path 298 | description: the id of privilege 299 | type: integer 300 | required: true 301 | default: 1 302 | definitions: 303 | Privilege: 304 | properties: 305 | id: 306 | type: integer 307 | description: the id of privilege 308 | name: 309 | type: string 310 | description: the name of privilege 311 | operation: 312 | type: integer 313 | description: the operationof privilege 314 | comment: 315 | type: string 316 | description: the comment privilege 317 | responses: 318 | 200: 319 | description: 请求结果 320 | schema: 321 | properties: 322 | code: 323 | type: integer 324 | description: response code 325 | msg: 326 | type: string 327 | description: response message 328 | data: 329 | $ref: "#/definitions/Privilege" 330 | examples: 331 | { 332 | "code": 100000, 333 | "msg": "获取成功!", 334 | "data": { 335 | "id": 37, 336 | "name": "ZONE#xx1.com#DELETE", 337 | "operation": 2, 338 | "resource_type": 2, 339 | "resource_id": 4, 340 | "comment": null 341 | } 342 | } 343 | """ 344 | current_p = DBPrivilege.query.get(privilege_id) 345 | results_wrapper = marshal(current_p, privilege_fields) 346 | return get_response(RequestCode.SUCCESS, '获取成功!', results_wrapper) 347 | 348 | @resource_exists_required(ResourceType.PRIVILEGE) 349 | def put(self, privilege_id): 350 | """ 351 | 功能: 修改指定ID的权限 352 | --- 353 | security: 354 | - UserSecurity: [] 355 | tags: 356 | - Privilege 357 | parameters: 358 | - name: privilege_id 359 | in: path 360 | description: the id of privilege 361 | type: integer 362 | required: true 363 | default: 1 364 | - name: body 365 | in: body 366 | schema: 367 | id: Update_Privilege 368 | $ref: "#/definitions/Privilege_Parm" 369 | responses: 370 | 200: 371 | description: 请求结果 372 | schema: 373 | properties: 374 | code: 375 | type: integer 376 | description: response code 377 | msg: 378 | type: string 379 | description: response message 380 | data: 381 | type: string 382 | examples: 383 | { 384 | "code": 100000, 385 | "msg": "修改成功!", 386 | "data": null 387 | } 388 | """ 389 | current_privilege = DBPrivilege.query.get(privilege_id) 390 | args = request.json 391 | privilege_name = args['name'] 392 | operation = args.get('operation') if args.get('operation') != '' else 100 393 | resource_type = args.get('resource_type') if args.get('resource_type') != '' else 100 394 | resource_id = args.get('resource_id') if args.get('resource_id') != '' else 0 395 | comment = args.get('comment') if args.get('comment') else '' 396 | current_privilege = DBPrivilege.query.get(privilege_id) 397 | uniq_privilege = DBPrivilege.query.filter( 398 | DBPrivilege.name==privilege_name, 399 | DBPrivilege.id!=privilege_id 400 | ).first() 401 | if uniq_privilege: 402 | return get_response(RequestCode.OTHER_FAILED, "{e} 权限名已存在!".format(e=str(uniq_privilege.name))) 403 | try: 404 | current_privilege.name = privilege_name 405 | current_privilege.operation = operation 406 | current_privilege.resource_type = resource_type 407 | current_privilege.resource_id = resource_id 408 | current_privilege.comment = comment 409 | db.session.add(current_privilege) 410 | except Exception as e: 411 | db.session.rollback() 412 | return get_response(RequestCode.OTHER_FAILED, '修改失败!') 413 | return get_response(RequestCode.SUCCESS, '修改成功!') 414 | 415 | @resource_exists_required(ResourceType.PRIVILEGE) 416 | def delete(self, privilege_id): 417 | """ 418 | 功能: 删除指定ID的权限 419 | --- 420 | security: 421 | - UserSecurity: [] 422 | tags: 423 | - Privilege 424 | parameters: 425 | - name: privilege_id 426 | in: path 427 | description: the id of privilege 428 | type: integer 429 | required: true 430 | default: 1 431 | responses: 432 | 200: 433 | description: 请求结果 434 | schema: 435 | properties: 436 | code: 437 | type: integer 438 | description: response code 439 | msg: 440 | type: string 441 | description: response message 442 | data: 443 | type: string 444 | examples: 445 | { 446 | "code": 100000, 447 | "msg": "删除成功", 448 | "data": null 449 | } 450 | """ 451 | current_privilege = DBPrivilege.query.get(privilege_id) 452 | try: 453 | db.session.delete(current_privilege) 454 | db.session.commit() 455 | except Exception as e: 456 | db.session.rollback() 457 | return get_response(RequestCode.OTHER_FAILED, '修改失败!') 458 | return get_response(RequestCode.SUCCESS, '修改成功!') 459 | 460 | 461 | -------------------------------------------------------------------------------- /peb_dns/resourses/admin/role.py: -------------------------------------------------------------------------------- 1 | from flask_restful import Resource, marshal_with, fields, marshal, reqparse, abort 2 | from flask import Blueprint, request, jsonify, current_app, g 3 | 4 | from peb_dns.models.dns import DBView, DBViewZone, DBZone, DBOperationLog, DBRecord, DBDNSServer 5 | from peb_dns.models.account import DBUser, DBUserRole, DBRole, DBRolePrivilege, DBPrivilege 6 | from peb_dns.common.decorators import token_required, admin_required, permission_required, indicated_privilege_required, owner_or_admin_required, resource_exists_required 7 | from peb_dns.common.util import getETCDclient, get_response, get_response_wrapper_fields 8 | from peb_dns.models.mappings import Operation, ResourceType, OPERATION_STR_MAPPING, ROLE_MAPPINGS, DefaultPrivilege 9 | from peb_dns import db 10 | from sqlalchemy import and_, or_ 11 | from datetime import datetime 12 | from peb_dns.common.request_code import RequestCode 13 | 14 | 15 | dns_role_common_parser = reqparse.RequestParser() 16 | dns_role_common_parser.add_argument('name', 17 | type = str, 18 | location = 'json', 19 | required=True, 20 | help='role name.') 21 | dns_role_common_parser.add_argument('privilege_ids', 22 | type = int, 23 | location = 'json', 24 | action='append', 25 | required=True) 26 | 27 | 28 | user_fields = { 29 | 'id': fields.Integer, 30 | 'username': fields.String, 31 | } 32 | 33 | 34 | privilege_fields = { 35 | 'id': fields.Integer, 36 | 'name': fields.String, 37 | 'operation': fields.Integer, 38 | 'resource_type': fields.Integer, 39 | 'resource_id': fields.Integer, 40 | 'comment': fields.String, 41 | } 42 | 43 | 44 | role_fields = { 45 | 'id': fields.Integer, 46 | 'name': fields.String, 47 | 'privileges': fields.List(fields.Nested(privilege_fields)) 48 | } 49 | 50 | 51 | paginated_role_fields = { 52 | 'total': fields.Integer, 53 | 'roles': fields.List(fields.Nested(role_fields)), 54 | 'current_page': fields.Integer 55 | } 56 | 57 | 58 | class RoleList(Resource): 59 | method_decorators = [admin_required, token_required] 60 | 61 | def get(self): 62 | """ 63 | 功能: 获取角色列表资源 64 | --- 65 | security: 66 | - UserSecurity: [] 67 | tags: 68 | - Role 69 | parameters: 70 | - name: currentPage 71 | in: query 72 | description: the page of Role 73 | type: integer 74 | default: 1 75 | - name: pageSize 76 | in: query 77 | description: the max records of page 78 | type: integer 79 | default: 10 80 | - name: id 81 | in: query 82 | description: Role id 83 | type: integer 84 | default: 1 85 | - name: name 86 | type: string 87 | in: query 88 | description: the name of Role 89 | default: Guest 90 | - name: user_id 91 | in: query 92 | type: integer 93 | description: the id of User 94 | default: 1 95 | definitions: 96 | Roles: 97 | properties: 98 | total: 99 | type: integer 100 | current_page: 101 | type: integer 102 | roles: 103 | type: array 104 | items: 105 | $ref: "#/definitions/Role" 106 | responses: 107 | 200: 108 | description: 请求结果 109 | schema: 110 | properties: 111 | code: 112 | type: integer 113 | msg: 114 | type: string 115 | data: 116 | $ref: "#/definitions/Roles" 117 | examples: 118 | { 119 | "code": 100000, 120 | "msg": "获取成功!", 121 | "data": { 122 | "total": 7, 123 | "roles": [ 124 | { 125 | "id": 6, 126 | "name": "zone_admin", 127 | "privileges": [ 128 | { 129 | "id": 2, 130 | "name": "ZONE_ADD", 131 | "operation": 0, 132 | "resource_type": 0, 133 | "resource_id": 0, 134 | "comment": null 135 | }, 136 | { 137 | "id": 6, 138 | "name": "ZONE#xcvwretwgvrfv3wf.com#UPDATE", 139 | "operation": 1, 140 | "resource_type": 2, 141 | "resource_id": 1, 142 | "comment": null 143 | } 144 | ] 145 | }, 146 | { 147 | "id": 2, 148 | "name": "server_admin", 149 | "privileges": [ 150 | { 151 | "id": 1, 152 | "name": "SERVER_ADD", 153 | "operation": 0, 154 | "resource_type": 0, 155 | "resource_id": 0, 156 | "comment": null 157 | }, 158 | { 159 | "id": 17, 160 | "name": "SERVER#s1#ACCESS", 161 | "operation": 0, 162 | "resource_type": 0, 163 | "resource_id": 1, 164 | "comment": null 165 | } 166 | ] 167 | } 168 | ] 169 | }, 170 | "current_page": 1 171 | } 172 | """ 173 | args = request.args 174 | current_page = args.get('currentPage', 1, type=int) 175 | page_size = args.get('pageSize', 10, type=int) 176 | user_id = args.get('user_id', type=int) 177 | 178 | id = args.get('id', type=int) 179 | name = args.get('name', type=str) 180 | role_query = DBRole.query 181 | if id is not None: 182 | role_query = role_query.filter_by(id=id) 183 | if name is not None: 184 | role_query = role_query.filter(DBRole.name.like('%'+name+'%')) 185 | if user_id is not None: 186 | role_query = role_query \ 187 | .join(DBUserRole, and_(DBUserRole.role_id == DBRole.id)) \ 188 | .join(DBUser, and_(DBUser.id == DBUserRole.user_id)) \ 189 | .filter(DBUser.id == user_id) 190 | 191 | marshal_records = marshal( 192 | role_query.order_by(DBRole.id.desc()).paginate( 193 | current_page, 194 | page_size, 195 | error_out=False).items, role_fields) 196 | results_wrapper = { 197 | 'total': role_query.count(), 198 | 'roles': marshal_records, 199 | 'current_page': current_page 200 | } 201 | response_wrapper_fields = get_response_wrapper_fields(fields.Nested(paginated_role_fields)) 202 | response_wrapper = get_response(RequestCode.SUCCESS, '获取成功!', results_wrapper) 203 | return marshal(response_wrapper, response_wrapper_fields) 204 | 205 | def post(self): 206 | """ 207 | 功能: 添加角色 208 | --- 209 | security: 210 | - UserSecurity: [] 211 | tags: 212 | - Role 213 | definitions: 214 | Role_Parm: 215 | properties: 216 | name: 217 | type: string 218 | default: Guest 219 | description: the name of Role 220 | privilege_ids: 221 | type: array 222 | description: the list of privilege 223 | items: 224 | type: integer 225 | default: 1 226 | parameters: 227 | - in: body 228 | name: body 229 | schema: 230 | id: Add_Role 231 | required: 232 | - name 233 | - privilege_ids 234 | $ref: "#/definitions/Role_Parm" 235 | responses: 236 | 200: 237 | description: 请求结果 238 | schema: 239 | properties: 240 | code: 241 | type: integer 242 | msg: 243 | type: string 244 | data: 245 | type: string 246 | examples: 247 | { 248 | "code": 100000, 249 | "msg": "创建成功!", 250 | "data": null 251 | } 252 | """ 253 | args = dns_role_common_parser.parse_args() 254 | role_name = args['name'] 255 | privilege_ids = args['privilege_ids'] 256 | try: 257 | new_role = DBRole(name=role_name) 258 | db.session.add(new_role) 259 | db.session.flush() 260 | for privilege_id in privilege_ids: 261 | new_rp = DBRolePrivilege( 262 | role_id=new_role.id, privilege_id=privilege_id) 263 | db.session.add(new_rp) 264 | db.session.commit() 265 | except Exception as e: 266 | db.session.rollback() 267 | return get_response(RequestCode.OTHER_FAILED, '修改失败!') 268 | return get_response(RequestCode.SUCCESS, '修改成功!') 269 | 270 | 271 | class Role(Resource): 272 | method_decorators = [admin_required, token_required] 273 | 274 | def get(self, role_id): 275 | """ 276 | 功能: 获取指定ID的角色详情 277 | --- 278 | security: 279 | - UserSecurity: [] 280 | tags: 281 | - Role 282 | parameters: 283 | - name: role_id 284 | in: path 285 | description: Role id 286 | type: integer 287 | required: true 288 | default: 1 289 | definitions: 290 | Role: 291 | properties: 292 | id: 293 | type: integer 294 | name: 295 | type: string 296 | privileges: 297 | type: array 298 | items: 299 | $ref: "#/definitions/Privilege" 300 | responses: 301 | 200: 302 | description: 请求结果 303 | schema: 304 | properties: 305 | code: 306 | type: integer 307 | msg: 308 | type: string 309 | data: 310 | $ref: "#/definitions/Role" 311 | examples: 312 | { 313 | "code": 100000, 314 | "msg": "获取成功!", 315 | "data": { 316 | "id": 3, 317 | "name": "server_guest", 318 | "privileges": [ 319 | { 320 | "id": 17, 321 | "name": "SERVER#s1#ACCESS", 322 | "operation": 0, 323 | "resource_type": 0, 324 | "resource_id": 1, 325 | "comment": null 326 | }, 327 | { 328 | "id": 20, 329 | "name": "SERVER#s2#ACCESS", 330 | "operation": 0, 331 | "resource_type": 0, 332 | "resource_id": 2, 333 | "comment": null 334 | } 335 | ] 336 | } 337 | } 338 | """ 339 | current_role = DBRole.query.get(role_id) 340 | if not current_role: 341 | return get_response(RequestCode.OTHER_FAILED, '角色不存在!') 342 | results_wrapper = marshal(current_role, role_fields) 343 | return get_response(RequestCode.SUCCESS, '获取成功!', results_wrapper) 344 | 345 | def put(self, role_id): 346 | """ 347 | 功能: 修改指定ID的角色 348 | --- 349 | security: 350 | - UserSecurity: [] 351 | tags: 352 | - Role 353 | parameters: 354 | - name: role_id 355 | in: path 356 | description: Role id 357 | type: integer 358 | required: true 359 | default: 1 360 | - in: body 361 | name: body 362 | schema: 363 | id: Update_Role 364 | $ref: "#/definitions/Role_Parm" 365 | responses: 366 | 200: 367 | description: 请求结果 368 | schema: 369 | properties: 370 | code: 371 | type: integer 372 | msg: 373 | type: string 374 | data: 375 | type: string 376 | examples: 377 | { 378 | "code": 100000, 379 | "msg": "修改成功", 380 | "data": null 381 | } 382 | """ 383 | args = dns_role_common_parser.parse_args() 384 | role_name = args['name'] 385 | privilege_ids = args['privilege_ids'] 386 | current_role = DBRole.query.get(role_id) 387 | if not current_role: 388 | return get_response(RequestCode.OTHER_FAILED, "角色不存在!") 389 | try: 390 | current_role.name = role_name 391 | for del_rp in DBRolePrivilege.query.filter( 392 | DBRolePrivilege.role_id==role_id, 393 | DBRolePrivilege.privilege_id.notin_(privilege_ids) 394 | ).all(): 395 | db.session.delete(del_rp) 396 | for privilege_id in privilege_ids: 397 | rp = DBRolePrivilege.query.filter( 398 | DBRolePrivilege.role_id==role_id, 399 | DBRolePrivilege.privilege_id==privilege_id 400 | ).first() 401 | if not rp: 402 | new_role_privilege = DBRolePrivilege( 403 | role_id=role_id, privilege_id=privilege_id) 404 | db.session.add(new_role_privilege) 405 | db.session.commit() 406 | except Exception as e: 407 | db.session.rollback() 408 | return get_response(RequestCode.OTHER_FAILED, '修改失败!') 409 | return get_response(RequestCode.SUCCESS, '修改成功!') 410 | 411 | def delete(self, role_id): 412 | """ 413 | 功能: 删除指定ID的角色 414 | --- 415 | security: 416 | - UserSecurity: [] 417 | tags: 418 | - Role 419 | parameters: 420 | - name: role_id 421 | in: path 422 | description: Role id 423 | type: integer 424 | required: true 425 | default: 1 426 | responses: 427 | 200: 428 | description: 请求结果 429 | schema: 430 | properties: 431 | code: 432 | type: string 433 | msg: 434 | type: string 435 | data: 436 | type: string 437 | examples: 438 | { 439 | "code": 100000, 440 | "msg": "删除成功", 441 | "data": null 442 | } 443 | """ 444 | current_role = DBRole.query.get(role_id) 445 | if not current_role: 446 | return get_response(RequestCode.OTHER_FAILED, "角色不存在!") 447 | related_users = current_role.users 448 | if related_users: 449 | return get_response(RequestCode.OTHER_FAILED, "这些用户依然关联当前角色 {e} ,请先解除关联!" 450 | .format(e=str([u.username for u in related_users]))) 451 | try: 452 | DBUserRole.query.filter(DBUserRole.role_id==role_id).delete() 453 | db.session.delete(current_role) 454 | db.session.commit() 455 | except Exception as e: 456 | db.session.rollback() 457 | return get_response(RequestCode.OTHER_FAILED, '删除失败!') 458 | return get_response(RequestCode.SUCCESS, '删除成功!') 459 | 460 | 461 | -------------------------------------------------------------------------------- /peb_dns/resourses/admin/user.py: -------------------------------------------------------------------------------- 1 | from flask_restful import Resource, marshal_with, fields, marshal, reqparse, abort 2 | from flask import Blueprint, request, jsonify, current_app, g 3 | 4 | from peb_dns.models.dns import DBView, DBViewZone, DBZone, DBOperationLog, DBRecord, DBDNSServer 5 | from peb_dns.models.account import DBUser, DBUserRole, DBRole, DBRolePrivilege, DBPrivilege 6 | from peb_dns.common.decorators import token_required, admin_required, permission_required, indicated_privilege_required, owner_or_admin_required, resource_exists_required 7 | from peb_dns.common.util import getETCDclient, get_response, get_response_wrapper_fields 8 | from peb_dns.models.mappings import Operation, ResourceType, OPERATION_STR_MAPPING, ROLE_MAPPINGS, DefaultPrivilege 9 | from peb_dns import db 10 | from sqlalchemy import and_, or_ 11 | from datetime import datetime 12 | from peb_dns.common.request_code import RequestCode 13 | 14 | 15 | dns_user_common_parser = reqparse.RequestParser() 16 | dns_user_common_parser.add_argument('role_ids', 17 | type = int, 18 | location = 'json', 19 | action='append') 20 | dns_user_common_parser.add_argument('email', 21 | type = str, 22 | location = 'json') 23 | dns_user_common_parser.add_argument('chinese_name', 24 | type = str, 25 | location = 'json') 26 | dns_user_common_parser.add_argument('cellphone', 27 | type = str, 28 | location = 'json') 29 | dns_user_common_parser.add_argument('position', 30 | type = str, 31 | location = 'json') 32 | dns_user_common_parser.add_argument('location', 33 | type = str, 34 | location = 'json') 35 | dns_user_common_parser.add_argument('actived', 36 | type = int, 37 | location = 'json') 38 | 39 | 40 | role_fields = { 41 | 'id': fields.Integer, 42 | 'name': fields.String, 43 | } 44 | 45 | 46 | user_fields = { 47 | 'id': fields.Integer, 48 | 'email': fields.String, 49 | 'username': fields.String, 50 | 'chinese_name': fields.String, 51 | 'cellphone': fields.String, 52 | 'position': fields.String, 53 | 'location': fields.String, 54 | 'member_since': fields.String, 55 | 'last_seen': fields.String, 56 | 'actived': fields.Integer, 57 | 'roles': fields.List(fields.Nested(role_fields)), 58 | } 59 | 60 | 61 | single_user_fields = { 62 | 'id': fields.Integer, 63 | 'email': fields.String, 64 | 'username': fields.String, 65 | 'chinese_name': fields.String, 66 | 'cellphone': fields.String, 67 | 'position': fields.String, 68 | 'location': fields.String, 69 | 'member_since': fields.String, 70 | 'last_seen': fields.String, 71 | 'can_add_server': fields.Boolean, 72 | 'can_add_view': fields.Boolean, 73 | 'can_add_zone': fields.Boolean, 74 | 'can_edit_bind_conf': fields.Boolean, 75 | 'roles': fields.List(fields.Nested(role_fields)), 76 | } 77 | 78 | 79 | paginated_user_fields = { 80 | 'total': fields.Integer, 81 | 'users': fields.List(fields.Nested(user_fields)), 82 | 'current_page': fields.Integer 83 | } 84 | 85 | class UserList(Resource): 86 | method_decorators = [admin_required, token_required] 87 | 88 | def get(self): 89 | """ 90 | 功能: 获取用户列表资源 91 | --- 92 | security: 93 | - UserSecurity: [] 94 | tags: 95 | - User 96 | parameters: 97 | - name: currentPage 98 | in: query 99 | description: the page of User 100 | type: integer 101 | default: 1 102 | - name: pageSize 103 | in: query 104 | description: the max records of page 105 | type: integer 106 | default: 10 107 | - name: id 108 | in: query 109 | description: User id 110 | type: integer 111 | default: 1 112 | - name: username 113 | type: string 114 | in: query 115 | description: the username of User 116 | default: test 117 | - name: chinese_name 118 | in: query 119 | type: string 120 | description: the chinese_name of User 121 | default: 小李 122 | - name: cellphone 123 | in: query 124 | type: string 125 | description: the cellphone of User 126 | default: 186121234 127 | - name: email 128 | in: query 129 | type: string 130 | description: the email of User 131 | default: test@163.com 132 | - name: actived 133 | in: query 134 | type: integer 135 | description: the active status of User 136 | enum: [0, 1] 137 | default: 1 138 | definitions: 139 | Users: 140 | properties: 141 | total: 142 | type: integer 143 | current_page: 144 | type: integer 145 | groups: 146 | type: array 147 | items: 148 | $ref: "#/definitions/User" 149 | responses: 150 | 200: 151 | description: 请求结果 152 | schema: 153 | properties: 154 | code: 155 | type: integer 156 | msg: 157 | type: string 158 | data: 159 | $ref: "#/definitions/Users" 160 | examples: 161 | { 162 | "code": 100000, 163 | "data": { 164 | "total": 8, 165 | "users": [ 166 | { 167 | "id": 8, 168 | "email": "xxx@qq.com", 169 | "username": "test222", 170 | "chinese_name": "", 171 | "cellphone": "", 172 | "position": "", 173 | "location": "", 174 | "member_since": "2017-12-04 17:34:25", 175 | "last_seen": "2017-12-04 17:34:25", 176 | "roles": [] 177 | }, 178 | { 179 | "id": 7, 180 | "email": "xxx@qq.com", 181 | "username": "test111", 182 | "chinese_name": "", 183 | "cellphone": "1371111", 184 | "position": "", 185 | "location": "", 186 | "member_since": "2017-11-29 14:16:27", 187 | "last_seen": "2017-11-29 14:16:27", 188 | "roles": [] 189 | } 190 | ], 191 | "current_page": 1 192 | }, 193 | "msg": "获取成功!" 194 | } 195 | """ 196 | args = request.args 197 | current_page = args.get('currentPage', 1, type=int) 198 | page_size = args.get('pageSize', 10, type=int) 199 | 200 | id = args.get('id', type=int) 201 | email = args.get('email', type=str) 202 | username = args.get('username', type=str) 203 | chinese_name = args.get('chinese_name', type=str) 204 | cellphone = args.get('cellphone', type=str) 205 | actived = args.get('actived', type=int) 206 | user_query = DBUser.query 207 | if id is not None: 208 | user_query = user_query.filter_by(id=id) 209 | if email is not None: 210 | user_query = user_query.filter(DBUser.email.like('%'+email+'%')) 211 | if username is not None: 212 | user_query = user_query.filter(DBUser.username.like('%'+username+'%')) 213 | if chinese_name is not None: 214 | user_query = user_query.filter(DBUser.chinese_name.like('%'+chinese_name+'%')) 215 | if cellphone is not None: 216 | user_query = user_query.filter(DBUser.cellphone.like('%'+cellphone+'%')) 217 | if actived is not None: 218 | user_query = user_query.filter_by(actived=actived) 219 | marshal_records = marshal( 220 | user_query.order_by(DBUser.id.desc()).paginate( 221 | current_page, 222 | page_size, 223 | error_out=False).items, user_fields 224 | ) 225 | results_wrapper = { 226 | 'total': user_query.count(), 227 | 'users': marshal_records, 228 | 'current_page': current_page 229 | } 230 | response_wrapper_fields = get_response_wrapper_fields(fields.Nested(paginated_user_fields)) 231 | response_wrapper = get_response(RequestCode.SUCCESS, '获取成功!', results_wrapper) 232 | return marshal(response_wrapper, response_wrapper_fields) 233 | 234 | 235 | class User(Resource): 236 | method_decorators = [token_required] 237 | 238 | @owner_or_admin_required 239 | def get(self, user_id): 240 | """ 241 | 功能: 获取指定ID的用户详情 242 | --- 243 | security: 244 | - UserSecurity: [] 245 | tags: 246 | - User 247 | parameters: 248 | - name: user_id 249 | in: path 250 | description: User id 251 | type: integer 252 | required: true 253 | default: 1 254 | definitions: 255 | User: 256 | properties: 257 | email: 258 | type: string 259 | username: 260 | type: string 261 | chinese_name: 262 | type: string 263 | cellphone: 264 | type: string 265 | position: 266 | type: string 267 | location: 268 | type: string 269 | member_since: 270 | type: string 271 | last_seen: 272 | type: string 273 | roles: 274 | type: array 275 | items: 276 | $ref: "#/definitions/Role" 277 | can_add_server: 278 | type: boolean 279 | can_add_view: 280 | type: boolean 281 | can_add_zone: 282 | type: boolean 283 | can_edit_bind_conf: 284 | type: boolean 285 | responses: 286 | 200: 287 | description: 请求结果 288 | schema: 289 | properties: 290 | code: 291 | type: integer 292 | msg: 293 | type: string 294 | data: 295 | $ref: "#/definitions/User" 296 | examples: 297 | { 298 | "code": 100000, 299 | "msg": "请求成功", 300 | "data": { 301 | "id": 9, 302 | "email": "test123@pingan.com.cn", 303 | "um_account": null, 304 | "username": "test1", 305 | "user_pic": null, 306 | "chinese_name": "", 307 | "cellphone": "", 308 | "position": "", 309 | "location": "", 310 | "member_since": "2017-11-29 15:47:45", 311 | "last_seen": "2017-11-29 15:47:45", 312 | "group": null, 313 | "roles": [] 314 | } 315 | } 316 | """ 317 | current_u = DBUser.query.get(user_id) 318 | if not current_u: 319 | return get_response(RequestCode.OTHER_FAILED, '用户不存在!') 320 | results_wrapper = marshal(current_u, single_user_fields) 321 | return get_response(RequestCode.SUCCESS, '获取成功!', results_wrapper) 322 | 323 | @owner_or_admin_required 324 | def put(self, user_id): 325 | """ 326 | 功能: 修改指定ID的用户 327 | --- 328 | security: 329 | - UserSecurity: [] 330 | tags: 331 | - User 332 | definitions: 333 | User_Parm: 334 | properties: 335 | email: 336 | type: string 337 | description: the email of User 338 | default: test@163.com 339 | chinese_name: 340 | type: string 341 | description: the chinese_name of User 342 | default: 小李 343 | cellphone: 344 | type: string 345 | description: the cellphone of User 346 | default: "186123423" 347 | position: 348 | type: string 349 | description: the position of User 350 | default: developer 351 | location: 352 | type: string 353 | description: the seat of User 354 | default: the 12th floor 355 | role_ids: 356 | type: array 357 | description: the role id of user 358 | items: 359 | type: integer 360 | default: 1 361 | actived: 362 | type: integer 363 | default: 1 364 | description: the status of user 365 | parameters: 366 | - name: user_id 367 | in: path 368 | description: User id 369 | type: integer 370 | required: true 371 | default: 1 372 | - in: body 373 | name: body 374 | schema: 375 | id: Update_User 376 | $ref: "#/definitions/User_Parm" 377 | responses: 378 | 200: 379 | description: 请求结果 380 | schema: 381 | properties: 382 | code: 383 | type: integer 384 | msg: 385 | type: string 386 | data: 387 | type: string 388 | examples: 389 | { 390 | "code": 100000, 391 | "msg": "修改成功", 392 | "data": null 393 | } 394 | """ 395 | current_u = DBUser.query.get(user_id) 396 | if not current_u: 397 | return get_response(RequestCode.OTHER_FAILED, "用户不存在!") 398 | args = dns_user_common_parser.parse_args() 399 | role_ids = args.get('role_ids') 400 | try: 401 | current_u.cellphone = args.get('cellphone', current_u.cellphone) 402 | current_u.chinese_name = args.get('chinese_name', current_u.chinese_name) 403 | current_u.email = args.get('email', current_u.email) 404 | current_u.location = args.get('location', current_u.location) 405 | current_u.position = args.get('position', current_u.position) 406 | current_u.actived = args.get('actived', current_u.actived) 407 | db.session.add(current_u) 408 | if role_ids is not None: 409 | for del_ur in DBUserRole.query.filter( 410 | DBUserRole.user_id==user_id, 411 | DBUserRole.role_id.notin_(role_ids)).all(): 412 | db.session.delete(del_ur) 413 | for role_id in role_ids: 414 | ur = DBUserRole.query.filter( 415 | DBUserRole.role_id==role_id, 416 | DBUserRole.user_id==user_id).first() 417 | if not ur: 418 | new_user_role = DBUserRole( 419 | user_id=user_id, role_id=role_id) 420 | db.session.add(new_user_role) 421 | db.session.commit() 422 | except Exception as e: 423 | db.session.rollback() 424 | return get_response(RequestCode.OTHER_FAILED, '修改失败!') 425 | return get_response(RequestCode.SUCCESS, '修改成功!') 426 | 427 | @admin_required 428 | def delete(self, user_id): 429 | """ 430 | 功能: 删除指定ID的用户 431 | --- 432 | security: 433 | - UserSecurity: [] 434 | tags: 435 | - User 436 | parameters: 437 | - name: user_id 438 | in: path 439 | description: User id 440 | type: integer 441 | required: true 442 | default: 1 443 | responses: 444 | 200: 445 | description: 请求结果 446 | schema: 447 | properties: 448 | code: 449 | type: integer 450 | msg: 451 | type: string 452 | data: 453 | type: string 454 | examples: 455 | { 456 | "code": 100000, 457 | "msg": "删除成功", 458 | "data": null 459 | } 460 | """ 461 | current_u = DBUser.query.get(user_id) 462 | if not current_u: 463 | return get_response(RequestCode.OTHER_FAILED, "用户不存在!") 464 | try: 465 | DBUserRole.query.filter( 466 | DBUserRole.user_id==user_id).delete() 467 | db.session.delete(current_u) 468 | db.session.commit() 469 | except Exception as e: 470 | db.session.rollback() 471 | return get_response(RequestCode.OTHER_FAILED, '删除失败!') 472 | return get_response(RequestCode.SUCCESS, '删除成功!') 473 | 474 | 475 | -------------------------------------------------------------------------------- /peb_dns/resourses/dns/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from flask import Flask, Blueprint 3 | from flask_restful import Api 4 | 5 | dns_bp = Blueprint('dns', __name__, url_prefix='/api/dns') 6 | 7 | dns_api = Api(dns_bp) 8 | 9 | 10 | from .server import DNSServerList, DNSServer, DNSBindConf, DNSServerEnvs 11 | dns_api.add_resource(DNSServerList, '/servers') 12 | dns_api.add_resource(DNSServer, '/servers/') 13 | dns_api.add_resource(DNSBindConf, '/bind_conf') 14 | dns_api.add_resource(DNSServerEnvs, '/server_envs') 15 | 16 | from .view import DNSViewList, DNSView 17 | dns_api.add_resource(DNSViewList, '/views') 18 | dns_api.add_resource(DNSView, '/views/') 19 | 20 | from .zone import DNSZoneList, DNSZone 21 | dns_api.add_resource(DNSZoneList, '/zones') 22 | dns_api.add_resource(DNSZone, '/zones/') 23 | 24 | from .record import DNSRecordList, DNSRecord, DNSRecordsForSearch 25 | dns_api.add_resource(DNSRecordList, '/records') 26 | dns_api.add_resource(DNSRecord, '/records/') 27 | dns_api.add_resource(DNSRecordsForSearch, '/records_searching') 28 | 29 | from .operation_log import DNSOperationLogList, DNSOperationLog 30 | dns_api.add_resource(DNSOperationLogList, '/oplogs') 31 | dns_api.add_resource(DNSOperationLog, '/oplogs/') 32 | 33 | -------------------------------------------------------------------------------- /peb_dns/resourses/dns/operation_log.py: -------------------------------------------------------------------------------- 1 | from flask_restful import Api, Resource, url_for, reqparse, abort, marshal_with, fields, marshal 2 | from flask import current_app, g, request 3 | 4 | from peb_dns.models.dns import DBView, DBViewZone, DBZone, DBOperationLog, DBRecord, DBDNSServer 5 | from peb_dns.models.account import DBUser, DBUserRole, DBRole, DBRolePrivilege, DBPrivilege 6 | from peb_dns.common.decorators import token_required, admin_required, permission_required, indicated_privilege_required, resource_exists_required 7 | from peb_dns.common.util import getETCDclient, get_response, get_response_wrapper_fields 8 | from peb_dns.models.mappings import Operation, ResourceType, OPERATION_STR_MAPPING, ROLE_MAPPINGS, DefaultPrivilege 9 | from peb_dns import db 10 | from sqlalchemy import and_, or_ 11 | from datetime import datetime 12 | from peb_dns.common.request_code import RequestCode 13 | 14 | 15 | log_fields = { 16 | 'id': fields.Integer, 17 | 'operation_time': fields.String, 18 | 'operation_type': fields.String, 19 | 'operator': fields.String, 20 | 'target_type': fields.String, 21 | 'target_name': fields.String, 22 | 'target_id': fields.String, 23 | 'target_detail': fields.String, 24 | } 25 | 26 | paginated_log_fields = { 27 | 'total': fields.Integer, 28 | 'operation_logs': fields.List(fields.Nested(log_fields)), 29 | 'current_page': fields.Integer 30 | } 31 | 32 | class DNSOperationLogList(Resource): 33 | method_decorators = [token_required] 34 | 35 | def __init__(self): 36 | self.get_reqparse = reqparse.RequestParser() 37 | super(DNSOperationLogList, self).__init__() 38 | 39 | def get(self): 40 | """ 41 | 功能:获取日志资源列表 42 | --- 43 | security: 44 | - UserSecurity: [] 45 | tags: 46 | - DNSOperationLog 47 | parameters: 48 | - name: currentPage 49 | in: query 50 | description: 当前是第几页 51 | type: integer 52 | required: false 53 | default: 1 54 | - name: pageSize 55 | in: query 56 | description: 每页显示的记录数 57 | type: integer 58 | required: false 59 | default: 10 60 | - name: id 61 | in: query 62 | description: 日志ID 63 | type: integer 64 | required: false 65 | - name: operation_type 66 | in: query 67 | description: 操作类型,添加/修改/删除 68 | type: string 69 | required: false 70 | - name: operator 71 | in: query 72 | description: 操作人 73 | type: string 74 | required: false 75 | - name: target_type 76 | in: query 77 | description: 资源类型,Server/View/Zone/Record 78 | type: string 79 | required: false 80 | - name: target_name 81 | in: query 82 | description: 资源名称 83 | type: string 84 | required: false 85 | - name: target_id 86 | in: query 87 | description: 资源ID 88 | type: integer 89 | required: false 90 | responses: 91 | 200: 92 | description: 请求结果 93 | schema: 94 | properties: 95 | code: 96 | type: integer 97 | description: response code 98 | msg: 99 | type: string 100 | description: response message 101 | data: 102 | type: string 103 | description: response data 104 | examples: 105 | { 106 | "code": 100000, 107 | "data": { 108 | "total": 66, 109 | "operation_logs": [ 110 | { 111 | "id": 67, 112 | "operation_time": "2017-12-04 18:22:19", 113 | "operation_type": "添加", 114 | "operator": "LIJIAJIA873", 115 | "target_type": "Record", 116 | "target_name": "xxx333", 117 | "target_id": "32", 118 | "target_detail": "id: 32\\n记录主机: xxx333\\n记录类型: A\\n记录值: 0.0.0.0\\nTTL: 600\\n线路类型: wqerqwer\\n备注: xxx111\\n创建人: None\\n创建时间: 2017-12-04 18:22:18.805320" 119 | } 120 | ], 121 | "current_page": 1 122 | }, 123 | "msg": "获取成功!" 124 | } 125 | """ 126 | args = request.args 127 | current_page = args.get('currentPage', 1, type=int) 128 | page_size = args.get('pageSize', 10, type=int) 129 | 130 | id = args.get('id', type=int) 131 | operation_type = args.get('operation_type', type=str) 132 | operator = args.get('operator', type=str) 133 | target_type = args.get('target_type', type=str) 134 | target_name = args.get('target_name', type=str) 135 | oplog_query = DBOperationLog.query 136 | if id is not None: 137 | oplog_query = oplog_query.filter(DBOperationLog.id==id) 138 | if operation_type is not None: 139 | oplog_query = oplog_query.filter(DBOperationLog.operation_type==operation_type) 140 | if operator is not None: 141 | oplog_query = oplog_query.filter(DBOperationLog.operator.like('%'+operator+'%')) 142 | if target_type is not None: 143 | oplog_query = oplog_query.filter(DBOperationLog.target_type==target_type) 144 | if target_name is not None: 145 | oplog_query = oplog_query.filter(DBOperationLog.target_name.like('%'+target_name+'%')) 146 | marshal_records = marshal( 147 | oplog_query.order_by(DBOperationLog.id.desc()).paginate( 148 | current_page, 149 | page_size, 150 | error_out=False).items, log_fields 151 | ) 152 | results_wrapper = { 153 | 'total': oplog_query.count(), 154 | 'operation_logs': marshal_records, 155 | 'current_page': current_page 156 | } 157 | response_wrapper_fields = get_response_wrapper_fields(fields.Nested(paginated_log_fields)) 158 | response_wrapper = get_response(RequestCode.SUCCESS, '获取成功!', results_wrapper) 159 | return marshal(response_wrapper, response_wrapper_fields) 160 | 161 | 162 | class DNSOperationLog(Resource): 163 | method_decorators = [token_required] 164 | 165 | def get(self, log_id): 166 | """ 167 | 功能:获取指定ID的日志资源详情 168 | --- 169 | security: 170 | - UserSecurity: [] 171 | tags: 172 | - DNSOperationLog 173 | parameters: 174 | - name: log_id 175 | in: path 176 | description: 177 | type: integer 178 | required: true 179 | default: 1 180 | responses: 181 | 200: 182 | description: 请求结果 183 | schema: 184 | properties: 185 | code: 186 | type: integer 187 | description: response code 188 | msg: 189 | type: string 190 | description: response message 191 | data: 192 | type: string 193 | description: response data 194 | examples: 195 | { 196 | "code": 100000, 197 | "data": {}, 198 | "msg": "获取成功!" 199 | } 200 | """ 201 | current_log = DBOperationLog.query.get(log_id) 202 | if not current_log: 203 | return get_response(RequestCode.OTHER_FAILED, "当前记录 {} 不存在!".format(str(log_id))) 204 | results_wrapper = marshal(current_log, log_fields) 205 | return get_response(RequestCode.SUCCESS, '获取成功!', results_wrapper) 206 | 207 | -------------------------------------------------------------------------------- /peb_dns/resourses/dns/view.py: -------------------------------------------------------------------------------- 1 | from flask_restful import Api, Resource, url_for, reqparse, abort, marshal_with, fields, marshal 2 | from flask import current_app, g, request 3 | 4 | from peb_dns.models.dns import DBView, DBViewZone, DBZone, DBOperationLog, DBRecord, DBDNSServer 5 | from peb_dns.models.account import DBUser, DBUserRole, DBRole, DBRolePrivilege, DBPrivilege 6 | from peb_dns.common.decorators import token_required, admin_required, permission_required, indicated_privilege_required, resource_exists_required 7 | from peb_dns.common.util import getETCDclient, get_response, get_response_wrapper_fields 8 | from peb_dns.models.mappings import Operation, ResourceType, OPERATION_STR_MAPPING, ROLE_MAPPINGS, DefaultPrivilege 9 | from peb_dns.common.util import DNSPod 10 | from peb_dns import db 11 | from sqlalchemy import and_, or_ 12 | from peb_dns.common.request_code import RequestCode 13 | 14 | dns_view_common_parser = reqparse.RequestParser() 15 | dns_view_common_parser.add_argument('name', 16 | type = str, 17 | location = 'json', 18 | required=True, 19 | help='host') 20 | dns_view_common_parser.add_argument('acl', 21 | type = str, 22 | location = 'json', 23 | required=True) 24 | 25 | 26 | view_fields = { 27 | 'id': fields.Integer, 28 | 'name': fields.String, 29 | 'acl': fields.String, 30 | 'can_update': fields.Boolean, 31 | 'can_delete': fields.Boolean 32 | } 33 | 34 | paginated_view_fields = { 35 | 'total': fields.Integer, 36 | 'views': fields.List(fields.Nested(view_fields)), 37 | 'current_page': fields.Integer 38 | } 39 | 40 | class DNSViewList(Resource): 41 | method_decorators = [token_required] 42 | 43 | def get(self): 44 | """ 45 | 功能:获取View资源列表 46 | --- 47 | security: 48 | - UserSecurity: [] 49 | tags: 50 | - View 51 | parameters: 52 | - name: currentPage 53 | in: query 54 | description: View list in current page 55 | type: integer 56 | default: 1 57 | - name: pageSize 58 | in: query 59 | description: the number of views per page. 60 | type: integer 61 | default: 10 62 | - name: id 63 | in: query 64 | description: View id 65 | type: integer 66 | default: 1 67 | - name: name 68 | type: string 69 | in: query 70 | description: the name of View 71 | default: z1.com 72 | - name: zone_id 73 | in: query 74 | description: the id of the zone which was related to views 75 | type: integer 76 | default: 1 77 | definitions: 78 | View: 79 | properties: 80 | total: 81 | type: integer 82 | description: the number of views 83 | current_page: 84 | type: integer 85 | description: current page number 86 | views: 87 | type: array 88 | items: 89 | $ref: "#/definitions/View" 90 | responses: 91 | 200: 92 | description: 请求结果 93 | schema: 94 | properties: 95 | code: 96 | type: integer 97 | description: response code 98 | msg: 99 | type: string 100 | description: response message 101 | data: 102 | $ref: "#/definitions/View" 103 | examples: 104 | { 105 | "code": 100000, 106 | "data": { 107 | "total": 4, 108 | "views": [ 109 | { 110 | "id": 2, 111 | "name": "vvvv111111111", 112 | "acl": "0.0.0.0", 113 | "can_update": true, 114 | "can_delete": true 115 | }, 116 | { 117 | "id": 1, 118 | "name": "wqerqwer", 119 | "acl": "0.0.0.0\\n1.1.1.1", 120 | "can_update": true, 121 | "can_delete": true 122 | } 123 | ], 124 | "current_page": 1 125 | }, 126 | "msg": "获取成功!" 127 | } 128 | """ 129 | args = request.args 130 | zone_id = args.get('zone_id', type=int) 131 | current_page = request.args.get('currentPage', 1, type=int) 132 | page_size = request.args.get('pageSize', 10, type=int) 133 | id = args.get('id', type=int) 134 | name = args.get('name', type=str) 135 | view_query = DBView.query \ 136 | .join(DBPrivilege, and_( 137 | DBView.id == DBPrivilege.resource_id, 138 | DBPrivilege.resource_type == ResourceType.VIEW, 139 | DBPrivilege.operation == Operation.ACCESS 140 | )) \ 141 | .join(DBRolePrivilege, and_( 142 | DBPrivilege.id == DBRolePrivilege.privilege_id 143 | )) \ 144 | .join(DBRole, and_(DBRole.id == DBRolePrivilege.role_id)) \ 145 | .join(DBUserRole, and_(DBUserRole.role_id == DBRole.id)) \ 146 | .join(DBUser, and_(DBUser.id == DBUserRole.user_id)) \ 147 | .filter(DBUser.id == g.current_user.id) 148 | if id is not None: 149 | view_query = view_query.filter_by(id=id) 150 | if name is not None: 151 | view_query = view_query.filter(DBView.name.like('%'+name+'%')) 152 | if zone_id is not None: 153 | current_zone = DBZone.query.get(zone_id) 154 | if current_zone.zone_group == 0: 155 | return get_response(RequestCode.SUCCESS, '获取成功!', DNSPod.getDNSPodLines(current_zone.name)) 156 | view_query = view_query.join( 157 | DBViewZone, and_(DBViewZone.view_id == DBView.id)) \ 158 | .join(DBZone, and_(DBZone.id == DBViewZone.zone_id)) \ 159 | .filter(DBZone.id == int(zone_id)) 160 | marshal_records = marshal( 161 | view_query.order_by(DBView.id.desc()).paginate( 162 | current_page, 163 | page_size, 164 | error_out=False).items, view_fields) 165 | results_wrapper = { 166 | 'total': view_query.count(), 167 | 'views': marshal_records, 168 | 'current_page': current_page 169 | } 170 | response_wrapper_fields = get_response_wrapper_fields(fields.Nested(paginated_view_fields)) 171 | response_wrapper = get_response(RequestCode.SUCCESS, '获取成功!', results_wrapper) 172 | return marshal(response_wrapper, response_wrapper_fields) 173 | 174 | @indicated_privilege_required(DefaultPrivilege.VIEW_ADD) 175 | def post(self): 176 | """ 177 | 功能:创建新的View 178 | --- 179 | security: 180 | - UserSecurity: [] 181 | tags: 182 | - View 183 | definitions: 184 | View_Parm: 185 | properties: 186 | name: 187 | type: string 188 | default: v1 189 | description: view name 190 | acl: 191 | type: string 192 | default: 0.0.0.0 193 | description: view name 194 | parameters: 195 | - in: body 196 | name: body 197 | schema: 198 | id: Add_View 199 | required: 200 | - name 201 | $ref: "#/definitions/View_Parm" 202 | responses: 203 | 200: 204 | description: 请求结果 205 | schema: 206 | properties: 207 | code: 208 | type: integer 209 | description: response code 210 | msg: 211 | type: string 212 | description: response message 213 | data: 214 | type: string 215 | examples: 216 | { 217 | "code": 100000, 218 | "msg": "添加成功", 219 | "data": null 220 | } 221 | """ 222 | args = dns_view_common_parser.parse_args() 223 | unique_view = DBView.query.filter_by(name=args['name']).first() 224 | if unique_view: 225 | return get_response(RequestCode.OTHER_FAILED, '创建失败!重复的View, 相同的名字的View已存在!!') 226 | new_view = DBView(**args) 227 | db.session.add(new_view) 228 | db.session.flush() 229 | log = DBOperationLog( 230 | operation_type='添加', 231 | operator=g.current_user.username, 232 | target_type='View', 233 | target_name=new_view.name, 234 | target_id=int(new_view.id), 235 | target_detail=new_view.get_content_str() 236 | ) 237 | db.session.add(log) 238 | try: 239 | self._add_privilege_for_view(new_view) 240 | view_list = db.session.query(DBView).all() 241 | new_view.make_view('create', view_list) 242 | db.session.commit() 243 | except Exception as e: 244 | db.session.rollback() 245 | return get_response(RequestCode.OTHER_FAILED, "创建失败!") 246 | return get_response(RequestCode.SUCCESS, '创建成功!') 247 | 248 | def _add_privilege_for_view(self, new_view): 249 | """Add privilege for the new view.""" 250 | access_privilege_name = 'VIEW#' + new_view.name + \ 251 | '#' + OPERATION_STR_MAPPING[Operation.ACCESS] 252 | update_privilege_name = 'VIEW#' + new_view.name + \ 253 | '#' + OPERATION_STR_MAPPING[Operation.UPDATE] 254 | delete_privilege_name = 'VIEW#' + new_view.name + \ 255 | '#' + OPERATION_STR_MAPPING[Operation.DELETE] 256 | access_privilege = DBPrivilege( 257 | name=access_privilege_name, 258 | resource_type=ResourceType.VIEW, 259 | operation=Operation.ACCESS, 260 | resource_id=new_view.id 261 | ) 262 | update_privilege = DBPrivilege( 263 | name=update_privilege_name, 264 | resource_type=ResourceType.VIEW, 265 | operation=Operation.UPDATE, 266 | resource_id=new_view.id 267 | ) 268 | delete_privilege = DBPrivilege( 269 | name=delete_privilege_name, 270 | resource_type=ResourceType.VIEW, 271 | operation=Operation.DELETE, 272 | resource_id=new_view.id 273 | ) 274 | db.session.add(access_privilege) 275 | db.session.add(update_privilege) 276 | db.session.add(delete_privilege) 277 | db.session.flush() 278 | for role in ['admin', 'view_admin', 'view_guest']: 279 | role_access = DBRolePrivilege( 280 | role_id=ROLE_MAPPINGS[role], 281 | privilege_id=access_privilege.id) 282 | db.session.add(role_access) 283 | if role not in ['view_guest']: 284 | role_update = DBRolePrivilege( 285 | role_id=ROLE_MAPPINGS[role], 286 | privilege_id=update_privilege.id) 287 | role_delete = DBRolePrivilege( 288 | role_id=ROLE_MAPPINGS[role], 289 | privilege_id=delete_privilege.id) 290 | db.session.add(role_update) 291 | db.session.add(role_delete) 292 | 293 | 294 | class DNSView(Resource): 295 | method_decorators = [token_required] 296 | 297 | @resource_exists_required(ResourceType.VIEW) 298 | @permission_required(ResourceType.VIEW, Operation.ACCESS) 299 | def get(self, view_id): 300 | """ 301 | 功能:获取指定ID的View详情 302 | --- 303 | security: 304 | - UserSecurity: [] 305 | tags: 306 | - View 307 | parameters: 308 | - name: view_id 309 | in: path 310 | description: 311 | type: integer 312 | required: true 313 | default: 1 314 | responses: 315 | 200: 316 | description: 请求结果 317 | schema: 318 | properties: 319 | code: 320 | type: integer 321 | description: response code 322 | msg: 323 | type: string 324 | description: response message 325 | data: 326 | type: string 327 | description: response data 328 | examples: 329 | { 330 | "code": 100000, 331 | "msg": "获取成功!", 332 | "data": { 333 | "id": 1, 334 | "name": "wqerqwer", 335 | "acl": "0.0.0.0\\n1.1.1.1", 336 | "can_update": true, 337 | "can_delete": true 338 | } 339 | } 340 | """ 341 | current_view = DBView.query.get(view_id) 342 | results_wrapper = marshal(current_view, view_fields) 343 | return get_response(RequestCode.SUCCESS, '获取成功!', results_wrapper) 344 | 345 | @resource_exists_required(ResourceType.VIEW) 346 | @permission_required(ResourceType.VIEW, Operation.UPDATE) 347 | def put(self, view_id): 348 | """ 349 | 功能:修改指定ID的View 350 | --- 351 | security: 352 | - UserSecurity: [] 353 | tags: 354 | - View 355 | parameters: 356 | - name: view_id 357 | in: path 358 | description: view id 359 | type: integer 360 | required: true 361 | default: 1 362 | - in: body 363 | name: body 364 | schema: 365 | id: Update_View 366 | $ref: "#/definitions/View_Parm" 367 | responses: 368 | 200: 369 | description: 请求结果 370 | schema: 371 | properties: 372 | code: 373 | type: integer 374 | msg: 375 | type: string 376 | data: 377 | type: string 378 | examples: 379 | { 380 | "code": 100000, 381 | "msg": "修改成功", 382 | "data": null 383 | } 384 | """ 385 | current_view = DBView.query.get(view_id) 386 | args = dns_view_common_parser.parse_args() 387 | try: 388 | self._update_view(current_view, args) 389 | db.session.commit() 390 | except Exception as e: 391 | db.session.rollback() 392 | return get_response(RequestCode.OTHER_FAILED, '修改失败!') 393 | return get_response(RequestCode.SUCCESS, '修改成功!') 394 | 395 | @resource_exists_required(ResourceType.VIEW) 396 | @permission_required(ResourceType.VIEW, Operation.DELETE) 397 | def delete(self, view_id): 398 | """ 399 | 功能: 删除指定ID的View 400 | --- 401 | security: 402 | - UserSecurity: [] 403 | tags: 404 | - View 405 | parameters: 406 | - name: view_id 407 | in: path 408 | description: View id 409 | type: integer 410 | required: true 411 | default: 1 412 | responses: 413 | 200: 414 | description: 请求结果 415 | schema: 416 | properties: 417 | code: 418 | type: string 419 | msg: 420 | type: string 421 | data: 422 | type: string 423 | examples: 424 | { 425 | "code": 100000, 426 | "msg": "删除成功", 427 | "data": null 428 | } 429 | """ 430 | current_view = DBView.query.get(view_id) 431 | current_view_related_zones = current_view.zone_name_list 432 | if current_view_related_zones: 433 | return get_response(RequestCode.OTHER_FAILED, "{e}".format( 434 | e='当前View还与Zone有关联,请先解除关联,再进行删除操作!\n' \ 435 | + str(current_view_related_zones))) 436 | try: 437 | self._remove_view_privileges(current_view) 438 | self._delete_view(current_view) 439 | db.session.commit() 440 | except Exception as e: 441 | db.session.rollback() 442 | return get_response(RequestCode.OTHER_FAILED, '删除失败!') 443 | return get_response(RequestCode.SUCCESS, '删除成功!') 444 | 445 | def _update_view(self, view, args): 446 | log = DBOperationLog( 447 | operation_type='修改', 448 | operator=g.current_user.username, 449 | target_type='View', 450 | target_name=view.name, \ 451 | target_id=int(view.id), 452 | target_detail=view.get_content_str(prefix="修改前:") 453 | ) 454 | db.session.add(log) 455 | view.name = args['name'] 456 | view.acl = args['acl'] 457 | db.session.add(view) 458 | view_list = db.session.query(DBView).all() 459 | view.make_view('modify', view_list) 460 | 461 | def _delete_view(self, view): 462 | log = DBOperationLog( 463 | operation_type='删除', 464 | operator=g.current_user.username, 465 | target_type='View', 466 | target_name=view.name, 467 | target_id=int(view.id), 468 | target_detail=view.get_content_str(prefix="修改前:") 469 | ) 470 | db.session.add(log) 471 | db.session.delete(view) 472 | view_list = db.session.query(DBView).all() 473 | view.make_view('del', view_list) 474 | 475 | def _remove_view_privileges(self, current_view): 476 | """Remove all the privileges from the indicated view.""" 477 | current_view_privileges_query = DBPrivilege.query.filter( 478 | DBPrivilege.resource_id==current_view.id, 479 | DBPrivilege.resource_type==ResourceType.VIEW 480 | ) 481 | current_view_privileges = current_view_privileges_query.all() 482 | for view_privilege in current_view_privileges: 483 | DBRolePrivilege.query.filter( 484 | DBRolePrivilege.privilege_id == view_privilege.id 485 | ).delete() 486 | current_view_privileges_query.delete() 487 | 488 | -------------------------------------------------------------------------------- /peb_dns/resourses/page/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, Blueprint 2 | from flask_restful import Api 3 | 4 | page_bp = Blueprint('page', __name__, url_prefix='/api/page') 5 | 6 | page_api = Api(page_bp) 7 | 8 | 9 | from .menu import MenuSidebar 10 | from .dashboard import ResourceAmount, DNSServerResolveRate, DNSServerStatus 11 | 12 | 13 | page_api.add_resource(MenuSidebar, '/menu_sidebar') 14 | page_api.add_resource(ResourceAmount, '/resource_amount') 15 | page_api.add_resource(DNSServerResolveRate, '/servers_resolve_rate') 16 | page_api.add_resource(DNSServerStatus, '/server_status') 17 | 18 | -------------------------------------------------------------------------------- /peb_dns/resourses/page/dashboard.py: -------------------------------------------------------------------------------- 1 | from flask_restful import Api, Resource, url_for, reqparse, abort 2 | from flask import current_app, g, request 3 | from peb_dns.models.dns import DBView, DBViewZone, DBZone, DBOperationLog, DBRecord, DBDNSServer 4 | from peb_dns.models.account import DBUser, DBUserRole, DBRole, DBRolePrivilege, DBPrivilege 5 | from peb_dns.common.decorators import token_required, admin_required, permission_required, indicated_privilege_required, resource_exists_required 6 | from peb_dns.common.util import getETCDclient, get_response, get_response_wrapper_fields 7 | from peb_dns.models.mappings import Operation, ResourceType, OPERATION_STR_MAPPING, ROLE_MAPPINGS, DefaultPrivilege 8 | 9 | from peb_dns import db 10 | from peb_dns.common.util import ZBapi 11 | from sqlalchemy import and_, or_ 12 | from datetime import datetime 13 | from peb_dns.common.request_code import RequestCode 14 | 15 | 16 | class ResourceAmount(Resource): 17 | method_decorators = [token_required] 18 | 19 | def get(self): 20 | """ 21 | 功能: 获取各个资源总数 22 | --- 23 | security: 24 | - UserSecurity: [] 25 | tags: 26 | - Page 27 | responses: 28 | 200: 29 | description: 请求结果 30 | schema: 31 | properties: 32 | code: 33 | type: integer 34 | description: response code 35 | msg: 36 | type: string 37 | description: response message 38 | data: 39 | type: string 40 | examples: 41 | { 42 | "code": 100000, 43 | "msg": "获取成功!", 44 | "data": { 45 | "server_amount": 10, 46 | "view_amount": 20, 47 | "zone_amount": 2, 48 | "record_amount": 2 49 | } 50 | } 51 | """ 52 | resources_amount = dict( 53 | server_amount=DBDNSServer.query.count(), 54 | view_amount=DBView.query.count(), 55 | zone_amount=DBZone.query.count(), 56 | record_amount=DBRecord.query.count() 57 | ) 58 | return get_response(RequestCode.SUCCESS, '获取成功!', resources_amount) 59 | 60 | 61 | class DNSServerResolveRate(Resource): 62 | """Get the resolve rate of all the dns servers.""" 63 | method_decorators = [token_required] 64 | 65 | def get(self): 66 | args = request.args 67 | start_time = datetime.strptime(args['start_time'], "%Y-%m-%d %H:%M:%S") 68 | end_time = datetime.now() 69 | if args.get('end_time'): 70 | end_time = datetime.strptime(args['end_time'], "%Y-%m-%d %H:%M:%S") 71 | dns_servers = DBDNSServer.query.all() 72 | resolve_rates = {} 73 | try: 74 | for dns_server in dns_servers: 75 | resolve_rate = dns_server.get_resolve_rate(start_time, end_time) 76 | resolve_rates[dns_server.host] = resolve_rate 77 | except IndexError as ie: 78 | return get_response(RequestCode.OTHER_FAILED, '获取数据失败!Zabbix上对应解析量itemid没有足够的记录!或解析量itemid有误!') 79 | except Exception as e: 80 | return get_response(RequestCode.OTHER_FAILED, '获取Zabbix数据失败!') 81 | return get_response(RequestCode.SUCCESS, '获取成功!', resolve_rates) 82 | 83 | 84 | class DNSServerStatus(Resource): 85 | method_decorators = [token_required] 86 | 87 | def get(self): 88 | """Get the server status by server id.""" 89 | args = request.args 90 | current_server = DBDNSServer.query.get(int(args['server_id'])) 91 | if not current_server: 92 | return get_response(RequestCode.OTHER_FAILED, '你请求的资源不存在!') 93 | try: 94 | results = current_server.get_server_status() 95 | except Exception as e: 96 | return get_response(RequestCode.OTHER_FAILED, '获取数据异常!') 97 | return get_response(RequestCode.SUCCESS, '获取成功!', results) 98 | 99 | 100 | -------------------------------------------------------------------------------- /peb_dns/resourses/page/menu.py: -------------------------------------------------------------------------------- 1 | from flask_restful import Api, Resource, url_for, reqparse, abort 2 | from flask import current_app, g 3 | from peb_dns.models.dns import DBView, DBViewZone, DBZone, DBOperationLog, DBRecord, DBDNSServer 4 | from peb_dns.models.account import DBUser, DBUserRole, DBRole, DBRolePrivilege, DBPrivilege 5 | from peb_dns.common.decorators import token_required, admin_required, permission_required, indicated_privilege_required, resource_exists_required 6 | from peb_dns.common.util import getETCDclient, get_response, get_response_wrapper_fields 7 | from peb_dns.models.mappings import Operation, ResourceType, OPERATION_STR_MAPPING, ROLE_MAPPINGS, DefaultPrivilege 8 | 9 | from peb_dns import db 10 | from sqlalchemy import and_, or_ 11 | from datetime import datetime 12 | from peb_dns.common.request_code import RequestCode 13 | 14 | 15 | 16 | class MenuSidebar(Resource): 17 | 18 | method_decorators = [token_required] 19 | 20 | def __init__(self): 21 | self.get_reqparse = reqparse.RequestParser() 22 | super(MenuSidebar, self).__init__() 23 | 24 | def get(self): 25 | """Get the sidebar menu data.""" 26 | menu_group = self._get_zones() 27 | menu_group['menu'].append({'title':'Zone管理', 'items':None, 'url':'/dns/zones'}) 28 | menu_group['menu'].append({'title':'View管理', 'items':None, 'url':'/dns/views'}) 29 | menu_group['menu'].append({'title':'DNS服务器', 'items':None, 'url':'/dns/servers'}) 30 | if g.current_user.is_admin(): 31 | admin_items = [ 32 | {'item_name':'用户管理', 'url':'/admin/users'}, 33 | {'item_name':'角色管理', 'url':'/admin/roles'}, 34 | {'item_name':'权限管理', 'url':'/admin/privileges'} 35 | ] 36 | menu_group['menu'].append({'title':'后台管理', 'items': admin_items}) 37 | menu_group['menu'].append({'title':'操作记录', 'items':None, 'url':'/dns/logs'}) 38 | return get_response(RequestCode.SUCCESS, '获取成功!', menu_group) 39 | 40 | def _get_zones(self): 41 | zone_query = db.session.query(DBZone) \ 42 | .join(DBPrivilege, and_( 43 | DBZone.id == DBPrivilege.resource_id, 44 | DBPrivilege.resource_type == ResourceType.ZONE, 45 | DBPrivilege.operation == Operation.ACCESS 46 | )) \ 47 | .join(DBRolePrivilege, and_( 48 | DBPrivilege.id == DBRolePrivilege.privilege_id 49 | )) \ 50 | .join(DBRole, and_(DBRole.id == DBRolePrivilege.role_id)) \ 51 | .join(DBUserRole, and_(DBUserRole.role_id == DBRole.id)) \ 52 | .join(DBUser, and_(DBUser.id == DBUserRole.user_id)) \ 53 | .filter(DBUser.id == g.current_user.id) 54 | inner_zones = [{'item_name':zone.name, 'url':'/#/dns/records/zoneId/'+ str(zone.id)} 55 | for zone in zone_query.filter(DBZone.zone_group == 1).all()] 56 | intercepted_zones = [{'item_name':zone.name, 'url':'/#/dns/records/zoneId/'+ str(zone.id)} 57 | for zone in zone_query.filter(DBZone.zone_group == 2).all()] 58 | outter_zones = [{'item_name':zone.name, 'url':'/#/dns/records/zoneId/'+ str(zone.id)} 59 | for zone in zone_query.filter(DBZone.zone_group == 0).all()] 60 | zone_groups = {'menu' : [ 61 | {'title':'内部域名', 'items':inner_zones}, 62 | {'title':'劫持域名', 'items':intercepted_zones}, 63 | {'title':'外部域名', 'items':outter_zones} 64 | ] 65 | } 66 | return zone_groups 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | DNS
-------------------------------------------------------------------------------- /static/static/fonts/fontawesome-webfont.674f50d.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackjiali/dns-manager/c7e983f17b79a1f0d8e71c789320d0b83dfcb6e9/static/static/fonts/fontawesome-webfont.674f50d.eot -------------------------------------------------------------------------------- /static/static/fonts/fontawesome-webfont.af7ae50.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackjiali/dns-manager/c7e983f17b79a1f0d8e71c789320d0b83dfcb6e9/static/static/fonts/fontawesome-webfont.af7ae50.woff2 -------------------------------------------------------------------------------- /static/static/fonts/fontawesome-webfont.b06871f.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackjiali/dns-manager/c7e983f17b79a1f0d8e71c789320d0b83dfcb6e9/static/static/fonts/fontawesome-webfont.b06871f.ttf -------------------------------------------------------------------------------- /static/static/fonts/fontawesome-webfont.fee66e7.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackjiali/dns-manager/c7e983f17b79a1f0d8e71c789320d0b83dfcb6e9/static/static/fonts/fontawesome-webfont.fee66e7.woff -------------------------------------------------------------------------------- /static/static/fonts/glyphicons-halflings-regular.448c34a.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackjiali/dns-manager/c7e983f17b79a1f0d8e71c789320d0b83dfcb6e9/static/static/fonts/glyphicons-halflings-regular.448c34a.woff2 -------------------------------------------------------------------------------- /static/static/fonts/glyphicons-halflings-regular.e18bbf6.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackjiali/dns-manager/c7e983f17b79a1f0d8e71c789320d0b83dfcb6e9/static/static/fonts/glyphicons-halflings-regular.e18bbf6.ttf -------------------------------------------------------------------------------- /static/static/fonts/glyphicons-halflings-regular.f4769f9.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackjiali/dns-manager/c7e983f17b79a1f0d8e71c789320d0b83dfcb6e9/static/static/fonts/glyphicons-halflings-regular.f4769f9.eot -------------------------------------------------------------------------------- /static/static/fonts/glyphicons-halflings-regular.fa27723.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackjiali/dns-manager/c7e983f17b79a1f0d8e71c789320d0b83dfcb6e9/static/static/fonts/glyphicons-halflings-regular.fa27723.woff -------------------------------------------------------------------------------- /static/static/fonts/iconfont.af12dbc.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackjiali/dns-manager/c7e983f17b79a1f0d8e71c789320d0b83dfcb6e9/static/static/fonts/iconfont.af12dbc.ttf -------------------------------------------------------------------------------- /static/static/fonts/iconfont.f47fda4.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackjiali/dns-manager/c7e983f17b79a1f0d8e71c789320d0b83dfcb6e9/static/static/fonts/iconfont.f47fda4.woff -------------------------------------------------------------------------------- /static/static/fonts/ionicons.05acfdb.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackjiali/dns-manager/c7e983f17b79a1f0d8e71c789320d0b83dfcb6e9/static/static/fonts/ionicons.05acfdb.woff -------------------------------------------------------------------------------- /static/static/fonts/ionicons.24712f6.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackjiali/dns-manager/c7e983f17b79a1f0d8e71c789320d0b83dfcb6e9/static/static/fonts/ionicons.24712f6.ttf -------------------------------------------------------------------------------- /static/static/fonts/ionicons.2c2ae06.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackjiali/dns-manager/c7e983f17b79a1f0d8e71c789320d0b83dfcb6e9/static/static/fonts/ionicons.2c2ae06.eot -------------------------------------------------------------------------------- /static/static/img/boxed-bg.7799dec.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackjiali/dns-manager/c7e983f17b79a1f0d8e71c789320d0b83dfcb6e9/static/static/img/boxed-bg.7799dec.jpg -------------------------------------------------------------------------------- /static/static/img/error.75f4c76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackjiali/dns-manager/c7e983f17b79a1f0d8e71c789320d0b83dfcb6e9/static/static/img/error.75f4c76.png -------------------------------------------------------------------------------- /static/static/img/success.f09cb57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackjiali/dns-manager/c7e983f17b79a1f0d8e71c789320d0b83dfcb6e9/static/static/img/success.f09cb57.png -------------------------------------------------------------------------------- /static/static/img/warn.99e51ab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackjiali/dns-manager/c7e983f17b79a1f0d8e71c789320d0b83dfcb6e9/static/static/img/warn.99e51ab.png -------------------------------------------------------------------------------- /static/static/js/manifest.47c127f6253f528e05ba.js: -------------------------------------------------------------------------------- 1 | /******/ (function(modules) { // webpackBootstrap 2 | /******/ // install a JSONP callback for chunk loading 3 | /******/ var parentJsonpFunction = window["webpackJsonp"]; 4 | /******/ window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) { 5 | /******/ // add "moreModules" to the modules object, 6 | /******/ // then flag all "chunkIds" as loaded and fire callback 7 | /******/ var moduleId, chunkId, i = 0, resolves = [], result; 8 | /******/ for(;i < chunkIds.length; i++) { 9 | /******/ chunkId = chunkIds[i]; 10 | /******/ if(installedChunks[chunkId]) { 11 | /******/ resolves.push(installedChunks[chunkId][0]); 12 | /******/ } 13 | /******/ installedChunks[chunkId] = 0; 14 | /******/ } 15 | /******/ for(moduleId in moreModules) { 16 | /******/ if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) { 17 | /******/ modules[moduleId] = moreModules[moduleId]; 18 | /******/ } 19 | /******/ } 20 | /******/ if(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules, executeModules); 21 | /******/ while(resolves.length) { 22 | /******/ resolves.shift()(); 23 | /******/ } 24 | /******/ if(executeModules) { 25 | /******/ for(i=0; i < executeModules.length; i++) { 26 | /******/ result = __webpack_require__(__webpack_require__.s = executeModules[i]); 27 | /******/ } 28 | /******/ } 29 | /******/ return result; 30 | /******/ }; 31 | /******/ 32 | /******/ // The module cache 33 | /******/ var installedModules = {}; 34 | /******/ 35 | /******/ // objects to store loaded and loading chunks 36 | /******/ var installedChunks = { 37 | /******/ 2: 0 38 | /******/ }; 39 | /******/ 40 | /******/ // The require function 41 | /******/ function __webpack_require__(moduleId) { 42 | /******/ 43 | /******/ // Check if module is in cache 44 | /******/ if(installedModules[moduleId]) { 45 | /******/ return installedModules[moduleId].exports; 46 | /******/ } 47 | /******/ // Create a new module (and put it into the cache) 48 | /******/ var module = installedModules[moduleId] = { 49 | /******/ i: moduleId, 50 | /******/ l: false, 51 | /******/ exports: {} 52 | /******/ }; 53 | /******/ 54 | /******/ // Execute the module function 55 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 56 | /******/ 57 | /******/ // Flag the module as loaded 58 | /******/ module.l = true; 59 | /******/ 60 | /******/ // Return the exports of the module 61 | /******/ return module.exports; 62 | /******/ } 63 | /******/ 64 | /******/ // This file contains only the entry chunk. 65 | /******/ // The chunk loading function for additional chunks 66 | /******/ __webpack_require__.e = function requireEnsure(chunkId) { 67 | /******/ var installedChunkData = installedChunks[chunkId]; 68 | /******/ if(installedChunkData === 0) { 69 | /******/ return new Promise(function(resolve) { resolve(); }); 70 | /******/ } 71 | /******/ 72 | /******/ // a Promise means "currently loading". 73 | /******/ if(installedChunkData) { 74 | /******/ return installedChunkData[2]; 75 | /******/ } 76 | /******/ 77 | /******/ // setup Promise in chunk cache 78 | /******/ var promise = new Promise(function(resolve, reject) { 79 | /******/ installedChunkData = installedChunks[chunkId] = [resolve, reject]; 80 | /******/ }); 81 | /******/ installedChunkData[2] = promise; 82 | /******/ 83 | /******/ // start chunk loading 84 | /******/ var head = document.getElementsByTagName('head')[0]; 85 | /******/ var script = document.createElement('script'); 86 | /******/ script.type = 'text/javascript'; 87 | /******/ script.charset = 'utf-8'; 88 | /******/ script.async = true; 89 | /******/ script.timeout = 120000; 90 | /******/ 91 | /******/ if (__webpack_require__.nc) { 92 | /******/ script.setAttribute("nonce", __webpack_require__.nc); 93 | /******/ } 94 | /******/ script.src = __webpack_require__.p + "static/js/" + chunkId + "." + {"0":"234aa88aab8f29cc10e1","1":"3c272e9bf5082eea090f"}[chunkId] + ".js"; 95 | /******/ var timeout = setTimeout(onScriptComplete, 120000); 96 | /******/ script.onerror = script.onload = onScriptComplete; 97 | /******/ function onScriptComplete() { 98 | /******/ // avoid mem leaks in IE. 99 | /******/ script.onerror = script.onload = null; 100 | /******/ clearTimeout(timeout); 101 | /******/ var chunk = installedChunks[chunkId]; 102 | /******/ if(chunk !== 0) { 103 | /******/ if(chunk) { 104 | /******/ chunk[1](new Error('Loading chunk ' + chunkId + ' failed.')); 105 | /******/ } 106 | /******/ installedChunks[chunkId] = undefined; 107 | /******/ } 108 | /******/ }; 109 | /******/ head.appendChild(script); 110 | /******/ 111 | /******/ return promise; 112 | /******/ }; 113 | /******/ 114 | /******/ // expose the modules object (__webpack_modules__) 115 | /******/ __webpack_require__.m = modules; 116 | /******/ 117 | /******/ // expose the module cache 118 | /******/ __webpack_require__.c = installedModules; 119 | /******/ 120 | /******/ // identity function for calling harmony imports with the correct context 121 | /******/ __webpack_require__.i = function(value) { return value; }; 122 | /******/ 123 | /******/ // define getter function for harmony exports 124 | /******/ __webpack_require__.d = function(exports, name, getter) { 125 | /******/ if(!__webpack_require__.o(exports, name)) { 126 | /******/ Object.defineProperty(exports, name, { 127 | /******/ configurable: false, 128 | /******/ enumerable: true, 129 | /******/ get: getter 130 | /******/ }); 131 | /******/ } 132 | /******/ }; 133 | /******/ 134 | /******/ // getDefaultExport function for compatibility with non-harmony modules 135 | /******/ __webpack_require__.n = function(module) { 136 | /******/ var getter = module && module.__esModule ? 137 | /******/ function getDefault() { return module['default']; } : 138 | /******/ function getModuleExports() { return module; }; 139 | /******/ __webpack_require__.d(getter, 'a', getter); 140 | /******/ return getter; 141 | /******/ }; 142 | /******/ 143 | /******/ // Object.prototype.hasOwnProperty.call 144 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; 145 | /******/ 146 | /******/ // __webpack_public_path__ 147 | /******/ __webpack_require__.p = "/"; 148 | /******/ 149 | /******/ // on error function for async loading 150 | /******/ __webpack_require__.oe = function(err) { console.error(err); throw err; }; 151 | /******/ }) 152 | /************************************************************************/ 153 | /******/ ([]); 154 | //# sourceMappingURL=manifest.47c127f6253f528e05ba.js.map -------------------------------------------------------------------------------- /static/static/js/manifest.47c127f6253f528e05ba.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["webpack:///webpack/bootstrap b94a30586fc1293d8b66"],"names":[],"mappings":";AAAA;AACA;AACA;AACA;AACA;AACA;AACA,gBAAQ,oBAAoB;AAC5B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,oBAAY,2BAA2B;AACvC;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,kDAA0C,WAAW,EAAE;AACvD;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,YAAI;AACJ;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,gFAAwE,sDAAsD;AAC9H;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA,mDAA2C,cAAc;;AAEzD;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aAAK;AACL;AACA;;AAEA;AACA;AACA;AACA,mCAA2B,0BAA0B,EAAE;AACvD,yCAAiC,eAAe;AAChD;AACA;AACA;;AAEA;AACA,8DAAsD,+DAA+D;;AAErH;AACA;;AAEA;AACA,kDAA0C,oBAAoB,WAAW","file":"static/js/manifest.47c127f6253f528e05ba.js","sourcesContent":[" \t// install a JSONP callback for chunk loading\n \tvar parentJsonpFunction = window[\"webpackJsonp\"];\n \twindow[\"webpackJsonp\"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {\n \t\t// add \"moreModules\" to the modules object,\n \t\t// then flag all \"chunkIds\" as loaded and fire callback\n \t\tvar moduleId, chunkId, i = 0, resolves = [], result;\n \t\tfor(;i < chunkIds.length; i++) {\n \t\t\tchunkId = chunkIds[i];\n \t\t\tif(installedChunks[chunkId]) {\n \t\t\t\tresolves.push(installedChunks[chunkId][0]);\n \t\t\t}\n \t\t\tinstalledChunks[chunkId] = 0;\n \t\t}\n \t\tfor(moduleId in moreModules) {\n \t\t\tif(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {\n \t\t\t\tmodules[moduleId] = moreModules[moduleId];\n \t\t\t}\n \t\t}\n \t\tif(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules, executeModules);\n \t\twhile(resolves.length) {\n \t\t\tresolves.shift()();\n \t\t}\n \t\tif(executeModules) {\n \t\t\tfor(i=0; i < executeModules.length; i++) {\n \t\t\t\tresult = __webpack_require__(__webpack_require__.s = executeModules[i]);\n \t\t\t}\n \t\t}\n \t\treturn result;\n \t};\n\n \t// The module cache\n \tvar installedModules = {};\n\n \t// objects to store loaded and loading chunks\n \tvar installedChunks = {\n \t\t2: 0\n \t};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n \t// This file contains only the entry chunk.\n \t// The chunk loading function for additional chunks\n \t__webpack_require__.e = function requireEnsure(chunkId) {\n \t\tvar installedChunkData = installedChunks[chunkId];\n \t\tif(installedChunkData === 0) {\n \t\t\treturn new Promise(function(resolve) { resolve(); });\n \t\t}\n\n \t\t// a Promise means \"currently loading\".\n \t\tif(installedChunkData) {\n \t\t\treturn installedChunkData[2];\n \t\t}\n\n \t\t// setup Promise in chunk cache\n \t\tvar promise = new Promise(function(resolve, reject) {\n \t\t\tinstalledChunkData = installedChunks[chunkId] = [resolve, reject];\n \t\t});\n \t\tinstalledChunkData[2] = promise;\n\n \t\t// start chunk loading\n \t\tvar head = document.getElementsByTagName('head')[0];\n \t\tvar script = document.createElement('script');\n \t\tscript.type = 'text/javascript';\n \t\tscript.charset = 'utf-8';\n \t\tscript.async = true;\n \t\tscript.timeout = 120000;\n\n \t\tif (__webpack_require__.nc) {\n \t\t\tscript.setAttribute(\"nonce\", __webpack_require__.nc);\n \t\t}\n \t\tscript.src = __webpack_require__.p + \"static/js/\" + chunkId + \".\" + {\"0\":\"234aa88aab8f29cc10e1\",\"1\":\"3c272e9bf5082eea090f\"}[chunkId] + \".js\";\n \t\tvar timeout = setTimeout(onScriptComplete, 120000);\n \t\tscript.onerror = script.onload = onScriptComplete;\n \t\tfunction onScriptComplete() {\n \t\t\t// avoid mem leaks in IE.\n \t\t\tscript.onerror = script.onload = null;\n \t\t\tclearTimeout(timeout);\n \t\t\tvar chunk = installedChunks[chunkId];\n \t\t\tif(chunk !== 0) {\n \t\t\t\tif(chunk) {\n \t\t\t\t\tchunk[1](new Error('Loading chunk ' + chunkId + ' failed.'));\n \t\t\t\t}\n \t\t\t\tinstalledChunks[chunkId] = undefined;\n \t\t\t}\n \t\t};\n \t\thead.appendChild(script);\n\n \t\treturn promise;\n \t};\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// identity function for calling harmony imports with the correct context\n \t__webpack_require__.i = function(value) { return value; };\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, {\n \t\t\t\tconfigurable: false,\n \t\t\t\tenumerable: true,\n \t\t\t\tget: getter\n \t\t\t});\n \t\t}\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"/\";\n\n \t// on error function for async loading\n \t__webpack_require__.oe = function(err) { console.error(err); throw err; };\n\n\n\n// WEBPACK FOOTER //\n// webpack/bootstrap b94a30586fc1293d8b66"],"sourceRoot":""} --------------------------------------------------------------------------------