fix rq-scheduler version, most of templates is now working, fix javascript issues

This commit is contained in:
Jordan Rodgers 2018-02-08 18:44:04 -05:00
parent 5fb7f66624
commit df39356c4d
11 changed files with 326 additions and 149 deletions

View file

@ -24,7 +24,10 @@ OIDC_CLIENT_CONFIG = {
}
# Proxmox
PROXMOX_HOSTS = [host.strip() for host in environ.get('PROXSTAR_PROXMOX_HOSTS', '').split(',')]
PROXMOX_HOSTS = [
host.strip()
for host in environ.get('PROXSTAR_PROXMOX_HOSTS', '').split(',')
]
PROXMOX_USER = environ.get('PROXSTAR_PROXMOX_USER', '')
PROXMOX_PASS = environ.get('PROXSTAR_PROXMOX_PASS', '')
PROXMOX_ISO_STORAGE = environ.get('PROXSTAR_PROXMOX_ISO_STORAGE', 'nfs-iso')

View file

@ -10,6 +10,7 @@ from werkzeug.contrib.cache import SimpleCache
from flask_pyoidc.flask_pyoidc import OIDCAuthentication
from flask import Flask, render_template, request, redirect, send_from_directory, session
from proxstar.db import *
from proxstar.util import *
from proxstar.tasks import *
from proxstar.starrs import *
from proxstar.ldapdb import *
@ -294,6 +295,7 @@ def create():
percents = get_user_usage_percent(proxmox, user, usage, limits)
isos = get_isos(proxmox, app.config['PROXMOX_ISO_STORAGE'])
pools = get_pools(proxmox, db)
templates = get_templates(db)
return render_template(
'create.html',
username=user,
@ -303,11 +305,13 @@ def create():
limits=limits,
percents=percents,
isos=isos,
pools=pools)
pools=pools,
templates=templates)
elif request.method == 'POST':
name = request.form['name']
cores = request.form['cores']
memory = request.form['mem']
template = request.form['template']
disk = request.form['disk']
iso = request.form['iso']
if iso != 'none':
@ -323,8 +327,28 @@ def create():
else:
valid, available = check_hostname(starrs, name)
if valid and available:
q.enqueue(create_vm_task, user, name, cores, memory, disk,
iso)
if template == 'none':
q.enqueue(
create_vm_task,
user,
name,
cores,
memory,
disk,
iso,
timeout=300)
else:
password = gen_password(16)
q.enqueue(
setup_template,
template,
name,
user,
password,
cores,
memory,
timeout=600)
return password, 200
return '', 200
else:
return '', 403
@ -362,12 +386,14 @@ def settings():
rtp = 'rtp' in session['userinfo']['groups']
active = 'active' in session['userinfo']['groups']
if rtp:
templates = get_templates(db)
ignored_pools = get_ignored_pools(db)
return render_template(
'settings.html',
username=user,
rtp=rtp,
active=active,
templates=templates,
ignored_pools=ignored_pools)
else:
return '', 403
@ -386,6 +412,14 @@ def ignored_pools(pool):
return '', 403
@app.route('/template/<string:template_id>/disk')
@auth.oidc_auth
def template_disk(template_id):
if template_id == 'none':
return '0'
return get_template_disk(db, template_id)
@app.route('/vm/<string:vmid>/rrd/<path:path>')
@auth.oidc_auth
def send_rrd(vmid, path):

View file

