├── .github └── ISSUE_TEMPLATE │ └── bug_report.md ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── dns_updater ├── __init__.py ├── api │ ├── __init__.py │ └── worker.py ├── app.py ├── config.py ├── templates │ └── healthcheck.html ├── updater.py ├── utils │ ├── __init__.py │ ├── handler.py │ ├── tool_classes.py │ └── updater_util.py └── workers │ ├── __init__.py │ ├── view_worker.py │ └── zone_worker.py ├── dnsdb ├── __init__.py ├── config.py ├── constant │ ├── __init__.py │ ├── constant.py │ └── operation_type.py ├── deploy.py ├── migrate.py ├── static │ ├── css │ │ ├── app.5137ea317b6c1209dd442cc030992313.css │ │ └── app.5137ea317b6c1209dd442cc030992313.css.map │ ├── favicon.ico │ ├── fonts │ │ ├── element-icons.6f0a763.ttf │ │ ├── fontawesome-webfont.674f50d.eot │ │ ├── fontawesome-webfont.af7ae50.woff2 │ │ ├── fontawesome-webfont.b06871f.ttf │ │ └── fontawesome-webfont.fee66e7.woff │ ├── img │ │ └── fontawesome-webfont.912ec66.svg │ └── js │ │ ├── app.8f6318bdebfe230b5a3f.js │ │ ├── app.8f6318bdebfe230b5a3f.js.map │ │ ├── manifest.cfdad16f39e54a963330.js │ │ ├── manifest.cfdad16f39e54a963330.js.map │ │ ├── vendor.35023790df232692a094.js │ │ └── vendor.35023790df232692a094.js.map ├── templates │ ├── healthcheck.html │ └── index.html └── view │ ├── __init__.py │ ├── api │ └── __init__.py │ └── web │ ├── __init__.py │ ├── auth.py │ ├── config.py │ ├── preview.py │ ├── record.py │ ├── root.py │ ├── subnet.py │ ├── user.py │ ├── view_isp.py │ └── view_record.py ├── dnsdb_command.py ├── dnsdb_common ├── __init__.py ├── dal │ ├── __init__.py │ ├── host_group_conf.py │ ├── models │ │ ├── __init__.py │ │ ├── deploy_history.py │ │ ├── dns_colos.py │ │ ├── dns_header.py │ │ ├── dns_host.py │ │ ├── dns_host_group.py │ │ ├── dns_named_conf.py │ │ ├── dns_record.py │ │ ├── dns_serial.py │ │ ├── dns_zone_conf.py │ │ ├── ippool.py │ │ ├── operation_log.py │ │ ├── operation_log_detail.py │ │ ├── operation_type.py │ │ ├── subnets.py │ │ ├── user.py │ │ ├── view_acl_city_code.py │ │ ├── view_acl_migrate_history.py │ │ ├── view_acl_subnets.py │ │ ├── view_config.py │ │ ├── view_domain_name_state.py │ │ ├── view_domain_names.py │ │ ├── view_isp_status.py │ │ ├── view_isps.py │ │ ├── view_migrate_detail.py │ │ ├── view_migrate_history.py │ │ ├── view_records.py │ │ ├── view_switch_ip_detail.py │ │ └── view_switch_ip_history.py │ ├── operation_log.py │ ├── subnet_ip.py │ ├── user.py │ ├── view_isp_acl.py │ ├── view_migrate.py │ ├── view_record.py │ └── zone_record.py └── library │ ├── IPy.py │ ├── __init__.py │ ├── api.py │ ├── database.py │ ├── decorators.py │ ├── email_util.py │ ├── exception.py │ ├── flaskapp.py │ ├── gunicorn_app.py │ ├── local.py │ ├── log.py │ ├── param_validator.py │ ├── singleton.py │ ├── utils.py │ └── validator.py ├── dnsdb_fe ├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .postcssrc.js ├── __init__.py ├── build │ ├── build.js │ ├── check-versions.js │ ├── dev-client.js │ ├── dev-server.js │ ├── utils.js │ ├── vue-loader.conf.js │ ├── webpack.base.conf.js │ ├── webpack.dev.conf.js │ ├── webpack.prod.conf.js │ └── webpack.test.conf.js ├── config │ ├── dev.env.js │ ├── index.js │ ├── prod.env.js │ └── test.env.js ├── index.html ├── package-lock.json ├── package.json ├── src │ ├── App.vue │ ├── common │ │ └── util.js │ ├── components │ │ ├── Menu.vue │ │ ├── admin │ │ │ └── Login.vue │ │ ├── conf │ │ │ ├── Conf.vue │ │ │ ├── HeaderEdit.vue │ │ │ ├── HostManager.vue │ │ │ └── ZoneManager.vue │ │ ├── log │ │ │ └── DnsLog.vue │ │ ├── preview │ │ │ └── Preview.vue │ │ ├── record │ │ │ ├── Record.vue │ │ │ ├── RecordManager.vue │ │ │ └── SubnetManager.vue │ │ ├── system │ │ │ ├── System.vue │ │ │ └── UserManager.vue │ │ └── view │ │ │ ├── AclManager.vue │ │ │ ├── DomainManager.vue │ │ │ ├── IpManager.vue │ │ │ ├── IspManager.vue │ │ │ ├── Migrate.vue │ │ │ └── View.vue │ ├── main.js │ ├── router │ │ └── index.js │ ├── store │ │ └── view.js │ └── style.css └── static │ ├── .!83440!favicon.ico │ ├── .!83441!favicon.ico │ ├── .!83442!favicon.ico │ ├── .!83443!favicon.ico │ ├── .!83444!favicon.ico │ ├── .gitkeep │ └── favicon.ico ├── docs ├── Makefile ├── conf.py ├── index.rst └── make.bat ├── etc ├── beta │ ├── common.conf │ ├── dnsdb-updater.conf │ ├── dnsdb.conf │ ├── supervisor-dnsdb.conf │ └── supervisor-updater.conf └── template │ └── zone_header ├── requirements.txt ├── setup.cfg ├── setup.py ├── test-requirements.txt └── tools ├── __init__.py ├── install_venv.py ├── install_venv_common.py ├── updater ├── mkrdns └── pre_updater_start.sh └── with_venv.sh /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: 便于贡献者复现和定位问题 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **问题描述** 11 | 12 | 13 | **环境配置** 14 | 15 | 16 | **复现步骤** 17 | 1. 18 | 2. 19 | 3. 20 | 21 | **实际输出结果** 22 | 23 | 24 | **期望输出结果** 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.egg-info 3 | *.venv 4 | AUTHORS 5 | ChangeLog 6 | build/* 7 | .idea 8 | */tmp/ 9 | 10 | dnsdb_fe/.DS_Store 11 | dnsdb_fe/node_modules/ 12 | dnsdb_fe/dist/ 13 | dnsdb_fe/npm-debug.log* 14 | dnsdb_fe/yarn-debug.log* 15 | dnsdb_fe/yarn-error.log* 16 | dnsdb_fe/test/unit/coverage 17 | dnsdb_fe/test/e2e/reports 18 | dnsdb_fe/selenium-debug.log 19 | # Editor directories and files 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.db 25 | 26 | docs/_build/ 27 | etc/dev/ 28 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at open-dev@qunar.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://img.shields.io/github/release/qunarcorp/open_dnsdb.svg) 2 | ![](https://img.shields.io/github/license/qunarcorp/open_dnsdb.svg) 3 | ![](https://img.shields.io/github/languages/code-size/qunarcorp/open_dnsdb.svg) 4 | # OpenDnsdb 5 | 6 | ## 项目主页 7 | 8 | OpenDnsdb 项目相关文档: [文档链接](../../wikis/home) 9 | 10 | 11 | ## 简介 12 | 13 | OpenDnsdb 是去哪儿网OPS团队开源的DNS管理系统,用于添加、修改、删除zones/records. 14 | 使用简单并可靠的方法管理View、ACL、网段等. 15 | 详尽的日志,便于审计. 16 | 17 | OpenDnsdb并不是一个DNS服务器,而是一个对现有DNS服务器的管理系统,提供Web管理UI以及命令行工具等. 18 | 19 | 对OpenDnsdb的操作,会生成DNS配置文件并同步给DNS服务器。也就是说OpenDnsdb的故障或不可用并不会对DNS服务本身造成任何影响. 20 | 21 | OpenDnsdb is an open source DNS management system for the OPS team. It is used to add, modify, and delete zones/records. Use simple and reliable methods to manage View, ACL, network segment, etc. Detailed logs for auditing. 22 | 23 | OpenDnsdb is not a DNS server, but a management system for existing DNS servers, providing Web management UI and command line tools. 24 | 25 | For OpenDnsdb operations, a DNS configuration file is generated and synchronized to the DNS server. That is to say, the failure or unavailability of OpenDnsdb does not affect the DNS service itself. 26 | 27 | 28 | ## 主要功能 29 | 30 | * 支持 Bind 9. 31 | * IP管理, 管理公司网段及ip,可以实现域名和ip的自动绑定 32 | * 域名管理, 域名的增、删、改、查. 33 | * View域名管理, view域名的增删改查、状态修改,view域名的迁移. 34 | * 配置管理, 管理zone文件,线上配置与数据库配置同步,修改配置自动完成部署. 35 | * 日志, 关键操作都有日志记录,并可通过页面进行查询,便于审查 36 | * 支持RESTful API, 支持Webhook. 37 | * 基于Python 3 开发, 支持Postgresql和SQLite. 38 | 39 | 40 | ## 应用结构 41 | 42 | * docs/ 43 | 各种说明文档、手册, copyright/license等. 44 | 45 | * dnsdb_fe/ 46 | web ui 47 | 48 | * tools/ 49 | 同步脚本, 各种工具. 50 | 51 | * etc/ 52 | 开发、测试环境的配置文件, 配置模板等. 53 | 54 | * dnsdb_command.py 55 | 数据库初始脚本 56 | 57 | * dnsdb/constant 58 | 常量设置,用到的正则匹配规则 59 | 60 | 61 | ## 安装手册 62 | * 环境 Python:3.6.8 pip:19.0.3 63 | 64 | * 支持的浏览器: chrome, Firefox 65 | 66 | * 安装virtualenv: `pip install virtualenv` 67 | 68 | * 项目克隆 69 | 70 | * 目录创建:`mkdir -p /var/log/open_dnsdb/` 71 | 72 | ```ini 73 | ; 日志目录配置: etc/beta/common.conf 74 | [log] 75 | log-dir = /var/log/open_dnsdb/ 76 | ``` 77 | 78 | * 切换到项目目录: `cd open_dnsdb ` 79 | 80 | * 初始化项目python环境: 81 | 82 | ```bash 83 | $ python tools/install_venv.py -p /usr/bin/python3.6 84 | # 命令行参数: 85 | # -p 使用的python解释器版本, 确保使用virtualenv创建虚拟环境是python3.6+ 86 | # 如果确认virtualenv命令是用python3安装的, 这个参数可以省略 87 | ``` 88 | 89 | * 启用虚拟环境 90 | 91 | ```bash 92 | $ source .venv/bin/activate 93 | $ python -V # 确认python版本为3.6+ 94 | ``` 95 | 96 | * 初始化数据库 97 | * 数据库配置: etc/beta/common.conf 98 | ```ini 99 | [DB] 100 | connection=sqlite:////usr/local/open_dnsdb/dnsdb.db 101 | ``` 102 | * touch /usr/local/open_dnsdb/dnsdb.db 新建数据文件 103 | * export FLASK_APP=dnsdb_command.py 104 | * export FLASK_ENV=beta 105 | * flask deploy (生成测试账号: test 密码:123456) 106 | 107 | * 生成程序控制脚本: tools/with_venv.sh python setup.py install 108 | 109 | * 安装supervisor用于管理python进程: 110 | * 安装: sudo pip install supervisor 111 | ``` 112 | # python3版本supervisor安装 113 | pip install git+https://github.com/Supervisor/supervisor 114 | ``` 115 | 116 | * 生成默认配置: echo_supervisord_conf > /etc/supervisord.conf 117 | 118 | * 修改配置文件 vim /etc/supervisord.conf 119 | ```ini 120 | [supervisord] 121 | childlogdir=/var/log/open_dnsdb ;日志文件位置 122 | 123 | [include] 124 | files = /etc/supervisor/conf.d/*.conf 125 | ``` 126 | 127 | * mkdir -p /etc/supervisor/conf.d 128 | 129 | * 添加openDnsdb项目配配置: 130 | * dnsdb: cp etc/beta/supervisor-dnsdb.conf /etc/supervisor/conf.d/open-dnsdb.conf 131 | * updater(仅bind服务器需要): cp etc/beta/supervisor-updater.conf /etc/supervisor/conf.d/open-dnsdb-updater.conf 132 | 133 | * 启动: supervisord -c /etc/supervisord.conf 134 | 135 | * 查看是否启动成功: ps aux | grep supervisord 136 | 137 | * supervisorctl -c /etc/supervisord.conf 138 | 139 | 140 | 141 | ## ChangeLog 142 | 143 | * v0.2 - 2019-03-21 144 | 145 | **添加** 146 | 147 | ​ 添加ipv6支持(暂不支持ipv6反解) 148 | 149 | **修改** 150 | 151 | ​ 升级python版本,支持python3.6+ 152 | 153 | ## 感谢 154 | 155 | 感谢以下同学对项目修改提出的宝贵建议: 156 | 157 | * [wss434631143](https://github.com/wss434631143) 158 | 159 | -------------------------------------------------------------------------------- /dns_updater/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/open_dnsdb/f717e9752f00a2937ff523c2ca120c2ebae57bca/dns_updater/__init__.py -------------------------------------------------------------------------------- /dns_updater/api/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from flask import (Blueprint) 4 | from oslo_config import cfg 5 | 6 | from dns_updater.api.worker import start_update_thread 7 | from dns_updater.utils.updater_util import send_alarm_email 8 | from dnsdb_common.library.decorators import authenticate 9 | from dnsdb_common.library.decorators import parse_params 10 | from dnsdb_common.library.decorators import resp_wrapper_json 11 | 12 | CONF = cfg.CONF 13 | 14 | bp = Blueprint('api', 'api') 15 | 16 | 17 | @bp.route('/notify_update/named', methods=['POST']) 18 | @authenticate 19 | @parse_params([dict(name='group_name', type=str, required=True, nullable=False), 20 | dict(name='group_conf_md5', type=str, required=True, nullable=False)]) 21 | @resp_wrapper_json 22 | def update_named(group_name, group_conf_md5): 23 | if group_name != CONF.host_group: 24 | return send_alarm_email( 25 | u'Host %s group not match: local %s, param: %s' % (CONF.host_ip, CONF.host_group, group_name)) 26 | start_update_thread('named.conf', group_conf_md5=group_conf_md5, group_name=group_name) 27 | return 28 | 29 | 30 | @bp.route('/notify_update', methods=['POST']) 31 | @authenticate 32 | @parse_params([dict(name='update_type', type=str, required=True, nullable=False), 33 | dict(name='group_name', type=str, required=True, nullable=False), 34 | dict(name='params', type=dict, required=True, nullable=False)]) 35 | @resp_wrapper_json 36 | def update_conf(update_type, group_name, params): 37 | start_update_thread(update_type, group_name=group_name, **params) 38 | return 39 | -------------------------------------------------------------------------------- /dns_updater/app.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import sys 5 | 6 | from flask import (Flask, render_template) 7 | from flask_restful import abort 8 | from jinja2 import TemplateNotFound 9 | from oslo_config import cfg 10 | 11 | from dns_updater.config import setup_config 12 | from dnsdb_common.library.gunicorn_app import GunicornApplication, number_of_workers 13 | from dnsdb_common.library.log import getLogger, setup 14 | 15 | CONF = cfg.CONF 16 | setup('dnsdb_updater_www') 17 | log = getLogger(__name__) 18 | 19 | 20 | def create_app(): 21 | setup_config(sys.argv[1], 'dnsdb-updater', conf_dir=os.path.dirname(os.path.dirname(__file__))) 22 | log.error('This host belong to host group %s' % CONF.host_group) 23 | 24 | app = Flask(__name__) 25 | app.config['SECRET_KEY'] = CONF.etc.secret_key 26 | 27 | from dns_updater.utils.updater_util import check_necessary_options 28 | check_necessary_options() 29 | 30 | @app.route("/healthcheck.html", methods=['GET']) 31 | def health_check(): 32 | try: 33 | return render_template('healthcheck.html') 34 | except TemplateNotFound: 35 | abort(404) 36 | 37 | @app.context_processor 38 | def default_context_processor(): 39 | result = {'config': {'BASE_URL': CONF.web.base_url}} 40 | return result 41 | 42 | from dns_updater import api 43 | app.register_blueprint(api.bp, url_prefix='/api') 44 | 45 | return app 46 | 47 | 48 | application = create_app() 49 | 50 | 51 | def app_start(): 52 | options = { 53 | 'workers': number_of_workers(), 54 | } 55 | for option in CONF.gunicorn: 56 | options[option] = CONF.gunicorn[option] 57 | 58 | GunicornApplication(application, options).run() 59 | 60 | 61 | if __name__ == '__main__': 62 | application.run(host='0.0.0.0', port=9000, debug=True) 63 | -------------------------------------------------------------------------------- /dns_updater/config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | 5 | from oslo_config import cfg 6 | 7 | CONF = cfg.CONF 8 | 9 | CONF.register_opts([ 10 | cfg.StrOpt('env'), 11 | cfg.StrOpt('secret_key'), 12 | cfg.StrOpt('log_dir'), 13 | cfg.StrOpt('tmp_dir'), 14 | cfg.StrOpt('pidfile'), 15 | cfg.StrOpt('backup_dir'), 16 | cfg.IntOpt('zone_update_interval'), 17 | cfg.StrOpt('allow_ip') 18 | ], 'etc') 19 | 20 | CONF.register_opts([ 21 | cfg.StrOpt('dnsdbapi_url'), 22 | ], 'api') 23 | 24 | CONF.register_opts([ 25 | cfg.StrOpt('log-dir'), 26 | cfg.StrOpt('log-file'), 27 | cfg.StrOpt('debug'), 28 | cfg.StrOpt('verbose'), 29 | ], 'log') 30 | 31 | CONF.register_opts([ 32 | cfg.StrOpt('server'), 33 | cfg.StrOpt('port'), 34 | cfg.StrOpt('from_addr'), 35 | cfg.StrOpt('password', default=''), 36 | cfg.StrOpt('info_list'), 37 | cfg.StrOpt('alert_list'), 38 | ], 'MAIL') 39 | 40 | CONF.register_opts([ 41 | cfg.StrOpt('base-url', 42 | default='/', 43 | help='The url prefix of this site.'), 44 | cfg.StrOpt('run-mode', 45 | default="werkzeug", 46 | choices=('gunicorn', 'werkzeug'), 47 | help="Run server use the specify mode."), 48 | cfg.StrOpt('bind', 49 | default='0.0.0.0', 50 | help='The IP address to bind'), 51 | cfg.IntOpt('port', 52 | default=8080, 53 | help='The port to listen'), 54 | cfg.BoolOpt('debug', 55 | default=False), 56 | ], 'web') 57 | 58 | CONF.register_opts([ 59 | cfg.StrOpt('config', 60 | default=None, 61 | help='The path to a Gunicorn config file.'), 62 | cfg.StrOpt('bind', 63 | default='0.0.0.0:8888'), 64 | cfg.IntOpt('workers', 65 | default=0, 66 | help='The number of worker processes for handling requests'), 67 | cfg.BoolOpt('daemon', 68 | default=False, 69 | help='Daemonize the Gunicorn process'), 70 | cfg.StrOpt('accesslog', 71 | default=None, 72 | help='The Access log file to write to.' 73 | '"-" means log to stderr.'), 74 | cfg.StrOpt('loglevel', 75 | default='info', 76 | help='The granularity of Error log outputs.', 77 | choices=('debug', 'info', 'warning', 'error', 'critical')), 78 | cfg.BoolOpt('ignore-healthcheck-accesslog', 79 | default=False), 80 | cfg.IntOpt('timeout', 81 | default=30, 82 | help='Workers silent for more than this many seconds are ' 83 | 'killed and restarted.'), 84 | cfg.StrOpt('worker-class', 85 | default='sync', 86 | help='The type of workers to use.', 87 | choices=('sync', 'eventlet', 'gevent', 'tornado')) 88 | ], 'gunicorn') 89 | 90 | CONF.register_opts([ 91 | cfg.StrOpt('server'), 92 | cfg.StrOpt('port'), 93 | cfg.StrOpt('from_addr'), 94 | cfg.StrOpt('info_list'), 95 | cfg.StrOpt('alert_list'), 96 | ], 'MAIL') 97 | 98 | CONF.register_opts([ 99 | cfg.StrOpt('named_dir'), 100 | cfg.StrOpt('zone_dir'), 101 | cfg.StrOpt('named_checkconf'), 102 | cfg.StrOpt('named_zonecheck'), 103 | cfg.StrOpt('mkrdns'), 104 | cfg.StrOpt('acl_dir'), 105 | cfg.StrOpt('rndc'), 106 | cfg.StrOpt('user', default='named'), 107 | cfg.StrOpt('group', default='named'), 108 | ], 'bind_default') 109 | 110 | CONF.register_opts([ 111 | cfg.StrOpt('named_dir'), 112 | cfg.StrOpt('zone_dir'), 113 | cfg.StrOpt('named_checkconf'), 114 | cfg.StrOpt('rndc'), 115 | ], 'ViewSlave') 116 | 117 | CONF.register_opts([ 118 | cfg.StrOpt('named_dir'), 119 | cfg.StrOpt('zone_dir'), 120 | cfg.StrOpt('acl_dir'), 121 | cfg.StrOpt('named_checkconf'), 122 | cfg.StrOpt('rndc'), 123 | ], 'ViewMaster') 124 | 125 | CONF.register_opts([ 126 | cfg.StrOpt('named_dir'), 127 | cfg.StrOpt('zone_dir'), 128 | cfg.StrOpt('named_checkconf'), 129 | cfg.StrOpt('rndc'), 130 | ], 'Master') 131 | 132 | 133 | def setup_config(app_env, app_kind, conf_dir): 134 | common_config_file = os.path.join(conf_dir, "etc/{}/common.conf".format(app_env)) 135 | default_config_files = [common_config_file] 136 | app_config_file = os.path.join(conf_dir, "etc/{}/{}.conf".format(app_env, app_kind)) 137 | default_config_files.append(app_config_file) 138 | CONF(default_config_files=default_config_files, args=[]) 139 | 140 | from dns_updater.utils.updater_util import (DnsdbApi, get_self_ip) 141 | CONF.host_ip = get_self_ip() 142 | CONF.host_group = DnsdbApi.get_host_group()['data'] 143 | setattr(CONF, 'bind_conf', CONF.bind_default) 144 | 145 | if getattr(CONF, CONF.host_group, None): 146 | for k, v in CONF[CONF.host_group].items(): 147 | if v is not None: 148 | setattr(CONF.bind_conf, k, v) 149 | -------------------------------------------------------------------------------- /dns_updater/templates/healthcheck.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 | 200 ok 9 | 10 | -------------------------------------------------------------------------------- /dns_updater/updater.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import fcntl 4 | import importlib 5 | import os 6 | import signal 7 | import sys 8 | import time 9 | 10 | from oslo_config import cfg 11 | 12 | from dns_updater.config import setup_config 13 | from dns_updater.utils.tool_classes import (QApplication) 14 | from dnsdb_common.library.exception import UpdaterErr 15 | from dnsdb_common.library.log import (getLogger, setup) 16 | 17 | setup('dnsdb_upater_zone') 18 | log = getLogger(__name__) 19 | 20 | fp = None 21 | CONF = cfg.CONF 22 | 23 | 24 | def _get_handler(): 25 | mapping = { 26 | 'ViewMaster': 'dns_updater.workers.view_worker', 27 | 'default': 'dns_updater.workers.zone_worker' 28 | } 29 | zone_group = CONF.host_group 30 | if not zone_group.endswith('Master'): 31 | raise UpdaterErr('%s, slave group does not need to start updater.' % zone_group) 32 | # dnsdb请求zone信息 33 | module = importlib.import_module(mapping.get(zone_group, mapping['default'])) 34 | return module.handler 35 | 36 | 37 | def _create_pid_file(): 38 | global fp 39 | pidfile = CONF.etc.pidfile 40 | if pidfile is None: 41 | raise UpdaterErr("No pidfile option found in config file.") 42 | try: 43 | fp = open(pidfile, 'w') 44 | # LOCK_EX /* exclusive lock */ 45 | # LOCK_NB * don't block when locking */ 46 | fcntl.flock(fp, fcntl.LOCK_EX | fcntl.LOCK_NB) 47 | fp.truncate() 48 | pid = os.getpid() 49 | fp.write(str(pid)) 50 | fp.flush() 51 | except Exception as e: 52 | raise UpdaterErr("Failed to lock pidfile, perhaps named_updater is already running.") 53 | 54 | 55 | def _signal_handler(n=0, e=0): 56 | log.error("Shuting down normally.") 57 | sys.exit(0) 58 | 59 | 60 | class DnsdbUpdater(QApplication): 61 | name = "dnsdb-zone-updater" 62 | version = "1.0" 63 | 64 | def init_config(self, argv=None): 65 | setup_config(sys.argv[1], 'dnsdb-updater', conf_dir=os.path.dirname(os.path.dirname(__file__))) 66 | log.error('Host belong to group: %s' % CONF.host_group) 67 | 68 | def init_app(self): # 可选 69 | from dns_updater.utils.updater_util import check_necessary_options 70 | super(DnsdbUpdater, self).init_app() 71 | try: 72 | check_necessary_options() 73 | _create_pid_file() 74 | # SIGTERM software termination signal 75 | signal.signal(signal.SIGTERM, _signal_handler) 76 | except Exception as e: 77 | log.exception(e, exc_info=1) 78 | sys.exit(1) 79 | 80 | def main_loop(self): 81 | handler = _get_handler() 82 | while True: 83 | handler() 84 | time.sleep(CONF.etc.zone_update_interval) 85 | 86 | 87 | updater = DnsdbUpdater().make_entry_point() 88 | 89 | if __name__ == '__main__': 90 | updater() 91 | -------------------------------------------------------------------------------- /dns_updater/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/open_dnsdb/f717e9752f00a2937ff523c2ca120c2ebae57bca/dns_updater/utils/__init__.py -------------------------------------------------------------------------------- /dns_updater/utils/handler.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from dns_updater.utils.updater_util import DnsdbApi 4 | from dnsdb_common.library.log import getLogger 5 | from dns_updater.utils.updater_util import send_alarm_email 6 | log = getLogger(__name__) 7 | 8 | from dns_updater.utils.tool_classes import GenericWorker 9 | 10 | 11 | class WatchZone(GenericWorker): 12 | def __init__(self, interval, queue_name): 13 | super(WatchZone, self).__init__(interval) 14 | self.queue_name = queue_name 15 | # todo 根据queue获取handle 16 | self.zone_handler = lambda x: x 17 | 18 | def handler(self): 19 | log.info('%s worker start' % self.queue_name) 20 | try: 21 | zones = DnsdbApi.get_update_zones(self.queue_name) 22 | if zones: 23 | self.zone_handler(zones) 24 | except Exception as e: 25 | log.exception(e) 26 | send_alarm_email(u"[CRITICAL] Failed to handle zone update of %s, because: %s" % 27 | (self.queue_name, e.message)) 28 | 29 | -------------------------------------------------------------------------------- /dns_updater/utils/tool_classes.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import sys 4 | import threading 5 | from queue import Queue 6 | 7 | from oslo_config import cfg 8 | 9 | from dnsdb_common.library.log import getLogger, setup 10 | from dnsdb_common.library.exception import UpdaterErr 11 | from dnsdb_common.library.singleton import Singleton 12 | log = getLogger(__name__) 13 | 14 | setup('QApplication') 15 | CONF = cfg.CONF 16 | 17 | 18 | def parse_args(argv, version, default_config_files=None): 19 | cfg.CONF(argv[1:], 20 | project='qlib', 21 | version=version, 22 | default_config_files=default_config_files) 23 | 24 | 25 | class QApplication(Singleton): 26 | name = "QApplication" 27 | version = "0" 28 | 29 | def init_config(self, argv=None): 30 | is_argv_specified = False 31 | if isinstance(argv, (list, tuple)): 32 | is_argv_specified = True 33 | argv = [sys.argv[0]] + list(argv) 34 | if not is_argv_specified: 35 | argv = sys.argv 36 | parse_args(argv, self.version) 37 | 38 | def setup_logger(self): 39 | setup(self.name) 40 | 41 | def init_app(self): 42 | pass 43 | 44 | def on_shutdown(self): 45 | pass 46 | 47 | def main_loop(self): 48 | raise UpdaterErr('This function has not been implemented yet') 49 | 50 | def run(self): 51 | log.debug("app: %s, version: %s", 52 | self.name, self.version) 53 | log.debug("Initializing the application.") 54 | self.init_config() 55 | self.setup_logger() 56 | self.init_app() 57 | log.debug("Starting the application.") 58 | self.main_loop() 59 | self.on_shutdown() 60 | log.debug("Shutdown the application.") 61 | 62 | def make_entry_point(self): 63 | def wrap(): 64 | self.run() 65 | 66 | return wrap 67 | 68 | 69 | class GenericWorker(threading.Thread): 70 | def __init__(self, interval): 71 | self.interval = interval 72 | self.event = threading.Event() 73 | self._is_running = False 74 | super(GenericWorker, self).__init__() 75 | 76 | def handler(self): 77 | raise NotImplementedError 78 | 79 | def run(self): 80 | log.info('%s thread start.' % self.__class__.__name__) 81 | self._is_running = True 82 | while self._is_running: 83 | self.handler() 84 | if self.event.wait(self.interval): 85 | break 86 | 87 | 88 | def stop(self): 89 | log.info('%s thread stop.' % self.__class__.__name__) 90 | self._is_running = False 91 | self.event.set() 92 | 93 | 94 | class ZoneUpdateHandler(threading.Thread): 95 | def __init__(self, queue, handler): 96 | super(ZoneUpdateHandler, self).__init__() 97 | self.event = threading.Event() 98 | self.lock = threading.Lock() 99 | self.zones_to_update = set() 100 | self.zones_queues = Queue() 101 | self.queue_name = queue 102 | self.daemon = True 103 | self.handler = handler 104 | 105 | def run(self): 106 | log.info('ZoneUpdateHandler thread start.') 107 | while not self.event.wait(0.1): 108 | zone = self.zones_queues.get() 109 | with self.lock: 110 | self.zones_to_update.remove(zone) 111 | self.handler(zone) 112 | log.error('ZoneUpdateHandler thread end.') 113 | 114 | def add_zones(self, zones): 115 | for zone in zones: 116 | with self.lock: 117 | if zone not in self.zones_to_update: 118 | self.zones_to_update.add(zone) 119 | self.zones_queues.put(zone) 120 | if not self.isAlive(): 121 | log.error('ZoneUpdateHandler thread is stopped by accident.') 122 | raise Exception 123 | 124 | -------------------------------------------------------------------------------- /dns_updater/workers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/open_dnsdb/f717e9752f00a2937ff523c2ca120c2ebae57bca/dns_updater/workers/__init__.py -------------------------------------------------------------------------------- /dns_updater/workers/view_worker.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import datetime 4 | 5 | from dns_updater.utils.updater_util import * 6 | 7 | CONF = cfg.CONF 8 | 9 | TMP_DIR = CONF.etc.tmp_dir 10 | ZONE_DIR = CONF.bind_conf.zone_dir 11 | 12 | 13 | def _make_zone_file_from_dnsdb(zone): 14 | zone_info = DnsdbApi.get_zone_info(zone)['data'] 15 | serial = zone_info["serial_num"] 16 | record_dict = zone_info["records"] 17 | header = zone_info['header'] 18 | 19 | isp_file_dict = {} 20 | for isp, record_list in record_dict.items(): 21 | tmp_dir = os.path.join(CONF.etc.tmp_dir, 'var/named', isp) 22 | make_dir(tmp_dir) 23 | tmp_zonefile_path = os.path.join(tmp_dir, zone) 24 | make_zone_file(zone, tmp_zonefile_path, serial, header, record_list) 25 | checkzone(zone, tmp_zonefile_path) 26 | isp_file_dict[isp] = { 27 | 'src': tmp_zonefile_path, 28 | 'dst': os.path.join(ZONE_DIR, isp, zone) 29 | } 30 | make_dir(os.path.join(ZONE_DIR, isp)) 31 | return isp_file_dict 32 | 33 | 34 | def _backup_debug_file(debug_file): 35 | error_log = debug_file + datetime.datetime.now().strftime("%Y-%m-%d-%H:%M:%S") 36 | shutil.copyfile(debug_file, error_log) 37 | return error_log 38 | 39 | 40 | def _copy_and_reload(isp_file_dict, zone): 41 | for isp, file_info in isp_file_dict.items(): 42 | if os.system("cp -f %s %s >/dev/null 2>&1" % (file_info['src'], file_info['dst'])) != 0: 43 | raise UpdaterErr("Failed to copy file: src: %s, dst: %s" % (file_info['src'], file_info['dst'])) 44 | backup_file(isp, file_info['src']) 45 | 46 | if CONF.etc.env != 'dev': 47 | rndc_debugfile = make_debugfile_path("rndc") 48 | if os.system("%s reload >%s 2>&1" % (CONF.bind_conf.rndc, rndc_debugfile)) != 0: 49 | error_log = backup_debug_file(rndc_debugfile) 50 | raise UpdaterErr("Failed to reload:%s, see %s." % (zone, error_log)) 51 | log.info("Reloaded %s." % zone) 52 | return True 53 | 54 | 55 | def _send_all_changes_to_opsteam(isp_file_dict): 56 | diff_content = '' 57 | for isp, files in isp_file_dict.items(): 58 | diff = get_file_diff(files['dst'], files['src']) 59 | if diff: 60 | diff_content += diff + "\n"*3 61 | 62 | send_zone_diff_email(diff_content) 63 | 64 | 65 | def handler(): 66 | zone_list = DnsdbApi.get_update_zones(CONF.host_group) 67 | for zone_name in zone_list: 68 | try: 69 | isp_file_dict = _make_zone_file_from_dnsdb(zone_name) 70 | _send_all_changes_to_opsteam(isp_file_dict) 71 | if DnsdbApi.can_reload(): 72 | _copy_and_reload(isp_file_dict, zone_name) 73 | DnsdbApi.update_zone_serial(zone_name) 74 | except Exception as e: 75 | log.exception(e) 76 | send_alarm_email(u'zone %s 更新失败\n原因: %s' % (zone_name, e)) 77 | -------------------------------------------------------------------------------- /dns_updater/workers/zone_worker.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from dns_updater.utils.updater_util import * 4 | import time 5 | 6 | TMP_DIR = CONF.etc.tmp_dir 7 | ZONE_DIR = CONF.bind_conf.zone_dir 8 | 9 | 10 | def _copy_named_files(): 11 | zone_dir = ZONE_DIR 12 | tmp = os.path.join(TMP_DIR, 'var/named') 13 | make_dir(tmp) 14 | if os.system("cp -Rf %s/* %s/var/named/ >/dev/null 2>&1" % (zone_dir, TMP_DIR)) != 0: 15 | raise UpdaterErr("Failed to copy zone files to tmp_dir.") 16 | log.info("Copied named's files to %s.", TMP_DIR) 17 | 18 | # Return the output from mkrdns which will be used extract all modified PTR zones. 19 | def _build_PTR_records(): 20 | debug_file = make_debugfile_path("mkrdns") 21 | if CONF.etc.env == 'dev': 22 | return debug_file 23 | if os.system("%s -rootdir %s %s >%s 2>&1" % (CONF.bind_conf.mkrdns, 24 | TMP_DIR, get_named_path(), debug_file)) != 0: 25 | error_log = backup_debug_file(debug_file) 26 | raise UpdaterErr("mkrdns did not return success, see %s." % error_log) 27 | log.info("PTR zones has been built.") 28 | return debug_file 29 | 30 | 31 | # Get all the PTR zones which should be reload. 32 | def _get_modified_PTR_zones(mkrdns_output, zone_file_dict): 33 | output, exit_code = run_command_with_code("grep 'Updating file' " + mkrdns_output, check_exit_code=False) 34 | if int(exit_code) != 0: 35 | log.info("No PTR zone needs to be reloaded.") 36 | return 37 | for buf in io.StringIO(output): 38 | ptn = re.match('^Updating file "(%s/var/named/[^"]*)".*' % 39 | TMP_DIR, buf) 40 | if ptn is not None: 41 | zone_file = ptn.group(1) 42 | zone = zone_file.split('/')[-1] 43 | 44 | zonename = _build_zonename_for_PTR_zone(zone) 45 | zone_file_dict[zonename] = { 46 | 'src': zone_file, 47 | 'dst': os.path.join(ZONE_DIR, zone) 48 | } 49 | checkzone(zonename, zone_file) 50 | 51 | 52 | def _build_zonename_for_PTR_zone(zone): 53 | tokens = zone.split(".") 54 | zonename = "" 55 | for j in range(len(tokens) - 2, -1, -1): 56 | zonename += tokens[j] + "." 57 | return zonename + "IN-ADDR.ARPA" 58 | 59 | 60 | def handler(): 61 | zone_list = DnsdbApi.get_update_zones(CONF.host_group) 62 | log.info('zones to update: %s' % zone_list) 63 | for name in zone_list: 64 | zone_file_dict = {} 65 | try: 66 | _copy_named_files() 67 | current_zonefile_path = os.path.join(ZONE_DIR, name) 68 | if not check_file_exists(current_zonefile_path): 69 | raise UpdaterErr('Zone file not exist: %s' % current_zonefile_path) 70 | 71 | tmp_zonefile_path = make_zone_file_from_dnsdb(name) 72 | if not is_need_update_zone(tmp_zonefile_path, current_zonefile_path): 73 | continue 74 | checkzone(name, tmp_zonefile_path) 75 | zone_file_dict[name] = { 76 | 'src': tmp_zonefile_path, 77 | 'dst': current_zonefile_path 78 | } 79 | 80 | mkrdns_output = _build_PTR_records() 81 | _get_modified_PTR_zones(mkrdns_output, zone_file_dict) 82 | log.info('Update zones:\n %s' % (','.join(zone_file_dict.keys()))) 83 | send_changes_to_opsteam(current_zonefile_path, tmp_zonefile_path) 84 | if DnsdbApi.can_reload(): 85 | reload_and_backup_zones(zone_file_dict) 86 | DnsdbApi.update_zone_serial(name) 87 | log.info('update_zone_serial') 88 | time.sleep(1) 89 | except UpdaterErr as e: 90 | log.error(e.message) 91 | send_alarm_email(u'zone %s 更新失败\n原因: %s' % (name, e.message)) 92 | except Exception as e: 93 | log.exception(e) 94 | send_alarm_email(u'zone %s 更新失败\n原因: %s' % (name, e)) 95 | -------------------------------------------------------------------------------- /dnsdb/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import sys 5 | 6 | from flask import Flask 7 | from flask_login import LoginManager 8 | from flask_restful import abort 9 | from oslo_config import cfg 10 | 11 | from dnsdb.config import Config, setup_config 12 | from dnsdb_common.dal import db 13 | from dnsdb_common.library.log import getLogger 14 | from dnsdb_common.library.gunicorn_app import GunicornApplication, number_of_workers 15 | from dnsdb_common.library.utils import make_tmp_dir 16 | from dnsdb_common.library.log import setup 17 | 18 | CONF = cfg.CONF 19 | 20 | setup('dnsdb') 21 | LOG = getLogger(__name__) 22 | 23 | def get_flask_app(): 24 | app = Flask(__name__) 25 | app.config.from_object(CONF.flask_conf) 26 | db.init_app(app) 27 | return app 28 | 29 | 30 | def init_login_manager(): 31 | login_manager = LoginManager() 32 | login_manager.session_protection = 'strong' 33 | login_manager.login_view = 'auth.login' 34 | 35 | from dnsdb_common.dal.models.user import User, AnonymousUser 36 | login_manager.anonymous_user = AnonymousUser 37 | 38 | @login_manager.user_loader 39 | def load_user(user_id): 40 | return User.query.get(int(user_id)) 41 | 42 | return login_manager 43 | 44 | 45 | def createApp(app_env, app_kind, conf_dir): 46 | app = Flask(__name__) 47 | config_obj = Config(app_env, app_kind, conf_dir) 48 | CONF.flask_conf = config_obj 49 | app.config.from_object(config_obj) 50 | 51 | CONF.tmp_dir = make_tmp_dir('./tmp') 52 | 53 | db.init_app(app) 54 | login_manager = init_login_manager() 55 | login_manager.init_app(app) 56 | 57 | @login_manager.unauthorized_handler 58 | def unauthorized(): 59 | return abort(401) 60 | 61 | LOG.info("dnsdb.started") 62 | 63 | @app.context_processor 64 | def default_context_processor(): 65 | result = {'config': {'BASE_URL': CONF.web.base_url}} 66 | return result 67 | 68 | from dnsdb.view.web import root 69 | app.register_blueprint(root.bp, url_prefix='/') 70 | 71 | from dnsdb.view.web import auth 72 | app.register_blueprint(auth.bp, url_prefix='/web/auth') 73 | 74 | from dnsdb.view.web import user 75 | app.register_blueprint(user.bp, url_prefix='/web/user') 76 | 77 | from dnsdb.view.web import preview 78 | app.register_blueprint(preview.bp, url_prefix='/web/preview') 79 | 80 | from dnsdb.view.web import config 81 | app.register_blueprint(config.bp, url_prefix='/web/config') 82 | 83 | from dnsdb.view.web import subnet 84 | app.register_blueprint(subnet.bp, url_prefix='/web/subnet') 85 | 86 | from dnsdb.view.web import record 87 | app.register_blueprint(record.bp, url_prefix='/web/record') 88 | 89 | from dnsdb.view.web import view_isp 90 | app.register_blueprint(view_isp.bp, url_prefix='/web/view') 91 | 92 | from dnsdb.view.web import view_record 93 | app.register_blueprint(view_record.bp, url_prefix='/web/view') 94 | 95 | from dnsdb.view import api 96 | app.register_blueprint(api.bp, url_prefix='/api') 97 | 98 | return app 99 | 100 | 101 | def main(): 102 | application = createApp(sys.argv[1], sys.argv[2], conf_dir=os.path.dirname(os.path.dirname(__file__))) 103 | options = { 104 | 'workers': number_of_workers(), 105 | } 106 | for option in CONF.gunicorn: 107 | options[option] = CONF.gunicorn[option] 108 | 109 | GunicornApplication(application, options).run() 110 | 111 | 112 | if __name__ == '__main__': 113 | application = createApp(app_env='dev', app_kind='dnsdb', conf_dir=os.path.dirname(os.path.abspath('.'))) 114 | application.run(host='0.0.0.0', port=8888, debug=True) 115 | -------------------------------------------------------------------------------- /dnsdb/config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import sys 5 | from datetime import timedelta 6 | 7 | from oslo_config import cfg 8 | 9 | CONF = cfg.CONF 10 | 11 | CONF.register_opts([ 12 | cfg.StrOpt('log-dir'), 13 | cfg.StrOpt('log-file'), 14 | cfg.StrOpt('debug'), 15 | cfg.StrOpt('verbose'), 16 | ], 'log') 17 | 18 | CONF.register_opts([ 19 | cfg.StrOpt('connection'), 20 | cfg.StrOpt('data'), 21 | ], 'DB') 22 | 23 | CONF.register_opts([ 24 | cfg.StrOpt('server'), 25 | cfg.StrOpt('port'), 26 | cfg.StrOpt('from_addr'), 27 | cfg.StrOpt('password', default=''), 28 | cfg.StrOpt('info_list'), 29 | cfg.StrOpt('alert_list'), 30 | ], 'MAIL') 31 | 32 | CONF.register_opts([ 33 | cfg.StrOpt('allow_ip'), 34 | cfg.StrOpt('secret_key'), 35 | cfg.StrOpt('env'), 36 | cfg.StrOpt('header_template', default='../etc/template/zone_header') 37 | ], 'etc') 38 | 39 | CONF.register_opts([ 40 | cfg.IntOpt('dnsupdater_port'), 41 | ], 'api') 42 | 43 | CONF.register_opts([ 44 | cfg.StrOpt('acl_groups'), 45 | cfg.IntOpt('cname_ttl'), 46 | cfg.StrOpt('view_zone'), 47 | cfg.DictOpt('normal_view'), 48 | cfg.DictOpt('normal_cname'), 49 | ], 'view') 50 | 51 | CONF.register_opts([ 52 | cfg.StrOpt('base-url', 53 | default='/', 54 | help='The url prefix of this site.'), 55 | cfg.StrOpt('run-mode', 56 | default="werkzeug", 57 | choices=('gunicorn', 'werkzeug'), 58 | help="Run server use the specify mode."), 59 | cfg.StrOpt('bind', 60 | default='0.0.0.0', 61 | help='The IP address to bind'), 62 | cfg.IntOpt('port', 63 | default=8080, 64 | help='The port to listen'), 65 | cfg.BoolOpt('debug', 66 | default=False), 67 | ], 'web') 68 | 69 | CONF.register_opts([ 70 | cfg.StrOpt('config', 71 | default=None, 72 | help='The path to a Gunicorn config file.'), 73 | cfg.StrOpt('bind', 74 | default='127.0.0.1:8888'), 75 | cfg.IntOpt('workers', 76 | default=0, 77 | help='The number of worker processes for handling requests'), 78 | cfg.BoolOpt('daemon', 79 | default=False, 80 | help='Daemonize the Gunicorn process'), 81 | cfg.StrOpt('accesslog', 82 | default=None, 83 | help='The Access log file to write to.' 84 | '"-" means log to stderr.'), 85 | cfg.StrOpt('loglevel', 86 | default='info', 87 | help='The granularity of Error log outputs.', 88 | choices=('debug', 'info', 'warning', 'error', 'critical')), 89 | cfg.BoolOpt('ignore-healthcheck-accesslog', 90 | default=False), 91 | cfg.IntOpt('timeout', 92 | default=30, 93 | help='Workers silent for more than this many seconds are ' 94 | 'killed and restarted.'), 95 | cfg.StrOpt('worker-class', 96 | default='sync', 97 | help='The type of workers to use.', 98 | choices=('sync', 'eventlet', 'gevent', 'tornado')) 99 | ], 'gunicorn') 100 | 101 | 102 | def setup_config(app_env, app_kind, conf_dir): 103 | if "--" in sys.argv: 104 | args = sys.argv[sys.argv.index("--") + 1:] 105 | else: 106 | args = [] 107 | 108 | common_config_file = os.path.join(conf_dir, "etc/{}/common.conf".format(app_env)) 109 | default_config_files = [common_config_file] 110 | app_config_file = os.path.join(conf_dir, "etc/{}/{}.conf".format(app_env, app_kind)) 111 | default_config_files.append(app_config_file) 112 | CONF(default_config_files=default_config_files, args=args) 113 | 114 | 115 | class Config(object): 116 | def __init__(self, app_env, app_kind, conf_dir): 117 | # print 'conf_dir: ', conf_dir 118 | if "--" in sys.argv: 119 | args = sys.argv[sys.argv.index("--") + 1:] 120 | else: 121 | args = [] 122 | 123 | common_config_file = os.path.join(conf_dir, "etc/{}/common.conf".format(app_env)) 124 | default_config_files = [common_config_file] 125 | app_config_file = os.path.join(conf_dir, "etc/{}/{}.conf".format(app_env, app_kind)) 126 | default_config_files.append(app_config_file) 127 | CONF(default_config_files=default_config_files, args=args) 128 | 129 | self.SECRET_KEY = os.environ.get('SECRET_KEY') or CONF.etc.secret_key 130 | self.SQLALCHEMY_DATABASE_URI = CONF.DB.connection 131 | self.SQLALCHEMY_TRACK_MODIFICATIONS = False 132 | self.PERMANENT_SESSION_LIFETIME = timedelta(days=1) 133 | 134 | # SECRET_KEY = os.environ.get('SECRET_KEY') or CONF.etc.secret_key 135 | # SQLALCHEMY_DATABASE_URI = CONF.DB.connection 136 | # SQLALCHEMY_TRACK_MODIFICATIONS = False 137 | # PERMANENT_SESSION_LIFETIME = timedelta(days=1) 138 | -------------------------------------------------------------------------------- /dnsdb/constant/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/open_dnsdb/f717e9752f00a2937ff523c2ca120c2ebae57bca/dnsdb/constant/__init__.py -------------------------------------------------------------------------------- /dnsdb/constant/constant.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from oslo_config import cfg 4 | CONF = cfg.CONF 5 | 6 | RE_PATTERN = { 7 | 'username': r'(^[\w\.]{1-64}$)', 8 | 'email': r'^([\w-_]+(?:\.[\w-_]+)*)@((?:[a-z0-9]+(?:-[a-zA-Z0-9]+)*)+\.[a-z]{2,6})$', 9 | 'password': r'^(?=[\s\S]{6,9}$)(?=[\s\S]*[A-Z])(?=[\s\S]*[a-z])(?=[\s\S]*[0-9]).*' 10 | } 11 | 12 | NORMAL_TO_CNAME = CONF.view.normal_cname 13 | NORMAL_TO_VIEW = CONF.view.normal_view 14 | VIEW_ZONE = CONF.view.view_zone 15 | -------------------------------------------------------------------------------- /dnsdb/constant/operation_type.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | operation_type_dict = { 4 | 'add_user': '用户新增', 5 | 'delete_user': '用户删除', 6 | 'update_user': '用户信息修改', 7 | 'add_host': '主机新增', 8 | 'delete_host': '主机删除', 9 | 'update_reload_status': '主机组reload修改', 10 | 'update_view_state': '状态修改', 11 | 'update_view_domain': '域名修改', 12 | 'add_view_domain': '域名新增', 13 | 'delete_view_domain': '域名删除', 14 | 'migrate_rooms': '机房迁移', 15 | 'recover_rooms': '机房恢复', 16 | # 'onekey_recover_rooms': '一键恢复', 17 | # 'switch_ip_to_aqb': 'IP高防切换', 18 | # 'switch_ip_from_aqb': 'IP高防恢复', 19 | # 'replace_ip': 'IP替换', 20 | # 'cancel_replace_ip': 'IP替换恢复', 21 | 'add_isp': 'ISP新增', 22 | 'delete_isp': 'ISP删除', 23 | 'update_zone_header': '头文件更新', 24 | 'update_named_zone': '配置更新zone', 25 | 'add_named_zone': '配置新增zone', 26 | 'delete_named_zone': '配置删除zone', 27 | 'update_named_conf_header': '配置更新named', 28 | 'conf_deploy': '配置部署', 29 | 'rename_subnet': '子网重命名', 30 | 'delete_subnet': '子网删除', 31 | 'add_subnet': '子网新增', 32 | 'manadd_record': '记录新增', 33 | 'delete_record': '记录删除', 34 | 'modify_record': '记录修改', 35 | 'autoadd_record': '记录自动绑定', 36 | 'acl_migration': 'acl运营商迁移', 37 | 'add_acl_subnet': 'acl网段新增', 38 | 'delete_acl_subnet': 'acl网段删除' 39 | # 'modify_isp_status': 'ISP状态修改' 40 | } 41 | 42 | 43 | filed_dict = { 44 | # user 45 | 'username': '用户名', 46 | 'role': '角色', 47 | 'email': '邮箱', 48 | # isp 49 | 'ename': '英文名', 50 | 'cname': '中文名', 51 | 'abbr': '别名', 52 | # view 53 | 'rooms': '机房', 54 | 'isps': '运营商' 55 | } 56 | -------------------------------------------------------------------------------- /dnsdb/deploy.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import json 4 | import threading 5 | import time 6 | 7 | from flask import Flask 8 | from oslo_config import cfg 9 | 10 | from dnsdb_common.dal import db 11 | from dnsdb_common.dal.operation_log import OperationLogDal 12 | from dnsdb_common.library.api import DnsUpdaterApi 13 | from dnsdb_common.library.exception import DnsdbException 14 | from dnsdb_common.library.log import setup, getLogger 15 | 16 | setup('dnsdb') 17 | log = getLogger(__name__) 18 | 19 | CONF = cfg.CONF 20 | 21 | 22 | def get_flask_app(flask_conf): 23 | app = Flask(__name__) 24 | app.config.from_object(flask_conf) 25 | db.init_app(app) 26 | return app 27 | 28 | 29 | class DeployException(DnsdbException): 30 | def __init__(self, message='Notify deploy conf file error', errcode=500, detail=None, msg_ch=u''): 31 | super(DeployException, self).__init__(message, errcode, detail, msg_ch) 32 | 33 | 34 | class DeployThread(threading.Thread): 35 | def __init__(self, job_id, is_retry=False): 36 | super(DeployThread, self).__init__() 37 | self.is_retry = is_retry 38 | self.state = 'wait' 39 | self.job_id = job_id 40 | self.done_host = [] 41 | self.app = get_flask_app(CONF.flask_conf) 42 | self.deploy_info = {} 43 | self.deploy_type = '' 44 | self.expire = 120 # 任务超时时间2分钟 45 | self.unfinished = {} 46 | self.notify_failed = [] 47 | 48 | def check_normal_update(self): 49 | time.sleep(self.expire) 50 | job = OperationLogDal.get_deploy_job(self.job_id) 51 | if job.op_result == 'start': 52 | OperationLogDal.update_opration_log(self.job_id, {'op_result': 'fail'}) 53 | 54 | def update_named_conf(self): 55 | self.unfinished = {} 56 | for group_name, info in self.deploy_info.items(): 57 | conf_md5 = info['md5'] 58 | hosts = info['hosts'] 59 | for host_ip in hosts: 60 | try: 61 | result = DnsUpdaterApi(host_ip=host_ip).notify_update(self.deploy_type, group_name, 62 | group_conf_md5=conf_md5, 63 | deploy_id=self.job_id) 64 | log.error('notify %s to update %s success, %s' % (host_ip, self.deploy_type, result)) 65 | except Exception as e: 66 | log.error('notify %s to update %s failed, %s' % (host_ip, self.deploy_type, e)) 67 | self.notify_failed.append(host_ip) 68 | self.unfinished[group_name] = hosts 69 | 70 | def update_acl(self): 71 | hosts = self.deploy_info.get('hosts', {}) 72 | acl_files = self.deploy_info.get('acl_files', []) 73 | if not hosts or not acl_files: 74 | OperationLogDal.update_opration_log(self.job_id, {'op_result': 'ok'}) 75 | for group_name, hosts in hosts.items(): 76 | for host in hosts: 77 | try: 78 | result = DnsUpdaterApi(host_ip=host).notify_update(self.deploy_type, group_name, 79 | deploy_id=self.job_id, acl_files=acl_files) 80 | log.info('notify %s to update %s success, %s' % (host, self.deploy_type, result)) 81 | except Exception as e: 82 | log.error('notify %s to update %s failed, %s' % (host, self.deploy_type, e)) 83 | self.notify_failed.append(e) 84 | 85 | def init_zone(self): 86 | hosts = self.deploy_info.get('hosts', []) 87 | if not hosts: 88 | OperationLogDal.update_opration_log(self.job_id, {'op_result': 'ok'}) 89 | group_name = self.deploy_info['group'] 90 | zone = self.deploy_info['zone'] 91 | for host in hosts: 92 | try: 93 | result = DnsUpdaterApi(host_ip=host).notify_update(self.deploy_type, group_name, 94 | deploy_id=self.job_id, zone=zone) 95 | log.info('notify %s to update %s success, %s' % (host, self.deploy_type, result)) 96 | except Exception as e: 97 | log.error('notify %s to update %s failed, %s' % (host, self.deploy_type, e)) 98 | self.notify_failed.append(e) 99 | 100 | def run(self): 101 | with self.app.app_context(): 102 | job = OperationLogDal.get_deploy_job(self.job_id) 103 | if not job or job.op_result != 'wait': 104 | raise DeployException('No deploy job id=%s or job state=wait.' % self.job_id) 105 | OperationLogDal.update_opration_log(self.job_id, { 106 | 'op_result': 'start' 107 | }) 108 | self.deploy_info = json.loads(job.op_before) 109 | self.deploy_type = job.op_domain 110 | if job.op_domain == 'named.conf': 111 | self.update_named_conf() 112 | elif job.op_domain == 'acl': 113 | self.update_acl() 114 | elif job.op_domain == 'zone': 115 | self.init_zone() 116 | if self.notify_failed: 117 | pass 118 | with self.app.app_context(): 119 | self.check_normal_update() 120 | 121 | 122 | def start_deploy_job(user, deploy_info, conf_type, unfinished): 123 | job_id = OperationLogDal.create_deploy_job(user, deploy_info, conf_type, unfinished) 124 | thread = DeployThread(job_id) 125 | thread.start() 126 | return job_id 127 | 128 | 129 | def retry_deploy_job(job_id, username): 130 | if not OperationLogDal.reset_deploy_job(username, job_id): 131 | raise DeployException('Reset deploy job %s failed' % job_id) 132 | thread = DeployThread(job_id, is_retry=True) 133 | thread.start() 134 | return dict(code=0, data='ok') 135 | -------------------------------------------------------------------------------- /dnsdb/migrate.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import threading 4 | import json 5 | from oslo_config import cfg 6 | 7 | from dnsdb import get_flask_app 8 | from dnsdb.constant.constant import NORMAL_TO_CNAME 9 | from dnsdb_common.dal.view_migrate import MigrateDal 10 | from dnsdb_common.library.log import setup, getLogger 11 | from dnsdb_common.library.exception import BadParam 12 | 13 | setup("dnsdb") 14 | log = getLogger(__name__) 15 | 16 | CONF = cfg.CONF 17 | 18 | def format_history(histories): 19 | history_list = [] 20 | trans = MigrateDal.get_isp_trans() 21 | for history in histories: 22 | history_list.append({ 23 | 'id': history.id, 24 | 'migrate_rooms': sorted(json.loads(history.migrate_rooms)), 25 | 'dst_rooms': sorted(json.loads(history.dst_rooms)), 26 | 'migrate_isps': sorted([trans[isp] for isp in json.loads(history.migrate_isps)]), 27 | 'cur': history.cur, 28 | 'all': history.all, 29 | 'state': history.state, 30 | 'rtx_id': history.rtx_id, 31 | 'update_at': history.updated_time.strftime('%Y-%m-%d %H:%M:%S') 32 | }) 33 | return history_list 34 | 35 | def get_migrate_info(history_id): 36 | try: 37 | history_id = int(history_id) 38 | except Exception: 39 | raise BadParam('id should be int') 40 | return format_history([MigrateDal.get_history_info(history_id)]) 41 | 42 | def migrate_rooms(src_rooms, dst_rooms, to_migrate_isps, username): 43 | history_id = MigrateDal.create_migrage_history(username) 44 | 45 | migrated_isp_rooms = MigrateDal.get_all_abnormal_isps(key='isp', value='room') 46 | to_migrate_dict = {isp: set(src_rooms) | migrated_isp_rooms.get(isp, set()) 47 | for isp in to_migrate_isps} 48 | 49 | migrate_isp_domains = MigrateDal.list_migrate_domain_by_isp(to_migrate_dict, dst_rooms) 50 | 51 | has_migrate_domains = False 52 | for isp, migrate_domains_list in migrate_isp_domains.items(): 53 | migrate_domains_list = [domain for domain in migrate_domains_list if domain['after_enabled_rooms']] 54 | if len(migrate_domains_list) == 0: 55 | continue 56 | has_migrate_domains = True 57 | MigrateDal.update_history_total(history_id, len(migrate_domains_list)) 58 | m_thread = MigrateThread(username, history_id, migrate_domains_list) 59 | m_thread.start() 60 | 61 | if has_migrate_domains: 62 | MigrateDal.add_batch_abnormal_isp(username, to_migrate_dict) 63 | # send_alert_email("[FROM DNSDB]: 机房{}上运营商{}迁移到{}啦.".format(src_rooms, to_migrate_isps, dst_rooms)) 64 | MigrateDal.update_history_by_id(history_id, 65 | migrate_rooms=json.dumps(src_rooms), 66 | migrate_isps=json.dumps(to_migrate_isps), 67 | dst_rooms=json.dumps(dst_rooms), 68 | migrate_info=json.dumps({})) 69 | else: 70 | MigrateDal.delete_history_by_id(history_id) 71 | raise BadParam("no domain can migrate, isp_rooms: %s" 72 | % to_migrate_dict, msg_ch=u'没有可迁移的机房') 73 | 74 | history_info = get_migrate_info(history_id) 75 | return history_info 76 | 77 | def list_migrate_history(): 78 | return format_history(MigrateDal.get_last_few_history(limit=15)) 79 | 80 | 81 | class MigrateThread(threading.Thread): 82 | def __init__(self, rtx_id, migrate_history_id, migrate_domain_list): 83 | super(MigrateThread, self).__init__() 84 | self.app = get_flask_app() 85 | self.step = 100 86 | self.rtx_id = rtx_id 87 | self.migrate_history_id = migrate_history_id 88 | self.migrate_domain_list = migrate_domain_list 89 | 90 | def run(self): 91 | with self.app.app_context(): 92 | for i in range(0, len(self.migrate_domain_list), self.step): 93 | try: 94 | MigrateDal.migrate_domains(self.migrate_domain_list[i: i + self.step], self.migrate_history_id) 95 | except Exception as e: 96 | log.error('migrate rooms failed:%s' % e) 97 | MigrateDal.update_history_by_id(self.migrate_history_id, state='error') 98 | return 99 | # 更新serial 100 | MigrateDal.increase_serial_num(NORMAL_TO_CNAME.values()) 101 | -------------------------------------------------------------------------------- /dnsdb/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/open_dnsdb/f717e9752f00a2937ff523c2ca120c2ebae57bca/dnsdb/static/favicon.ico -------------------------------------------------------------------------------- /dnsdb/static/fonts/element-icons.6f0a763.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/open_dnsdb/f717e9752f00a2937ff523c2ca120c2ebae57bca/dnsdb/static/fonts/element-icons.6f0a763.ttf -------------------------------------------------------------------------------- /dnsdb/static/fonts/fontawesome-webfont.674f50d.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/open_dnsdb/f717e9752f00a2937ff523c2ca120c2ebae57bca/dnsdb/static/fonts/fontawesome-webfont.674f50d.eot -------------------------------------------------------------------------------- /dnsdb/static/fonts/fontawesome-webfont.af7ae50.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/open_dnsdb/f717e9752f00a2937ff523c2ca120c2ebae57bca/dnsdb/static/fonts/fontawesome-webfont.af7ae50.woff2 -------------------------------------------------------------------------------- /dnsdb/static/fonts/fontawesome-webfont.b06871f.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/open_dnsdb/f717e9752f00a2937ff523c2ca120c2ebae57bca/dnsdb/static/fonts/fontawesome-webfont.b06871f.ttf -------------------------------------------------------------------------------- /dnsdb/static/fonts/fontawesome-webfont.fee66e7.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/open_dnsdb/f717e9752f00a2937ff523c2ca120c2ebae57bca/dnsdb/static/fonts/fontawesome-webfont.fee66e7.woff -------------------------------------------------------------------------------- /dnsdb/static/js/manifest.cfdad16f39e54a963330.js: -------------------------------------------------------------------------------- 1 | !function(e){function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}var r=window.webpackJsonp;window.webpackJsonp=function(t,c,i){for(var u,a,f,s=0,l=[];s 2 | 3 | 4 | 5 | Title 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /dnsdb/templates/index.html: -------------------------------------------------------------------------------- 1 | Dns管理系统
-------------------------------------------------------------------------------- /dnsdb/view/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/open_dnsdb/f717e9752f00a2937ff523c2ca120c2ebae57bca/dnsdb/view/__init__.py -------------------------------------------------------------------------------- /dnsdb/view/api/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | from flask import (Blueprint) 5 | 6 | from dnsdb_common.dal.host_group_conf import HostGroupConfDal 7 | from dnsdb_common.dal.zone_record import ZoneRecordDal 8 | from dnsdb_common.dal.operation_log import OperationLogDal 9 | from dnsdb_common.dal.view_isp_acl import ViewIspAclDal 10 | from dnsdb_common.library.decorators import authenticate 11 | from dnsdb_common.library.decorators import parse_params 12 | from dnsdb_common.library.decorators import resp_wrapper_json 13 | 14 | bp = Blueprint('api', 'api') 15 | 16 | 17 | @bp.before_request 18 | @authenticate 19 | def require_authorization(): 20 | pass 21 | 22 | 23 | @bp.route('/get/reload_status', methods=['GET']) 24 | @parse_params([dict(name='group_name', type=str, required=True, nullable=False)]) 25 | @resp_wrapper_json 26 | def get_reload_status(group_name): 27 | return HostGroupConfDal.get_group_by_name(group_name).reload_status 28 | 29 | 30 | @bp.route('/get/host_group', methods=['GET']) 31 | @parse_params([dict(name='host_ip', type=str, required=True, nullable=False)]) 32 | @resp_wrapper_json 33 | def get_host_group(host_ip): 34 | return HostGroupConfDal.get_group_by_ip(host_ip) 35 | 36 | 37 | @bp.route('/get/named_conf', methods=['GET']) 38 | @parse_params([dict(name='group_name', type=str, required=True, nullable=False)]) 39 | @resp_wrapper_json 40 | def get_group_named(group_name): 41 | return HostGroupConfDal.build_complete_named_conf(group_name) 42 | 43 | 44 | @bp.route('/update/host_conf_md5', methods=['POST']) 45 | @parse_params([dict(name='host_ip', type=str, required=True, nullable=False), 46 | dict(name='host_conf_md5', type=str, required=True, nullable=False)]) 47 | @resp_wrapper_json 48 | def update_host_conf_md5(host_ip, host_conf_md5): 49 | return HostGroupConfDal.update_host_conf_md5(host_ip, host_conf_md5) 50 | 51 | 52 | @bp.route('/get/acl_file', methods=['GET']) 53 | @parse_params([dict(name='acl_file', type=str, required=True, nullable=False)]) 54 | @resp_wrapper_json 55 | def get_acl_file_content(acl_file): 56 | return ViewIspAclDal.get_acl_file_content(acl_file) 57 | 58 | 59 | @bp.route('/update/deploy_info', methods=['POST']) 60 | @parse_params([dict(name='deploy_id', type=int, required=True, nullable=False), 61 | dict(name='host', type=str, required=True, nullable=False), 62 | dict(name='is_success', type=bool, required=True, nullable=False), 63 | dict(name='msg', type=str, required=True, nullable=False)]) 64 | @resp_wrapper_json 65 | def update_deploy_info(deploy_id, host, is_success, msg): 66 | OperationLogDal.update_deploy_info(deploy_id, host, is_success, msg) 67 | 68 | 69 | @bp.route('/get/update_zones', methods=['GET']) 70 | @parse_params([dict(name='group_name', type=str, required=True, nullable=False)]) 71 | @resp_wrapper_json 72 | def get_update_zones(group_name): 73 | return ZoneRecordDal.get_zone_need_update(group_name) 74 | 75 | 76 | @bp.route('/get/zone_info', methods=['GET']) 77 | @parse_params([dict(name='zone_name', type=str, required=True, nullable=False)]) 78 | @resp_wrapper_json 79 | def get_zone_info(zone_name): 80 | zone_info = ZoneRecordDal.get_zone_header(zone_name) 81 | zone_info['records'] = ZoneRecordDal.get_zone_records(zone_name) 82 | return zone_info 83 | 84 | @bp.route('/update/zone_serial', methods=['POST']) 85 | @parse_params([dict(name='zone_name', type=str, required=True, nullable=False)]) 86 | @resp_wrapper_json 87 | def update_zone_serial(zone_name): 88 | return ZoneRecordDal.update_serial_num(zone_name) 89 | -------------------------------------------------------------------------------- /dnsdb/view/web/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/open_dnsdb/f717e9752f00a2937ff523c2ca120c2ebae57bca/dnsdb/view/web/__init__.py -------------------------------------------------------------------------------- /dnsdb/view/web/auth.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | from dnsdb_common.dal.user import UserDal 5 | from dnsdb_common.library.decorators import resp_wrapper_json 6 | from dnsdb_common.library.exception import DnsdbException 7 | from dnsdb_common.library.exception import Unauthorized 8 | from flask import (Blueprint) 9 | from flask import request 10 | from flask_login import login_user, logout_user, login_required, current_user 11 | 12 | bp = Blueprint('auth', 'auth') 13 | 14 | 15 | @bp.route('/login', methods=['GET', 'POST']) 16 | @resp_wrapper_json 17 | def login(): 18 | if request.method == 'POST': 19 | form = request.get_json(force=True) 20 | user = UserDal.get_user_info(username=form['username']) 21 | if user is not None and user.verify_password(form['password']): 22 | login_user(user, remember=True) 23 | return current_user.username 24 | raise DnsdbException('Invalid username or password.', msg_ch=u'账号或密码错误') 25 | else: 26 | raise Unauthorized() 27 | 28 | 29 | @bp.route('/logout', methods=['POST']) 30 | @login_required 31 | @resp_wrapper_json 32 | def logout(): 33 | logout_user() 34 | raise Unauthorized() 35 | 36 | 37 | @bp.route("/logged_in_user", methods=['GET']) 38 | @login_required 39 | @resp_wrapper_json 40 | def logged_in_user(): 41 | return current_user.username 42 | -------------------------------------------------------------------------------- /dnsdb/view/web/preview.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import json 4 | 5 | from flask import (Blueprint, redirect, url_for) 6 | from flask_login import current_user 7 | 8 | from dnsdb.constant.operation_type import operation_type_dict, filed_dict 9 | from dnsdb_common.dal.operation_log import OperationLogDal 10 | from dnsdb_common.dal.view_isp_acl import ViewIspAclDal 11 | from dnsdb_common.dal.view_migrate import MigrateDal 12 | from dnsdb_common.dal.view_record import ViewRecordDal 13 | from dnsdb_common.library.decorators import parse_params 14 | from dnsdb_common.library.decorators import resp_wrapper_json 15 | from dnsdb import deploy 16 | 17 | bp = Blueprint('preview', 'preview') 18 | 19 | 20 | @bp.before_request 21 | def require_authorization(): 22 | if not current_user.is_authenticated: 23 | redirect(url_for('auth.login')) 24 | 25 | 26 | @bp.route('/list/operation_constant', methods=['GET']) 27 | @resp_wrapper_json 28 | def list_operationtype(): 29 | return {'type_dict': operation_type_dict, 'filed_dict': filed_dict} 30 | 31 | 32 | @bp.route('/get/dns_log_detail', methods=['GET']) 33 | @parse_params([dict(name='id', type=int, required=True, nullable=False)]) 34 | @resp_wrapper_json 35 | def get_dns_log_detail(id): 36 | OperationLogDal.get_log_detail(id) 37 | 38 | 39 | @bp.route('/list/operation_log', methods=['GET']) 40 | @parse_params([dict(name='page', type=int, required=True, nullable=False), 41 | dict(name='page_size', type=int, required=True, nullable=False), 42 | dict(name='start_time', type=str, required=False), 43 | dict(name='end_time', type=str, required=False), 44 | dict(name='domain', type=str, required=False), 45 | dict(name='type', type=str, required=False), 46 | dict(name='rtx_id', type=str, required=False), 47 | ]) 48 | @resp_wrapper_json 49 | def list_operation_log(**kwargs): 50 | return OperationLogDal.list_operation_log(kwargs) 51 | 52 | 53 | @bp.route('retry_deploy_job', methods=['POST']) 54 | @parse_params([dict(name='deploy_id', type=int, required=True, nullable=False)], need_username=True) 55 | @resp_wrapper_json 56 | def retry_deploy_job(deploy_id, username): 57 | return deploy.retry_deploy_job(deploy_id, username) 58 | 59 | 60 | @bp.route('/get/previewinfo', methods=['GET']) 61 | @resp_wrapper_json 62 | def get_previewinfo(): 63 | trans = MigrateDal.get_isp_trans() 64 | domain_count = ViewRecordDal.zone_domain_count() 65 | migrate_list = [] 66 | histories = MigrateDal.get_migrated_history() 67 | for history in histories: 68 | migrate_list.append({ 69 | 'migrate_rooms': sorted(json.loads(history.migrate_rooms)), 70 | 'dst_rooms': sorted(json.loads(history.dst_rooms)), 71 | 'migrate_isps': sorted([trans[isp] for isp in json.loads(history.migrate_isps)]) 72 | }) 73 | 74 | migrate_acl_subnet = ViewIspAclDal.get_migrate_subnet() 75 | 76 | return {'domain_count': domain_count, 77 | 'migrate': migrate_list, 78 | 'acl_migrate': migrate_acl_subnet} 79 | -------------------------------------------------------------------------------- /dnsdb/view/web/root.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | from flask import (abort, Blueprint) 5 | from flask import render_template, url_for 6 | from flask_login import login_required 7 | from jinja2 import TemplateNotFound 8 | 9 | bp = Blueprint('root', 'root') 10 | 11 | 12 | @bp.route("/", methods=['GET']) 13 | @bp.route("/dnsdb/", methods=['GET']) 14 | @bp.route("/api/", methods=['GET']) 15 | def root(path=''): 16 | try: 17 | return render_template('index.html') 18 | except TemplateNotFound: 19 | abort(404) 20 | 21 | 22 | @bp.route("/index", methods=['GET']) 23 | @login_required 24 | def index(path=''): 25 | try: 26 | return render_template('index.html') 27 | except TemplateNotFound: 28 | abort(404) 29 | 30 | 31 | @bp.route("/healthcheck.html", methods=['GET']) 32 | def health_check(): 33 | try: 34 | return render_template('healthcheck.html') 35 | except TemplateNotFound: 36 | abort(404) 37 | -------------------------------------------------------------------------------- /dnsdb/view/web/subnet.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import re 4 | import ipaddress 5 | 6 | from flask import (Blueprint) 7 | from flask_login import login_required 8 | 9 | from dnsdb_common.dal.subnet_ip import SubnetIpDal 10 | from dnsdb_common.library.decorators import add_web_opration_log 11 | from dnsdb_common.library.decorators import parse_params 12 | from dnsdb_common.library.decorators import resp_wrapper_json 13 | from dnsdb_common.library.exception import BadParam 14 | 15 | bp = Blueprint('subnet', 'subnet') 16 | 17 | 18 | @bp.before_request 19 | @login_required 20 | def require_authorization(): 21 | pass 22 | 23 | 24 | def _is_valide_region(region): 25 | if len(region) > 64: 26 | raise BadParam('The length of region should <80', msg_ch=u'名称长度必须<80') 27 | 28 | if not re.match(r'^[a-zA-Z0-9_]+$', region): 29 | raise BadParam('region only a-z,A-Z,0-9,_ allowed.', msg_ch=u'名称中只能包含大小写字母、数字、下划线') 30 | 31 | 32 | def _validate_args(subnet, region, colo): 33 | if '/' not in subnet: 34 | raise BadParam('Invalid subnet.', msg_ch=u'请使用cidr格式的网段') 35 | 36 | _is_valide_region(region) 37 | 38 | if colo not in SubnetIpDal.get_colo_by_group('subnet'): 39 | raise BadParam('Invalid colo', msg_ch=u'请先配置机房') 40 | 41 | try: 42 | sub = ipaddress.ip_network(subnet) 43 | except Exception as e: 44 | raise BadParam('Invalid subnet: %s' % e, msg_ch=u'错误的网段格式') 45 | 46 | prefix = sub.prefixlen 47 | if sub.version == 4: 48 | if prefix < 16 or prefix > 32: 49 | raise BadParam('Invalid subnet.', msg_ch=u'IPv4掩码长度在[16-32]之间') 50 | else: 51 | if prefix < 64 or prefix > 128: 52 | raise BadParam('Invalid subnet.', msg_ch=u'IPv6掩码长度在[64-128]之间') 53 | 54 | 55 | 56 | def add_subnet_log(result, **kwargs): 57 | return kwargs['region'], {}, {'subnet': kwargs['subnet']} 58 | 59 | 60 | def delete_subnet_log(result, **kwargs): 61 | return kwargs['region'], {'subnet': kwargs['subnet']}, {} 62 | 63 | 64 | def rename_subnet_log(result, **kwargs): 65 | return kwargs['new_region'], {'region': kwargs['old_region']}, {'region': kwargs['new_region']} 66 | 67 | 68 | @bp.route('/add/subnet', methods=['POST']) 69 | @parse_params([dict(name='subnet', type=str, required=True, nullable=False), 70 | dict(name='region', type=str, required=True, nullable=False), 71 | dict(name='colo', type=str, required=True, nullable=False), 72 | dict(name='comment', type=str, required=False, nullable=False)], 73 | need_username=True) 74 | @resp_wrapper_json 75 | @add_web_opration_log('add_subnet', get_op_info=add_subnet_log) 76 | def add_subnet(subnet, region, colo, comment, username): 77 | _validate_args(subnet, region, colo) 78 | return SubnetIpDal.add_subnet(subnet, region, colo, comment, username) 79 | 80 | 81 | @bp.route('/delete', methods=['POST']) 82 | @parse_params([dict(name='subnet', type=str, required=True, nullable=False), 83 | dict(name='region', type=str, required=True, nullable=False)]) 84 | @resp_wrapper_json 85 | @add_web_opration_log('delete_subnet', get_op_info=delete_subnet_log) 86 | def delete_subnet(subnet, region): 87 | return SubnetIpDal.delete_subnet(subnet, region) 88 | 89 | 90 | @bp.route('/rename_subnet', methods=['POST']) 91 | @parse_params([dict(name='old_region', type=str, required=True, nullable=False), 92 | dict(name='new_region', type=str, required=True, nullable=False)], 93 | need_username=True) 94 | @resp_wrapper_json 95 | @add_web_opration_log('rename_subnet', get_op_info=rename_subnet_log) 96 | def rename_subnet(old_region, new_region, username): 97 | _is_valide_region(new_region) 98 | return SubnetIpDal.rename_subnet(old_region, new_region, username) 99 | 100 | 101 | @bp.route('/get/subnet_colos', methods=['GET']) 102 | @resp_wrapper_json 103 | def get_subnet_colos(): 104 | return SubnetIpDal.get_colo_by_group('subnet') 105 | 106 | 107 | @bp.route('get/subnet_ip', methods=['GET']) 108 | @parse_params([dict(name='region', type=str, required=True, nullable=False)]) 109 | @resp_wrapper_json 110 | def get_subnet_ip(region): 111 | return SubnetIpDal.get_subnet_ip(region) 112 | 113 | 114 | @bp.route('/list/region', methods=['GET']) 115 | @resp_wrapper_json 116 | def list_region(): 117 | return SubnetIpDal.list_region() 118 | 119 | 120 | @bp.route('/get/region_by_ip', methods=['GET']) 121 | @parse_params([dict(name='ip', type=str, required=True, nullable=False)]) 122 | @resp_wrapper_json 123 | def get_region_by_ip(ip): 124 | return [SubnetIpDal.get_region_by_ip(ip)] 125 | 126 | 127 | @bp.route('/get/region_by_name', methods=['GET']) 128 | @parse_params([dict(name='region', type=str, required=True, nullable=False)]) 129 | @resp_wrapper_json 130 | def get_region_by_name(region): 131 | return SubnetIpDal.get_region_by_name_like(region) 132 | -------------------------------------------------------------------------------- /dnsdb/view/web/user.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import re 4 | 5 | from flask import (Blueprint) 6 | from flask_login import current_user, login_required 7 | 8 | from dnsdb.constant.constant import RE_PATTERN 9 | from dnsdb_common.dal.user import UserDal 10 | from dnsdb_common.library.decorators import add_web_opration_log 11 | from dnsdb_common.library.decorators import parse_params 12 | from dnsdb_common.library.decorators import resp_wrapper_json 13 | from dnsdb_common.library.exception import BadParam 14 | 15 | bp = Blueprint('user', 'user') 16 | 17 | 18 | def _is_valid_user(user): 19 | ptn = re.search(RE_PATTERN['username'], user) 20 | if ptn is None: 21 | return False 22 | if len(ptn.groups()) == 0: 23 | return False 24 | return ptn.group(1) == user 25 | 26 | 27 | def get_add_info(result, **kwargs): 28 | op_after = kwargs.copy() 29 | op_after.pop('password', None) 30 | role_id = op_after.pop('role_id') 31 | op_after['role'] = UserDal.get_role_name(role_id) 32 | return kwargs['username'], {}, op_after 33 | 34 | 35 | def get_update_info(result, **kwargs): 36 | op_after = kwargs.copy() 37 | op_after.pop('password', None) 38 | role_id = op_after.pop('role_id') 39 | op_after['role'] = UserDal.get_role_name(role_id) 40 | return kwargs['username'], {}, op_after 41 | 42 | 43 | def get_delete_info(result, **kwargs): 44 | username = kwargs['username'] 45 | return username, result, {} 46 | 47 | 48 | @bp.route('/roles', methods=['GET']) 49 | @login_required 50 | @resp_wrapper_json 51 | def get_roles(): 52 | return UserDal.get_roles() 53 | 54 | 55 | @bp.route('/get', methods=['GET']) 56 | @login_required 57 | @parse_params([dict(name='username', type=str, required=False, nullable=False)]) 58 | @resp_wrapper_json 59 | def get_user(username): 60 | user = UserDal.get_user_info(username=username) 61 | if user is None: 62 | return [] 63 | return [UserDal.get_user_info(username=username).json_serialize()] 64 | 65 | 66 | @bp.route('/list', methods=['GET']) 67 | @login_required 68 | @parse_params([dict(name='page', type=int, required=True, nullable=False), 69 | dict(name='page_size', type=int, required=True, nullable=False), 70 | dict(name='role_id', type=str, required=False, nullable=False)]) 71 | @resp_wrapper_json 72 | def list_user(**kwargs): 73 | return UserDal.list_user(**kwargs) 74 | 75 | 76 | @bp.route('/add', methods=['POST']) 77 | @login_required 78 | @parse_params([dict(name='username', type=str, required=True, nullable=False), 79 | dict(name='email', type=str, required=True, nullable=False), 80 | dict(name='password', type=str, required=True, nullable=False), 81 | dict(name='role_id', type=int, required=True, nullable=False)]) 82 | @resp_wrapper_json 83 | @add_web_opration_log('add_user', get_op_info=get_add_info) 84 | def add_user(username, email, password, role_id): 85 | UserDal.add_user(username, email, password, role_id) 86 | 87 | 88 | @bp.route('/delete', methods=['POST']) 89 | @login_required 90 | @parse_params([dict(name='username', type=str, required=True, nullable=False)]) 91 | @resp_wrapper_json 92 | @add_web_opration_log('delete_user', get_op_info=get_delete_info) 93 | def delete_user(username): 94 | if username == current_user.username: 95 | raise BadParam('cannot delete yourself') 96 | user = UserDal.get_user_info(username=username) 97 | if not user: 98 | raise BadParam('No such user with name: %s' % username) 99 | result = user.json_serialize(include=('username', 'email', 'role')) 100 | UserDal.delete_user(username) 101 | return result 102 | -------------------------------------------------------------------------------- /dnsdb/view/web/view_isp.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from flask import (Blueprint, request) 4 | from flask_login import login_required 5 | 6 | from dnsdb_common.dal.view_isp_acl import ViewIspAclDal 7 | from dnsdb_common.library.decorators import add_web_opration_log 8 | from dnsdb_common.library.decorators import parse_params 9 | from dnsdb_common.library.decorators import resp_wrapper_json 10 | from dnsdb_common.library.exception import BadParam 11 | from dnsdb_common.library.validator import valid_string 12 | 13 | bp = Blueprint('view', 'view') 14 | 15 | 16 | @bp.before_request 17 | @login_required 18 | def require_authorization(): 19 | pass 20 | 21 | 22 | def _validate_args(param_dict): 23 | validator_param = { 24 | "name_in_english": dict(max_len=32, pattern='[a-zA-Z_]+'), 25 | "name_in_chinese": dict(max_len=32), 26 | "acl_name": dict(max_len=16, pattern='[a-zA-Z]+', allow_blank=True), 27 | "abbreviation": dict(max_len=8, pattern='[a-z]+'), 28 | "acl_file": dict(max_len=16, pattern='[a-zA-Z\.]+', allow_blank=True), 29 | } 30 | 31 | param = {} 32 | for k, v in validator_param.items(): 33 | ok, format_str = valid_string(param_dict[k], **v) 34 | if not ok: 35 | raise BadParam(u'param validate error: %s, %s' % (k, v), msg_ch=u'%s: %s' % (k, format_str)) 36 | if format_str: 37 | param[k] = format_str 38 | return param 39 | 40 | 41 | def add_isp_log(result, **kwargs): 42 | domain = result.pop('name_in_english') 43 | result.pop('username', None) 44 | return domain, {}, result 45 | 46 | 47 | def delete_isp_log(result, **kwargs): 48 | domain = kwargs['name_in_english'] 49 | return domain, {}, {} 50 | 51 | 52 | def update_isp_log(result, **kwargs): 53 | domain = kwargs['name_in_english'] 54 | return domain, {}, kwargs['update_data'] 55 | 56 | def acl_migration_log(result, **kwargs): 57 | subnet = result.pop('subnet', kwargs['acl_subnet_id']) 58 | return subnet, {}, result 59 | 60 | def add_acl_subnet_log(result, subnet, acl, username): 61 | return acl, {}, {'subnet': subnet} 62 | 63 | def delete_acl_subnet_log(result, **kwargs): 64 | return result['origin_acl'] , {}, {'subnet': result['subnet']} 65 | 66 | 67 | @bp.route('/add/isp', methods=['POST']) 68 | @parse_params([], need_username=True) 69 | @resp_wrapper_json 70 | @add_web_opration_log('add_isp', get_op_info=add_isp_log) 71 | def add_isp(username): 72 | params = request.get_json(force=True) 73 | data = _validate_args(params) 74 | data['username'] = username 75 | ViewIspAclDal.add_isp(data) 76 | return data 77 | 78 | 79 | @bp.route('/delete/isp', methods=['POST']) 80 | @parse_params([dict(name='name_in_english', type=str, required=True, nullable=False)]) 81 | @resp_wrapper_json 82 | @add_web_opration_log('delete_isp', get_op_info=delete_isp_log) 83 | def delete_isp(name_in_english): 84 | count = ViewIspAclDal.delete_isp(name_in_english) 85 | if count == 0: 86 | raise BadParam('No such isp: %s' % name_in_english, msg_ch=u'没有对应的运营商记录') 87 | 88 | 89 | @bp.route('/update/isp', methods=['POST']) 90 | @parse_params([dict(name='name_in_english', type=str, required=True, nullable=False), 91 | dict(name='update_data', type=dict, required=True, nullable=False)], need_username=True) 92 | @resp_wrapper_json 93 | @add_web_opration_log('update_isp', get_op_info=update_isp_log) 94 | def update_isp(name_in_english, update_data, username): 95 | count = ViewIspAclDal.update_isp(name_in_english, update_data, username) 96 | if count == 0: 97 | raise BadParam('No such isp: %s' % name_in_english, msg_ch=u'没有对应的运营商记录') 98 | 99 | @bp.route('/migrate_subnet_acl', methods=['POST']) 100 | @parse_params([dict(name='acl_subnet_id', type=int, required=True, nullable=False), 101 | dict(name='to_acl', type=str, required=True, nullable=False)]) 102 | @resp_wrapper_json 103 | @add_web_opration_log('acl_migration', get_op_info=acl_migration_log) 104 | def migrate_subnet_acl(acl_subnet_id, to_acl): 105 | return ViewIspAclDal.migrate_acl(acl_subnet_id, to_acl) 106 | 107 | @bp.route('/add/acl_subnet', methods=['POST']) 108 | @parse_params([dict(name='subnet', type=str, required=True, nullable=False), 109 | dict(name='acl', type=str, required=True, nullable=False)], need_username=True) 110 | @resp_wrapper_json 111 | @add_web_opration_log('add_acl_subnet', get_op_info=add_acl_subnet_log) 112 | def add_acl_subnet(subnet, acl, username): 113 | return ViewIspAclDal.add_acl_subnet(subnet, acl, username) 114 | 115 | @bp.route('/delete/acl_subnet', methods=['POST']) 116 | @parse_params([dict(name='subnet_id', type=int, required=True, nullable=False)], need_username=True) 117 | @resp_wrapper_json 118 | @add_web_opration_log('delete_acl_subnet', get_op_info=delete_acl_subnet_log) 119 | def delete_acl_subnet(subnet_id, username): 120 | return ViewIspAclDal.delete_acl_subnet(subnet_id, username) 121 | 122 | 123 | @bp.route('/list/isp', methods=['GET']) 124 | @resp_wrapper_json 125 | def list_isp(): 126 | return ViewIspAclDal.list_isp() 127 | 128 | @bp.route('/list/acl_subnet_by_ip', methods=['GET']) 129 | @parse_params([dict(name='ip', type=str, required=True, nullable=False)]) 130 | @resp_wrapper_json 131 | def list_acl_subnet_by_ip(ip): 132 | return ViewIspAclDal.list_acl_subnet_by_ip(ip) 133 | 134 | @bp.route('/list/acl_isp_info', methods=['GET']) 135 | @resp_wrapper_json 136 | def list_acl_isp_info(): 137 | return ViewIspAclDal.list_acl_isp() 138 | 139 | 140 | @bp.route('/list/migrate_subnet', methods=['GET']) 141 | @resp_wrapper_json 142 | def list_migrate_subnet(): 143 | return ViewIspAclDal.get_migrate_subnet() 144 | 145 | 146 | 147 | -------------------------------------------------------------------------------- /dnsdb_common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/open_dnsdb/f717e9752f00a2937ff523c2ca120c2ebae57bca/dnsdb_common/__init__.py -------------------------------------------------------------------------------- /dnsdb_common/dal/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from functools import wraps 4 | 5 | from flask_sqlalchemy import SQLAlchemy 6 | 7 | db = SQLAlchemy(session_options={ 8 | 'autocommit': True 9 | }) 10 | 11 | 12 | def commit_on_success(func): 13 | @wraps(func) 14 | def decorator(*kargs, **kwargs): 15 | with db.session.begin(subtransactions=True): 16 | return func(*kargs, **kwargs) 17 | 18 | return decorator 19 | -------------------------------------------------------------------------------- /dnsdb_common/dal/models/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from datetime import date 4 | from datetime import datetime 5 | 6 | from dnsdb_common.library.exception import BadParam 7 | 8 | from .. import db 9 | 10 | db.Model = db.Model 11 | 12 | __all__ = [ 13 | "AnonymousUser", 14 | "AuditTimeMixin", 15 | "DeployHistory", 16 | "DnsHeader", 17 | "DnsHost", 18 | "DnsHostGroup", 19 | "OperationLog", 20 | "OperationLogDetail", 21 | "DnsNamedConf", 22 | "DnsRecord", 23 | "DnsSerial", 24 | "DnsZoneConf", 25 | "IpPool", 26 | "DnsColo", 27 | "Role", 28 | "Subnets", 29 | "User", 30 | "ViewAclCityCode", 31 | "ViewAclMigrateHistory", 32 | "ViewAclSubnet", 33 | "ViewConfigs", 34 | "ViewDomainNameState", 35 | "ViewDomainNames", 36 | "ViewIspStatus", 37 | "ViewIsps", 38 | "ViewMigrateDetail", 39 | "ViewMigrateHistory", 40 | "ViewRecords", 41 | "ViewSwitchIpDetail", 42 | "ViewSwitchIpHistory" 43 | ] 44 | 45 | 46 | class JsonMixin(object): 47 | def json_serialize(self, include=None, exclude=None): 48 | def __format(val): 49 | if isinstance(val, datetime): 50 | return val.strftime('%Y-%m-%d %H:%M:%S') 51 | elif isinstance(val, date): 52 | return val.strftime('%Y-%m-%d') 53 | return val 54 | 55 | filter_func = None 56 | if include is not None: 57 | if not isinstance(include, (list, tuple)): 58 | raise BadParam('param should be [list, tuple]') 59 | filter_func = lambda x: x in include 60 | elif exclude is not None: 61 | if not isinstance(exclude, (list, tuple)): 62 | raise BadParam('param should be [list, tuple]') 63 | filter_func = lambda x: x not in include 64 | 65 | fields = self.__mapper__.mapped_table.columns.keys() 66 | if filter_func is not None: 67 | fields = filter(filter_func, fields) 68 | return dict(((f, __format(getattr(self, f))) for f in fields)) 69 | 70 | 71 | class AuditTimeMixin(object): 72 | created_time = db.Column(db.DateTime, default=datetime.now) 73 | updated_time = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now) 74 | 75 | 76 | from .deploy_history import DeployHistory 77 | from .dns_header import DnsHeader 78 | from .dns_host import DnsHost 79 | from .dns_host_group import DnsHostGroup 80 | from .operation_log import OperationLog 81 | from .operation_log_detail import OperationLogDetail 82 | from .dns_named_conf import DnsNamedConf 83 | from .dns_record import DnsRecord 84 | from .dns_serial import DnsSerial 85 | from .dns_zone_conf import DnsZoneConf 86 | from .ippool import IpPool 87 | from .dns_colos import DnsColo 88 | from .subnets import Subnets 89 | from .user import AnonymousUser 90 | from .user import Role 91 | from .user import User 92 | from .view_acl_city_code import ViewAclCityCode 93 | from .view_acl_migrate_history import ViewAclMigrateHistory 94 | from .view_acl_subnets import ViewAclSubnet 95 | from .view_config import ViewConfigs 96 | from .view_domain_name_state import ViewDomainNameState 97 | from .view_domain_names import ViewDomainNames 98 | from .view_isp_status import ViewIspStatus 99 | from .view_isps import ViewIsps 100 | from .view_migrate_detail import ViewMigrateDetail 101 | from .view_migrate_history import ViewMigrateHistory 102 | from .view_records import ViewRecords 103 | from .view_switch_ip_detail import ViewSwitchIpDetail 104 | from .view_switch_ip_history import ViewSwitchIpHistory 105 | -------------------------------------------------------------------------------- /dnsdb_common/dal/models/deploy_history.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from . import AuditTimeMixin 4 | from .. import db 5 | 6 | 7 | class DeployHistory(db.Model, AuditTimeMixin): 8 | __tablename__ = 'tb_deploy_history' 9 | 10 | id = db.Column(db.Integer, primary_key=True) 11 | rtx_id = db.Column(db.String(50), nullable=False) 12 | deploy_desc = db.Column(db.Text, nullable=False) 13 | state = db.Column(db.String(50), nullable=False) 14 | -------------------------------------------------------------------------------- /dnsdb_common/dal/models/dns_colos.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from . import AuditTimeMixin 4 | from .. import db 5 | 6 | 7 | class DnsColo(db.Model, AuditTimeMixin): 8 | __tablename__ = 'tb_colo_config' 9 | 10 | id = db.Column(db.Integer) 11 | colo_name = db.Column(db.String(64), primary_key=True) 12 | colo_group = db.Column(db.String(64), primary_key=True) 13 | create_user = db.Column(db.String(64), nullable=False) 14 | -------------------------------------------------------------------------------- /dnsdb_common/dal/models/dns_header.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from . import AuditTimeMixin 4 | from .. import db 5 | 6 | 7 | class DnsHeader(db.Model, AuditTimeMixin): 8 | __tablename__ = 'tb_dns_zone_header' 9 | 10 | zone_name = db.Column(db.String(50), primary_key=True) 11 | header_content = db.Column(db.Text, nullable=False) 12 | -------------------------------------------------------------------------------- /dnsdb_common/dal/models/dns_host.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from . import AuditTimeMixin, JsonMixin 4 | from .. import db 5 | 6 | 7 | class DnsHost(db.Model, AuditTimeMixin, JsonMixin): 8 | __tablename__ = 'tb_dns_host' 9 | 10 | id = db.Column(db.Integer, primary_key=True) 11 | host_name = db.Column(db.String(100), unique=True, nullable=False) 12 | host_group = db.Column(db.String(32), nullable=False) 13 | host_ip = db.Column(db.String(32), nullable=False) 14 | host_conf_md5 = db.Column(db.String(100)) 15 | -------------------------------------------------------------------------------- /dnsdb_common/dal/models/dns_host_group.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from . import AuditTimeMixin, JsonMixin 4 | from .. import db 5 | 6 | 7 | class DnsHostGroup(db.Model, AuditTimeMixin, JsonMixin): 8 | __tablename__ = 'tb_dns_host_group' 9 | 10 | id = db.Column(db.Integer, primary_key=True) 11 | group_name = db.Column(db.String(32), unique=True, nullable=False) 12 | group_type = db.Column(db.String(32), nullable=False) 13 | # 组配置md5 和线上配置定时对比 14 | group_conf_md5 = db.Column(db.String(100)) 15 | reload_status = db.Column(db.Boolean, default=True) 16 | -------------------------------------------------------------------------------- /dnsdb_common/dal/models/dns_named_conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from . import AuditTimeMixin 4 | from .. import db 5 | 6 | 7 | # named conf for host group 8 | class DnsNamedConf(db.Model, AuditTimeMixin): 9 | __tablename__ = 'tb_dns_named_conf' 10 | 11 | name = db.Column(db.String(64), primary_key=True) 12 | conf_content = db.Column(db.Text, nullable=False) 13 | -------------------------------------------------------------------------------- /dnsdb_common/dal/models/dns_record.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from . import AuditTimeMixin, JsonMixin 4 | from .. import db 5 | 6 | 7 | class DnsRecord(db.Model, AuditTimeMixin, JsonMixin): 8 | __tablename__ = 'tb_dns_record' 9 | 10 | id = db.Column(db.Integer, primary_key=True) 11 | domain_name = db.Column(db.String(512), nullable=False) 12 | record = db.Column(db.String(512), nullable=False) 13 | zone_name = db.Column(db.String(20), nullable=False) 14 | update_user = db.Column(db.String(50), nullable=False) 15 | record_type = db.Column(db.String(20), nullable=False) 16 | ttl = db.Column(db.Integer, nullable=False, default=0) 17 | onoff = db.Column(db.Boolean, nullable=False, default=True) 18 | -------------------------------------------------------------------------------- /dnsdb_common/dal/models/dns_serial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from . import AuditTimeMixin 4 | from .. import db 5 | 6 | 7 | class DnsSerial(db.Model, AuditTimeMixin): 8 | __tablename__ = 'tb_dns_zone_serial' 9 | 10 | id = db.Column(db.Integer, primary_key=True) 11 | zone_name = db.Column(db.String(50), unique=True, nullable=False) 12 | zone_group = db.Column(db.String(64), nullable=False) 13 | serial_num = db.Column(db.BigInteger, nullable=False) 14 | update_serial_num = db.Column(db.BigInteger, nullable=False) 15 | -------------------------------------------------------------------------------- /dnsdb_common/dal/models/dns_zone_conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from . import AuditTimeMixin 4 | from .. import db 5 | 6 | 7 | class DnsZoneConf(db.Model, AuditTimeMixin): 8 | __tablename__ = 'tb_dns_named_zone' 9 | 10 | zone_name = db.Column(db.String(50), primary_key=True) 11 | # zone在不同group上的配置 12 | zone_conf = db.Column(db.Text, nullable=False) 13 | zone_group = db.Column(db.String(64), primary_key=True) 14 | # 0 反解 15 | # 1 机房 16 | # 2 普通 17 | zone_type = db.Column(db.Integer, nullable=False) 18 | -------------------------------------------------------------------------------- /dnsdb_common/dal/models/ippool.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from . import AuditTimeMixin 4 | from .. import db 5 | 6 | 7 | class IpPool(db.Model, AuditTimeMixin): 8 | __tablename__ = 'tb_ippool' 9 | 10 | id = db.Column(db.Integer) 11 | fixed_ip = db.Column(db.String(256), primary_key=True) 12 | region = db.Column(db.String(50), nullable=False) 13 | allocated = db.Column(db.Boolean, nullable=False, default=True) 14 | is_ipv6 = db.Column(db.Boolean, nullable=False, default=False) 15 | 16 | def __repr__(self): 17 | return ' [fixed_ip: %s, region: %s]' % (self.ip, self.region) 18 | -------------------------------------------------------------------------------- /dnsdb_common/dal/models/operation_log.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .. import db 4 | from . import JsonMixin 5 | 6 | 7 | class OperationLog(db.Model, JsonMixin): 8 | __tablename__ = 'tb_operation_log' 9 | 10 | id = db.Column(db.Integer, primary_key=True) 11 | rtx_id = db.Column(db.String(256), nullable=False) 12 | op_domain = db.Column(db.String(256), nullable=False) 13 | op_type = db.Column(db.String(32), nullable=False) 14 | op_before = db.Column(db.String(2048), nullable=False) 15 | op_after = db.Column(db.String(2048), nullable=False) 16 | op_time = db.Column(db.DateTime, nullable=False) 17 | op_result = db.Column(db.String(32), nullable=False) 18 | -------------------------------------------------------------------------------- /dnsdb_common/dal/models/operation_log_detail.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .. import db 4 | 5 | 6 | class OperationLogDetail(db.Model): 7 | __tablename__ = 'tb_operation_log_detail' 8 | 9 | id = db.Column(db.Integer, primary_key=True) 10 | log_id = db.Column(db.Integer, nullable=False) 11 | detail = db.Column(db.String(1024), default='') 12 | -------------------------------------------------------------------------------- /dnsdb_common/dal/models/operation_type.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from . import AuditTimeMixin, JsonMixin 4 | from .. import db 5 | 6 | 7 | class OperationType(db.Model, AuditTimeMixin, JsonMixin): 8 | __tablename__ = 'tb_operation_type' 9 | 10 | id = db.Column(db.Integer, primary_key=True) 11 | op_type = db.Column(db.String(64), unique=True, nullable=False) 12 | op_chinese = db.Column(db.String(128), unique=True, nullable=False) 13 | # logs = db.relationship('User', backref='role', lazy='dynamic') 14 | -------------------------------------------------------------------------------- /dnsdb_common/dal/models/subnets.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from . import AuditTimeMixin, JsonMixin 4 | from .. import db 5 | 6 | 7 | class Subnets(db.Model, AuditTimeMixin, JsonMixin): 8 | __tablename__ = 'tb_subnets' 9 | 10 | id = db.Column(db.Integer, primary_key=True) 11 | region_name = db.Column(db.String(80), nullable=False, unique=True) 12 | subnet = db.Column(db.String(50), nullable=False) 13 | create_user = db.Column(db.String(50), nullable=False) 14 | comment = db.Column(db.String(100)) 15 | colo = db.Column(db.String(64), nullable=False, default='') 16 | intranet = db.Column(db.Boolean, nullable=False, default=False) 17 | is_ipv6 = db.Column(db.Boolean, nullable=False, default=False) 18 | 19 | def __repr__(self): 20 | return ' [region_name: %s, subnet: %s]' % (self.region_name, self.subnet) 21 | -------------------------------------------------------------------------------- /dnsdb_common/dal/models/user.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from datetime import datetime 4 | 5 | from flask import current_app 6 | from flask_login import UserMixin, AnonymousUserMixin 7 | from itsdangerous import TimedJSONWebSignatureSerializer as Serializer 8 | from werkzeug.security import generate_password_hash, check_password_hash 9 | 10 | from . import AuditTimeMixin, JsonMixin 11 | from .. import db 12 | 13 | 14 | class Permission(object): 15 | GET = 1 16 | POST = 2 17 | ADMIN = 4 18 | 19 | 20 | class Role(db.Model, JsonMixin): 21 | __tablename__ = 'roles' 22 | id = db.Column(db.Integer, primary_key=True) 23 | name = db.Column(db.String(64), unique=True) 24 | default = db.Column(db.Boolean, default=False, index=True) 25 | permissions = db.Column(db.Integer) 26 | name_ch = db.Column(db.String(64), unique=True) 27 | users = db.relationship('User', backref='role', lazy='dynamic') 28 | 29 | def __init__(self, **kwargs): 30 | super(Role, self).__init__(**kwargs) 31 | if self.permissions is None: 32 | self.permissions = 0 33 | 34 | @staticmethod 35 | def insert_roles(): 36 | roles = { 37 | # 'User': ([Permission.GET], u'普通用户'), 38 | 'Operator': ([Permission.GET, Permission.POST], u'管理员'), 39 | 'Administrator': ([Permission.GET, Permission.POST, Permission.ADMIN], u'超级管理员') 40 | } 41 | default_role = 'Admin' 42 | for r in roles: 43 | role = Role.query.filter_by(name=r).first() 44 | if role is None: 45 | role = Role(name=r) 46 | role.reset_permissions() 47 | for perm in roles[r][0]: 48 | role.add_permission(perm) 49 | role.default = (role.name == default_role) 50 | role.name_ch = roles[r][1] 51 | db.session.add(role) 52 | db.session.commit() 53 | 54 | def add_permission(self, perm): 55 | if not self.has_permission(perm): 56 | self.permissions += perm 57 | 58 | def remove_permission(self, perm): 59 | if self.has_permission(perm): 60 | self.permissions -= perm 61 | 62 | def reset_permissions(self): 63 | self.permissions = 0 64 | 65 | def has_permission(self, perm): 66 | return self.permissions & perm == perm 67 | 68 | def __repr__(self): 69 | return '' % self.name 70 | 71 | 72 | class User(db.Model, UserMixin, AuditTimeMixin, JsonMixin): 73 | __tablename__ = 'users' 74 | id = db.Column(db.Integer, primary_key=True) 75 | username = db.Column(db.String(64), unique=True, index=True) 76 | email = db.Column(db.String(64), unique=True, index=True) 77 | role_id = db.Column(db.Integer, db.ForeignKey('roles.id')) 78 | password_hash = db.Column(db.String(128)) 79 | 80 | def __init__(self, **kwargs): 81 | super(User, self).__init__(**kwargs) 82 | self.role = Role.query.get(self.role_id) 83 | if self.role is None: 84 | self.role = Role.query.filter_by(default=True).first() 85 | self.role_id = self.role.id 86 | 87 | def json_serialize(self, include=('username', 'email', 'role_id'), exclude=None): 88 | data = super(User, self).json_serialize(include=include, exclude=exclude) 89 | if 'role' in include: 90 | data['role'] = self.role.name 91 | return data 92 | 93 | @property 94 | def password(self): 95 | raise AttributeError('password is not a readable attribute') 96 | 97 | @password.setter 98 | def password(self, password): 99 | self.password_hash = generate_password_hash(password) 100 | 101 | def verify_password(self, password): 102 | return check_password_hash(self.password_hash, password) 103 | 104 | def generate_confirmation_token(self, expiration=3600): 105 | s = Serializer(current_app.config['SECRET_KEY'], expiration) 106 | return s.dumps({'confirm': self.id}).decode('utf-8') 107 | 108 | def can(self, perm): 109 | return self.role is not None and self.role.has_permission(perm) 110 | 111 | def is_user(self): 112 | return self.can(Permission.GET) 113 | 114 | def is_administrator(self): 115 | return self.can(Permission.ADMIN) 116 | 117 | def ping(self): 118 | self.update_time = datetime.utcnow() 119 | db.session.add(self) 120 | 121 | def generate_auth_token(self, expiration): 122 | s = Serializer(current_app.config['SECRET_KEY'], 123 | expires_in=expiration) 124 | return s.dumps({'id': self.id}).decode('utf-8') 125 | 126 | def __repr__(self): 127 | return '' % self.username 128 | 129 | 130 | class AnonymousUser(AnonymousUserMixin): 131 | def can(self, permissions): 132 | return False 133 | 134 | def is_administrator(self): 135 | return False 136 | -------------------------------------------------------------------------------- /dnsdb_common/dal/models/view_acl_city_code.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from . import AuditTimeMixin 4 | from .. import db 5 | 6 | 7 | class ViewAclCityCode(db.Model, AuditTimeMixin): 8 | __tablename__ = 'tb_view_acl_city_code' 9 | code = db.Column(db.String(64), primary_key=True) 10 | country = db.Column(db.String(64), nullable=False) 11 | province = db.Column(db.String(64), nullable=False) 12 | city = db.Column(db.String(64)) 13 | 14 | def __repr__(self): 15 | return 'ViewAclCityCode [code={}, province={}]'.format( 16 | self.code, 17 | self.province 18 | ) 19 | -------------------------------------------------------------------------------- /dnsdb_common/dal/models/view_acl_migrate_history.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from sqlalchemy.orm import relationship 4 | 5 | from .. import db 6 | 7 | 8 | class ViewAclMigrateHistory(db.Model): 9 | __tablename__ = 'tb_view_acl_migrate_history' 10 | id = db.Column(db.Integer, primary_key=True) 11 | subnet_id = db.Column(db.Integer, db.ForeignKey('tb_view_acl_subnets.id')) 12 | from_acl = db.Column(db.String(32)) 13 | to_acl = db.Column(db.String(32), nullable=False) 14 | origin_acl = db.Column(db.String(32), nullable=False) 15 | create_user = db.Column(db.String(64), nullable=False) 16 | create_time = db.Column(db.DateTime, nullable=False) 17 | # 'migrated', 'recovered', 'remigrated' 18 | status = db.Column(db.String(32), default='migrated') 19 | reloaded = db.Column(db.Boolean, nullable=False, default=False) 20 | subnet = relationship("ViewAclSubnet") 21 | 22 | def __str__(self): 23 | return 'ViewAclSubnet[subnet=%s, from_isp=%s, to_isp=%s]' % ( 24 | self.subnet_id, 25 | self.from_view, 26 | self.to_view 27 | ) 28 | -------------------------------------------------------------------------------- /dnsdb_common/dal/models/view_acl_subnets.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from . import AuditTimeMixin, JsonMixin 4 | from .. import db 5 | 6 | 7 | class ViewAclSubnet(db.Model, AuditTimeMixin, JsonMixin): 8 | __tablename__ = 'tb_view_acl_subnets' 9 | id = db.Column(db.Integer, primary_key=True) 10 | subnet = db.Column(db.String(32), nullable=False, unique=True) 11 | start_ip = db.Column(db.Float, nullable=False) 12 | end_ip = db.Column(db.Float, nullable=False) 13 | origin_acl = db.Column(db.String(32), nullable=False) 14 | now_acl = db.Column(db.String(32), nullable=False) 15 | update_user = db.Column(db.String(64), nullable=False) 16 | is_ipv6 = db.Column(db.Boolean, nullable=False, default=False) 17 | 18 | def __str__(self): 19 | return 'ViewAclSubnet[subnet=%s, now_acl=%s, start=%s, end=%s]' % ( 20 | self.subnet, 21 | self.now_acl, 22 | self.start, 23 | self.end 24 | ) 25 | -------------------------------------------------------------------------------- /dnsdb_common/dal/models/view_config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from . import AuditTimeMixin 4 | from .. import db 5 | 6 | 7 | class ViewConfigs(db.Model, AuditTimeMixin): 8 | __tablename__ = 'tb_view_configs' 9 | 10 | key = db.Column(db.String(256), nullable=False, primary_key=True) 11 | value = db.Column(db.String(256), default='') 12 | -------------------------------------------------------------------------------- /dnsdb_common/dal/models/view_domain_name_state.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from . import AuditTimeMixin 4 | from .. import db 5 | 6 | 7 | class ViewDomainNameState(db.Model, AuditTimeMixin): 8 | __tablename__ = 'tb_view_domain_name_state' 9 | 10 | domain_name = db.Column(db.String(256), nullable=False, primary_key=True) 11 | origin_enabled_rooms = db.Column(db.String(256), default="[]", nullable=False) 12 | origin_state = db.Column(db.String(32), default='disabled', nullable=False) 13 | enabled_rooms = db.Column(db.String(256), default="[]", nullable=False) 14 | isp = db.Column(db.String(256), primary_key=True) 15 | state = db.Column(db.String(32), default='disabled') 16 | -------------------------------------------------------------------------------- /dnsdb_common/dal/models/view_domain_names.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from . import AuditTimeMixin 4 | from .. import db 5 | 6 | 7 | class ViewDomainNames(db.Model, AuditTimeMixin): 8 | __tablename__ = 'tb_view_domain_name' 9 | 10 | domain_name = db.Column(db.String(256), primary_key=True) 11 | cname = db.Column(db.String(256), nullable=False) 12 | -------------------------------------------------------------------------------- /dnsdb_common/dal/models/view_isp_status.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from . import AuditTimeMixin 4 | from .. import db 5 | 6 | 7 | class ViewIspStatus(db.Model, AuditTimeMixin): 8 | __tablename__ = 'tb_view_isps_status' 9 | 10 | id = db.Column(db.Integer, primary_key=True) 11 | history_id = db.Column(db.Integer) 12 | recover_id = db.Column(db.Integer) 13 | room = db.Column(db.String(64), nullable=False) 14 | # chinanet, cmnet, unicom 15 | isp = db.Column(db.String(64), nullable=False) 16 | is_health = db.Column(db.Boolean, default=False, nullable=False) 17 | closed = db.Column(db.Boolean, default=False, nullable=False) 18 | update_user = db.Column(db.String(64), nullable=False) 19 | -------------------------------------------------------------------------------- /dnsdb_common/dal/models/view_isps.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from . import AuditTimeMixin, JsonMixin 4 | from .. import db 5 | 6 | 7 | class ViewIsps(db.Model, AuditTimeMixin, JsonMixin): 8 | __tablename__ = 'tb_view_isps' 9 | 10 | name_in_english = db.Column(db.String(64), primary_key=True) 11 | abbreviation = db.Column(db.String(32), unique=True, nullable=False) 12 | name_in_chinese = db.Column(db.String(64), unique=True, nullable=False) 13 | acl_name = db.Column(db.String(64), unique=True) 14 | acl_file = db.Column(db.String(64)) 15 | username = db.Column(db.String(64), nullable=False) 16 | -------------------------------------------------------------------------------- /dnsdb_common/dal/models/view_migrate_detail.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import json 4 | 5 | from .. import db 6 | 7 | 8 | # 记录每次迁移的详情 9 | class ViewMigrateDetail(db.Model): 10 | __tablename__ = 'tb_view_migrate_detail' 11 | id = db.Column(db.Integer, primary_key=True) 12 | migrate_id = db.Column(db.Integer, nullable=False) 13 | domain_name = db.Column(db.String(256), nullable=False) 14 | before_enabled_server_rooms = db.Column(db.String(256), default='[]') 15 | after_enabled_server_rooms = db.Column(db.String(256), default='[]') 16 | isp = db.Column(db.String(256)) 17 | before_state = db.Column(db.String(32), default='disabled') 18 | after_state = db.Column(db.String(32), default='disabled') 19 | 20 | -------------------------------------------------------------------------------- /dnsdb_common/dal/models/view_migrate_history.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from . import AuditTimeMixin, JsonMixin 4 | from .. import db 5 | 6 | 7 | # 记录迁移进度 8 | class ViewMigrateHistory(db.Model, AuditTimeMixin, JsonMixin): 9 | __tablename__ = 'tb_view_migrate_history' 10 | 11 | id = db.Column(db.Integer, primary_key=True) 12 | migrate_rooms = db.Column(db.String(256), nullable=False) 13 | migrate_isps = db.Column(db.String(256), nullable=False) 14 | dst_rooms = db.Column(db.String(256), nullable=False) 15 | migrate_info = db.Column(db.Text) 16 | # 迁移规则: 'migrating', 'migrated', 'error', 'recovered' 17 | state = db.Column(db.String(32), default='migrating') 18 | cur = db.Column(db.Integer, nullable=False) 19 | all = db.Column(db.Integer, nullable=False) 20 | rtx_id = db.Column(db.String(256), nullable=False) 21 | 22 | # 不能并发的状态 23 | check_states = ('recovering', 'migrating') 24 | 25 | def __init__(self, migrate_rooms, migrate_isps, dst_rooms, state, cur, all, rtx_id): 26 | self.migrate_rooms = migrate_rooms 27 | self.migrate_isps = migrate_isps 28 | self.dst_rooms = dst_rooms 29 | self.state = state 30 | self.cur = cur 31 | self.all = all 32 | self.rtx_id = rtx_id 33 | 34 | def update(self, migrate_rooms, migrate_isps, dst_rooms, state, cur, all, rtx_id): 35 | self.migrate_rooms = migrate_rooms 36 | self.migrate_isps = migrate_isps 37 | self.dst_rooms = dst_rooms 38 | self.state = state 39 | self.cur = cur 40 | self.all = all 41 | self.rtx_id = rtx_id 42 | -------------------------------------------------------------------------------- /dnsdb_common/dal/models/view_records.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from . import AuditTimeMixin 4 | from .. import db 5 | 6 | 7 | class ViewRecords(db.Model, AuditTimeMixin): 8 | __tablename__ = 'tb_view_record' 9 | 10 | id = db.Column(db.Integer, primary_key=True) 11 | domain_name = db.Column(db.String(256), nullable=False) 12 | record = db.Column(db.String(256), nullable=False) 13 | record_type = db.Column(db.String(32), nullable=False) 14 | ttl = db.Column(db.Integer, nullable=False, default=60) 15 | property = db.Column(db.String(256), default='none') 16 | zone_name = db.Column(db.String(50), nullable=False) 17 | -------------------------------------------------------------------------------- /dnsdb_common/dal/models/view_switch_ip_detail.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import json 4 | 5 | from .. import db 6 | 7 | 8 | class ViewSwitchIpDetail(db.Model): 9 | __tablename__ = 'tb_view_switch_ip_detail' 10 | id = db.Column(db.Integer, primary_key=True) 11 | switch_id = db.Column(db.Integer, nullable=False) 12 | domain_name = db.Column(db.String(256), nullable=False, primary_key=True) 13 | before_enabled_server_rooms = db.Column(db.String(256), default='[]') 14 | isp = db.Column(db.String(256), primary_key=True) 15 | before_state = db.Column(db.String(32), default='disabled') 16 | after_state = db.Column(db.String(32), default='disabled') 17 | 18 | def __init__(self, switch_id, domain_name, before_enabled_server_rooms, isp, before_state, after_state): 19 | self.switch_id = switch_id 20 | self.domain_name = domain_name 21 | self.before_enabled_server_rooms = json.dumps(before_enabled_server_rooms) 22 | self.isp = isp 23 | self.before_state = before_state 24 | self.after_state = after_state 25 | -------------------------------------------------------------------------------- /dnsdb_common/dal/models/view_switch_ip_history.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from . import AuditTimeMixin 4 | from .. import db 5 | 6 | 7 | # 记录ip切换记录 8 | class ViewSwitchIpHistory(db.Model, AuditTimeMixin): 9 | __tablename__ = 'tb_view_switch_ip_history' 10 | id = db.Column(db.Integer, primary_key=True) 11 | switch_ip = db.Column(db.String(32), nullable=False) 12 | switch_type = db.Column(db.String(32), nullable=False) 13 | switch_to = db.Column(db.String(32), nullable=False) 14 | state = db.Column(db.String(32), default='switched') 15 | rtx_id = db.Column(db.String(256), nullable=False) 16 | 17 | def __init__(self, switch_ip, switch_type, switch_to, state, update_at, rtx_id): 18 | self.switch_ip = switch_ip 19 | self.switch_type = switch_type 20 | self.switch_to = switch_to 21 | self.state = state 22 | self.update_at = update_at 23 | self.rtx_id = rtx_id 24 | -------------------------------------------------------------------------------- /dnsdb_common/dal/operation_log.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import json 4 | from datetime import datetime 5 | from datetime import timedelta 6 | 7 | from . import db, commit_on_success 8 | from .models import OperationLog 9 | from .models import OperationLogDetail 10 | 11 | 12 | def format_time(time_str, time_type): 13 | if time_type == 'start': 14 | t = datetime.strptime(time_str, '%Y-%m-%d') 15 | else: 16 | t = datetime.strptime(time_str, '%Y-%m-%d') 17 | t = t + timedelta(days=1) 18 | return t.strftime('%Y-%m-%d %H:%M:%S') 19 | 20 | 21 | class OperationLogDal(object): 22 | @staticmethod 23 | def insert_operation_log_with_dict(rtx_id, op_domain, op_type, op_before, op_after, op_result, reason=None): 24 | session = db.session 25 | with session.begin(subtransactions=True): 26 | item = OperationLog( 27 | rtx_id=rtx_id, 28 | op_domain=op_domain, 29 | op_type=op_type, 30 | op_before=json.dumps(op_before), 31 | op_after=json.dumps(op_after), 32 | op_time=datetime.now(), 33 | op_result=op_result 34 | ) 35 | session.add(item) 36 | 37 | if op_result == 'fail' and reason is not None: 38 | OperationLogDal.add_log_detail(OperationLog.id, reason) 39 | return item.id 40 | 41 | @staticmethod 42 | def add_log_detail(log_id, reason): 43 | session = db.session 44 | if not isinstance(reason, str): 45 | reason = str(reason) 46 | if len(reason) > 1024: 47 | reason = reason[:1024] 48 | with session.begin(subtransactions=True): 49 | session.add(OperationLogDetail( 50 | log_id=log_id, 51 | detail=reason 52 | )) 53 | 54 | @staticmethod 55 | def list_operation_log(condition): 56 | query = OperationLog.query 57 | if condition['start_time'] != '' and condition['start_time'] != '': 58 | query = query.filter( 59 | OperationLog.op_time.between(format_time(condition['start_time'], 'start'), 60 | format_time(condition['end_time'], 'end'))) 61 | if 'domain' in condition: 62 | query = query.filter(OperationLog.op_domain == condition['domain']) 63 | if 'type' in condition: 64 | query = query.filter(OperationLog.op_type == condition['type']) 65 | if 'rtx_id' in condition: 66 | query = query.filter(OperationLog.rtx_id == condition['rtx_id']) 67 | total = query.count() 68 | logs = query.order_by(OperationLog.op_time.desc()).offset( 69 | condition['page_size'] * (condition['page'] - 1)).limit( 70 | condition['page_size']).all() 71 | result = [] 72 | for log in logs: 73 | result.append(log.json_serialize()) 74 | 75 | return {'total': total, 'logs': result} 76 | 77 | @staticmethod 78 | def get_log_detail(log_id): 79 | item = OperationLogDetail.query.filter_by(log_id=log_id).first() 80 | if not item: 81 | return '' 82 | return item.detail 83 | 84 | @staticmethod 85 | def create_deploy_job(user, deploy_info, conf_type, unfinished): 86 | return OperationLogDal.insert_operation_log_with_dict(user, conf_type, 87 | 'conf_deploy', deploy_info, 88 | dict(successed=[], failed={}, unfinished=unfinished), 'wait') 89 | 90 | @staticmethod 91 | def get_deploy_job(job_id): 92 | return OperationLog.query.get(job_id) 93 | 94 | @staticmethod 95 | def reset_deploy_job(user, job_id): 96 | item = OperationLog.query.get(job_id) 97 | if not item: 98 | return item 99 | 100 | op_info = json.loads(item.op_before) 101 | op_domain = item.op_domain 102 | unfinished = [] 103 | if op_domain == 'named.conf': 104 | for group, info in op_info.items(): 105 | unfinished.extend(info['hosts']) 106 | elif op_domain == 'acl': 107 | for group, hosts in op_info.get('hosts', {}).items(): 108 | unfinished.extend(hosts) 109 | elif op_domain == 'zone': 110 | unfinished.extend(op_info.get('hosts', [])) 111 | 112 | data = dict(op_time=datetime.now(), op_result='wait', op_after=json.dumps(dict(successed=[], failed={}, unfinished=unfinished))) 113 | return OperationLogDal.update_opration_log(job_id, data) 114 | 115 | @staticmethod 116 | @commit_on_success 117 | def update_opration_log(job_id, data): 118 | return OperationLog.query.filter_by(id=job_id).update(data) 119 | 120 | @staticmethod 121 | def update_deploy_info(deploy_id, host, is_success, msg): 122 | # json.dumps(dict(success=[], failed={}, unfinish=[])) 123 | job = OperationLogDal.get_deploy_job(deploy_id) 124 | op_after = json.loads(job.op_after) 125 | success = op_after.get('successed', []) 126 | unfinish = op_after.get('unfinished', []) 127 | failed = op_after.get('failed', {}) 128 | 129 | if host in unfinish: 130 | unfinish.remove(host) 131 | op_after['unfinished'] = unfinish 132 | 133 | if is_success: 134 | success.append(host) 135 | op_after['successed'] = success 136 | else: 137 | failed[host] = msg 138 | op_after['failed'] = failed 139 | 140 | data = dict(op_after=json.dumps(op_after)) 141 | if not unfinish: 142 | data['op_result'] = 'ok' 143 | if failed: 144 | data['op_result'] = 'fail' 145 | OperationLogDal.update_opration_log(deploy_id, data) 146 | -------------------------------------------------------------------------------- /dnsdb_common/dal/subnet_ip.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import ipaddress 3 | 4 | from dnsdb_common.library.exception import BadParam 5 | from dnsdb_common.library.utils import format_ip 6 | from . import commit_on_success 7 | from . import db 8 | from .models import DnsColo 9 | from .models import DnsRecord 10 | from .models import IpPool 11 | from .models import Subnets 12 | 13 | 14 | class SubnetIpDal(object): 15 | @staticmethod 16 | def get_colo_by_group(group): 17 | return [record.colo_name 18 | for record in 19 | db.session.query(DnsColo.colo_name).filter_by(colo_group=group).order_by(DnsColo.colo_name)] 20 | 21 | @staticmethod 22 | def list_region(**condition): 23 | q = Subnets.query 24 | if condition: 25 | q = q.filter_by(**condition) 26 | return [item.json_serialize() for item in q.order_by(Subnets.region_name, Subnets.subnet)] 27 | 28 | @staticmethod 29 | def get_region_by_ip(ip): 30 | ip, _ = format_ip(ip) 31 | record = IpPool.query.filter_by(fixed_ip=ip).first() 32 | if not record: 33 | raise BadParam('no such ip: %s' % ip, msg_ch=u'没有对应的ip记录') 34 | return SubnetIpDal.get_region_by_name(record.region) 35 | 36 | @staticmethod 37 | def get_region_by_name(region): 38 | record = Subnets.query.filter_by(region_name=region).first() 39 | if not record: 40 | raise BadParam('no such subnet with region_name: %s' % region, msg_ch=u'没有对应的网段记录') 41 | return record.json_serialize() 42 | 43 | @staticmethod 44 | def get_region_by_name_like(region): 45 | region = '%{}%'.format(region) 46 | records = Subnets.query.filter(Subnets.region_name.like(region)) 47 | return [record.json_serialize() for record in records] 48 | 49 | @staticmethod 50 | def is_intranet_region(region): 51 | record = Subnets.query.filter_by(region_name=region).first() 52 | if not record: 53 | raise BadParam('no such subnet with region_name: %s' % region, msg_ch=u'没有对应的网段记录') 54 | return record.intranet 55 | 56 | @staticmethod 57 | def is_ip_exist(record): 58 | return IpPool.query.filter_by(fixed_ip=record).first() is not None 59 | 60 | @staticmethod 61 | def get_subnet_ip(region): 62 | records = IpPool.query.outerjoin(DnsRecord, DnsRecord.record == IpPool.fixed_ip).add_columns( 63 | IpPool.fixed_ip, IpPool.allocated, 64 | DnsRecord.domain_name).filter(IpPool.region == region).order_by(IpPool.fixed_ip) 65 | result = [{"ip": item.fixed_ip, "domain": item.domain_name} for item in records] 66 | return result 67 | 68 | @staticmethod 69 | def add_subnet(subnet, region, colo, comment, username): 70 | subnet = ipaddress.ip_network(subnet) 71 | intranet = subnet.is_private 72 | net_id = subnet.network_address 73 | broadcast_ip = subnet.broadcast_address 74 | is_ipv6 = (subnet.version == 6) 75 | ips_dict_list = [] 76 | for i in subnet: 77 | if i == net_id or i == broadcast_ip: 78 | continue 79 | ips_dict_list.append({ 80 | 'region': region, 81 | 'fixed_ip': str(i), 82 | 'is_ipv6': is_ipv6 83 | }) 84 | if Subnets.query.filter_by(region_name=region).first(): 85 | raise BadParam('region already exist', msg_ch='网段名已存在') 86 | try: 87 | with db.session.begin(subtransactions=True): 88 | subnet_item = Subnets( 89 | region_name=region, 90 | subnet=str(subnet), 91 | create_user=username, 92 | intranet=intranet, 93 | colo=colo, 94 | is_ipv6=is_ipv6 95 | ) 96 | if comment: 97 | subnet_item.comment = comment 98 | db.session.add(subnet_item) 99 | db.session.bulk_insert_mappings(IpPool, ips_dict_list) 100 | except Exception: 101 | raise BadParam('Ip conflict with other regions', msg_ch=u'和已有的网段有交叉,请检查后重试') 102 | 103 | @staticmethod 104 | @commit_on_success 105 | def delete_subnet(subnet, region): 106 | record = Subnets.query.filter_by(region_name=region, subnet=subnet).first() 107 | if not record: 108 | raise BadParam('Region does not exist: %s' % region, msg_ch=u'网段不存在') 109 | # 删除一个region 110 | ip_records = SubnetIpDal.get_subnet_ip(region) 111 | if list(filter(lambda x: x['domain'], ip_records)): 112 | raise BadParam('Region %s has records,delete failed!' % region, msg_ch=u'网段正在使用中,不允许删除') 113 | 114 | Subnets.query.filter_by(region_name=region, subnet=subnet).delete() 115 | IpPool.query.filter_by(region=region).delete() 116 | 117 | 118 | @staticmethod 119 | @commit_on_success 120 | def rename_subnet(old_region, new_region, username): 121 | if Subnets.query.filter_by(region_name=new_region).first(): 122 | raise BadParam("Region %s existed, rename %s failed" % (new_region, old_region), 123 | msg_ch=u'%s已经存在' % new_region) 124 | if not Subnets.query.filter_by(region_name=old_region).first(): 125 | raise BadParam("Region %s does not existed, rename failed" % old_region, 126 | msg_ch=u'%s不存在' % old_region) 127 | Subnets.query.filter(Subnets.region_name == old_region).update({ 128 | "region_name": new_region 129 | }) 130 | IpPool.query.filter(IpPool.region == old_region).update({ 131 | 'region': new_region 132 | }) 133 | 134 | @staticmethod 135 | def get_subnets_by_condition(**kwargs): 136 | session = db.session 137 | query = session.query(Subnets) 138 | if kwargs: 139 | query = query.filter_by(**kwargs) 140 | return query.order_by(Subnets.region_name, Subnets.subnet).all() 141 | 142 | @staticmethod 143 | def bulk_update_subnet(update_mapping): 144 | session = db.session 145 | with session.begin(subtransactions=True): 146 | session.bulk_update_mappings(Subnets, update_mapping) 147 | -------------------------------------------------------------------------------- /dnsdb_common/dal/user.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from . import commit_on_success, db 4 | from .models import User, Role 5 | from ..library.exception import BadParam 6 | 7 | 8 | class UserDal(object): 9 | ALLOWED_ROLES = [u'owner', u'dev'] 10 | 11 | def __init__(self): 12 | pass 13 | 14 | @staticmethod 15 | def get_roles(): 16 | return {item.id: item.name_ch for item in Role.query.all()} 17 | 18 | @staticmethod 19 | def get_role_name(role_id): 20 | role = Role.query.get(role_id) 21 | if role: 22 | return role.name 23 | else: 24 | return None 25 | 26 | @staticmethod 27 | def get_user_info(**kwargs): 28 | return User.query.filter_by(**kwargs).first() 29 | 30 | @staticmethod 31 | @commit_on_success 32 | def add_user(username, email, password, role_id): 33 | if User.query.filter_by(username=username).first(): 34 | raise BadParam('user with username %s already exist.' % username) 35 | if User.query.filter_by(email=email).first(): 36 | raise BadParam('user with email %s already exist.' % email) 37 | user = User(username=username, email=email, password=password, role_id=role_id) 38 | db.session.add(user) 39 | 40 | @staticmethod 41 | def list_user(role_id='', page=1, page_size=10): 42 | query = User.query 43 | if role_id: 44 | query = query.filter_by(role_id=role_id) 45 | # if page <= 0: 46 | # page = 1 47 | return [obj.json_serialize() for obj in (query. 48 | offset((page - 1) * page_size). 49 | limit(page_size).all())] 50 | 51 | # @staticmethod 52 | # def reset_password(token, new_password): 53 | # s = Serializer(current_app.config['SECRET_KEY']) 54 | # try: 55 | # data = s.loads(token.encode('utf-8')) 56 | # except: 57 | # return False 58 | # user = User.query.get(data.get('reset')) 59 | # if user is None: 60 | # return False 61 | # user.password = new_password 62 | # db.session.add(user) 63 | # return True 64 | 65 | @staticmethod 66 | @commit_on_success 67 | def delete_user(username): 68 | User.query.filter_by(username=username).delete() 69 | -------------------------------------------------------------------------------- /dnsdb_common/library/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/open_dnsdb/f717e9752f00a2937ff523c2ca120c2ebae57bca/dnsdb_common/library/__init__.py -------------------------------------------------------------------------------- /dnsdb_common/library/api.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import json 4 | 5 | import requests 6 | from oslo_config import cfg 7 | from requests.auth import HTTPBasicAuth 8 | 9 | from .exception import DnsdbException 10 | 11 | CONF = cfg.CONF 12 | 13 | 14 | class Api(object): 15 | def __init__(self, root, username='', password='', headers=None, 16 | resp_wrapper=lambda x, y: x): 17 | self.root = root 18 | self.auth = HTTPBasicAuth(username, password) 19 | self.content_type_form = { 20 | 'Content-type': 'application/x-www-form-urlencoded'} 21 | self.content_type_json = { 22 | 'Content-type': 'application/json'} 23 | self.headers = headers or {} 24 | self.resp_wrapper = resp_wrapper 25 | 26 | def _wrap_headers(self, headers, ctype='form'): 27 | _headers = {} 28 | _headers.update(self.headers) 29 | _headers.update(headers or {}) 30 | content_type = 'application/x-www-form-urlencoded' if ctype == 'form' \ 31 | else 'application/json' 32 | _headers.update({ 33 | 'Content-type': content_type 34 | }) 35 | return _headers 36 | 37 | def _get(self, url, params=None, headers=None, ctype='form', **kwargs): 38 | url = '%s%s' % (self.root, url) 39 | headers = self._wrap_headers(headers, ctype=ctype) 40 | params = params if params else {} 41 | resp = requests.get( 42 | url, auth=self.auth, headers=headers, params=params, **kwargs) 43 | req = dict(url=url, headers=headers, params=params) 44 | req.update(kwargs) 45 | return self.resp_wrapper(resp, req) 46 | 47 | def _post(self, url, headers=None, data=None, params=None, 48 | ctype='form', **kwargs): 49 | url = '%s%s' % (self.root, url) 50 | headers = self._wrap_headers(headers, ctype=ctype) 51 | params = params if params else {} 52 | data = data if data else {} 53 | if ctype == 'json': 54 | data = json.dumps(data) 55 | resp = requests.post( 56 | url, auth=self.auth, headers=headers, params=params, data=data, 57 | verify=False, **kwargs) 58 | req = dict( 59 | url=url, headers=headers, params=params, data=data, verify=False) 60 | req.update(kwargs) 61 | return self.resp_wrapper(resp, req) 62 | 63 | def get_form(self, url, *args, **kwargs): 64 | return self._get(url, *args, **dict(kwargs, **{'ctype': 'form'})) 65 | 66 | def get_json(self, url, *args, **kwargs): 67 | return self._get(url, *args, **dict(kwargs, **{'ctype': 'json'})) 68 | 69 | def post_form(self, url, *args, **kwargs): 70 | return self._post(url, *args, **dict(kwargs, **{'ctype': 'form'})) 71 | 72 | def post_json(self, url, *args, **kwargs): 73 | return self._post(url, *args, **dict(kwargs, **{'ctype': 'json'})) 74 | 75 | 76 | def _updater_resp_wrapper(resp, req): 77 | try: 78 | resp = resp.json() 79 | except Exception as ex: 80 | raise DnsdbException( 81 | u'DnsdbApi request error', 500, detail=dict( 82 | request=req, ex=str(ex), reason=resp.reason, 83 | status=resp.status_code), 84 | msg_ch=u'DnsUpdater调用失败') 85 | if int(resp.get('status', 200)) != 200 or resp.get('errcode', 0) != 0: 86 | raise DnsdbException(u'DnsUpdater调用失败', 400, json.dumps(resp)) 87 | return resp 88 | 89 | 90 | class DnsUpdaterApi(Api): 91 | def __init__(self, host_ip, username='', password='', headers=None): 92 | port = CONF.api.dnsupdater_port 93 | root = 'http://{}:{}/api'.format(host_ip, port) 94 | super(DnsUpdaterApi, self).__init__(root, username='', password='', headers=None, 95 | resp_wrapper=_updater_resp_wrapper) 96 | 97 | def notify_update_named(self, group_name, group_conf_md5): 98 | return self.post_json('/notify_update/named', data={ 99 | 'group_name': group_name, 100 | 'group_conf_md5': group_conf_md5 101 | }) 102 | 103 | def notify_update(self, deploy_type, group_name, **kwargs): 104 | return self.post_json('/notify_update', 105 | data={ 106 | 'update_type': deploy_type, 107 | 'group_name': group_name, 108 | 'params': kwargs 109 | }) 110 | -------------------------------------------------------------------------------- /dnsdb_common/library/database.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import sqlite3 4 | from oslo_config import cfg 5 | 6 | from ..library import exception as err 7 | from ..library.log import getLogger 8 | log = getLogger(__name__) 9 | 10 | CONF = cfg.CONF 11 | 12 | 13 | class SqliteDatabase(object): 14 | def __init__(self): 15 | self.data = CONF.database.data 16 | 17 | def connect(self): 18 | try: 19 | self.conn = sqlite3.connect(self.data) 20 | self.cur = self.conn.cursor() 21 | return True 22 | except Exception as e: 23 | log.error("Unable to connect to db: %s" % (e)) 24 | return False 25 | 26 | def execute_sql(self, sql): 27 | try: 28 | self.cur.execute(sql) 29 | return True 30 | except Exception as e: 31 | log.error("Failed to execute sql: %s" % (e)) 32 | return False 33 | 34 | def fetchall(self): 35 | try: 36 | result = self.cur.fetchall() 37 | return True, result 38 | except Exception as e: 39 | log.error("Failed to fetchall: %s" % (e)) 40 | return False, None 41 | 42 | def insert(self, sql): 43 | try: 44 | self.cur.execute(sql) 45 | return err.ENONE 46 | except sqlite3.IntegrityError as e: 47 | log.error("Failed to execute sql: %s" % e) 48 | return err.ECONFLICT 49 | except Exception as e: 50 | log.error("Failed to execute sql: %s" % e) 51 | return err.EDBGONE 52 | 53 | def execute_and_fetch(self, sql, *args, **kargs): 54 | try: 55 | self.cur.execute(sql, *args, **kargs) 56 | result = self.cur.fetchall() 57 | return True, result 58 | except Exception as e: 59 | log.error("Failed to execute_and_fetch sql: %s" % e) 60 | return False, None 61 | 62 | def execute_and_get_count(self, sql, *args, **kargs): 63 | try: 64 | self.cur.execute(sql, *args, **kargs) 65 | result = self.cur.rowcount 66 | return True, result 67 | except Exception as e: 68 | log.error("Failed to execute_and_get_count sql: %s" % (e)) 69 | return False, None 70 | 71 | def commit(self): 72 | self.conn.commit() 73 | 74 | def commit_and_close(self): 75 | self.conn.commit() 76 | self.conn.close() 77 | 78 | def rollback(self): 79 | self.conn.rollback() 80 | 81 | def rollback_and_close(self): 82 | self.conn.rollback() 83 | self.conn.close() 84 | 85 | def get_rowcount(self): 86 | return self.cur.rowcount 87 | 88 | def close(self): 89 | self.conn.close() 90 | 91 | 92 | def get_db_connection(): 93 | db = SqliteDatabase() 94 | if not db.connect(): 95 | return None 96 | return db 97 | -------------------------------------------------------------------------------- /dnsdb_common/library/email_util.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import smtplib 4 | from email.header import Header 5 | from email.mime.text import MIMEText 6 | 7 | from oslo_config import cfg 8 | 9 | from ..library.log import getLogger 10 | 11 | log = getLogger(__name__) 12 | 13 | CONF = cfg.CONF 14 | 15 | 16 | def send_email(subject, content, sender=None, password=None, receivers=None): 17 | msg = '' 18 | try: 19 | if content is None: 20 | content = "" 21 | msg = MIMEText(content, 'plain', 'utf-8') 22 | if sender is None: 23 | sender = CONF.MAIL.from_addr 24 | password = CONF.MAIL.password 25 | elif not isinstance(sender, str): 26 | raise TypeError('sender should be str type.') 27 | if receivers is None: 28 | receivers = CONF.MAIL.info_list 29 | elif not isinstance(receivers, str): 30 | raise TypeError('Receivers should be str type.') 31 | to_list = receivers.split(';') 32 | 33 | msg['Subject'] = Header(subject, 'utf-8') 34 | msg['From'] = Header(sender, 'utf-8') 35 | msg['To'] = Header(receivers, 'utf-8') 36 | s = smtplib.SMTP() 37 | s.connect(CONF.MAIL.server, CONF.MAIL.port) 38 | if password: 39 | s.login(sender, password) 40 | s.sendmail(sender, to_list, msg.as_string()) 41 | except Exception as e: 42 | log.error("Failed to send email:%s, because: %s" % (msg, e)) 43 | finally: 44 | try: 45 | s.close() 46 | except: 47 | pass 48 | 49 | 50 | def send_alert_email(content, sender=None): 51 | receivers = CONF.MAIL.alert_list 52 | subject = "[DNSDB alarm]" 53 | send_email(subject, content, sender, receivers) 54 | 55 | -------------------------------------------------------------------------------- /dnsdb_common/library/exception.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | BADREQUEST = 400 4 | UNAUTHORIZED = 401 5 | FORBIDDEN = 403 6 | GONE = 410 7 | TOOMANYREQUESTS = 412 8 | 9 | 10 | class DnsdbException(Exception): 11 | def __init__(self, message, errcode=500, detail=None, msg_ch=u''): 12 | self.message = message 13 | self.errcode = errcode 14 | self.detail = detail 15 | self.msg_ch = msg_ch 16 | super(DnsdbException, self).__init__() 17 | 18 | def __str__(self): 19 | return self.message 20 | 21 | def json(self): 22 | return dict(code=self.errcode, why=self.message) 23 | 24 | 25 | class Unauthorized(DnsdbException): 26 | def __init__(self, message='Unauthorized', errcode=UNAUTHORIZED, detail=None, msg_ch=u''): 27 | super(Unauthorized, self).__init__(message, errcode, detail, msg_ch) 28 | 29 | class Forbidden(DnsdbException): 30 | def __init__(self, message='Forbidden', errcode=FORBIDDEN, detail=None, msg_ch=u''): 31 | super(Forbidden, self).__init__(message, errcode, detail, msg_ch) 32 | 33 | 34 | class OperationLogErr(DnsdbException): 35 | def __init__(self, message, errcode=500, detail=None, msg_ch=u''): 36 | super(OperationLogErr, self).__init__(message, errcode, detail, msg_ch) 37 | 38 | 39 | class BadParam(DnsdbException): 40 | def __init__(self, message='Bad params', errcode=BADREQUEST, detail=None, msg_ch=u''): 41 | super(BadParam, self).__init__(message, errcode, detail, msg_ch) 42 | 43 | 44 | class UpdaterErr(DnsdbException): 45 | pass 46 | 47 | 48 | class ConfigErr(UpdaterErr): 49 | def __init__(self, message): 50 | super(ConfigErr, self).__init__(message=message, errcode=501) 51 | -------------------------------------------------------------------------------- /dnsdb_common/library/gunicorn_app.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import multiprocessing 4 | 5 | from gunicorn import glogging 6 | from gunicorn.app import base 7 | from gunicorn.six import iteritems 8 | 9 | 10 | class GunicornLogger(glogging.Logger): 11 | def access(self, resp, req, environ, request_time): 12 | # ignore healthcheck 13 | if environ.get('RAW_URI') == '/healthcheck.html': 14 | return 15 | super(GunicornLogger, self).access(resp, req, environ, request_time) 16 | 17 | 18 | def number_of_workers(): 19 | return (multiprocessing.cpu_count() * 2) + 1 20 | 21 | 22 | class GunicornApplication(base.BaseApplication): 23 | def __init__(self, app, options=None): 24 | self.options = options or {} 25 | self.application = app 26 | super(GunicornApplication, self).__init__() 27 | 28 | def load_config(self): 29 | config = dict([(key, value) for key, value in iteritems(self.options) 30 | if key in self.cfg.settings and value is not None]) 31 | config['logger_class'] = GunicornLogger 32 | for key, value in iteritems(config): 33 | self.cfg.set(key.lower(), value) 34 | 35 | def load(self): 36 | return self.application 37 | -------------------------------------------------------------------------------- /dnsdb_common/library/local.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # vim: tabstop=4 shiftwidth=4 softtabstop=4 et 3 | 4 | # Copyright 2011 OpenStack Foundation. 5 | # All Rights Reserved. 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 8 | # not use this file except in compliance with the License. You may obtain 9 | # a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 15 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 16 | # License for the specific language governing permissions and limitations 17 | # under the License. 18 | 19 | """Local storage of variables using weak references""" 20 | 21 | import threading 22 | import weakref 23 | 24 | 25 | class WeakLocal(threading.local): 26 | def __getattribute__(self, attr): 27 | rval = super(WeakLocal, self).__getattribute__(attr) 28 | if rval: 29 | # NOTE(mikal): this bit is confusing. What is stored is a weak 30 | # reference, not the value itself. We therefore need to lookup 31 | # the weak reference and return the inner value here. 32 | rval = rval() 33 | return rval 34 | 35 | def __setattr__(self, attr, value): 36 | value = weakref.ref(value) 37 | return super(WeakLocal, self).__setattr__(attr, value) 38 | 39 | 40 | # NOTE(mikal): the name "store" should be deprecated in the future 41 | store = WeakLocal() 42 | 43 | # A "weak" store uses weak references and allows an object to fall out of scope 44 | # when it falls out of scope in the code that uses the thread local storage. A 45 | # "strong" store will hold a reference to the object so that it never falls out 46 | # of scope. 47 | weak_store = WeakLocal() 48 | strong_store = threading.local() 49 | -------------------------------------------------------------------------------- /dnsdb_common/library/singleton.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | class Singleton(object): 4 | __instance = None 5 | 6 | def __new__(cls, *args, **kwargs): 7 | if not isinstance(cls.__instance, cls): 8 | cls.__instance = object.__new__(cls, *args, **kwargs) 9 | cls.__instance.init() 10 | return cls.__instance 11 | 12 | def init(self): 13 | pass 14 | 15 | 16 | class MetaSingleton(type): 17 | def __init__(self, *args, **kwargs): 18 | self.__instance = None 19 | super(MetaSingleton, self).__init__(*args, **kwargs) 20 | 21 | def __call__(self, *args, **kwargs): 22 | if self.__instance is None: 23 | self.__instance = super(MetaSingleton, self).__call__(*args, **kwargs) 24 | return self.__instance 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /dnsdb_common/library/validator.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import re 4 | 5 | def _match_pattern(pattern, string): 6 | ptn = re.search(r'(%s)' % pattern, string) 7 | if ptn is None: 8 | return False 9 | if len(ptn.groups()) == 0: 10 | return False 11 | return ptn.group(1) == string 12 | 13 | 14 | def valid_string(s, min_len=None, max_len=None, 15 | allow_blank=False, auto_trim=True, pattern=None): 16 | """ 17 | @param s str/unicode 要校验的字符串 18 | @param min_len None/int 19 | @param max_len None/int 20 | @param allow_blank boolean 21 | @param auto_trim boolean 22 | @:param pattern re.pattern 23 | @return boolean is_ok 24 | @return string/int value 若是ok,返回int值,否则返回错误信息 25 | """ 26 | if s is None: 27 | return False, u'不能为None' 28 | if not isinstance(s, str): 29 | return False, u"参数类型需要是字符串" 30 | if auto_trim: 31 | s = s.strip() 32 | str_len = len(s) 33 | if not allow_blank and str_len < 1: 34 | return False, u"参数不允许为空" 35 | if max_len is not None and str_len > max_len: 36 | return False, u"参数长度需小于%d" % max_len 37 | if min_len is not None and str_len < min_len: 38 | return False, u"参数长度需大于 %d" % min_len 39 | if pattern is not None and s and not _match_pattern(pattern, s): 40 | return False, u'参数包含的字符: %s' % pattern 41 | return True, s 42 | 43 | 44 | def valid_int(s, min_value=None, max_value=None): 45 | """\ 46 | @param s str/unicode 要校验的字符串 47 | @param min_value None/int 48 | @param max_value None/int 49 | @return boolean is_ok 50 | @return string/int value 若是ok,返回int值,否则返回错误信息 51 | """ 52 | if s is None: 53 | return False, "cannot is None" 54 | if not isinstance(s, str): 55 | return False, "must a string value" 56 | s = int(s) 57 | if max_value is not None and s > max_value: 58 | return False, "%d must less than %d" % (s, max_value) 59 | if min_value is not None and s < min_value: 60 | return False, "%d must greater than %d" % (s, min_value) 61 | return True, s 62 | -------------------------------------------------------------------------------- /dnsdb_fe/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false, 5 | "targets": { 6 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] 7 | } 8 | }], 9 | "stage-2" 10 | ], 11 | "plugins": ["transform-runtime"], 12 | "env": { 13 | "test": { 14 | "presets": ["env", "stage-2"], 15 | "plugins": ["istanbul"] 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /dnsdb_fe/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /dnsdb_fe/.eslintignore: -------------------------------------------------------------------------------- 1 | build/*.js 2 | config/*.js 3 | -------------------------------------------------------------------------------- /dnsdb_fe/.eslintrc.js: -------------------------------------------------------------------------------- 1 | // http://eslint.org/docs/user-guide/configuring 2 | 3 | module.exports = { 4 | root: true, 5 | parser: 'babel-eslint', 6 | parserOptions: { 7 | sourceType: 'module' 8 | }, 9 | env: { 10 | browser: true, 11 | }, 12 | // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style 13 | extends: 'standard', 14 | // required to lint *.vue files 15 | plugins: [ 16 | 'html' 17 | ], 18 | // add your custom rules here 19 | 'rules': { 20 | // allow paren-less arrow functions 21 | 'arrow-parens': 0, 22 | // allow async-await 23 | 'generator-star-spacing': 0, 24 | // allow debugger during development 25 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /dnsdb_fe/.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | "plugins": { 5 | // to edit target browsers: use "browserslist" field in package.json 6 | "autoprefixer": {} 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /dnsdb_fe/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/open_dnsdb/f717e9752f00a2937ff523c2ca120c2ebae57bca/dnsdb_fe/__init__.py -------------------------------------------------------------------------------- /dnsdb_fe/build/build.js: -------------------------------------------------------------------------------- 1 | require('./check-versions')() 2 | 3 | process.env.NODE_ENV = 'production' 4 | 5 | var ora = require('ora') 6 | var rm = require('rimraf') 7 | var path = require('path') 8 | var chalk = require('chalk') 9 | var webpack = require('webpack') 10 | var config = require('../config') 11 | var webpackConfig = require('./webpack.prod.conf') 12 | 13 | var spinner = ora('building for production...') 14 | spinner.start() 15 | 16 | rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => { 17 | if (err) throw err 18 | webpack(webpackConfig, function (err, stats) { 19 | spinner.stop() 20 | if (err) throw err 21 | process.stdout.write(stats.toString({ 22 | colors: true, 23 | modules: false, 24 | children: false, 25 | chunks: false, 26 | chunkModules: false 27 | }) + '\n\n') 28 | 29 | console.log(chalk.cyan(' Build complete.\n')) 30 | console.log(chalk.yellow( 31 | ' Tip: built files are meant to be served over an HTTP server.\n' + 32 | ' Opening index.html over file:// won\'t work.\n' 33 | )) 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /dnsdb_fe/build/check-versions.js: -------------------------------------------------------------------------------- 1 | var chalk = require('chalk') 2 | var semver = require('semver') 3 | var packageConfig = require('../package.json') 4 | var shell = require('shelljs') 5 | function exec (cmd) { 6 | return require('child_process').execSync(cmd).toString().trim() 7 | } 8 | 9 | var versionRequirements = [ 10 | { 11 | name: 'node', 12 | currentVersion: semver.clean(process.version), 13 | versionRequirement: packageConfig.engines.node 14 | }, 15 | ] 16 | 17 | if (shell.which('npm')) { 18 | versionRequirements.push({ 19 | name: 'npm', 20 | currentVersion: exec('npm --version'), 21 | versionRequirement: packageConfig.engines.npm 22 | }) 23 | } 24 | 25 | module.exports = function () { 26 | var warnings = [] 27 | for (var i = 0; i < versionRequirements.length; i++) { 28 | var mod = versionRequirements[i] 29 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { 30 | warnings.push(mod.name + ': ' + 31 | chalk.red(mod.currentVersion) + ' should be ' + 32 | chalk.green(mod.versionRequirement) 33 | ) 34 | } 35 | } 36 | 37 | if (warnings.length) { 38 | console.log('') 39 | console.log(chalk.yellow('To use this template, you must update following to modules:')) 40 | console.log() 41 | for (var i = 0; i < warnings.length; i++) { 42 | var warning = warnings[i] 43 | console.log(' ' + warning) 44 | } 45 | console.log() 46 | process.exit(1) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /dnsdb_fe/build/dev-client.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | require('eventsource-polyfill') 3 | var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true') 4 | 5 | hotClient.subscribe(function (event) { 6 | if (event.action === 'reload') { 7 | window.location.reload() 8 | } 9 | }) 10 | -------------------------------------------------------------------------------- /dnsdb_fe/build/dev-server.js: -------------------------------------------------------------------------------- 1 | require('./check-versions')() 2 | 3 | var config = require('../config') 4 | if (!process.env.NODE_ENV) { 5 | process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV) 6 | } 7 | 8 | var opn = require('opn') 9 | var path = require('path') 10 | var express = require('express') 11 | var webpack = require('webpack') 12 | var proxyMiddleware = require('http-proxy-middleware') 13 | var webpackConfig = process.env.NODE_ENV === 'testing' 14 | ? require('./webpack.prod.conf') 15 | : require('./webpack.dev.conf') 16 | 17 | // default port where dev server listens for incoming traffic 18 | var port = process.env.PORT || config.dev.port 19 | // automatically open browser, if not set will be false 20 | var autoOpenBrowser = !!config.dev.autoOpenBrowser 21 | // Define HTTP proxies to your custom API backend 22 | // https://github.com/chimurai/http-proxy-middleware 23 | var proxyTable = config.dev.proxyTable 24 | 25 | var app = express() 26 | var compiler = webpack(webpackConfig) 27 | 28 | var devMiddleware = require('webpack-dev-middleware')(compiler, { 29 | publicPath: webpackConfig.output.publicPath, 30 | quiet: true 31 | }) 32 | 33 | var hotMiddleware = require('webpack-hot-middleware')(compiler, { 34 | log: false, 35 | heartbeat: 2000 36 | }) 37 | // force page reload when html-webpack-plugin template changes 38 | compiler.plugin('compilation', function (compilation) { 39 | compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) { 40 | hotMiddleware.publish({ action: 'reload' }) 41 | cb() 42 | }) 43 | }) 44 | 45 | // proxy api requests 46 | Object.keys(proxyTable).forEach(function (context) { 47 | var options = proxyTable[context] 48 | if (typeof options === 'string') { 49 | options = { target: options } 50 | } 51 | app.use(proxyMiddleware(options.filter || context, options)) 52 | }) 53 | 54 | // handle fallback for HTML5 history API 55 | app.use(require('connect-history-api-fallback')()) 56 | 57 | // serve webpack bundle output 58 | app.use(devMiddleware) 59 | 60 | // enable hot-reload and state-preserving 61 | // compilation error display 62 | app.use(hotMiddleware) 63 | 64 | // serve pure static assets 65 | var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory) 66 | app.use(staticPath, express.static('./static')) 67 | 68 | var uri = 'http://localhost:' + port 69 | 70 | var _resolve 71 | var readyPromise = new Promise(resolve => { 72 | _resolve = resolve 73 | }) 74 | 75 | console.log('> Starting dev server...') 76 | devMiddleware.waitUntilValid(() => { 77 | console.log('> Listening at ' + uri + '\n') 78 | // when env is testing, don't need open it 79 | if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') { 80 | opn(uri) 81 | } 82 | _resolve() 83 | }) 84 | 85 | var server = app.listen(port) 86 | 87 | module.exports = { 88 | ready: readyPromise, 89 | close: () => { 90 | server.close() 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /dnsdb_fe/build/utils.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var config = require('../config') 3 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 4 | 5 | exports.assetsPath = function (_path) { 6 | var assetsSubDirectory = process.env.NODE_ENV === 'production' 7 | ? config.build.assetsSubDirectory 8 | : config.dev.assetsSubDirectory 9 | return path.posix.join(assetsSubDirectory, _path) 10 | } 11 | 12 | exports.cssLoaders = function (options) { 13 | options = options || {} 14 | 15 | var cssLoader = { 16 | loader: 'css-loader', 17 | options: { 18 | minimize: process.env.NODE_ENV === 'production', 19 | sourceMap: options.sourceMap 20 | } 21 | } 22 | 23 | // generate loader string to be used with extract text plugin 24 | function generateLoaders (loader, loaderOptions) { 25 | var loaders = [cssLoader] 26 | if (loader) { 27 | loaders.push({ 28 | loader: loader + '-loader', 29 | options: Object.assign({}, loaderOptions, { 30 | sourceMap: options.sourceMap 31 | }) 32 | }) 33 | } 34 | 35 | // Extract CSS when that option is specified 36 | // (which is the case during production build) 37 | if (options.extract) { 38 | return ExtractTextPlugin.extract({ 39 | use: loaders, 40 | fallback: 'vue-style-loader' 41 | }) 42 | } else { 43 | return ['vue-style-loader'].concat(loaders) 44 | } 45 | } 46 | 47 | // https://vue-loader.vuejs.org/en/configurations/extract-css.html 48 | return { 49 | css: generateLoaders(), 50 | postcss: generateLoaders(), 51 | less: generateLoaders('less'), 52 | sass: generateLoaders('sass', { indentedSyntax: true }), 53 | scss: generateLoaders('sass'), 54 | stylus: generateLoaders('stylus'), 55 | styl: generateLoaders('stylus') 56 | } 57 | } 58 | 59 | // Generate loaders for standalone style files (outside of .vue) 60 | exports.styleLoaders = function (options) { 61 | var output = [] 62 | var loaders = exports.cssLoaders(options) 63 | for (var extension in loaders) { 64 | var loader = loaders[extension] 65 | output.push({ 66 | test: new RegExp('\\.' + extension + '$'), 67 | use: loader 68 | }) 69 | } 70 | return output 71 | } 72 | -------------------------------------------------------------------------------- /dnsdb_fe/build/vue-loader.conf.js: -------------------------------------------------------------------------------- 1 | var utils = require('./utils') 2 | var config = require('../config') 3 | var isProduction = process.env.NODE_ENV === 'production' 4 | 5 | module.exports = { 6 | loaders: utils.cssLoaders({ 7 | sourceMap: isProduction 8 | ? config.build.productionSourceMap 9 | : config.dev.cssSourceMap, 10 | extract: isProduction 11 | }), 12 | transformToRequire: { 13 | video: 'src', 14 | source: 'src', 15 | img: 'src', 16 | image: 'xlink:href' 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /dnsdb_fe/build/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var utils = require('./utils') 3 | var config = require('../config') 4 | var vueLoaderConfig = require('./vue-loader.conf') 5 | 6 | function resolve (dir) { 7 | return path.join(__dirname, '..', dir) 8 | } 9 | 10 | module.exports = { 11 | entry: { 12 | app: './src/main.js' 13 | }, 14 | output: { 15 | path: config.build.assetsRoot, 16 | filename: '[name].js', 17 | publicPath: process.env.NODE_ENV === 'production' 18 | ? config.build.assetsPublicPath 19 | : config.dev.assetsPublicPath 20 | }, 21 | resolve: { 22 | extensions: ['.js', '.vue', '.json'], 23 | alias: { 24 | 'vue$': 'vue/dist/vue.esm.js', 25 | '@': resolve('src') 26 | } 27 | }, 28 | module: { 29 | rules: [ 30 | { 31 | test: /\.(js|vue)$/, 32 | loader: 'eslint-loader', 33 | enforce: 'pre', 34 | include: [resolve('src'), resolve('test')], 35 | options: { 36 | formatter: require('eslint-friendly-formatter') 37 | } 38 | }, 39 | { 40 | test: /\.vue$/, 41 | loader: 'vue-loader', 42 | options: vueLoaderConfig 43 | }, 44 | { 45 | test: /\.js$/, 46 | loader: 'babel-loader', 47 | include: [resolve('src'), resolve('test')] 48 | }, 49 | { 50 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 51 | loader: 'url-loader', 52 | options: { 53 | limit: 10000, 54 | name: utils.assetsPath('img/[name].[hash:7].[ext]') 55 | } 56 | }, 57 | { 58 | test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, 59 | loader: 'url-loader', 60 | options: { 61 | limit: 10000, 62 | name: utils.assetsPath('media/[name].[hash:7].[ext]') 63 | } 64 | }, 65 | { 66 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 67 | loader: 'url-loader', 68 | options: { 69 | limit: 10000, 70 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]') 71 | } 72 | } 73 | ] 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /dnsdb_fe/build/webpack.dev.conf.js: -------------------------------------------------------------------------------- 1 | var utils = require('./utils') 2 | var webpack = require('webpack') 3 | var config = require('../config') 4 | var merge = require('webpack-merge') 5 | var baseWebpackConfig = require('./webpack.base.conf') 6 | var HtmlWebpackPlugin = require('html-webpack-plugin') 7 | var FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') 8 | 9 | // add hot-reload related code to entry chunks 10 | Object.keys(baseWebpackConfig.entry).forEach(function (name) { 11 | baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name]) 12 | }) 13 | 14 | module.exports = merge(baseWebpackConfig, { 15 | module: { 16 | rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap }) 17 | }, 18 | // cheap-module-eval-source-map is faster for development 19 | devtool: '#cheap-module-eval-source-map', 20 | plugins: [ 21 | new webpack.DefinePlugin({ 22 | 'process.env': config.dev.env 23 | }), 24 | // https://github.com/glenjamin/webpack-hot-middleware#installation--usage 25 | new webpack.HotModuleReplacementPlugin(), 26 | new webpack.NoEmitOnErrorsPlugin(), 27 | // https://github.com/ampedandwired/html-webpack-plugin 28 | new HtmlWebpackPlugin({ 29 | filename: 'index.html', 30 | template: 'index.html', 31 | inject: true 32 | }), 33 | new FriendlyErrorsPlugin() 34 | ] 35 | }) 36 | -------------------------------------------------------------------------------- /dnsdb_fe/build/webpack.prod.conf.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var utils = require('./utils') 3 | var webpack = require('webpack') 4 | var config = require('../config') 5 | var merge = require('webpack-merge') 6 | var baseWebpackConfig = require('./webpack.base.conf') 7 | var CopyWebpackPlugin = require('copy-webpack-plugin') 8 | var HtmlWebpackPlugin = require('html-webpack-plugin') 9 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 10 | var OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin') 11 | 12 | var env = process.env.NODE_ENV === 'testing' 13 | ? require('../config/test.env') 14 | : config.build.env 15 | 16 | var webpackConfig = merge(baseWebpackConfig, { 17 | module: { 18 | rules: utils.styleLoaders({ 19 | sourceMap: config.build.productionSourceMap, 20 | extract: true 21 | }) 22 | }, 23 | devtool: config.build.productionSourceMap ? '#source-map' : false, 24 | output: { 25 | path: config.build.assetsRoot, 26 | filename: utils.assetsPath('js/[name].[chunkhash].js'), 27 | chunkFilename: utils.assetsPath('js/[id].[chunkhash].js') 28 | }, 29 | plugins: [ 30 | // http://vuejs.github.io/vue-loader/en/workflow/production.html 31 | new webpack.DefinePlugin({ 32 | 'process.env': env 33 | }), 34 | new webpack.optimize.UglifyJsPlugin({ 35 | compress: { 36 | warnings: false 37 | }, 38 | sourceMap: true 39 | }), 40 | // extract css into its own file 41 | new ExtractTextPlugin({ 42 | filename: utils.assetsPath('css/[name].[contenthash].css') 43 | }), 44 | // Compress extracted CSS. We are using this plugin so that possible 45 | // duplicated CSS from different components can be deduped. 46 | new OptimizeCSSPlugin({ 47 | cssProcessorOptions: { 48 | safe: true 49 | } 50 | }), 51 | // generate dist index.html with correct asset hash for caching. 52 | // you can customize output by editing /index.html 53 | // see https://github.com/ampedandwired/html-webpack-plugin 54 | new HtmlWebpackPlugin({ 55 | filename: process.env.NODE_ENV === 'testing' 56 | ? 'index.html' 57 | : config.build.index, 58 | template: 'index.html', 59 | inject: true, 60 | minify: { 61 | removeComments: true, 62 | collapseWhitespace: true, 63 | removeAttributeQuotes: true 64 | // more options: 65 | // https://github.com/kangax/html-minifier#options-quick-reference 66 | }, 67 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin 68 | chunksSortMode: 'dependency' 69 | }), 70 | // split vendor js into its own file 71 | new webpack.optimize.CommonsChunkPlugin({ 72 | name: 'vendor', 73 | minChunks: function (module, count) { 74 | // any required modules inside node_modules are extracted to vendor 75 | return ( 76 | module.resource && 77 | /\.js$/.test(module.resource) && 78 | module.resource.indexOf( 79 | path.join(__dirname, '../node_modules') 80 | ) === 0 81 | ) 82 | } 83 | }), 84 | // extract webpack runtime and module manifest to its own file in order to 85 | // prevent vendor hash from being updated whenever app bundle is updated 86 | new webpack.optimize.CommonsChunkPlugin({ 87 | name: 'manifest', 88 | chunks: ['vendor'] 89 | }), 90 | // copy custom static assets 91 | new CopyWebpackPlugin([ 92 | { 93 | from: path.resolve(__dirname, '../static'), 94 | to: config.build.assetsSubDirectory, 95 | ignore: ['.*'] 96 | } 97 | ]) 98 | ] 99 | }) 100 | 101 | if (config.build.productionGzip) { 102 | var CompressionWebpackPlugin = require('compression-webpack-plugin') 103 | 104 | webpackConfig.plugins.push( 105 | new CompressionWebpackPlugin({ 106 | asset: '[path].gz[query]', 107 | algorithm: 'gzip', 108 | test: new RegExp( 109 | '\\.(' + 110 | config.build.productionGzipExtensions.join('|') + 111 | ')$' 112 | ), 113 | threshold: 10240, 114 | minRatio: 0.8 115 | }) 116 | ) 117 | } 118 | 119 | if (config.build.bundleAnalyzerReport) { 120 | var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 121 | webpackConfig.plugins.push(new BundleAnalyzerPlugin()) 122 | } 123 | 124 | module.exports = webpackConfig 125 | -------------------------------------------------------------------------------- /dnsdb_fe/build/webpack.test.conf.js: -------------------------------------------------------------------------------- 1 | // This is the webpack config used for unit tests. 2 | 3 | var utils = require('./utils') 4 | var webpack = require('webpack') 5 | var merge = require('webpack-merge') 6 | var baseConfig = require('./webpack.base.conf') 7 | 8 | var webpackConfig = merge(baseConfig, { 9 | // use inline sourcemap for karma-sourcemap-loader 10 | module: { 11 | rules: utils.styleLoaders() 12 | }, 13 | devtool: '#inline-source-map', 14 | resolveLoader: { 15 | alias: { 16 | // necessary to to make lang="scss" work in test when using vue-loader's ?inject option 17 | // see discussion at https://github.com/vuejs/vue-loader/issues/724 18 | 'scss-loader': 'sass-loader' 19 | } 20 | }, 21 | plugins: [ 22 | new webpack.DefinePlugin({ 23 | 'process.env': require('../config/test.env') 24 | }) 25 | ] 26 | }) 27 | 28 | // no need for app entry during tests 29 | delete webpackConfig.entry 30 | 31 | module.exports = webpackConfig 32 | -------------------------------------------------------------------------------- /dnsdb_fe/config/dev.env.js: -------------------------------------------------------------------------------- 1 | var merge = require('webpack-merge') 2 | var prodEnv = require('./prod.env') 3 | 4 | module.exports = merge(prodEnv, { 5 | NODE_ENV: '"development"' 6 | }) 7 | -------------------------------------------------------------------------------- /dnsdb_fe/config/index.js: -------------------------------------------------------------------------------- 1 | // see http://vuejs-templates.github.io/webpack for documentation. 2 | var path = require('path') 3 | 4 | module.exports = { 5 | build: { 6 | env: require('./prod.env'), 7 | index: path.resolve(__dirname, '../dist/index.html'), 8 | assetsRoot: path.resolve(__dirname, '../dist'), 9 | assetsSubDirectory: 'static', 10 | assetsPublicPath: '/', 11 | productionSourceMap: true, 12 | // Gzip off by default as many popular static hosts such as 13 | // Surge or Netlify already gzip all static assets for you. 14 | // Before setting to `true`, make sure to: 15 | // npm install --save-dev compression-webpack-plugin 16 | productionGzip: false, 17 | productionGzipExtensions: ['js', 'css'], 18 | // Run the build command with an extra argument to 19 | // View the bundle analyzer report after build finishes: 20 | // `npm run build --report` 21 | // Set to `true` or `false` to always turn it on or off 22 | bundleAnalyzerReport: process.env.npm_config_report 23 | }, 24 | dev: { 25 | env: require('./dev.env'), 26 | port: 8081, 27 | autoOpenBrowser: true, 28 | assetsSubDirectory: 'static', 29 | assetsPublicPath: '/', 30 | proxyTable: {}, 31 | // CSS Sourcemaps off by default because relative paths are "buggy" 32 | // with this option, according to the CSS-Loader README 33 | // (https://github.com/webpack/css-loader#sourcemaps) 34 | // In our experience, they generally work as expected, 35 | // just be aware of this issue when enabling this option. 36 | cssSourceMap: false 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /dnsdb_fe/config/prod.env.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | NODE_ENV: '"production"' 3 | } 4 | -------------------------------------------------------------------------------- /dnsdb_fe/config/test.env.js: -------------------------------------------------------------------------------- 1 | var merge = require('webpack-merge') 2 | var devEnv = require('./dev.env') 3 | 4 | module.exports = merge(devEnv, { 5 | NODE_ENV: '"testing"' 6 | }) 7 | -------------------------------------------------------------------------------- /dnsdb_fe/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dns管理系统 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /dnsdb_fe/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dnsdb-fe", 3 | "version": "0.1.0", 4 | "description": "A Vue.js project", 5 | "author": "wanghuiwh.wang ", 6 | "private": true, 7 | "scripts": { 8 | "dev": "node build/dev-server.js", 9 | "start": "node build/dev-server.js", 10 | "build": "node build/build.js", 11 | "unit": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js --single-run", 12 | "e2e": "node test/e2e/runner.js", 13 | "test": "npm run unit && npm run e2e", 14 | "pack_prod": "npm run build && rm -rf ../dnsdb/static && cp -R ./dist/static ../dnsdb/static && cp ./dist/index.html ../dnsdb/templates/", 15 | "lint": "eslint --ext .js,.vue src test/unit/specs test/e2e/specs" 16 | }, 17 | "dependencies": { 18 | "axios": "^0.18.1", 19 | "element-ui": "^2.4.8", 20 | "font-awesome": "^4.7.0", 21 | "cryptiles": ">=4.1.2", 22 | "http2": "^3.3.7", 23 | "less": "^3.9.0", 24 | "less-loader": "^4.0.5", 25 | "style-loader": "^0.19.1", 26 | "vue": "^2.5.17", 27 | "vue-resource": "^1.5.1", 28 | "vue-router": "^3.0.1", 29 | "vuex": "^3.0.1" 30 | }, 31 | "devDependencies": { 32 | "autoprefixer": "^7.1.2", 33 | "babel-core": "^6.22.1", 34 | "babel-eslint": "^7.1.1", 35 | "babel-loader": "^7.1.1", 36 | "babel-plugin-istanbul": "^4.1.1", 37 | "babel-plugin-transform-runtime": "^6.22.0", 38 | "babel-preset-env": "^1.3.2", 39 | "babel-preset-stage-2": "^6.22.0", 40 | "babel-register": "^6.22.0", 41 | "chai": "^3.5.0", 42 | "chalk": "^2.0.1", 43 | "connect-history-api-fallback": "^1.3.0", 44 | "copy-webpack-plugin": "^4.0.1", 45 | "cross-env": "^5.0.1", 46 | "cross-spawn": "^5.0.1", 47 | "css-loader": "^0.28.7", 48 | "cssnano": "^3.10.0", 49 | "eslint": "^4.18.2", 50 | "eslint-config-standard": "^6.2.1", 51 | "eslint-friendly-formatter": "^3.0.0", 52 | "eslint-loader": "^3.0.0", 53 | "eslint-plugin-html": "^3.0.0", 54 | "eslint-plugin-promise": "^3.4.0", 55 | "eslint-plugin-standard": "^2.0.1", 56 | "eventsource-polyfill": "^0.9.6", 57 | "express": "^4.14.1", 58 | "extract-text-webpack-plugin": "^2.0.0", 59 | "file-loader": "^0.11.2", 60 | "friendly-errors-webpack-plugin": "^1.1.3", 61 | "html-webpack-plugin": "^2.28.0", 62 | "http-proxy-middleware": "^0.20.0", 63 | "inject-loader": "^3.0.0", 64 | "lolex": "^1.5.2", 65 | "mocha": "^5.2.0", 66 | "nightwatch": "^0.1.0", 67 | "opn": "^5.1.0", 68 | "optimize-css-assets-webpack-plugin": "^2.0.0", 69 | "ora": "^1.2.0", 70 | "phantomjs-prebuilt": "^2.1.14", 71 | "rimraf": "^2.6.0", 72 | "selenium-server": "^3.0.1", 73 | "semver": "^5.3.0", 74 | "shelljs": "^0.7.6", 75 | "sinon": "^2.1.0", 76 | "sinon-chai": "^2.8.0", 77 | "url-loader": "^0.5.8", 78 | "vue-loader": "^12.1.0", 79 | "vue-style-loader": "^3.0.1", 80 | "vue-template-compiler": "^2.5.13", 81 | "webpack": "^3.12.0", 82 | "webpack-bundle-analyzer": "^3.3.2", 83 | "webpack-dev-middleware": "^1.10.0", 84 | "webpack-hot-middleware": "^2.18.0", 85 | "webpack-merge": "^4.1.0" 86 | }, 87 | "engines": { 88 | "node": ">= 4.0.0", 89 | "npm": ">= 3.0.0" 90 | }, 91 | "browserslist": [ 92 | "> 1%", 93 | "last 2 versions", 94 | "not ie <= 8" 95 | ] 96 | } 97 | -------------------------------------------------------------------------------- /dnsdb_fe/src/App.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 24 | 64 | 123 | -------------------------------------------------------------------------------- /dnsdb_fe/src/components/Menu.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 59 | 60 | 62 | 63 | -------------------------------------------------------------------------------- /dnsdb_fe/src/components/admin/Login.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 92 | 93 | 132 | -------------------------------------------------------------------------------- /dnsdb_fe/src/components/conf/Conf.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 44 | 45 | 46 | 49 | -------------------------------------------------------------------------------- /dnsdb_fe/src/components/conf/HeaderEdit.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 115 | 116 | 122 | -------------------------------------------------------------------------------- /dnsdb_fe/src/components/preview/Preview.vue: -------------------------------------------------------------------------------- 1 | 92 | 93 | 139 | 140 | 167 | -------------------------------------------------------------------------------- /dnsdb_fe/src/components/record/Record.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 42 | 43 | 46 | -------------------------------------------------------------------------------- /dnsdb_fe/src/components/system/System.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 25 | 26 | 29 | -------------------------------------------------------------------------------- /dnsdb_fe/src/components/view/View.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | 44 | 45 | 48 | -------------------------------------------------------------------------------- /dnsdb_fe/src/main.js: -------------------------------------------------------------------------------- 1 | // The Vue build version to load with the `import` command 2 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias. 3 | import Vue from 'vue' 4 | import App from './App' 5 | import router from './router' 6 | import Element from 'element-ui' 7 | import VueResource from 'vue-resource' 8 | import Vuex from 'vuex' 9 | import 'element-ui/lib/theme-chalk/index.css' 10 | import 'font-awesome/css/font-awesome.min.css' 11 | import './style.css' 12 | import store from './store/view.js' 13 | 14 | Vue.use(Element) 15 | Vue.use(VueResource) 16 | Vue.use(Vuex) 17 | Vue.config.productionTip = false 18 | 19 | /* eslint-disable no-new */ 20 | new Vue({ 21 | el: '#app', 22 | store, 23 | router, 24 | template: '', 25 | components: { App } 26 | }) 27 | -------------------------------------------------------------------------------- /dnsdb_fe/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import Conf from '@/components/conf/Conf' 4 | import HostManager from '@/components/conf/HostManager' 5 | import ZoneManager from '@/components/conf/ZoneManager' 6 | import View from '@/components/view/View' 7 | import Record from '@/components/record/Record' 8 | import DnsLog from '@/components/log/DnsLog' 9 | import Preview from '@/components/preview/Preview' 10 | import Login from '@/components/admin/Login' 11 | import System from '@/components/system/System' 12 | import UserManager from '@/components/system/UserManager' 13 | import Menu from '@/components/Menu' 14 | 15 | Vue.use(Router) 16 | 17 | export default new Router({ 18 | routes: [ 19 | { 20 | path: '/login', 21 | name: 'login', 22 | component: Login 23 | }, 24 | { 25 | path: '/', 26 | name: 'approot', 27 | component: Menu, 28 | children: [ 29 | { 30 | path: 'system', 31 | component: System, 32 | children: [ 33 | { 34 | path: 'user', 35 | component: UserManager 36 | } 37 | ] 38 | }, 39 | { 40 | path: 'preview', 41 | name: 'preview', 42 | component: Preview 43 | }, 44 | { 45 | path: 'view', 46 | name: 'view', 47 | component: View 48 | }, 49 | { 50 | path: 'record', 51 | name: 'Record', 52 | component: Record 53 | }, 54 | { 55 | path: 'conf', 56 | name: 'Conf', 57 | component: Conf, 58 | children: [ 59 | { 60 | path: 'zone', 61 | component: ZoneManager 62 | }, 63 | { 64 | path: 'hostgroup', 65 | component: HostManager 66 | } 67 | ] 68 | }, 69 | { 70 | path: 'dnslog', 71 | name: 'DnsLog', 72 | component: DnsLog 73 | } 74 | ] 75 | } 76 | ] 77 | }) 78 | -------------------------------------------------------------------------------- /dnsdb_fe/src/store/view.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | Vue.use(Vuex) 4 | 5 | export default new Vuex.Store({ 6 | state: { 7 | menu_index: '/system', 8 | loading: false, 9 | activeTab: 'user', 10 | userName: '' 11 | }, 12 | mutations: { 13 | changeTab (state, tabName) { 14 | state.activeTab = tabName 15 | console.log(tabName) 16 | }, 17 | changeMenuIndex (state, idx) { 18 | console.log(idx) 19 | state.menu_index = idx 20 | }, 21 | changeUsername (state, username) { 22 | state.userName = username 23 | } 24 | } 25 | }) 26 | -------------------------------------------------------------------------------- /dnsdb_fe/src/style.css: -------------------------------------------------------------------------------- 1 | 2 | hr { 3 | border-top:1px; 4 | margin-top: 15px; 5 | } 6 | 7 | .button { 8 | vertical-align: middle; 9 | } 10 | 11 | .tab-panel { 12 | background: white; 13 | padding-left: 20px; 14 | padding-right: 20px; 15 | padding-top: 10px; 16 | padding-bottom: 10px; 17 | } 18 | 19 | .small-title { 20 | line-height: 30px; 21 | height: 30px; 22 | font-size: 16px; 23 | padding-left: 5px; 24 | color: #428bca 25 | } 26 | 27 | .medium-title { 28 | color: #428bca; 29 | font-size: 22px; 30 | margin-right: 20px; 31 | vertical-align: middle; 32 | } 33 | 34 | .large-title { 35 | font-size: 24px; 36 | color: #428bca; 37 | } 38 | 39 | .small-table { 40 | margin-top: 10px; 41 | width: 900px; 42 | } 43 | 44 | .medium-table { 45 | margin-top: 10px; 46 | width: 1200px; 47 | } 48 | 49 | .medium-input { 50 | width: 200px; 51 | margin-right: 10px; 52 | } 53 | 54 | .large-input { 55 | width: 300px; 56 | margin-right: 10px; 57 | } 58 | 59 | .xlarge-input { 60 | width: 80%; 61 | margin-right: 10px; 62 | } 63 | 64 | .list-item { 65 | width:100%; 66 | line-height: 40px; 67 | cursor:pointer; 68 | height: 40px; 69 | display: inline-block; 70 | border-bottom: 1px solid lightgray; 71 | } 72 | 73 | .small-list-item { 74 | padding-left: 5px; 75 | margin-right: 15px; 76 | font-size:13px; 77 | cursor:pointer; 78 | height:30px; 79 | line-height:30px; 80 | border-bottom-width:1px; 81 | border-bottom-color: lightgray; 82 | border-bottom-style: solid; 83 | } 84 | 85 | .el-button--info { 86 | color: #fff; 87 | background-color: #409EFF; 88 | border-color: #409EFF; 89 | } 90 | 91 | .el-button--default { 92 | color: #409eff; 93 | background: #ecf5ff; 94 | border-color: #b3d8ff; 95 | } 96 | 97 | .el-table-add-row { 98 | margin-top: 10px; 99 | width: 100%; 100 | height: 34px; 101 | border: 1px dashed #c1c1cd; 102 | border-radius: 3px; 103 | cursor: pointer; 104 | justify-content: center; 105 | display: flex; 106 | line-height: 34px; 107 | } 108 | -------------------------------------------------------------------------------- /dnsdb_fe/static/.!83440!favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/open_dnsdb/f717e9752f00a2937ff523c2ca120c2ebae57bca/dnsdb_fe/static/.!83440!favicon.ico -------------------------------------------------------------------------------- /dnsdb_fe/static/.!83441!favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/open_dnsdb/f717e9752f00a2937ff523c2ca120c2ebae57bca/dnsdb_fe/static/.!83441!favicon.ico -------------------------------------------------------------------------------- /dnsdb_fe/static/.!83442!favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/open_dnsdb/f717e9752f00a2937ff523c2ca120c2ebae57bca/dnsdb_fe/static/.!83442!favicon.ico -------------------------------------------------------------------------------- /dnsdb_fe/static/.!83443!favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/open_dnsdb/f717e9752f00a2937ff523c2ca120c2ebae57bca/dnsdb_fe/static/.!83443!favicon.ico -------------------------------------------------------------------------------- /dnsdb_fe/static/.!83444!favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/open_dnsdb/f717e9752f00a2937ff523c2ca120c2ebae57bca/dnsdb_fe/static/.!83444!favicon.ico -------------------------------------------------------------------------------- /dnsdb_fe/static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/open_dnsdb/f717e9752f00a2937ff523c2ca120c2ebae57bca/dnsdb_fe/static/.gitkeep -------------------------------------------------------------------------------- /dnsdb_fe/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/open_dnsdb/f717e9752f00a2937ff523c2ca120c2ebae57bca/dnsdb_fe/static/favicon.ico -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SOURCEDIR = . 8 | BUILDDIR = _build 9 | 10 | # Put it first so that "make" without argument is like "make help". 11 | help: 12 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 13 | 14 | .PHONY: help Makefile 15 | 16 | # Catch-all target: route all unknown targets to Sphinx using the new 17 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 18 | %: Makefile 19 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 20 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/master/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | # import os 16 | # import sys 17 | # sys.path.insert(0, os.path.abspath('.')) 18 | 19 | import sphinx_rtd_theme 20 | 21 | # -- Project information ----------------------------------------------------- 22 | 23 | project = u'DNSDB' 24 | copyright = u'2018, Qunar.com' 25 | author = u'Qunar.com' 26 | 27 | # The short X.Y version 28 | version = u'0.1' 29 | # The full version, including alpha/beta/rc tags 30 | release = u'0.1' 31 | 32 | 33 | # -- General configuration --------------------------------------------------- 34 | 35 | # If your documentation needs a minimal Sphinx version, state it here. 36 | # 37 | # needs_sphinx = '1.0' 38 | 39 | # Add any Sphinx extension module names here, as strings. They can be 40 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 41 | # ones. 42 | extensions = [ 43 | ] 44 | 45 | # Add any paths that contain templates here, relative to this directory. 46 | templates_path = ['_templates'] 47 | 48 | # The suffix(es) of source filenames. 49 | # You can specify multiple suffix as a list of string: 50 | # 51 | # source_suffix = ['.rst', '.md'] 52 | source_suffix = '.rst' 53 | 54 | # The master toctree document. 55 | master_doc = 'index' 56 | 57 | # The language for content autogenerated by Sphinx. Refer to documentation 58 | # for a list of supported languages. 59 | # 60 | # This is also used if you do content translation via gettext catalogs. 61 | # Usually you set "language" from the command line for these cases. 62 | language = u'zh_cn' 63 | 64 | # List of patterns, relative to source directory, that match files and 65 | # directories to ignore when looking for source files. 66 | # This pattern also affects html_static_path and html_extra_path. 67 | exclude_patterns = [u'_build', 'Thumbs.db', '.DS_Store'] 68 | 69 | # The name of the Pygments (syntax highlighting) style to use. 70 | pygments_style = None 71 | 72 | 73 | # -- Options for HTML output ------------------------------------------------- 74 | 75 | # The theme to use for HTML and HTML Help pages. See the documentation for 76 | # a list of builtin themes. 77 | # 78 | html_theme = "sphinx_rtd_theme" 79 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 80 | 81 | # Theme options are theme-specific and customize the look and feel of a theme 82 | # further. For a list of options available for each theme, see the 83 | # documentation. 84 | # 85 | # html_theme_options = {} 86 | 87 | # Add any paths that contain custom static files (such as style sheets) here, 88 | # relative to this directory. They are copied after the builtin static files, 89 | # so a file named "default.css" will overwrite the builtin "default.css". 90 | html_static_path = ['_static'] 91 | 92 | # Custom sidebar templates, must be a dictionary that maps document names 93 | # to template names. 94 | # 95 | # The default sidebars (for documents that don't match any pattern) are 96 | # defined by theme itself. Builtin themes are using these templates by 97 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 98 | # 'searchbox.html']``. 99 | # 100 | # html_sidebars = {} 101 | 102 | 103 | # -- Options for HTMLHelp output --------------------------------------------- 104 | 105 | # Output file base name for HTML help builder. 106 | htmlhelp_basename = 'DNSDBdoc' 107 | 108 | 109 | # -- Options for LaTeX output ------------------------------------------------ 110 | 111 | latex_elements = { 112 | # The paper size ('letterpaper' or 'a4paper'). 113 | # 114 | # 'papersize': 'letterpaper', 115 | 116 | # The font size ('10pt', '11pt' or '12pt'). 117 | # 118 | # 'pointsize': '10pt', 119 | 120 | # Additional stuff for the LaTeX preamble. 121 | # 122 | # 'preamble': '', 123 | 124 | # Latex figure (float) alignment 125 | # 126 | # 'figure_align': 'htbp', 127 | } 128 | 129 | # Grouping the document tree into LaTeX files. List of tuples 130 | # (source start file, target name, title, 131 | # author, documentclass [howto, manual, or own class]). 132 | latex_documents = [ 133 | (master_doc, 'DNSDB.tex', u'DNSDB Documentation', 134 | u'Qunar.com', 'manual'), 135 | ] 136 | 137 | 138 | # -- Options for manual page output ------------------------------------------ 139 | 140 | # One entry per manual page. List of tuples 141 | # (source start file, name, description, authors, manual section). 142 | man_pages = [ 143 | (master_doc, 'dnsdb', u'DNSDB Documentation', 144 | [author], 1) 145 | ] 146 | 147 | 148 | # -- Options for Texinfo output ---------------------------------------------- 149 | 150 | # Grouping the document tree into Texinfo files. List of tuples 151 | # (source start file, target name, title, author, 152 | # dir menu entry, description, category) 153 | texinfo_documents = [ 154 | (master_doc, 'DNSDB', u'DNSDB Documentation', 155 | author, 'DNSDB', 'One line description of project.', 156 | 'Miscellaneous'), 157 | ] 158 | 159 | 160 | # -- Options for Epub output ------------------------------------------------- 161 | 162 | # Bibliographic Dublin Core info. 163 | epub_title = project 164 | 165 | # The unique identifier of the text. This can be a ISBN number 166 | # or the project homepage. 167 | # 168 | # epub_identifier = '' 169 | 170 | # A unique identification for the text. 171 | # 172 | # epub_uid = '' 173 | 174 | # A list of files that should not be packed into the epub file. 175 | epub_exclude_files = ['search.html'] 176 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. DNSDB documentation master file, created by 2 | sphinx-quickstart on Mon Dec 10 17:00:36 2018. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to DNSDB's documentation! 7 | ================================= 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | 13 | 14 | 15 | Indices and tables 16 | ================== 17 | 18 | * :ref:`genindex` 19 | * :ref:`modindex` 20 | * :ref:`search` 21 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /etc/beta/common.conf: -------------------------------------------------------------------------------- 1 | [etc] 2 | env = beta 3 | secret_key = SecretKeyForFlask 4 | 5 | [log] 6 | log-dir = /var/log/open_dnsdb/ 7 | debug = True 8 | verbose = True 9 | 10 | [DB] 11 | connection=sqlite:////usr/local/open_dnsdb/dnsdb.db 12 | 13 | [mail] 14 | server = mail.server.corp.com 15 | port = 25 16 | from_addr = dnsdb@corp.com 17 | password = xxxxxx 18 | alert_list = ops@corp.com;opsdev@corp.com 19 | info_list = ops@corp.com 20 | 21 | [web] 22 | base_url = / 23 | run-mode = werkzeug 24 | bind = 0.0.0.0 25 | debug = True 26 | 27 | [gunicorn] 28 | timeout=600 29 | workers=4 30 | worker_class = eventlet 31 | daemon = False 32 | loglevel = debug 33 | ignore_healthcheck_accesslog=True 34 | accesslog = /var/log/open_dnsdb/access.log 35 | 36 | [api] 37 | dnsdbapi_url=http://127.0.0.1:9001/api 38 | dnsupdater_port = 9000 39 | -------------------------------------------------------------------------------- /etc/beta/dnsdb-updater.conf: -------------------------------------------------------------------------------- 1 | [etc] 2 | tmp_dir=/usr/local/open_dnsdb/tmp 3 | log_dir = /var/log/open_dnsdb 4 | backup_dir=/usr/local/backup 5 | pidfile=/usr/local/open_dnsdb/tmp/named_updater.pid 6 | zone_update_interval = 5 7 | allow_ip = 127.0.0.1 8 | 9 | [log] 10 | log-file = dnsdb_updater.log 11 | 12 | [web] 13 | port = 9000 14 | 15 | [gunicorn] 16 | bind = 0.0.0.0:9000 17 | 18 | [bind_default] 19 | named_dir = /var/named/chroot/etc 20 | zone_dir = /var/named/chroot/var/named 21 | acl_dir = /var/named/chroot/var/named 22 | named_checkconf = /usr/sbin/named-checkconf 23 | named_zonecheck = /usr/sbin/named-checkzone 24 | mkrdns = /sbin/mkrdns 25 | rndc = /usr/sbin/rndc 26 | # bind运行时的用户名和属组 27 | user = named 28 | group = named 29 | -------------------------------------------------------------------------------- /etc/beta/dnsdb.conf: -------------------------------------------------------------------------------- 1 | [etc] 2 | allow_ip = 127.0.0.1 3 | secret_key = SessionKeyForFalsk 4 | header_template = /usr/local/open_dnsdb/etc/template/zone_header 5 | 6 | [log] 7 | log-file = dnsdb.log 8 | 9 | [web] 10 | port = 9001 11 | 12 | [gunicorn] 13 | bind = 0.0.0.0:9001 14 | 15 | [view] 16 | acl_groups = ViewSlave 17 | cname_ttl = 300 18 | view_zone = view.com 19 | normal_view = corp.com:corp.view.com 20 | normal_cname = corp.com:view.corp.com 21 | -------------------------------------------------------------------------------- /etc/beta/supervisor-dnsdb.conf: -------------------------------------------------------------------------------- 1 | [program:open-dnsdb] 2 | directory=/usr/local/open_dnsdb/ 3 | command=/usr/local/open_dnsdb/tools/with_venv.sh dnsdb beta dnsdb 4 | autostart=True ;; 是否开机自动启动 5 | autorestart=True ;; 是否挂了自动重启 6 | redirect_stderr=True ;; 是否把 stderr 定向到 stdout 7 | stopasgroup=True 8 | -------------------------------------------------------------------------------- /etc/beta/supervisor-updater.conf: -------------------------------------------------------------------------------- 1 | [program:open-dnsdb-conf-updater] 2 | directory=/usr/local/open_dnsdb/ 3 | command=/usr/local/open_dnsdb/tools/with_venv.sh dnsdb-conf-updater beta dnsdb-updater 4 | autostart=True ;; 是否开机自动启动 5 | autorestart=True ;; 是否挂了自动重启 6 | redirect_stderr=True ;; 是否把 stderr 定向到 stdout 7 | stopasgroup=True 8 | 9 | 10 | [program:open-dnsdb-zone-updater] 11 | directory=/usr/local/open_dnsdb/ 12 | command=/usr/local/open_dnsdb/tools/with_venv.sh dnsdb-zone-updater beta dnsdb-updater 13 | autostart=True ;; 是否开机自动启动 14 | autorestart=True ;; 是否挂了自动重启 15 | redirect_stderr=True ;; 是否把 stderr 定向到 stdout 16 | stopasgroup=True 17 | -------------------------------------------------------------------------------- /etc/template/zone_header: -------------------------------------------------------------------------------- 1 | $TTL 7200 ; 2 hours 2 | @ IN SOA localhost. root.localhost. ( 3 | pre_serial ; Serial 4 | 3600 ; Refresh (1 hour) 5 | 900 ; Retry (15 minutes) 6 | 3600000 ; Expire (5 weeks 6 days 16 hours) 7 | 3600 ; Minimum (1 hour) 8 | ) 9 | @ 2D IN NS localhost. 10 | $ORIGIN zone_name. -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | concurrent-log-handler==0.9.12 2 | Flask==1.0.2 3 | Flask-Login==0.4.1 4 | Flask-Migrate==2.4.0 5 | Flask-RESTful==0.3.7 6 | Flask-SQLAlchemy==2.3.2 7 | httplib2==0.12.1 8 | jsonschema==3.0.1 9 | oslo.config==6.8.1 10 | multiping==1.1.2 11 | SQLAlchemy==1.3.0 12 | requests==2.21.0 13 | Sphinx==1.8.4 14 | sphinx-rtd-theme==0.4.3 15 | gunicorn==19.9.0 16 | eventlet==0.24.1 17 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = dnsdb 3 | platforms = any 4 | license_file = LICENSE 5 | description = Open-source dns management platform based on bind running on Python 2.7. 6 | long_description = file: README.md 7 | keywords = dns, view 8 | classifier = 9 | Development Status :: 1 - Beta 10 | Intended Audience :: End Users 11 | Intended Audience :: Developers 12 | Operating System :: OS Independent 13 | Programming Language :: Python :: 2.7 14 | Topic :: dnsdb 15 | 16 | [files] 17 | packages = 18 | dnsdb_updater 19 | data_files = 20 | 21 | [entry_points] 22 | console_scripts = 23 | dnsdb-conf-updater = dns_updater.app:app_start 24 | dnsdb-zone-updater = dns_updater.updater:updater 25 | dnsdb = dnsdb:main 26 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # vim: tabstop=4 shiftwidth=4 softtabstop=4 et 3 | # 4 | 5 | from setuptools import setup 6 | 7 | setup( 8 | setup_requires=['pbr'], 9 | pbr=True, 10 | ) 11 | -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | testtools 2 | WebTest 3 | tox 4 | fixture 5 | mock 6 | -------------------------------------------------------------------------------- /tools/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qunarcorp/open_dnsdb/f717e9752f00a2937ff523c2ca120c2ebae57bca/tools/__init__.py -------------------------------------------------------------------------------- /tools/install_venv.py: -------------------------------------------------------------------------------- 1 | # Copyright 2010 United States Government as represented by the 2 | # Administrator of the National Aeronautics and Space Administration. 3 | # All Rights Reserved. 4 | # 5 | # Copyright 2010 OpenStack Foundation 6 | # Copyright 2013 IBM Corp. 7 | # 8 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 9 | # not use this file except in compliance with the License. You may obtain 10 | # a copy of the License at 11 | # 12 | # http://www.apache.org/licenses/LICENSE-2.0 13 | # 14 | # Unless required by applicable law or agreed to in writing, software 15 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 16 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 17 | # License for the specific language governing permissions and limitations 18 | # under the License. 19 | 20 | from __future__ import print_function 21 | 22 | import os 23 | import sys 24 | 25 | import install_venv_common as install_venv 26 | 27 | 28 | def print_help(venv, root): 29 | help = """ 30 | Nova development environment setup is complete. 31 | 32 | Nova development uses virtualenv to track and manage Python dependencies 33 | while in development and testing. 34 | 35 | To activate the Nova virtualenv for the extent of your current shell 36 | session you can run: 37 | 38 | $ source %s/bin/activate 39 | 40 | Or, if you prefer, you can run commands in the virtualenv on a case by case 41 | basis by running: 42 | 43 | $ %s/tools/with_venv.sh 44 | 45 | Also, make test will automatically use the virtualenv. 46 | """ 47 | print(help % (venv, root)) 48 | 49 | 50 | def main(argv): 51 | root = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) 52 | 53 | if os.environ.get('tools_path'): 54 | root = os.environ['tools_path'] 55 | venv = os.path.join(root, '.venv') 56 | if os.environ.get('venv'): 57 | venv = os.environ['venv'] 58 | 59 | pip_requires = os.path.join(root, 'requirements.txt') 60 | test_requires = os.path.join(root, 'test-requirements.txt') 61 | py_version = "python%s.%s" % (sys.version_info[0], sys.version_info[1]) 62 | project = 'Nova' 63 | install = install_venv.InstallVenv(root, venv, pip_requires, test_requires, 64 | py_version, project) 65 | options = install.parse_args(argv) 66 | install.check_python_version() 67 | install.check_dependencies() 68 | install.create_virtualenv(no_site_packages=options.no_site_packages, python_interpreter=options.python_interpreter) 69 | install.install_dependencies() 70 | print_help(venv, root) 71 | 72 | 73 | if __name__ == '__main__': 74 | main(sys.argv) 75 | -------------------------------------------------------------------------------- /tools/updater/pre_updater_start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PROJPATH=/usr/local/open-dnsdb 4 | MKDIR=/bin/mkdir 5 | CP=/bin/cp 6 | CHMOD=/bin/chmod 7 | 8 | 9 | $MKDIR $PROJPATH/tmp/var/named -p &>/dev/null 10 | $MKDIR $PROJPATH/tmp/etc -p &>/dev/null 11 | 12 | if [ ! -f "/sbin/mkrdns" ]; then 13 | $CP $PROJPATH/tools/mkrdns /sbin/ 14 | $CHMOD +x /sbin/mkrdns 15 | fi 16 | -------------------------------------------------------------------------------- /tools/with_venv.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | tools_path=${tools_path:-$(dirname $0)} 3 | APP_PATH=$(dirname $tools_path) 4 | venv_path=${venv_path:-${tools_path}} 5 | venv_dir=${venv_name:-/../.venv} 6 | TOOLS=${tools_path} 7 | VENV=${venv:-${venv_path}/${venv_dir}} 8 | if [ -n "$PYTHONPATH" ]; then 9 | export PYTHONPATH=$APP_PATH 10 | else 11 | export PYTHONPATH=$APP_PATH:$PYTHONPATH 12 | fi 13 | source ${VENV}/bin/activate && "$@" 14 | --------------------------------------------------------------------------------