1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2024-12-24 15:15:22 +00:00
This commit is contained in:
Retspen 2015-02-27 14:25:41 +02:00
parent fa3df5bff3
commit 7dee5b94ac
23 changed files with 727 additions and 31 deletions

0
console/__init__.py Normal file
View file

3
console/admin.py Normal file
View file

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

View file

3
console/models.py Normal file
View file

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

3
console/tests.py Normal file
View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

56
console/views.py Normal file
View file

@ -0,0 +1,56 @@
import re
from django.shortcuts import render
from django.http import HttpResponseRedirect
from django.core.urlresolvers import reverse
from instances.models import Instance
from vrtManager.instance import wvmInstance
from webvirtcloud.settings import WS_PORT
from webvirtcloud.settings import WS_PUBLIC_HOST
from libvirt import libvirtError
def console(request):
"""
:param request:
:return:
"""
if not request.user.is_authenticated():
return HttpResponseRedirect(reverse('login'))
if request.method == 'GET':
token = request.GET.get('token', '')
try:
temptoken = token.split('-', 1)
host = int(temptoken[0])
uuid = temptoken[1]
instance = Instance.objects.get(compute_id=host, uuid=uuid)
conn = wvmInstance(instance.compute.hostname,
instance.compute.login,
instance.compute.password,
instance.compute.type,
instance.name)
console_type = conn.get_console_type()
console_websocket_port = conn.get_console_websocket_port()
console_passwd = conn.get_console_passwd()
except libvirtError as lib_err:
console_type = None
console_websocket_port = None
console_passwd = None
ws_port = console_websocket_port if console_websocket_port else WS_PORT
ws_host = WS_PUBLIC_HOST if WS_PUBLIC_HOST else request.get_host()
if ':' in ws_host:
ws_host = re.sub(':[0-9]+', '', ws_host)
if console_type == 'vnc':
response = render(request, 'console-vnc.html', locals())
elif console_type == 'spice':
response = render(request, 'console-spice.html', locals())
else:
response = "Console type %s no support" % console_type
response.set_cookie('token', token)
return response

0
definst/__init__.py Normal file
View file

3
definst/admin.py Normal file
View file

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

55
definst/forms.py Normal file
View file

@ -0,0 +1,55 @@
import re
from django import forms
from django.utils.translation import ugettext_lazy as _
from definst.models import Flavor
class FlavorAddForm(forms.Form):
label = forms.CharField(label="Name",
error_messages={'required': _('No flavor name has been entered')},
max_length=20)
vcpu = forms.IntegerField(label="VCPU",
error_messages={'required': _('No VCPU has been entered')}, )
disk = forms.IntegerField(label="HDD",
error_messages={'required': _('No HDD image has been entered')}, )
memory = forms.IntegerField(label="RAM",
error_messages={'required': _('No RAM size has been entered')}, )
def clean_name(self):
label = self.cleaned_data['label']
have_symbol = re.match('^[a-zA-Z0-9._-]+$', label)
if not have_symbol:
raise forms.ValidationError(_('The flavor name must not contain any special characters'))
elif len(label) > 20:
raise forms.ValidationError(_('The flavor name must not exceed 20 characters'))
try:
Flavor.objects.get(label=label)
except Flavor.DoesNotExist:
return label
raise forms.ValidationError(_('Flavor name is already use'))
class NewVMForm(forms.Form):
name = forms.CharField(error_messages={'required': _('No Virtual Machine name has been entered')},
max_length=20)
vcpu = forms.IntegerField(error_messages={'required': _('No VCPU has been entered')})
host_model = forms.BooleanField(required=False)
disk = forms.IntegerField(required=False)
memory = forms.IntegerField(error_messages={'required': _('No RAM size has been entered')})
networks = forms.CharField(error_messages={'required': _('No Network pool has been choice')})
storage = forms.CharField(max_length=20, required=False)
template = forms.CharField(required=False)
images = forms.CharField(required=False)
hdd_size = forms.IntegerField(required=False)
meta_prealloc = forms.BooleanField(required=False)
virtio = forms.BooleanField(required=False)
mac = forms.CharField(required=False)
def clean_name(self):
name = self.cleaned_data['name']
have_symbol = re.match('^[a-zA-Z0-9._-]+$', name)
if not have_symbol:
raise forms.ValidationError(_('The name of the virtual machine must not contain any special characters'))
elif len(name) > 20:
raise forms.ValidationError(_('The name of the virtual machine must not exceed 20 characters'))
return name

