use oidc for auth, remove proxtar from paths, check if rtp when necessary

This commit is contained in:
Jordan Rodgers 2017-12-12 15:41:25 -05:00
parent e6fd8b1c0c
commit b656df33ba
6 changed files with 115 additions and 74 deletions

97
app.py
View file

@ -5,7 +5,8 @@ import subprocess
from db import *
from starrs import *
from proxmox import *
from flask import Flask, render_template, request, redirect, send_from_directory
from flask_pyoidc.flask_pyoidc import OIDCAuthentication
from flask import Flask, render_template, request, redirect, send_from_directory, session
app = Flask(__name__)
@ -16,11 +17,16 @@ app.config.from_pyfile(config)
app.config["GIT_REVISION"] = subprocess.check_output(
['git', 'rev-parse', '--short', 'HEAD']).decode('utf-8').rstrip()
user = 'proxstar'
auth = OIDCAuthentication(app,
issuer=app.config['OIDC_ISSUER'],
client_registration_info=app.config['OIDC_CLIENT_CONFIG'])
@app.route("/")
@auth.oidc_auth
def list_vms():
user = session['userinfo']['preferred_username']
rtp = 'rtp' in session['userinfo']['groups']
proxmox = connect_proxmox(app.config['PROXMOX_HOST'],
app.config['PROXMOX_USER'],
app.config['PROXMOX_PASS'])
@ -29,7 +35,7 @@ def list_vms():
if 'name' not in vm:
vms.remove(vm)
vms = sorted(vms, key=lambda k: k['name'])
return render_template('list_vms.html', username='com6056', vms=vms)
return render_template('list_vms.html', username=user, rtp=rtp, vms=vms)
@app.route("/isos")
@ -57,13 +63,15 @@ def hostname(name):
@app.route("/vm/<string:vmid>")
def vm_details(vmid):
user = session['userinfo']['preferred_username']
rtp = 'rtp' in session['userinfo']['groups']
proxmox = connect_proxmox(app.config['PROXMOX_HOST'],
app.config['PROXMOX_USER'],
app.config['PROXMOX_PASS'])
starrs = connect_starrs(
app.config['STARRS_DB_NAME'], app.config['STARRS_DB_USER'],
app.config['STARRS_DB_HOST'], app.config['STARRS_DB_PASS'])
if int(vmid) in get_user_allowed_vms(proxmox, user):
if int(vmid) in get_user_allowed_vms(proxmox, user) or 'rtp' in session['userinfo']['groups']:
vm = get_vm(proxmox, vmid)
vm['vmid'] = vmid
vm['config'] = get_vm_config(proxmox, vmid)
@ -76,17 +84,18 @@ def vm_details(vmid):
usage = get_user_usage(proxmox, 'proxstar')
limits = get_user_usage_limits(user)
usage_check = check_user_usage(proxmox, user, vm['config']['cores'], vm['config']['memory'], 0)
return render_template('vm_details.html', username='com6056', vm=vm, usage=usage, limits=limits, usage_check=usage_check)
return render_template('vm_details.html', username=user, rtp=rtp, vm=vm, usage=usage, limits=limits, usage_check=usage_check)
else:
return '', 403
@app.route("/vm/<string:vmid>/power/<string:action>", methods=['POST'])
def vm_power(vmid, action):
user = session['userinfo']['preferred_username']
proxmox = connect_proxmox(app.config['PROXMOX_HOST'],
app.config['PROXMOX_USER'],
app.config['PROXMOX_PASS'])
if int(vmid) in get_user_allowed_vms(proxmox, user):
if int(vmid) in get_user_allowed_vms(proxmox, user) or 'rtp' in session['userinfo']['groups']:
if action == 'start':
config = get_vm_config(proxmox, vmid)
usage_check = check_user_usage(proxmox, user, config['cores'], config['memory'], 0)
@ -100,10 +109,11 @@ def vm_power(vmid, action):
@app.route("/vm/<string:vmid>/cpu/<int:cores>", methods=['POST'])
def vm_cpu(vmid, cores):
user = session['userinfo']['preferred_username']
proxmox = connect_proxmox(app.config['PROXMOX_HOST'],
app.config['PROXMOX_USER'],
app.config['PROXMOX_PASS'])
if int(vmid) in get_user_allowed_vms(proxmox, user):
if int(vmid) in get_user_allowed_vms(proxmox, user) or 'rtp' in session['userinfo']['groups']:
cur_cores = get_vm_config(proxmox, vmid)['cores']
if cores >= cur_cores:
status = get_vm(proxmox, vmid)['qmpstatus']
@ -121,10 +131,11 @@ def vm_cpu(vmid, cores):
@app.route("/vm/<string:vmid>/mem/<int:mem>", methods=['POST'])
def vm_mem(vmid, mem):
user = session['userinfo']['preferred_username']
proxmox = connect_proxmox(app.config['PROXMOX_HOST'],
app.config['PROXMOX_USER'],
app.config['PROXMOX_PASS'])
if int(vmid) in get_user_allowed_vms(proxmox, user):
if int(vmid) in get_user_allowed_vms(proxmox, user) or 'rtp' in session['userinfo']['groups']:
cur_mem = get_vm_config(proxmox, vmid)['memory'] // 1024
if mem >= cur_mem:
status = get_vm(proxmox, vmid)['qmpstatus']
@ -142,13 +153,14 @@ def vm_mem(vmid, mem):
@app.route("/vm/<string:vmid>/renew", methods=['POST'])
def vm_renew(vmid):
user = session['userinfo']['preferred_username']
proxmox = connect_proxmox(app.config['PROXMOX_HOST'],
app.config['PROXMOX_USER'],
app.config['PROXMOX_PASS'])
starrs = connect_starrs(
app.config['STARRS_DB_NAME'], app.config['STARRS_DB_USER'],
app.config['STARRS_DB_HOST'], app.config['STARRS_DB_PASS'])
if int(vmid) in get_user_allowed_vms(proxmox, user):
if int(vmid) in get_user_allowed_vms(proxmox, user) or 'rtp' in session['userinfo']['groups']:
renew_vm_expire(vmid, app.config['VM_EXPIRE_MONTHS'])
for interface in get_vm_interfaces(proxmox, vmid):
renew_ip(starrs, get_ip_for_mac(starrs, interface[1]))
@ -159,10 +171,11 @@ def vm_renew(vmid):
@app.route("/vm/<string:vmid>/eject", methods=['POST'])
def iso_eject(vmid):
user = session['userinfo']['preferred_username']
proxmox = connect_proxmox(app.config['PROXMOX_HOST'],
app.config['PROXMOX_USER'],
app.config['PROXMOX_PASS'])
if int(vmid) in get_user_allowed_vms(proxmox, user):
if int(vmid) in get_user_allowed_vms(proxmox, user) or 'rtp' in session['userinfo']['groups']:
eject_vm_iso(proxmox, vmid)
return '', 200
else:
@ -171,10 +184,11 @@ def iso_eject(vmid):
@app.route("/vm/<string:vmid>/mount/<string:iso>", methods=['POST'])
def iso_mount(vmid, iso):
user = session['userinfo']['preferred_username']
proxmox = connect_proxmox(app.config['PROXMOX_HOST'],
app.config['PROXMOX_USER'],
app.config['PROXMOX_PASS'])
if int(vmid) in get_user_allowed_vms(proxmox, user):
if int(vmid) in get_user_allowed_vms(proxmox, user) or 'rtp' in session['userinfo']['groups']:
iso = "{}:iso/{}".format(app.config['PROXMOX_ISO_STORAGE'], iso)
mount_vm_iso(proxmox, vmid, iso)
return '', 200
@ -184,13 +198,14 @@ def iso_mount(vmid, iso):
@app.route("/vm/<string:vmid>/delete", methods=['POST'])
def delete(vmid):
user = session['userinfo']['preferred_username']
proxmox = connect_proxmox(app.config['PROXMOX_HOST'],
app.config['PROXMOX_USER'],
app.config['PROXMOX_PASS'])
starrs = connect_starrs(
app.config['STARRS_DB_NAME'], app.config['STARRS_DB_USER'],
app.config['STARRS_DB_HOST'], app.config['STARRS_DB_PASS'])
if int(vmid) in get_user_allowed_vms(proxmox, user):
if int(vmid) in get_user_allowed_vms(proxmox, user) or 'rtp' in session['userinfo']['groups']:
vmname = get_vm_config(proxmox, vmid)['name']
delete_vm(proxmox, starrs, vmid)
delete_starrs(starrs, vmname)
@ -202,6 +217,8 @@ def delete(vmid):
@app.route("/vm/create", methods=['GET', 'POST'])
def create():
user = session['userinfo']['preferred_username']
rtp = 'rtp' in session['userinfo']['groups']
proxmox = connect_proxmox(app.config['PROXMOX_HOST'],
app.config['PROXMOX_USER'],
app.config['PROXMOX_PASS'])
@ -215,7 +232,8 @@ def create():
isos = get_isos(proxmox, app.config['PROXMOX_ISO_STORAGE'])
return render_template(
'create.html',
username='com6056',
username=user,
rtp=rtp,
usage=usage,
limits=limits,
percents=percents,
@ -245,32 +263,43 @@ def create():
@app.route('/limits/<string:user>', methods=['POST'])
def set_limits(user):
cpu = request.form['cpu']
mem = request.form['mem']
disk = request.form['disk']
set_user_usage_limits(user, cpu, mem, disk)
return '', 200
if 'rtp' in session['userinfo']['groups']:
cpu = request.form['cpu']
mem = request.form['mem']
disk = request.form['disk']
set_user_usage_limits(user, cpu, mem, disk)
return '', 200
else:
return '', 403
@app.route('/limits/<string:user>/reset', methods=['POST'])
def reset_limits(user):
delete_user_usage_limits(user)
return '', 200
if 'rtp' in session['userinfo']['groups']:
delete_user_usage_limits(user)
return '', 200
else:
return '', 403
@app.route('/limits')
def limits():
proxmox = connect_proxmox(app.config['PROXMOX_HOST'],
app.config['PROXMOX_USER'],
app.config['PROXMOX_PASS'])
pools = get_pools(proxmox)
pools = sorted(pools)
user_limits = []
for pool in pools:
if pool not in app.config['IGNORED_POOLS']:
limits = get_user_usage_limits(pool)
user_limits.append([pool, limits['cpu'], limits['mem'], limits['disk']])
return render_template('limits.html', username='com6056', user_limits=user_limits)
if 'rtp' in session['userinfo']['groups']:
user = session['userinfo']['preferred_username']
rtp = 'rtp' in session['userinfo']['groups']
proxmox = connect_proxmox(app.config['PROXMOX_HOST'],
app.config['PROXMOX_USER'],
app.config['PROXMOX_PASS'])
pools = get_pools(proxmox)
pools = sorted(pools)
user_limits = []
for pool in pools:
if pool not in app.config['IGNORED_POOLS']:
limits = get_user_usage_limits(pool)
user_limits.append([pool, limits['cpu'], limits['mem'], limits['disk']])
return render_template('limits.html', username=user, rtp=rtp, user_limits=user_limits)
else:
return '', 403
@app.route('/novnc/<path:path>')
@ -278,5 +307,11 @@ def send_novnc(path):
return send_from_directory('static/novnc-pve/novnc', path)
@app.route("/logout")
@auth.oidc_logout
def logout():
return redirect(url_for('list_vms'), 302)
if __name__ == "__main__":
app.run(debug=True)

View file

@ -14,6 +14,8 @@ def connect_proxmox(host, user, password):
def get_vms_for_user(proxmox, user):
if user not in get_pools(proxmox):
proxmox.pools.post(poolid=user, comment='Managed by Proxstar')
return proxmox.pools(user).get()['members']
@ -96,7 +98,7 @@ def get_vm_disks(proxmox, vmid, config=None):
for key, val in config.items():
valid_disk_types = ['virtio', 'ide', 'sata', 'scsi']
if any(disk_type in key for disk_type in valid_disk_types):
if 'cdrom' not in val:
if 'scsihw' not in key and 'cdrom' not in val:
disk_size = val.split(',')
if 'size' in disk_size[0]:
disk_size = disk_size[0].split('=')[1].rstrip('G')

View file

@ -34,6 +34,8 @@ def get_ip_for_mac(starrs, mac):
c.execute("COMMIT")
finally:
c.close()
if not results:
return 'No IP'
return results[0][3]

View file

@ -16,7 +16,7 @@ $("#delete-vm").click(function(){
.then((willDelete) => {
if (willDelete) {
const vmid = $(this).data('vmid')
fetch(`/proxstar/vm/${vmid}/delete`, {
fetch(`/vm/${vmid}/delete`, {
credentials: 'same-origin',
method: 'post'
}).then((response) => {
@ -24,7 +24,7 @@ $("#delete-vm").click(function(){
icon: "success",
});
}).then(() => {
window.location = "/proxstar";
window.location = "";
}).catch(err => {
if (err) {
swal("Uh oh...", `Unable to delete ${vmname}. Please try again later.`, "error");
@ -55,7 +55,7 @@ $("#stop-vm").click(function(){
.then((willStop) => {
if (willStop) {
const vmid = $(this).data('vmid')
fetch(`/proxstar/vm/${vmid}/power/stop`, {
fetch(`/vm/${vmid}/power/stop`, {
credentials: 'same-origin',
method: 'post'
}).then((response) => {
@ -63,7 +63,7 @@ $("#stop-vm").click(function(){
icon: "success",
});
}).then(() => {
window.location = `/proxstar/vm/${vmid}`;
window.location = `/vm/${vmid}`;
}).catch(err => {
if (err) {
swal("Uh oh...", `Unable to stop ${vmname}. Please try again later.`, "error");
@ -94,7 +94,7 @@ $("#reset-vm").click(function(){
.then((willReset) => {
if (willReset) {
const vmid = $(this).data('vmid')
fetch(`/proxstar/vm/${vmid}/power/reset`, {
fetch(`/vm/${vmid}/power/reset`, {
credentials: 'same-origin',
method: 'post'
}).then((response) => {
@ -102,7 +102,7 @@ $("#reset-vm").click(function(){
icon: "success",
});
}).then(() => {
window.location = `/proxstar/vm/${vmid}`;
window.location = `/vm/${vmid}`;
}).catch(err => {
if (err) {
swal("Uh oh...", `Unable to reset ${vmname}. Please try again later.`, "error");
@ -133,7 +133,7 @@ $("#shutdown-vm").click(function(){
.then((willShutdown) => {
if (willShutdown) {
const vmid = $(this).data('vmid')
fetch(`/proxstar/vm/${vmid}/power/shutdown`, {
fetch(`/vm/${vmid}/power/shutdown`, {
credentials: 'same-origin',
method: 'post'
}).then((response) => {
@ -141,7 +141,7 @@ $("#shutdown-vm").click(function(){
icon: "success",
});
}).then(() => {
window.location = `/proxstar/vm/${vmid}`;
window.location = `/vm/${vmid}`;
}).catch(err => {
if (err) {
swal("Uh oh...", `Unable to shutdown ${vmname}. Please try again later.`, "error");
@ -172,7 +172,7 @@ $("#suspend-vm").click(function(){
.then((willSuspend) => {
if (willSuspend) {
const vmid = $(this).data('vmid')
fetch(`/proxstar/vm/${vmid}/power/suspend`, {
fetch(`/vm/${vmid}/power/suspend`, {
credentials: 'same-origin',
method: 'post'
}).then((response) => {
@ -180,7 +180,7 @@ $("#suspend-vm").click(function(){
icon: "success",
});
}).then(() => {
window.location = `/proxstar/vm/${vmid}`;
window.location = `/vm/${vmid}`;
}).catch(err => {
if (err) {
swal("Uh oh...", `Unable to suspend ${vmname}. Please try again later.`, "error");
@ -196,7 +196,7 @@ $("#suspend-vm").click(function(){
$("#start-vm").click(function(){
const vmname = $(this).data('vmname')
const vmid = $(this).data('vmid')
fetch(`/proxstar/vm/${vmid}/power/start`, {
fetch(`/vm/${vmid}/power/start`, {
credentials: 'same-origin',
method: 'post'
}).then((response) => {
@ -204,7 +204,7 @@ $("#start-vm").click(function(){
icon: "success",
});
}).then(() => {
window.location = `/proxstar/vm/${vmid}`;
window.location = `/vm/${vmid}`;
}).catch(err => {
if (err) {
swal("Uh oh...", `Unable to start ${vmname}. Please try again later.`, "error");
@ -218,7 +218,7 @@ $("#start-vm").click(function(){
$("#resume-vm").click(function(){
const vmname = $(this).data('vmname')
const vmid = $(this).data('vmid')
fetch(`/proxstar/vm/${vmid}/power/resume`, {
fetch(`/vm/${vmid}/power/resume`, {
credentials: 'same-origin',
method: 'post'
}).then((response) => {
@ -226,7 +226,7 @@ $("#resume-vm").click(function(){
icon: "success",
});
}).then(() => {
window.location = `/proxstar/vm/${vmid}`;
window.location = `/vm/${vmid}`;
}).catch(err => {
if (err) {
swal("Uh oh...", `Unable to resume ${vmname}. Please try again later.`, "error");
@ -261,7 +261,7 @@ $("#eject-iso").click(function(){
.then((willEject) => {
if (willEject) {
const vmid = $(this).data('vmid')
fetch(`/proxstar/vm/${vmid}/eject`, {
fetch(`/vm/${vmid}/eject`, {
credentials: 'same-origin',
method: 'post'
}).then((response) => {
@ -276,7 +276,7 @@ $("#eject-iso").click(function(){
}
});
}).then(() => {
window.location = `/proxstar/vm/${vmid}`;
window.location = `/vm/${vmid}`;
}).catch(err => {
if (err) {
swal("Uh oh...", `Unable to eject ${iso}. Please try again later.`, "error");
@ -292,7 +292,7 @@ $("#eject-iso").click(function(){
$("#change-iso").click(function(){
const vmid = $(this).data('vmid')
fetch(`/proxstar/isos`, {
fetch(`/isos`, {
credentials: 'same-origin',
}).then((response) => {
return response.text()
@ -324,7 +324,7 @@ $("#change-iso").click(function(){
if (willChange) {
const vmid = $(this).data('vmid')
const iso = $(iso_list).val()
fetch(`/proxstar/vm/${vmid}/mount/${iso}`, {
fetch(`/vm/${vmid}/mount/${iso}`, {
credentials: 'same-origin',
method: 'post'
}).then((response) => {
@ -339,7 +339,7 @@ $("#change-iso").click(function(){
}
});
}).then(() => {
window.location = `/proxstar/vm/${vmid}`;
window.location = `/vm/${vmid}`;
}).catch(err => {
if (err) {
swal("Uh oh...", `Unable to mount ${iso}. Please try again later.`, "error");
@ -363,7 +363,7 @@ $("#change-iso").click(function(){
$("#renew-vm").click(function(){
const vmname = $(this).data('vmname')
const vmid = $(this).data('vmid')
fetch(`/proxstar/vm/${vmid}/renew`, {
fetch(`/vm/${vmid}/renew`, {
credentials: 'same-origin',
method: 'post'
}).then((response) => {
@ -371,7 +371,7 @@ $("#renew-vm").click(function(){
icon: "success",
});
}).then(() => {
window.location = `/proxstar/vm/${vmid}`;
window.location = `/vm/${vmid}`;
}).catch(err => {
if (err) {
swal("Uh oh...", `Unable to renew ${vmname}. Please try again later.`, "error");
@ -393,7 +393,7 @@ $("#create-vm").click(function(){
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(`/proxstar/hostname/${name}`, {
fetch(`/hostname/${name}`, {
credentials: 'same-origin',
}).then((response) => {
return response.text()
@ -424,14 +424,14 @@ $("#create-vm").click(function(){
data.append('mem', mem);
data.append('disk', disk);
data.append('iso', iso);
fetch('/proxstar/vm/create', {
fetch('/vm/create', {
credentials: 'same-origin',
method: 'post',
body: data
}).then((response) => {
return response.text()
}).then((vmid) => {
window.location = `/proxstar/vm/${vmid}`;
window.location = `/vm/${vmid}`;
});
}
});
@ -488,7 +488,7 @@ $("#change-cores").click(function(){
.then((willChange) => {
if (willChange) {
const cores = $(core_list).val()
fetch(`/proxstar/vm/${vmid}/cpu/${cores}`, {
fetch(`/vm/${vmid}/cpu/${cores}`, {
credentials: 'same-origin',
method: 'post'
}).then((response) => {
@ -503,7 +503,7 @@ $("#change-cores").click(function(){
}
});
}).then(() => {
window.location = `/proxstar/vm/${vmid}`;
window.location = `/vm/${vmid}`;
});
}
}).catch(err => {
@ -546,7 +546,7 @@ $("#change-mem").click(function(){
.then((willChange) => {
if (willChange) {
const mem = $(mem_list).val()
fetch(`/proxstar/vm/${vmid}/mem/${mem}`, {
fetch(`/vm/${vmid}/mem/${mem}`, {
credentials: 'same-origin',
method: 'post'
}).then((response) => {
@ -561,7 +561,7 @@ $("#change-mem").click(function(){
}
});
}).then(() => {
window.location = `/proxstar/vm/${vmid}`;
window.location = `/vm/${vmid}`;
});
}
}).catch(err => {
@ -624,7 +624,7 @@ $(".edit-limit").click(function(){
data.append('cpu', $(cpu).val());
data.append('mem', $(mem).val());
data.append('disk', $(disk).val());
fetch(`/proxstar/limits/${user}`, {
fetch(`/limits/${user}`, {
credentials: 'same-origin',
method: 'post',
body: data
@ -640,7 +640,7 @@ $(".edit-limit").click(function(){
}
});
}).then(() => {
window.location = `/proxstar/limits`;
window.location = `/limits`;
});
}
}).catch(err => {
@ -670,7 +670,7 @@ $(".reset-limit").click(function(){
})
.then((willReset) => {
if (willReset) {
fetch(`/proxstar/limits/${user}/reset`, {
fetch(`/limits/${user}/reset`, {
credentials: 'same-origin',
method: 'post'
}).then((response) => {
@ -678,7 +678,7 @@ $(".reset-limit").click(function(){
icon: "success",
});
}).then(() => {
window.location = `/proxstar/limits`;
window.location = `/limits`;
}).catch(err => {
if (err) {
swal("Uh oh...", `Unable to reset the usage limits for ${user}. Please try again later.`, "error");

View file

@ -5,9 +5,9 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<meta name="theme-color" content="#B0197E">
<link rel="stylesheet" href="/proxstar/static/css/csh-material-bootstrap/dist/css/csh-material-bootstrap.css">
<link rel="stylesheet" href="/proxstar/static/css/styles.css">
<link rel="stylesheet" href="/proxstar/static/css/circle.css">
<link rel="stylesheet" href="/static/css/csh-material-bootstrap/dist/css/csh-material-bootstrap.css">
<link rel="stylesheet" href="/static/css/styles.css">
<link rel="stylesheet" href="/static/css/circle.css">
<link rel="manifest" href="/static/manifest.json">
</head>
<body>
@ -22,28 +22,30 @@
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/proxstar">Proxstar</a>
<a class="navbar-brand" href="/">Proxstar</a>
</div>
<div id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li>
<a href="/proxstar">
<a href="/">
<span class="glyphicon glyphicon-th-list"></span>
VM List
</a>
</li>
<li>
<a href="/proxstar/vm/create">
<a href="/vm/create">
<span class="glyphicon glyphicon-plus-sign"></span>
Create VM
</a>
</li>
{% if rtp %}
<li>
<a href="/proxstar/limits">
<a href="/limits">
<span class="glyphicon glyphicon-pencil"></span>
Usage Limits
</a>
</li>
{% endif %}
</ul>
<ul class="nav navbar-nav navbar-right">
<li class="dropdown navbar-user">
@ -98,7 +100,7 @@
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script src="https://unpkg.com/sweetalert/dist/sweetalert.min.js"></script>
<script src="/proxstar/static/js/script.js"></script>
<script src="/static/js/script.js"></script>
</body>
</body>
</html>

View file

@ -7,7 +7,7 @@
<div class="col-md-12 col-sm-12">
<div class="panel panel-default">
<div class="panel-body">
<p>It looks like you don't have any VMs! If you want to create a VM, click <a href="/proxstar/vm/create">here</a>.</p>
<p>It looks like you don't have any VMs! If you want to create a VM, click <a href="/vm/create">here</a>.</p>
</div>
</div>
</div>
@ -16,7 +16,7 @@
<div class="col-md-3 col-sm-4 col-xs-6">
<div class="panel panel-default">
<div class="panel-body">
<a href="/proxstar/vm/{{ vm['vmid'] }}">
<a href="/vm/{{ vm['vmid'] }}">
<p>{{ vm['name'] }}</p>
</a>
<p>Status: {{ vm['status'] }}</p>