@ -2,7 +2,7 @@ import datetime
from sqlalchemy import exists
from dateutil.relativedelta import relativedelta
from proxstar.ldapdb import *
from proxstar.models import VM_Expiration, Usage_Limit, Pool_Cache, Ignored_Pools, Base
from proxstar.models import VM_Expiration, Usage_Limit, Pool_Cache, Ignored_Pools, Template, Base
def get_vm_expire(db, vmid, months):
@ -135,7 +135,8 @@ def get_ignored_pools(db):
def delete_ignored_pool(db, pool):
if db.query(exists().where(Ignored_Pools.id == pool)).scalar():
ignored_pool = db.query(Ignored_Pools).filter(Ignored_Pools.id == pool).one()
ignored_pool = db.query(Ignored_Pools).filter(
Ignored_Pools.id == pool).one()
db.delete(ignored_pool)
db.commit()
@ -145,3 +146,37 @@ def add_ignored_pool(db, pool):
ignored_pool = Ignored_Pools(id=pool)
db.add(ignored_pool)
db.commit()
def get_templates(db):
templates = []
for template in db.query(Template).all():
template_dict = dict()
template_dict['id'] = template.id
template_dict['name'] = template.name
template_dict['desc'] = template.desc
template_dict['username'] = template.username
template_dict['disk'] = template.disk
templates.append(template_dict)
return templates
def get_template(db, template_id):
template_dict = dict()
if db.query(exists().where(Template.id == template_id)).scalar():
template = db.query(Template).filter(Template.id == template_id).one()
template_dict['id'] = template.id
template_dict['name'] = template.name
template_dict['desc'] = template.desc
template_dict['username'] = template.username
template_dict['password'] = template.password
template_dict['disk'] = template.disk
return template_dict
def get_template_disk(db, template_id):
disk = 0
if db.query(exists().where(Template.id == template_id)).scalar():
template = db.query(Template).filter(Template.id == template_id).one()
disk = template.disk
return str(disk)

View file

