Merge pull request #46 from ivanfilippov/new_ui

Update 'new_ui' branch on main repo
This commit is contained in:
Ivan Filippov 2016-05-27 00:22:54 -06:00
commit a4a48d6b01
13 changed files with 277 additions and 177 deletions

View file

@ -5,12 +5,15 @@ PowerDNS Web-GUI - Built by Flask
- Multiple domain management
- Local / LDAP user authentication
- User management
- User access management base on domain
- User access management based on domain
- User activity logging
- Dashboard and pdns service statistics
## Setup
### PowerDNS Version Support:
PowerDNS-Admin supports PowerDNS autoritative server versions **3.4.2** and higher but does **not** yet support PowerDNS 4.0.0
### pdns Service
I assume that you have already installed powerdns service. Make sure that your `/etc/pdns/pdns.conf` has these contents
```
@ -30,7 +33,7 @@ MariaDB [(none)]> GRANT ALL PRIVILEGES ON powerdnsadmin.* TO powerdnsadmin@'%' I
### PowerDNS-Admin
In this installation guide, I am using CentOS 7 and run my python stuffs with *virtualenv*. If you don't have it, let install:
In this installation guide, I am using CentOS 7 and run my python stuffs with *virtualenv*. If you don't have it, lets install it:
```
$ sudo yum install python-pip
$ sudo pip install virtualenv
@ -55,7 +58,7 @@ Web application configuration is stored in `config.py` file. Let's clone it from
Create database after having proper configs
```
(flask)% ./createdb.py
(flask)% ./create_db.py
```
@ -64,5 +67,9 @@ Run the application and enjoy!
(flask)$ ./run.py
```
### Screenshot
![Alt text](http://i.imgur.com/wA5qy2d.png)
### Screenshots
![login page](https://github.com/ngoduykhanh/PowerDNS-Admin/wiki/images/readme_screenshots/fullscreen-login.png?raw=true)
![dashboard](https://github.com/ngoduykhanh/PowerDNS-Admin/wiki/images/readme_screenshots/fullscreen-dashboard.png?raw=true)
![create domain page](https://github.com/ngoduykhanh/PowerDNS-Admin/wiki/images/readme_screenshots/fullscreen-domaincreate.png?raw=true)
![manage domain page](https://github.com/ngoduykhanh/PowerDNS-Admin/wiki/images/readme_screenshots/fullscreen-domainmanage.png?raw=true)

View file

@ -1,4 +1,4 @@
function applyChanges(data, url, showResult) {
function applyChanges(data, url, showResult, refreshPage) {
var success = false;
$.ajax({
type : "POST",
@ -14,6 +14,9 @@ function applyChanges(data, url, showResult) {
modal.find('.modal-body p').text("Applied changes successfully");
modal.modal('show');
}
if (refreshPage) {
location.reload(true);
}
},
error : function(jqXHR, status) {
@ -91,7 +94,7 @@ function editRow(oTable, nRow) {
jqTds[1].innerHTML = '<select class="form-control" id="record_type" name="record_type" value="' + aData[1] + '"' + '>' + record_types + '</select>';
jqTds[2].innerHTML = '<select class="form-control" id="record_status" name="record_status" value="' + aData[2] + '"' + '><option value="false">Active</option><option value="true">Disabled</option></select>';
jqTds[3].innerHTML = '<select class="form-control" id="record_ttl" name="record_ttl" value="' + aData[3] + '"' + '><option value="60">1 minute</option><option value="300">5 minutes</option><option value="1800">30 minutes</option><option value="3600">60 minutes</option><option value="86400">24 hours</option></select>';
jqTds[4].innerHTML = '<input type="text" style="display:table-cell; width:100% !important" class="form-control input-small advance-data" value="' + aData[4].replace(/\"/g,"&quot;") + '">';
jqTds[4].innerHTML = '<input type="text" style="display:table-cell; width:100% !important" id="current_edit_record_data" name="current_edit_record_data" class="form-control input-small advance-data" value="' + aData[4].replace(/\"/g,"&quot;") + '">';
jqTds[5].innerHTML = '<button type="button" class="btn btn-flat btn-primary button_save">Save</button>';
jqTds[6].innerHTML = '<button type="button" class="btn btn-flat btn-primary button_cancel">Cancel</button>';

View file

@ -0,0 +1,91 @@
{% extends "base.html" %}
{% block title %}<title>DNS Control Panel - Create User</title>{% endblock %}
{% block dashboard_stat %}
<!-- Content Header (Page header) -->
<section class="content-header">
<h1>
User
<small>Create new</small>
</h1>
<ol class="breadcrumb">
<li><a href="{{ url_for('dashboard') }}"><i class="fa fa-dashboard"></i>Home</a></li>
<li><a href="{{ url_for('dashboard') }}">Admin</a></li>
<li class="active">Create user</li>
</ol>
</section>
{% endblock %}
{% block content %}
<section class="content">
<div class="row">
<div class="col-md-4">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title">Create new user</h3>
</div>
<!-- /.box-header -->
<!-- form start -->
<form role="form" method="post" action="{{ url_for('admin_createuser') }}">
<div class="box-body">
<div class="form-group has-feedback">
<label class="control-label" for="firstname">First Name</label>
<input type="text" class="form-control" placeholder="First Name"
name="firstname" {% if user %}value={{ user.firstname }}{% endif %}> <span
class="glyphicon glyphicon-user form-control-feedback"></span>
</div>
<div class="form-group has-feedback">
<label class="control-label" for="lastname">Last Name</label>
<input type="text" class="form-control" placeholder="Last name"
name="lastname" {% if user %}value={{ user.lastname }}{% endif %}> <span
class="glyphicon glyphicon-user form-control-feedback"></span>
</div>
<div class="form-group has-feedback {% if duplicate_email %}has-error{% endif %}">
<label class="control-label" for="email">E-mail address</label>
<input type="email" class="form-control" placeholder="Email"
name="email" id="email" {% if user %}value={{ user.email }}{% endif %}> <span
class="glyphicon glyphicon-envelope form-control-feedback"></span>
{% if duplicate_email %}
<span class="help-block">This e-mail address is already in use.</span>
{% endif %}
</div>
<p class="login-box-msg">Enter the account details below</p>
<div class="form-group has-feedback {% if duplicate_username %}has-error{% endif %}">
<label class="control-label" for="username">Username</label>
<input type="text" class="form-control" placeholder="Username"
name="username" {% if user %}value={{ user.username }}{% endif %}> <span
class="glyphicon glyphicon-user form-control-feedback"></span>
{% if duplicate_username %}
<span class="help-block">This username is already in use.</span>
{% endif %}
</div>
<div class="form-group has-feedback {% if blank_password %}has-error{% endif %}">
<label class="control-label" for="username">Password</label>
<input type="password" class="form-control" placeholder="Password"
name="password"> <span
class="glyphicon glyphicon-lock form-control-feedback"></span>
{% if blank_password %}
<span class="help-block">The password cannot be blank.</span>
{% endif %}
</div>
</div>
<div class="box-footer">
<button type="submit" class="btn btn-flat btn-primary">Create User</button>
</div>
</form>
</div>
</div>
<div class="col-md-8">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title">Help with creating a new user</h3>
</div>
<div class="box-body">
<p>Fill in all the fields to the in the form to the left.</p>
<p><strong>Newly created users do not have access to any domains.</strong> You will need to grant access to the user once it is created via the domain management buttons on the dashboard.</p>
</div>
</div>
</div>
</div>
</section>
{% endblock %}

View file

@ -19,6 +19,13 @@
<div class="box-header">
<h3 class="box-title">User Management</h3>
</div>
<div class="box-body">
<a href="{{ url_for('admin_createuser') }}">
<button type="button" class="btn btn-flat btn-primary pull-left button_add_user">
Add User&nbsp;<i class="fa fa-plus"></i>
</button>
</a>
</div>
<div class="box-body">
<table id="tbl_users" class="table table-bordered table-striped">
<thead>
@ -97,9 +104,8 @@
modal.find('.modal-body p').text(info);
modal.find('#button_delete_confirm').click(function() {
var postdata = {'action': 'delete_user', 'data': username}
applyChanges(postdata, '/admin/manageuser');
applyChanges(postdata, '/admin/manageuser', false, true);
modal.modal('hide');
location.reload();
})
modal.modal('show');

View file

@ -73,65 +73,13 @@
$(".setting-toggle-button").click(function() {
var setting = $(this).prop('id');
applyChanges('','/admin/setting/' + setting + '/toggle')
location.reload();
applyChanges('','/admin/setting/' + setting + '/toggle', false, true)
});
// TODO: allow editing of value field
$(".setting-edit-button").click(function() {
var setting = $(this).prop('id');
applyChanges('','/admin/setting/' + setting + '/edit')
location.reload();
applyChanges('','/admin/setting/' + setting + '/edit', false, true)
});
</script>
{% endblock %}
{% block modals %}
<!-- Clear History Confirmation Box -->
<div class="modal fade modal-warning" id="modal_clear_history">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal"
aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">Confirmation</h4>
</div>
<div class="modal-body">
<p>Are you sure you want to remove all history?</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-flat btn-default pull-left"
data-dismiss="modal">Close</button>
<button type="button" class="btn btn-flat btn-danger" onclick="applyChanges('', '/admin/history');location.reload();">Clear
History</button>
</div>
</div>
<!-- /.modal-content -->
</div>
<!-- /.modal-dialog -->
</div>
<div class="modal fade" id="modal_history_info">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal"
aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">History Details</h4>
</div>
<div class="modal-body">
<p></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-flat btn-default pull-right"
data-dismiss="modal">Close</button>
</div>
</div>
<!-- /.modal-content -->
</div>
<!-- /.modal-dialog -->
</div>
<!-- /.modal -->
{% endblock %}

View file

@ -57,6 +57,7 @@
</a>
<div class="navbar-custom-menu">
{% if current_user.id is defined %}
<ul class="nav navbar-nav">
<!-- User Account: style can be found in dropdown.less -->
<li class="dropdown user user-menu">
@ -71,18 +72,18 @@
</span>
</a>
<ul class="dropdown-menu">
<!-- User image -->
<li class="user-header">
{% if current_user.avatar %}
<img src="{{ url_for('user_avatar', filename=current_user.avatar) }}" class="img-circle" alt="User Image"/>
{% else %}
<img src="{{ url_for('static', filename='adminlte2/dist/img/avatar.png') }}" class="img-circle" alt="User Image"/>
{% endif %}
<p>
{{ current_user.firstname }} {{ current_user.lastname }}
<small>{{ current_user.role.name }}</small>
</p>
</li>
<li class="user-header">
{% if current_user.avatar %}
<img src="{{ url_for('user_avatar', filename=current_user.avatar) }}" class="img-circle" alt="User Image"/>
{% else %}
<img src="{{ url_for('static', filename='adminlte2/dist/img/avatar.png') }}" class="img-circle" alt="User Image"/>
{% endif %}
<p>
{{ current_user.firstname }} {{ current_user.lastname }}
<small>{{ current_user.role.name }}</small>
</p>
</li>
<!-- Menu Footer-->
<li class="user-footer">
<div class="pull-left">
@ -95,6 +96,7 @@
</ul>
</li>
</ul>
{% endif %}
</div>
</nav>
</header>
@ -103,7 +105,7 @@
<aside class="main-sidebar">
<!-- sidebar: style can be found in sidebar.less -->
<section class="sidebar">
<!-- Sidebar user panel -->
{% if current_user.id is defined %}
<div class="user-panel">
<div class="pull-left image">
{% if current_user.avatar %}
@ -117,7 +119,6 @@
<a href="#"><i class="fa fa-circle text-success"></i> Logged In</a>
</div>
</div>
<!-- /.search form -->
<!-- sidebar menu: : style can be found in sidebar.less -->
<ul class="sidebar-menu">
<li class="header">USER ACTIONS</li>
@ -130,6 +131,7 @@
<li><a href="{{ url_for('admin_history') }}"><i class="fa fa-calendar"></i> <span>History</span></a></li>
<li><a href="{{ url_for('admin_settings') }}"><i class="fa fa-cog"></i> <span>Settings</span></a></li>
{% endif %}
{% endif %}
</section>
<!-- /.sidebar -->
</aside>
@ -149,102 +151,8 @@
</ol>
</section>
{% endblock %}
<!-- Main content -->
{% block content %}
<section class="content">
<!-- Small boxes (Stat box) -->
<div class="row">
<div class="col-lg-3 col-xs-6">
<!-- small box -->
<div class="small-box bg-aqua">
<div class="inner">
<h3>150</h3>
<p>Domains</p>
</div>
<div class="icon">
<i class="ion ion-bag"></i>
</div>
<a href="#" class="small-box-footer">More info <i class="fa fa-arrow-circle-right"></i></a>
</div>
</div>
<!-- ./col -->
<div class="col-lg-3 col-xs-6">
<!-- small box -->
<div class="small-box bg-green">
<div class="inner">
<h3>53<sup style="font-size: 20px">%</sup></h3>
<p>Users</p>
</div>
<div class="icon">
<i class="ion ion-stats-bars"></i>
</div>
<a href="#" class="small-box-footer">More info <i class="fa fa-arrow-circle-right"></i></a>
</div>
</div>
<!-- ./col -->
<div class="col-lg-3 col-xs-6">
<!-- small box -->
<div class="small-box bg-yellow">
<div class="inner">
<h3>44</h3>
<p>Histories</p>
</div>
<div class="icon">
<i class="ion ion-person-add"></i>
</div>
<a href="#" class="small-box-footer">More info <i class="fa fa-arrow-circle-right"></i></a>
</div>
</div>
<!-- ./col -->
<div class="col-lg-3 col-xs-6">
<!-- small box -->
<div class="small-box bg-red">
<div class="inner">
<h3>65</h3>
<p>Uptime</p>
</div>
<div class="icon">
<i class="ion ion-pie-graph"></i>
</div>
<a href="#" class="small-box-footer">More info <i class="fa fa-arrow-circle-right"></i></a>
</div>
</div>
<!-- ./col -->
</div>
<!-- /.row -->
<!-- Main row -->
<div class="row">
<!-- Left col -->
<section class="col-lg-7 connectedSortable">
<!-- Custom tabs (Charts with tabs)-->
<div class="nav-tabs-custom">
<!-- Tabs within a box -->
<ul class="nav nav-tabs pull-right">
<li class="active"><a href="#revenue-chart" data-toggle="tab">Area</a></li>
<li><a href="#sales-chart" data-toggle="tab">Donut</a></li>
<li class="pull-left header"><i class="fa fa-inbox"></i> Sales</li>
</ul>
<div class="tab-content no-padding">
<!-- Morris chart - Sales -->
<div class="chart tab-pane active" id="revenue-chart" style="position: relative; height: 300px;"></div>
<div class="chart tab-pane" id="sales-chart" style="position: relative; height: 300px;"></div>
</div>
</div>
<!-- /.nav-tabs-custom -->
</section>
<!-- /.Left col -->
<!-- right col (We are only adding the ID to make the widgets sortable)-->
<section class="col-lg-5 connectedSortable">
</section>
<!-- right col -->
</div>
<!-- /.row (main row) -->
</section>
{% endblock %}
<!-- /.content -->
</div>
<!-- /.content-wrapper -->
<footer class="main-footer">

View file

@ -246,6 +246,75 @@
var domain = $(this).prop('id');
applyChanges({'domain': domain}, '/domain/' + domain + '/update');
});
{% if record_helper_setting %}
//handle wacky record types
$(document).on("focus", "#current_edit_record_data", function (e) {
var record_type = $(this).parents("tr").find('#record_type').val();
var record_data = $(this);
if (record_type == "MX") {
var modal = $("#modal_custom_record");
if (record_data.val() == "") {
var form = " <label for=\"mx_priority\">MX Priority</label> \
<input type=\"text\" class=\"form-control\" name=\"mx_priority\" id=\"mx_priority\" placeholder=\"10\"> \
<label for=\"mx_server\">MX Server</label> \
<input type=\"text\" class=\"form-control\" name=\"mx_server\" id=\"mx_server\" placeholder=\"postfix.example.com\"> \
";
} else {
var parts = record_data.val().split(" ");
var form = " <label for=\"mx_priority\">MX Priority</label> \
<input type=\"text\" class=\"form-control\" name=\"mx_priority\" id=\"mx_priority\" placeholder=\"10\" value=\"" + parts[0] + "\"> \
<label for=\"mx_server\">MX Server</label> \
<input type=\"text\" class=\"form-control\" name=\"mx_server\" id=\"mx_server\" placeholder=\"postfix.example.com\" value=\"" + parts[1] + "\"> \
";
}
modal.find('.modal-body p').html(form);
modal.find('#button_save').click(function() {
mx_server = modal.find('#mx_server').val();
mx_priority = modal.find('#mx_priority').val();
data = mx_priority + " " + mx_server;
record_data.val(data);
modal.modal('hide');
})
modal.modal('show');
} else if (record_type == "SRV") {
var modal = $("#modal_custom_record");
if (record_data.val() == "") {
var form = " <label for=\"srv_priority\">SRV Priority</label> \
<input type=\"text\" class=\"form-control\" name=\"srv_priority\" id=\"srv_priority\" placeholder=\"0\"> \
<label for=\"srv_weight\">SRV Weight</label> \
<input type=\"text\" class=\"form-control\" name=\"srv_weight\" id=\"srv_weight\" placeholder=\"10\"> \
<label for=\"srv_port\">SRV Port</label> \
<input type=\"text\" class=\"form-control\" name=\"srv_port\" id=\"srv_port\" placeholder=\"5060\"> \
<label for=\"srv_target\">SRV Target</label> \
<input type=\"text\" class=\"form-control\" name=\"srv_target\" id=\"srv_target\" placeholder=\"sip.example.com\"> \
";
} else {
var parts = record_data.val().split(" ");
var form = " <label for=\"srv_priority\">SRV Priority</label> \
<input type=\"text\" class=\"form-control\" name=\"srv_priority\" id=\"srv_priority\" placeholder=\"0\" value=\"" + parts[0] + "\"> \
<label for=\"srv_weight\">SRV Weight</label> \
<input type=\"text\" class=\"form-control\" name=\"srv_weight\" id=\"srv_weight\" placeholder=\"10\" value=\"" + parts[1] + "\"> \
<label for=\"srv_port\">SRV Port</label> \
<input type=\"text\" class=\"form-control\" name=\"srv_port\" id=\"srv_port\" placeholder=\"5060\" value=\"" + parts[2] + "\"> \
<label for=\"srv_target\">SRV Target</label> \
<input type=\"text\" class=\"form-control\" name=\"srv_target\" id=\"srv_target\" placeholder=\"sip.example.com\" value=\"" + parts[3] + "\"> \
";
}
modal.find('.modal-body p').html(form);
modal.find('#button_save').click(function() {
srv_priority = modal.find('#srv_priority').val();
srv_weight = modal.find('#srv_weight').val();
srv_port = modal.find('#srv_port').val();
srv_target = modal.find('#srv_target').val();
data = srv_priority + " " + srv_weight + " " + srv_port + " " + srv_target;
record_data.val(data);
modal.modal('hide');
})
modal.modal('show');
}
});
{% endif %}
</script>
{% endblock %}
{% block modals %}
@ -295,4 +364,25 @@
</div>
<!-- /.modal-dialog -->
</div>
<div class="modal fade modal-primary" id="modal_custom_record">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal"
aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">Custom Record</h4>
</div>
<div class="modal-body">
<p></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-flat btn-primary" id="button_save">Save</button>
</div>
</div>
<!-- /.modal-content -->
</div>
<!-- /.modal-dialog -->
</div>
{% endblock %}

View file

@ -22,6 +22,11 @@ def inject_fullscreen_layout_setting():
fullscreen_layout_setting = Setting.query.filter(Setting.name == 'fullscreen_layout').first()
return dict(fullscreen_layout_setting=strtobool(fullscreen_layout_setting.value))
@app.context_processor
def inject_record_helper_setting():
record_helper_setting = Setting.query.filter(Setting.name == 'record_helper').first()
return dict(record_helper_setting=strtobool(record_helper_setting.value))
# START USER AUTHENTICATION HANDLER
@app.before_request
def before_request():
@ -54,13 +59,29 @@ def admin_role_required(f):
# END CUSTOMIZE DECORATOR
# START VIEWS
@app.errorhandler(400)
def http_bad_request(e):
return redirect(url_for('error', code=400))
@app.errorhandler(401)
def http_unauthorized(e):
return redirect(url_for('error', code=401))
@app.errorhandler(404)
def http_internal_server_error(e):
return redirect(url_for('error', code=404))
@app.errorhandler(500)
def http_page_not_found(e):
return redirect(url_for('error', code=500))
@app.route('/error/<string:code>')
def error(code, msg=None):
supported_code = ('400', '401', '404', '500')
if code in supported_code:
return render_template('%s.html' % code, msg=msg), int(code)
return render_template('errors/%s.html' % code, msg=msg), int(code)
else:
return render_template('404.html'), 404
return render_template('errors/404.html'), 404
@app.route('/register', methods=['GET'])
def register():
@ -195,7 +216,7 @@ def domain_add():
soa_edit_api = request.form.getlist('radio_type_soa_edit_api')[0]
if ' ' in domain_name or not domain_name or not domain_type:
return render_template('400.html', msg="Please correct your input"), 400
return render_template('errors/400.html', msg="Please correct your input"), 400
if domain_type == 'slave':
if request.form.getlist('domain_master_address'):
@ -211,7 +232,7 @@ def domain_add():
history.add()
return redirect(url_for('dashboard'))
else:
return render_template('400.html', msg=result['msg']), 400
return render_template('errors/400.html', msg=result['msg']), 400
except:
return redirect(url_for('error', code=500))
return render_template('domain_add.html')
@ -368,13 +389,37 @@ def admin():
return render_template('admin.html', domains=domains, users=users, configs=configs, statistics=statistics, uptime=uptime, history_number=history_number)
@app.route('/admin/user/create', methods=['GET', 'POST'])
@login_required
@admin_role_required
def admin_createuser():
if request.method == 'GET':
return render_template('admin_createuser.html')
if request.method == 'POST':
fdata = request.form
user = User(username=fdata['username'], plain_text_password=fdata['password'], firstname=fdata['firstname'], lastname=fdata['lastname'], email=fdata['email'])
if fdata['password'] == "":
return render_template('admin_createuser.html', user=user, blank_password=True)
result = user.create_local_user();
if result == 'Email already existed':
return render_template('admin_createuser.html', user=user, duplicate_email=True)
if result == 'Username already existed':
return render_template('admin_createuser.html', user=user, duplicate_username=True)
return redirect(url_for('admin_manageuser'))
@app.route('/admin/manageuser', methods=['GET', 'POST'])
@login_required
@admin_role_required
def admin_manageuser():
if request.method == 'GET':
users = User.query.all()
users = User.query.order_by(User.username).all()
return render_template('admin_manageuser.html', users=users)
if request.method == 'POST':
@ -460,7 +505,7 @@ def admin_settings_toggle(setting):
if (result):
return make_response(jsonify( { 'status': 'ok', 'msg': 'Toggled setting successfully.' } ), 200)
else:
return make_response(jsonify( { 'status': 'error', 'msg': 'Can toggle setting.' } ), 500)
return make_response(jsonify( { 'status': 'error', 'msg': 'Unable to toggle setting.' } ), 500)
@app.route('/user/profile', methods=['GET', 'POST'])
@login_required

View file

@ -11,10 +11,12 @@ admin_role = Role('Administrator', 'Administrator')
user_role = Role('User', 'User')
maintenance_setting = Setting('maintenance', 'False')
fullscreen_layout_setting = Setting('fullscreen_layout', 'True')
record_helper_setting = Setting('record_helper_layout', 'True')
db.session.add(admin_role)
db.session.add(user_role)
db.session.add(maintenance_setting)
db.session.add(fullscreen_layout_setting)
db.session.add(record_helper_setting)
db.session.commit()
if not os.path.exists(SQLALCHEMY_MIGRATE_REPO):
api.create(SQLALCHEMY_MIGRATE_REPO, 'database repository')