View file

11
definst/models.py Normal file
View file

@ -0,0 +1,11 @@
from django.db import models
class Flavor(models.Model):
label = models.CharField(max_length=12)
memory = models.IntegerField()
vcpu = models.IntegerField()
disk = models.IntegerField()
def __unicode__(self):
return self.name

3
definst/tests.py Normal file
View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

136
definst/views.py Normal file
View file

@ -0,0 +1,136 @@
from django.shortcuts import render
from django.http import HttpResponseRedirect
from django.utils.translation import ugettext_lazy as _
from django.core.urlresolvers import reverse
from computes.models import Compute
from definst.models import Flavor
from definst.forms import FlavorAddForm, NewVMForm
from instances.models import Instance
from vrtManager.create import wvmCreate
from vrtManager import util
from libvirt import libvirtError
def create(request, host_id):
"""
:param request:
:return:
"""
if not request.user.is_authenticated():
return HttpResponseRedirect(reverse('index'))
conn = None
error_messages = []
storages = []
networks = []
meta_prealloc = False
compute = Compute.objects.get(id=host_id)
flavors = Flavor.objects.filter().order_by('id')
try:
conn = wvmCreate(compute.hostname,
compute.login,
compute.password,
compute.type)
storages = sorted(conn.get_storages())
networks = sorted(conn.get_networks())
instances = conn.get_instances()
get_images = sorted(conn.get_storages_images())
mac_auto = util.randomMAC()
except libvirtError as lib_err:
error_messages.append(lib_err)
if conn:
if not storages:
msg = _("You haven't defined have any storage pools")
error_messages.append(msg)
if not networks:
msg = _("You haven't defined have any network pools")
error_messages.append(msg)
if request.method == 'POST':
if 'create_flavor' in request.POST:
form = FlavorAddForm(request.POST)
if form.is_valid():
data = form.cleaned_data
create_flavor = Flavor(label=data['label'],
vcpu=data['vcpu'],
memory=data['memory'],
disk=data['disk'])
create_flavor.save()
return HttpResponseRedirect(request.get_full_path())
if 'delete_flavor' in request.POST:
flavor_id = request.POST.get('flavor', '')
delete_flavor = Flavor.objects.get(id=flavor_id)
delete_flavor.delete()
return HttpResponseRedirect(request.get_full_path())
if 'create_xml' in request.POST:
xml = request.POST.get('from_xml', '')
try:
name = util.get_xml_path(xml, '/domain/name')
except util.libxml2.parserError:
name = None
if name in instances:
error_msg = _("A virtual machine with this name already exists")
error_messages.append(error_msg)
else:
try:
conn._defineXML(xml)
return HttpResponseRedirect(reverse('instance', args=[host_id, name]))
except libvirtError as lib_err:
error_messages.append(lib_err.message)
if 'create' in request.POST:
volumes = {}
form = NewVMForm(request.POST)
if form.is_valid():
data = form.cleaned_data
if data['meta_prealloc']:
meta_prealloc = True
if instances:
if data['name'] in instances:
msg = _("A virtual machine with this name already exists")
error_messages.append(msg)
if not error_messages:
if data['hdd_size']:
if not data['mac']:
error_msg = _("No Virtual Machine MAC has been entered")
error_messages.append(error_msg)
else:
try:
path = conn.create_volume(data['storage'], data['name'], data['hdd_size'],
metadata=meta_prealloc)
volumes[path] = conn.get_volume_type(path)
except libvirtError as lib_err:
error_messages.append(lib_err.message)
elif data['template']:
templ_path = conn.get_volume_path(data['template'])
clone_path = conn.clone_from_template(data['name'], templ_path, metadata=meta_prealloc)
volumes[clone_path] = conn.get_volume_type(clone_path)
else:
if not data['images']:
error_msg = _("First you need to create or select an image")
error_messages.append(error_msg)
else:
for vol in data['images'].split(','):
try:
path = conn.get_volume_path(vol)
volumes[path] = conn.get_volume_type(path)
except libvirtError as lib_err:
error_messages.append(lib_err.message)
if not error_messages:
uuid = util.randomUUID()
try:
conn.create_instance(data['name'], data['memory'], data['vcpu'], data['host_model'],
uuid, volumes, data['networks'], data['virtio'], data['mac'])
create_instance = Instance(compute_id=host_id, name=data['name'], uuid=uuid)
create_instance.save()
return HttpResponseRedirect(reverse('instance', args=[host_id, data['name']]))
except libvirtError as lib_err:
if data['hdd_size']:
conn.delete_volume(volumes.keys()[0])
error_messages.append(lib_err)
conn.close()
return render('definst.html', locals())