@ -24,11 +24,11 @@ def send_vm_expire_email(user, vms):
body = "The following VMs in Proxstar are expiring soon:\n\n"
for vm in vms:
if vm[1] == 0:
body += " - {} today (VM has been stopped)\n".format(
body += " - {} expires today (VM has been stopped and will be deleted tomorrow)\n".format(
vm[0], vm[1])
if vm[1] == 1:
body += " - {} in 1 day\n".format(vm[0])
elif vm[1] == 1:
body += " - {} expires in 1 day\n".format(vm[0])
else:
body += " - {} in {} days\n".format(vm[0], vm[1])
body += " - {} expires in {} days\n".format(vm[0], vm[1])
body += "\nPlease login to Proxstar and renew any VMs you would like to keep."
send_email(toaddr, subject, body)

View file

@ -35,6 +35,9 @@ class Template(Base):
id = Column(Integer, primary_key=True)
name = Column(String(32), nullable=False)
desc = Column(Text)
username = Column(Text, nullable=False)
password = Column(Text, nullable=False)
disk = Column(Integer, nullable=False)
class Ignored_Pools(Base):

View file

@ -1,9 +1,11 @@
/*jshint esversion: 6 */
$(document).ready(function(){
$('[data-toggle="tooltip"]').tooltip();
});
$("#delete-vm").click(function(){
const vmname = $(this).data('vmname')
const vmname = $(this).data('vmname');
swal({
title: `Are you sure you want to delete ${vmname}?`,
icon: "warning",
@ -19,7 +21,7 @@ $("#delete-vm").click(function(){
})
.then((willDelete) => {
if (willDelete) {
const vmid = $(this).data('vmid')
const vmid = $(this).data('vmid');
fetch(`/vm/${vmid}/delete`, {
credentials: 'same-origin',
method: 'post'
@ -42,7 +44,7 @@ $("#delete-vm").click(function(){
});
$("#stop-vm").click(function(){
const vmname = $(this).data('vmname')
const vmname = $(this).data('vmname');
swal({
title: `Are you sure you want to stop ${vmname}?`,
icon: "warning",
@ -81,7 +83,7 @@ $("#stop-vm").click(function(){
});
$("#reset-vm").click(function(){
const vmname = $(this).data('vmname')
const vmname = $(this).data('vmname');
swal({
title: `Are you sure you want to reset ${vmname}?`,
icon: "warning",
@ -97,7 +99,7 @@ $("#reset-vm").click(function(){
})
.then((willReset) => {
if (willReset) {
const vmid = $(this).data('vmid')
const vmid = $(this).data('vmid');
fetch(`/vm/${vmid}/power/reset`, {
credentials: 'same-origin',
method: 'post'
@ -120,7 +122,7 @@ $("#reset-vm").click(function(){
});
$("#shutdown-vm").click(function(){
const vmname = $(this).data('vmname')
const vmname = $(this).data('vmname');
swal({
title: `Are you sure you want to shutdown ${vmname}?`,
icon: "warning",
@ -136,7 +138,7 @@ $("#shutdown-vm").click(function(){
})
.then((willShutdown) => {
if (willShutdown) {
const vmid = $(this).data('vmid')
const vmid = $(this).data('vmid');
fetch(`/vm/${vmid}/power/shutdown`, {
credentials: 'same-origin',
method: 'post'
@ -159,7 +161,7 @@ $("#shutdown-vm").click(function(){
});
$("#suspend-vm").click(function(){
const vmname = $(this).data('vmname')
const vmname = $(this).data('vmname');
swal({
title: `Are you sure you want to suspend ${vmname}?`,
icon: "warning",
@ -175,7 +177,7 @@ $("#suspend-vm").click(function(){
})
.then((willSuspend) => {
if (willSuspend) {
const vmid = $(this).data('vmid')
const vmid = $(this).data('vmid');
fetch(`/vm/${vmid}/power/suspend`, {
credentials: 'same-origin',
method: 'post'
@ -198,8 +200,8 @@ $("#suspend-vm").click(function(){
});
$("#start-vm").click(function(){
const vmname = $(this).data('vmname')
const vmid = $(this).data('vmid')
const vmname = $(this).data('vmname');
const vmid = $(this).data('vmid');
fetch(`/vm/${vmid}/power/start`, {
credentials: 'same-origin',
method: 'post'
@ -220,8 +222,8 @@ $("#start-vm").click(function(){
});
$("#resume-vm").click(function(){
const vmname = $(this).data('vmname')
const vmid = $(this).data('vmid')
const vmname = $(this).data('vmname');
const vmid = $(this).data('vmid');
fetch(`/vm/${vmid}/power/resume`, {
credentials: 'same-origin',
method: 'post'
@ -242,8 +244,7 @@ $("#resume-vm").click(function(){
});
$("#eject-iso").click(function(){
const vmid = $(this).data('vmid')
const iso = $(this).data('iso')
const iso = $(this).data('iso');
swal({
title: `Are you sure you want to eject ${iso}?`,
icon: "warning",
@ -264,7 +265,7 @@ $("#eject-iso").click(function(){
})
.then((willEject) => {
if (willEject) {
const vmid = $(this).data('vmid')
const vmid = $(this).data('vmid');
fetch(`/vm/${vmid}/eject`, {
credentials: 'same-origin',
method: 'post'
@ -295,7 +296,6 @@ $("#eject-iso").click(function(){
$("#change-iso").click(function(){
const vmid = $(this).data('vmid')
fetch(`/isos`, {
credentials: 'same-origin',
}).then((response) => {
@ -326,8 +326,8 @@ $("#change-iso").click(function(){
})
.then((willChange) => {
if (willChange) {
const vmid = $(this).data('vmid')
const iso = $(iso_list).val()
const vmid = $(this).data('vmid');
const iso = $(iso_list).val();
fetch(`/vm/${vmid}/mount/${iso}`, {
credentials: 'same-origin',
method: 'post'
@ -365,8 +365,8 @@ $("#change-iso").click(function(){
});
$("#renew-vm").click(function(){
const vmname = $(this).data('vmname')
const vmid = $(this).data('vmid')
const vmname = $(this).data('vmname');
const vmid = $(this).data('vmid');
fetch(`/vm/${vmid}/renew`, {
credentials: 'same-origin',
method: 'post'
@ -387,99 +387,125 @@ $("#renew-vm").click(function(){
});
$("#create-vm").click(function(){
const name = document.getElementById('name').value
const cores = document.getElementById('cores').value
const mem = document.getElementById('mem').value
const disk = document.getElementById('disk').value
const iso = document.getElementById('iso').value
const user = document.getElementById('user')
const max_disk = $(this).data('max_disk')
if (name && disk) {
if (disk > max_disk) {
swal("Uh oh...", `You do not have enough disk resources available! Please lower the VM disk size to ${max_disk}GB or lower.`, "error");
} else {
fetch(`/hostname/${name}`, {
credentials: 'same-origin',
}).then((response) => {
return response.text()
}).then((text) => {
if (text == 'ok') {
var loader = document.createElement('div');
loader.setAttribute('class', 'loader');
var info = document.createElement('span');
info.innerHTML = `Cores: ${cores}<br>Memory: ${mem/1024} GB<br>Disk: ${disk} GB<br>ISO: ${iso}`,
swal({
title: `Are you sure you want to create ${name}?`,
content: info,
icon: "info",
buttons: {
cancel: true,
create: {
text: "Create",
closeModal: false,
className: "swal-button",
}
}
})
.then((willCreate) => {
if (willCreate) {
var data = new FormData();
data.append('name', name);
data.append('cores', cores);
data.append('mem', mem);
data.append('disk', disk);
data.append('iso', iso);
if (user) {
data.append('user', user.value);
}
fetch('/vm/create', {
credentials: 'same-origin',
method: 'post',
body: data
}).then((response) => {
return swal(`${name} is now being created. Check back soon and it should be good to go.`, {
icon: "success",
buttons: {
ok: {
text: "OK",
closeModal: true,
className: "",
}
}
});
}).then(() => {
window.location = "/";
});
}
});
} else if (text == 'invalid') {
swal("Uh oh...", `That name is not a valid name! Please try another name.`, "error");
} else if (text == 'taken') {
swal("Uh oh...", `That name is not available! Please try another name.`, "error");
}
}).catch(err => {
if (err) {
swal("Uh oh...", `Unable to verify name! Please try again later.`, "error");
} else {
swal.stopLoading();
swal.close();
}
});
const name = document.getElementById('name').value;
const cores = document.getElementById('cores').value;
const mem = document.getElementById('mem').value;
const template = document.getElementById('template').value;
const iso = document.getElementById('iso').value;
const user = document.getElementById('user');
const max_disk = $(this).data('max_disk');
var disk = document.getElementById('disk').value;
fetch(`/template/${template}/disk`, {
credentials: 'same-origin',
}).then((response) => {
return response.text()
}).then((template_disk) => {
if (template != 'none') {
disk = template_disk
}
} else if (!name && !disk) {
swal("Uh oh...", `You must enter a name and disk size for your VM!`, "error");
} else if (!name) {
swal("Uh oh...", `You must enter a name for your VM!`, "error");
} else if (!disk) {
swal("Uh oh...", `You must enter a disk size for your VM!`, "error");
}
return disk
}).then((disk) => {
if (name && disk) {
if (disk > max_disk) {
swal("Uh oh...", `You do not have enough disk resources available! Please lower the VM disk size to ${max_disk}GB or lower.`, "error");
} else {
fetch(`/hostname/${name}`, {
credentials: 'same-origin',
}).then((response) => {
return response.text()
}).then((text) => {
if (text == 'ok') {
var loader = document.createElement('div');
loader.setAttribute('class', 'loader');
var info = document.createElement('span');
if (template == 'none') {
info.innerHTML = `Cores: ${cores}<br>Memory: ${mem/1024}GB<br>Disk: ${disk}GB<br>ISO: ${iso}`;
} else {
const template_select = document.getElementById('template');
const template_name = template_select.options[template_select.selectedIndex].text;
info.innerHTML = `Cores: ${cores}<br>Memory: ${mem/1024}GB<br>Template: ${template_name}`;
}
swal({
title: `Are you sure you want to create ${name}?`,
content: info,
icon: "info",
buttons: {
cancel: true,
create: {
text: "Create",
closeModal: false,
className: "swal-button",
}
}
})
.then((willCreate) => {
if (willCreate) {
var data = new FormData();
data.append('name', name);
data.append('cores', cores);
data.append('mem', mem);
data.append('template', template);
data.append('disk', disk);
data.append('iso', iso);
if (user) {
data.append('user', user.value);
}
fetch('/vm/create', {
credentials: 'same-origin',
method: 'post',
body: data
}).then((response) => {
return response.text()
}).then((password) => {
if (template == 'none') {
var swal_text = `${name} is now being created. Check back soon and it should be good to go.`
} else {
var swal_text = `${name} is now being created. Check back soon and it should be good to go. The SSH credentials are your CSH username for the user and ${password} for the password. Save this password because you will not be able to retrieve it again!`
}
return swal(`${swal_text}`, {
icon: "success",
buttons: {
ok: {
text: "OK",
closeModal: true,
className: "",
}
}
});
}).then(() => {
window.location = "/";
});
}
});
} else if (text == 'invalid') {
swal("Uh oh...", `That name is not a valid name! Please try another name.`, "error");
} else if (text == 'taken') {
swal("Uh oh...", `That name is not available! Please try another name.`, "error");
}
}).catch(err => {
if (err) {
swal("Uh oh...", `Unable to verify name! Please try again later.`, "error");
} else {
swal.stopLoading();
swal.close();
}
});
}
} else if (!name && !disk) {
swal("Uh oh...", `You must enter a name and disk size for your VM!`, "error");
} else if (!name) {
swal("Uh oh...", `You must enter a name for your VM!`, "error");
} else if (!disk) {
swal("Uh oh...", `You must enter a disk size for your VM!`, "error");
}
});
});
$("#change-cores").click(function(){
const vmid = $(this).data('vmid')
const cur_cores = $(this).data('cores')
const usage = $(this).data('usage')
const limit = $(this).data('limit')
const vmid = $(this).data('vmid');
const cur_cores = $(this).data('cores');
const usage = $(this).data('usage');
const limit = $(this).data('limit');
var core_list = document.createElement('select');
core_list.setAttribute('style', 'width: 25px');
for (i = 1; i < limit - usage + 1; i++) {
@ -504,7 +530,7 @@ $("#change-cores").click(function(){
})
.then((willChange) => {
if (willChange) {
const cores = $(core_list).val()
const cores = $(core_list).val();
fetch(`/vm/${vmid}/cpu/${cores}`, {
credentials: 'same-origin',
method: 'post'
@ -534,10 +560,10 @@ $("#change-cores").click(function(){
});
$("#change-mem").click(function(){
const vmid = $(this).data('vmid')
const cur_mem = $(this).data('mem')
const usage = $(this).data('usage')
const limit = $(this).data('limit')
const vmid = $(this).data('vmid');
const cur_mem = $(this).data('mem');
const usage = $(this).data('usage');
const limit = $(this).data('limit');
var mem_list = document.createElement('select');
mem_list.setAttribute('style', 'width: 45px');
for (i = 1; i < limit - usage + 1; i++) {
@ -562,7 +588,7 @@ $("#change-mem").click(function(){
})
.then((willChange) => {
if (willChange) {
const mem = $(mem_list).val()
const mem = $(mem_list).val();
fetch(`/vm/${vmid}/mem/${mem}`, {
credentials: 'same-origin',
method: 'post'
@ -592,10 +618,10 @@ $("#change-mem").click(function(){
});
$(".edit-limit").click(function(){
const user = $(this).data('user')
const cur_cpu = $(this).data('cpu')
const cur_mem = $(this).data('mem')
const cur_disk = $(this).data('disk')
const user = $(this).data('user');
const cur_cpu = $(this).data('cpu');
const cur_mem = $(this).data('mem');
const cur_disk = $(this).data('disk');
var options = document.createElement('div');
cpu_text = document.createElement('p');
cpu_text.innerHTML = 'CPU';
@ -671,7 +697,7 @@ $(".edit-limit").click(function(){
});
$(".reset-limit").click(function(){
const user = $(this).data('user')
const user = $(this).data('user');
swal({
title: `Are you sure you want to reset the usage limits for ${user} to the defaults?`,
icon: "warning",
@ -709,7 +735,7 @@ $(".reset-limit").click(function(){
});
$(".delete-user").click(function(){
const user = $(this).data('user')
const user = $(this).data('user');
swal({
title: `Are you sure you want to delete the pool for ${user}?`,
icon: "warning",
@ -747,7 +773,7 @@ $(".delete-user").click(function(){
});
$(".delete-ignored-pool").click(function(){
const pool = $(this).data('pool')
const pool = $(this).data('pool');
fetch(`/pool/${pool}/ignore`, {
credentials: 'same-origin',
method: 'delete'
@ -756,10 +782,23 @@ $(".delete-ignored-pool").click(function(){
});
$(".add-ignored-pool").click(function(){
const pool = document.getElementById('pool').value
const pool = document.getElementById('pool').value;
fetch(`/pool/${pool}/ignore`, {
credentials: 'same-origin',
method: 'post'
});
location.reload();
});
function hide_for_template(obj) {
var template_element = obj;
var selected = template_element.options[template_element.selectedIndex].value;
var hide_area = document.getElementById('hide-for-template');
if(selected === 'none'){
hide_area.style.display = 'block';
}
else{
hide_area.style.display = 'none';
}
}

View file

@ -4,6 +4,7 @@ from flask import Flask
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from proxstar.db import *
from proxstar.util import *
from proxstar.mail import *
from proxstar.starrs import *
from proxstar.proxmox import *
@ -103,30 +104,45 @@ def generate_pool_cache_task():
store_pool_cache(db, pools)
def setup_template(template_id, name, user, cores, memory):
def setup_template(template_id, name, user, password, cores, memory):
with app.app_context():
proxmox = connect_proxmox()
starrs = connect_starrs()
db = connect_db()
template = get_template(db, template_id)
vmid, mac = clone_vm(proxmox, template_id, name, user)
ip = get_next_ip(starrs, app.config['STARRS_IP_RANGE'])
register_starrs(starrs, name, app.config['STARRS_USER'], mac, ip)
get_vm_expire(db, vmid, app.config['VM_EXPIRE_MONTHS'])
change_vm_cpu(proxmox, vmid, cores)
change_vm_mem(proxmox, vmid, memory)
time.sleep(60)
time.sleep(90)
change_vm_power(proxmox, vmid, 'start')
time.sleep(20)
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
retry = 0
while retry < 30:
try:
client.connect(ip, username='root', password='')
client.connect(
ip,
username=template['username'],
password=template['password'])
break
except:
retry += 1
time.sleep(3)
stdin, stdout, stderr = client.exec_command('ls')
for line in stdout:
print('... ' + line.strip('\n'))
stdin, stdout, stderr = client.exec_command("useradd {}".format(user))
exit_status = stdout.channel.recv_exit_status()
root_password = gen_password(32)
stdin, stdout, stderr = client.exec_command(
"echo '{}' | passwd root --stdin".format(root_password))
exit_status = stdout.channel.recv_exit_status()
stdin, stdout, stderr = client.exec_command(
"echo '{}' | passwd '{}' --stdin".format(password, user))
exit_status = stdout.channel.recv_exit_status()
stdin, stdout, stderr = client.exec_command(
"echo '{} ALL=(ALL:ALL) ALL' | sudo EDITOR='tee -a' visudo".format(
user))
exit_status = stdout.channel.recv_exit_status()
client.close()

View file

@ -38,18 +38,29 @@
</select>
</div>
<div class="form-group">
<label for="disk" class="pull-left">Disk (GB)</label>
<input type="number" value=32 name="disk" id="disk" class="form-control" min="1" max="{{ limits['disk'] - usage['disk'] }}">
</div>
<div class="form-group">
<label for="iso" class="pull-left">ISO</label>
<select name="iso" id="iso" class="form-control">
<label for="template" class="pull-left">Template</label>
<select name="template" id="template" class="form-control" onchange="hide_for_template(this)">
<option value="none"></option>
{% for iso in isos %}
<option value="{{ iso }}">{{ iso }}</option>
{% for template in templates %}
<option value="{{ template['id'] }}">{{ template['name'] }} ({{ template['disk'] }}GB)</option>
{% endfor %}
</select>
</div>
<div id="hide-for-template">
<div class="form-group">
<label for="disk" class="pull-left">Disk (GB)</label>
<input type="number" value=32 name="disk" id="disk" class="form-control" min="1" max="{{ limits['disk'] - usage['disk'] }}">
</div>
<div class="form-group">
<label for="iso" class="pull-left">ISO</label>
<select name="iso" id="iso" class="form-control">
<option value="none"></option>
{% for iso in isos %}
<option value="{{ iso }}">{{ iso }}</option>
{% endfor %}
</select>
</div>
</div>
{% if rtp %}
<div class="form-group">
<label for="user" class="pull-left">User</label>

View file

@ -9,6 +9,34 @@
<h3 class="panel-title">Templates</h3>
</div>
<div class="panel-body">
<table class="table table-bordered table-striped">
<thead>
<tr role="row">
<th>Proxmox ID</th>
<th>Name</th>
<th>Username</th>
<th>Disk Size (GB)</th>
<th>Description</th>
<th>Delete</th>
</tr>
</thead>
<tbody>
{% for template in templates %}
<tr role="row">
<td>{{ template['id'] }}</td>
<td>{{ template['name'] }}</td>
<td>{{ template['username'] }}</td>
<td>{{ template['disk'] }}</td>
<td>{{ template['desc'] }}</td>
<td>
<button class="btn btn-sm btn-danger delete-template" data-template_id="{{ template['id'] }}">
<span class="glyphicon glyphicon-remove"></span>
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>

8
proxstar/util.py Normal file
View file

@ -0,0 +1,8 @@
import secrets
def gen_password(
length,
charset="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*"
):
return "".join([secrets.choice(charset) for _ in range(0, length)])

View file

@ -6,7 +6,7 @@ sqlalchemy
python-dateutil
csh_ldap
rq
rq-scheduler
rq-scheduler==0.7.0
gunicorn
raven
paramiko