15
templates/404.html Normal file
View file

@ -0,0 +1,15 @@
{% extends "base.html" %}
{% load i18n %}
{% block title %}{% trans "404" %}{% endblock %}
{% block content %}
<div class="row">
<div class="col-xs-12" id="error">
<h1>Oops!</h1>
<p class="lead">{% trans "404 Not Found" %}</p>
<p>{% trans "The requested page was not found on this server." %}</p>
<a class="btn btn-medium btn-success" href="javascript:history.back()">&larr; Back</a>
</div>
</div>
{% endblock %}

15
templates/500.html Normal file
View file

@ -0,0 +1,15 @@
{% extends "base.html" %}
{% load i18n %}
{% block title %}{% trans "500" %}{% endblock %}
{% block content %}
<div class="row">
<div class="col-xs-12" id="error">
<h1>Oops!</h1>
<p class="lead">{% trans "500 Internal Server Error" %}</p>
<p>{% trans "The server encountered an internal error or misconfiguration and was unable to complete you request." %}</p>
<a class="btn btn-medium btn-success" href="javascript:history.back()">&larr; Back</a>
</div>
</div>
{% endblock %}

View file

@ -5,13 +5,16 @@
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}{% endblock %}</title>
<link rel="icon" href="{% static "favicon.ico" %}">
<title>Dashboard Template for Bootstrap</title>
<!-- Bootstrap core CSS -->
<link href="{% static "css/bootstrap.min.css" %}" rel="stylesheet">
<!-- Custom styles for this template -->
<link href="{% static "css/dashboard.css" %}" rel="stylesheet">
</head>
</head>
<body>
{% block content %}{% endblock %}
<script src="{% static "js/jquery.min.js" %}"></script>
<script src="{% static "js/bootstrap.min.js" %}"></script>
</body>
</html>

View file

@ -1,7 +1,7 @@
{% load static %}
{% extends "base.html" %}
{% load i18n %}
{% include 'header.html' %}
<body>
{% block title %}{% trans "Compute" %} - {{ compute.name }}{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row">
@ -53,5 +53,4 @@
</div>
</div>
</div>
{% include 'footer.html' %}
{% endblock %}

View file

@ -1,7 +1,7 @@
{% load static %}
{% extends "base.html" %}
{% load i18n %}
{% include 'header.html' %}
<body>
{% block title %}{% trans "Computes" %}{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row">
@ -227,5 +227,4 @@
</div>
</div>
</div>
{% include 'footer.html' %}
{% endblock %}

View file

@ -0,0 +1,208 @@
{% load i18n %}
<html>
<head>
<link rel="shortcut icon" href="{{ STATIC_URL }}img/favicon.ico">
<script src="{{ STATIC_URL }}js/spice-html5/spicearraybuffer.js"></script>
<script src="{{ STATIC_URL }}js/spice-html5/enums.js"></script>
<script src="{{ STATIC_URL }}js/spice-html5/atKeynames.js"></script>
<script src="{{ STATIC_URL }}js/spice-html5/utils.js"></script>
<script src="{{ STATIC_URL }}js/spice-html5/png.js"></script>
<script src="{{ STATIC_URL }}js/spice-html5/lz.js"></script>
<script src="{{ STATIC_URL }}js/spice-html5/quic.js"></script>
<script src="{{ STATIC_URL }}js/spice-html5/bitmap.js"></script>
<script src="{{ STATIC_URL }}js/spice-html5/spicedataview.js"></script>
<script src="{{ STATIC_URL }}js/spice-html5/spicetype.js"></script>
<script src="{{ STATIC_URL }}js/spice-html5/spicemsg.js"></script>
<script src="{{ STATIC_URL }}js/spice-html5/wire.js"></script>
<script src="{{ STATIC_URL }}js/spice-html5/spiceconn.js"></script>
<script src="{{ STATIC_URL }}js/spice-html5/display.js"></script>
<script src="{{ STATIC_URL }}js/spice-html5/main.js"></script>
<script src="{{ STATIC_URL }}js/spice-html5/inputs.js"></script>
<script src="{{ STATIC_URL }}js/spice-html5/webm.js"></script>
<script src="{{ STATIC_URL }}js/spice-html5/playback.js"></script>
<script src="{{ STATIC_URL }}js/spice-html5/simulatecursor.js"></script>
<script src="{{ STATIC_URL }}js/spice-html5/cursor.js"></script>
<script src="{{ STATIC_URL }}js/spice-html5/thirdparty/jsbn.js"></script>
<script src="{{ STATIC_URL }}js/spice-html5/thirdparty/rsa.js"></script>
<script src="{{ STATIC_URL }}js/spice-html5/thirdparty/prng4.js"></script>
<script src="{{ STATIC_URL }}js/spice-html5/thirdparty/rng.js"></script>
<script src="{{ STATIC_URL }}js/spice-html5/thirdparty/sha1.js"></script>
<script src="{{ STATIC_URL }}js/spice-html5/ticket.js"></script>
<script src="{{ STATIC_URL }}js/spice-html5/resize.js"></script>
<script src="{{ STATIC_URL }}js/spice-html5/filexfer.js"></script>
<style>
body {
margin: 0;
padding: 0;
font-family: Helvetica;
background-color:#494949;
}
#status {
text-align: center;
width: 100%;
margin: 0;
font-size: 1em;
height: 1.6em;
padding-top: 0.3em;
background-color: #222;
}
#spice-area {
border-bottom-right-radius: 800px 600px;
background-color: #313131;
height: 100%;
}
#spice-screen canvas {
padding-left: 0;
padding-right: 0;
margin-left: auto;
margin-right: auto;
display: block;
}
</style>
</head>
<body>
<div id="status"></div>
<div id="spice-area">
<div id="spice-screen" class="spice-screen"></div>
</div>
<script>
var sc;
function spice_set_cookie(name, value, days) {
var date, expires;
date = new Date();
date.setTime(date.getTime() + (days*24*60*60*1000));
expires = "; expires=" + date.toGMTString();
document.cookie = name + "=" + value + expires + "; path=/";
};
function spice_query_var(name, defvalue) {
var match = RegExp('[?&]' + name + '=([^&]*)')
.exec(window.location.search);
return match ?
decodeURIComponent(match[1].replace(/\+/g, ' '))
: defvalue;
}
var status_div=false;
function log_message(msg,color,bgcolor) {
if (!status_div) {
status_div=document.getElementById('status');
}
status_div.innerHTML=msg;
if (color) {
status_div.style.color=color;
}
if (bgcolor) {
status_div.style.backgroundColor=bgcolor;
}
}
function log_error(msg) {
log_message(msg,'#000','#f44');
}
function log_info(msg) {
log_message(msg,'#000','#eee');
}
function spice_error(e)
{
console.log(e);
disconnect();
if (e.message != undefined) {
log_error(e.message);
}
else {
log_error('Unknown error');
}
}
function spice_success(msg) {
log_info(msg);
}
function connect(uri,password)
{
// If a token variable is passed in, set the parameter in a cookie.
// This is used by nova-spiceproxy.
token = spice_query_var('token', null);
if (token) {
spice_set_cookie('token', token, 1)
}
if (sc) {
sc.stop();
}
try
{
sc = new SpiceMainConn({uri: uri, password: password, screen_id: "spice-screen",
onsuccess: spice_success, onerror: spice_error, onagent: agent_connected });
}
catch (e)
{
console.log(e);
log_error(e.toString());
disconnect();
}
}
function disconnect()
{
console.log(">> disconnect");
if (sc) {
sc.stop();
}
if (window.File && window.FileReader && window.FileList && window.Blob)
{
console.log(" -> Disable drag/drop transfer");
var spice_xfer_area = document.getElementById('spice-xfer-area');
try {
document.getElementById('spice-area').removeChild(spice_xfer_area);
document.getElementById('spice-area').removeEventListener('dragover', handle_file_dragover, false);
document.getElementById('spice-area').removeEventListener('drop', handle_file_drop, false);
}
catch(e) {
console.log(' -> Error disabling drag/drop transfer');
}
}
console.log("<< disconnect");
}
function agent_connected(sc) {
console.log('Connected');
window.addEventListener('resize', handle_resize);
window.spice_connection = this;
resize_helper(this);
if (window.File && window.FileReader && window.FileList && window.Blob)
{
var spice_xfer_area = document.createElement("div");
spice_xfer_area.setAttribute('id', 'spice-xfer-area');
document.getElementById('spice-area').addEventListener('dragover', handle_file_dragover, false);
document.getElementById('spice-area').addEventListener('drop', handle_file_drop, false);
log_info('Drag and drop transfer enabled.');
}
else
{
console.log("File API is not supported");
log_info('Drag and drop transfer not supported.');
}
log_info('Connected');
}
var uri = 'ws://{{ ws_host }}:{{ ws_port }}';
var password = '{{ console_passwd }}';
log_info('Connecting ...');
connect(uri,password);
</script>
</body>
</html>

186
templates/console-vnc.html Normal file
View file

@ -0,0 +1,186 @@
{% load i18n %}
<html>
<head>
<link rel="shortcut icon" href="{{ STATIC_URL }}img/favicon.ico">
<link rel="stylesheet" href="{{ STATIC_URL }}js/novnc/base.css" title="plain">
<!--
<script type='text/javascript'
src='http://getfirebug.com/releases/lite/1.2/firebug-lite-compressed.js'></script>
-->
<script src="{{ STATIC_URL }}js/novnc/util.js"></script>
</head>
<body style="margin: 0px;">
<div id="noVNC_screen">
<div id="noVNC_status_bar" class="noVNC_status_bar" style="margin-top: 0px;">
<table border=0 width="100%">
<tr>
<td>
<div id="noVNC_status">{% trans "Loading..." %}</div>
</td>
<td width="18%" style="text-align:right;">
<div id="noVNC_buttons">
<!-- dirty fix for keyboard on iOS devices -->
<input type="button" id="showKeyboard" value="Keyboard" title="Show Keyboard"/>
<!-- Note that Google Chrome on Android doesn't respect any of these,
html attributes which attempt to disable text suggestions on the
on-screen keyboard. Let's hope Chrome implements the ime-mode
style for example -->
<!-- TODO: check if this is needed on iOS -->
<textarea id="keyboardinput" autocapitalize="off"
autocorrect="off" autocomplete="off" spellcheck="false"
mozactionhint="Enter" onsubmit="return false;"
style="ime-mode: disabled;">
</textarea>
<input type=button value="Ctrl+Alt+Del" id="sendCtrlAltDelButton">
</div>
</td>
</tr>
</table>
</div>
<canvas id="noVNC_canvas" width="640px" height="20px">
{% trans "Canvas not supported." %}
</canvas>
</div>
<script>
/*jslint white: false */
/*global window, $, Util, RFB, */
"use strict";
// dirty fix for keyboard on iOS devices
var keyboardVisible = false;
var isTouchDevice = false;
isTouchDevice = 'ontouchstart' in document.documentElement;
// Load supporting scripts
Util.load_scripts(["webutil.js", "base64.js", "websock.js", "des.js",
"input.js", "display.js", "jsunzip.js", "rfb.js"]);
var rfb;
function passwordRequired(rfb) {
var msg;
msg = '<form onsubmit="return setPassword();"';
msg += 'role="form"';
msg += ' style="margin-bottom: 0px">';
msg += 'Password Required: ';
msg += '<input type=password size=10 id="password_input" class="noVNC_status">';
msg += '<\/form>';
$D('noVNC_status_bar').setAttribute("class", "noVNC_status_warn");
$D('noVNC_status').innerHTML = msg;
}
function setPassword() {
rfb.sendPassword($D('password_input').value);
return false;
}
function sendCtrlAltDel() {
rfb.sendCtrlAltDel();
return false;
}
// dirty fix for keyboard on iOS devices
function showKeyboard() {
var kbi, skb, l;
kbi = $D('keyboardinput');
skb = $D('showKeyboard');
l = kbi.value.length;
if (keyboardVisible === false) {
kbi.focus();
try {
kbi.setSelectionRange(l, l);
} // Move the caret to the end
catch (err) {
} // setSelectionRange is undefined in Google Chrome
keyboardVisible = true;
//skb.className = "noVNC_status_button_selected";
} else if (keyboardVisible === true) {
kbi.blur();
//skb.className = "noVNC_status_button";
keyboardVisible = false;
}
}
function updateState(rfb, state, oldstate, msg) {
var s, sb, cad, level;
s = $D('noVNC_status');
sb = $D('noVNC_status_bar');
cad = $D('sendCtrlAltDelButton');
switch (state) {
case 'failed':
level = "error";
break;
case 'fatal':
level = "error";
break;
case 'normal':
level = "normal";
break;
case 'disconnected':
level = "normal";
break;
case 'loaded':
level = "normal";
break;
default:
level = "warn";
break;
}
if (state === "normal") {
cad.disabled = false;
}
else {
cad.disabled = true;
}
if (typeof(msg) !== 'undefined') {
sb.setAttribute("class", "noVNC_status_" + level);
s.innerHTML = msg;
}
}
window.onscriptsload = function () {
var host, port, password, path, token;
$D('sendCtrlAltDelButton').style.display = "inline";
$D('sendCtrlAltDelButton').onclick = sendCtrlAltDel;
// dirty fix for keyboard on iOS devices
if (isTouchDevice) {
$D('showKeyboard').onclick = showKeyboard;
// Remove the address bar
setTimeout(function () {
window.scrollTo(0, 1);
}, 100);
} else {
$D('showKeyboard').style.display = "none";
}
WebUtil.init_logging(WebUtil.getQueryVar('logging', 'warn'));
document.title = unescape(WebUtil.getQueryVar('title', 'noVNC'));
// By default, use the host and port of server that served this file
host = '{{ ws_host }}';
port = '{{ ws_port }}';
password = '{{ console_passwd }}';
if ((!host) || (!port)) {
updateState('failed',
"Must specify host and port in URL");
return;
}
rfb = new RFB({'target': $D('noVNC_canvas'),
'encrypt': WebUtil.getQueryVar('encrypt',
(window.location.protocol === "https:")),
'repeaterID': WebUtil.getQueryVar('repeaterID', ''),
'true_color': WebUtil.getQueryVar('true_color', true),
'local_cursor': WebUtil.getQueryVar('cursor', true),
'shared': WebUtil.getQueryVar('shared', true),
'view_only': WebUtil.getQueryVar('view_only', false),
'updateState': updateState,
'onPasswordRequired': passwordRequired});
rfb.connect(host, port, password, path);
};
</script>
</body>
</html>

View file

@ -1,3 +0,0 @@
{% load static %}
<script src="{% static "js/jquery.min.js" %}"></script>
<script src="{% static "js/bootstrap.min.js" %}"></script>

View file

@ -1,7 +1,7 @@
{% load static %}
{% extends "base.html" %}
{% load i18n %}
{% include 'header.html' %}
<body>
{% block title %}{% trans "Instances" %}{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row">
@ -100,8 +100,8 @@
</tbody>
</table>
</div>
</div>
</div>
</div>
{% include 'footer.html' %}
{% endblock %}

View file

@ -1,11 +1,6 @@
"""
Django settings for webvirtcloud project.
For more information on this file, see
https://docs.djangoproject.com/en/1.7/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.7/ref/settings/
"""
import os
@ -70,4 +65,10 @@ STATICFILES_DIRS = (
TEMPLATE_DIRS = (
os.path.join(BASE_DIR, 'templates'),
)
)
# WebVirtCloud settings
WS_PORT = 6080
WS_HOST = '0.0.0.0'
WS_PUBLIC_HOST = None
WS_CERT = None