mirror of
https://github.com/retspen/webvirtcloud
synced 2024-10-31 19:44:16 +00:00
Instances overhaul
This commit is contained in:
parent
f23e6b000f
commit
47009d47ca
69 changed files with 5011 additions and 4127 deletions
2
.github/workflows/linter.yml
vendored
2
.github/workflows/linter.yml
vendored
|
@ -6,7 +6,7 @@ name: linter
|
|||
# events but only for the master branch
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
branches: [ '*' ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
|
|
17
README.md
17
README.md
|
@ -316,6 +316,23 @@ pip3 install -U -r conf/requirements.txt
|
|||
python3 manage.py migrate
|
||||
sudo service supervisor restart
|
||||
```
|
||||
|
||||
### Running tests
|
||||
Server on which tests will be performed must have libvirt up and running.
|
||||
It must not contain vms.
|
||||
It must have `default` storage which not contain any disk images.
|
||||
It must have `default` network which must be on.
|
||||
Setup venv
|
||||
```bash
|
||||
python -m venv venv
|
||||
source venv/bin/activate
|
||||
pip install -r conf/requirements.txt
|
||||
```
|
||||
Run tests
|
||||
```bash
|
||||
python menage.py test
|
||||
```
|
||||
|
||||
### Screenshots
|
||||
Instance Detail:
|
||||
<img src="doc/images/instance.PNG" width="95%" align="center"/>
|
||||
|
|
|
@ -6,6 +6,9 @@ from django.utils.translation import ugettext_lazy as _
|
|||
|
||||
from instances.models import Instance
|
||||
|
||||
class UserInstanceManager(models.Manager):
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().select_related('instance', 'user')
|
||||
|
||||
class UserInstance(models.Model):
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
|
@ -14,6 +17,8 @@ class UserInstance(models.Model):
|
|||
is_delete = models.BooleanField(default=False)
|
||||
is_vnc = models.BooleanField(default=False)
|
||||
|
||||
objects = UserInstanceManager()
|
||||
|
||||
def __str__(self):
|
||||
return _('Instance "%(inst)s" of user %(user)s') % {'inst': self.instance, 'user': self.user}
|
||||
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
{% for inst in user_insts %}
|
||||
<tr>
|
||||
<td>{{ forloop.counter }}</td>
|
||||
<td><a href="{% url 'instances:instance' inst.instance.compute.id inst.instance.name %}">{{ inst.instance.name }}</a></td>
|
||||
<td><a href="{% url 'instances:instance' inst.instance.id %}">{{ inst.instance.name }}</a></td>
|
||||
<td>{{ inst.is_vnc }}</td>
|
||||
<td>{{ inst.is_change }}</td>
|
||||
<td>{{ inst.is_delete }}</td>
|
||||
|
|
|
@ -55,7 +55,7 @@
|
|||
</td>
|
||||
<td>{% if user.is_staff %}<span class="fa fa-check"></span>{% endif %}</td>
|
||||
<td>{% if user.is_superuser %}<span class="fa fa-check"></span>{% endif %}</td>
|
||||
<td>{% if user.userattributes.can_clone_instances %}<span class="fa fa-check"></span>{% endif %}</td>
|
||||
<td>{% if perms.instances.clone_instances %}<span class="fa fa-check"></span>{% endif %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
|
|
@ -126,6 +126,10 @@ def user_instance_delete(request, pk):
|
|||
if request.method == 'POST':
|
||||
user = user_instance.user
|
||||
user_instance.delete()
|
||||
next = request.GET.get('next', None)
|
||||
if next:
|
||||
return redirect(next)
|
||||
else:
|
||||
return redirect(reverse('account', args=[user.id]))
|
||||
|
||||
return render(
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
from django.db.models import CharField, IntegerField, Model
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from libvirt import virConnect
|
||||
|
||||
from vrtManager.connection import connection_manager
|
||||
from vrtManager.hostdetails import wvmHostDetails
|
||||
|
||||
|
||||
class Compute(Model):
|
||||
|
@ -15,7 +17,45 @@ class Compute(Model):
|
|||
|
||||
@cached_property
|
||||
def status(self):
|
||||
return connection_manager.host_is_up(self.type, self.hostname)
|
||||
# return connection_manager.host_is_up(self.type, self.hostname)
|
||||
# TODO: looks like socket has problems connecting via VPN
|
||||
if isinstance(self.connection, virConnect):
|
||||
return True
|
||||
else:
|
||||
return self.connection
|
||||
|
||||
@cached_property
|
||||
def connection(self):
|
||||
try:
|
||||
return connection_manager.get_connection(
|
||||
self.hostname,
|
||||
self.login,
|
||||
self.password,
|
||||
self.type,
|
||||
)
|
||||
except Exception as e:
|
||||
return e
|
||||
|
||||
@cached_property
|
||||
def proxy(self):
|
||||
return wvmHostDetails(
|
||||
self.hostname,
|
||||
self.login,
|
||||
self.password,
|
||||
self.type,
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def cpu_count(self):
|
||||
return self.proxy.get_node_info()[3]
|
||||
|
||||
@cached_property
|
||||
def ram_size(self):
|
||||
return self.proxy.get_node_info()[2]
|
||||
|
||||
@cached_property
|
||||
def ram_usage(self):
|
||||
return self.proxy.get_memory_usage()['percent']
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
|
121
computes/templates/computes/instances.html
Normal file
121
computes/templates/computes/instances.html
Normal file
|
@ -0,0 +1,121 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
{% load staticfiles %}
|
||||
{% load icons %}
|
||||
{% block title %}{% trans "Instances" %} - {{ compute.name }}{% endblock %}
|
||||
{% block style %}
|
||||
<link rel="stylesheet" href="{% static "css/sortable-theme-bootstrap.css" %}" />
|
||||
{% endblock %}
|
||||
{% block page_header %}{{ compute.name }}{% endblock page_header %}
|
||||
|
||||
{% block page_header_extra %}
|
||||
<a href="{% url 'instances:create_instance_select_type' compute.id %}"
|
||||
class="btn btn-success btn-header float-right">
|
||||
{% icon 'plus' %}
|
||||
</a>
|
||||
{% if instances %}
|
||||
<div class="float-right search">
|
||||
<input id="filter" class="form-control" type="text" placeholder="{% trans 'Search' %}">
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock page_header_extra %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb bg-light shadow-sm">
|
||||
<li class="breadcrumb-item active">
|
||||
<a href="{% url 'overview' compute.id %}">{% icon 'dashboard' %} {% trans "Overview" %}</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item">
|
||||
<span class="font-weight-bold">{% icon 'server' %} {% trans "Instances" %}</span>
|
||||
</li>
|
||||
<li class="breadcrumb-item">
|
||||
<a href="{% url 'storages' compute.id %}">{% icon 'hdd-o' %} {% trans "Storages" %}</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item">
|
||||
<a href="{% url 'networks' compute.id %}">{% icon 'sitemap' %} {% trans "Networks" %}</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item">
|
||||
<a href="{% url 'interfaces' compute.id %}">{% icon 'wifi' %} {% trans "Interfaces" %}</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item">
|
||||
<a href="{% url 'nwfilters' compute.id %}">{% icon 'filter' %} {% trans "NWFilters" %}</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item">
|
||||
<a href="{% url 'secrets' compute.id %}">{% icon 'key' %} {% trans "Secrets" %}</a>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
{% if not instances %}
|
||||
<div class="alert alert-warning alert-dismissable fade show">
|
||||
{% icon 'exclamation-triangle' %} <strong>{% trans "Warning" %}:</strong>
|
||||
{% trans "Hypervisor doesn't have any Instances" %}
|
||||
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<table class="table table-hover sortable-theme-bootstrap" data-sortable>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{% trans 'Name' %}<br>{% trans 'Description' %}</th>
|
||||
<th scope="col">{% trans 'User' %}</th>
|
||||
<th scope="col">{% trans 'Status' %}</th>
|
||||
<th scope="col">{% trans 'VCPU' %}</th>
|
||||
<th scope="col">{% trans 'Memory' %}</th>
|
||||
<th scope="col" data-sortable="false">{% trans 'Actions' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="searchable">
|
||||
{% for instance in instances %}
|
||||
<tr>
|
||||
<td>
|
||||
<a class="text-secondary" href="{% url 'instances:instance' instance.id %}">
|
||||
{{ instance.name }}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<em>
|
||||
{% if instance.userinstance_set.all.count > 0 %}
|
||||
{{ instance.userinstance_set.all.0.user }}
|
||||
{% if instance.userinstance_set.all.count > 1 %}
|
||||
(+{{ instance.userinstance_set.all.count|add:"-1" }})
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
{% if instance.proxy.instance.info.0 == 1 %}<span
|
||||
class="text-success">{% trans "Active" %}</span>{% endif %}
|
||||
{% if instance.proxy.instance.info.0 == 5 %}<span
|
||||
class="text-danger">{% trans "Off" %}</span>{% endif %}
|
||||
{% if instance.proxy.instance.info.0 == 3 %}<span
|
||||
class="text-warning">{% trans "Suspended" %}</span>{% endif %}
|
||||
</td>
|
||||
<td>{{ instance.proxy.instance.info.3 }}</td>
|
||||
<td>{% widthratio instance.proxy.instance.info.1 1024 1 %} MiB</td>
|
||||
<td class="text-nowrap">
|
||||
{% include 'instance_actions.html' %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block script %}
|
||||
<script src="{% static "js/sortable.min.js" %}"></script>
|
||||
<script>
|
||||
function open_console(uuid) {
|
||||
window.open("{% url 'console' %}?token=" + uuid, "", "width=850,height=685");
|
||||
}
|
||||
</script>
|
||||
<script src="{% static 'js/filter-table.js' %}"></script>
|
||||
{% endblock %}
|
|
@ -83,6 +83,9 @@ class ComputesTestCase(TestCase):
|
|||
response = self.client.get(reverse('storages', args=[1]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_storage(self):
|
||||
pass
|
||||
|
||||
def test_default_storage_volumes(self):
|
||||
response = self.client.get(reverse('volumes', kwargs={'compute_id': 1, 'pool': 'default'}))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
@ -115,9 +118,9 @@ class ComputesTestCase(TestCase):
|
|||
response = self.client.get(reverse('secrets', args=[1]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_create_instance_select_type(self):
|
||||
response = self.client.get(reverse('create_instance_select_type', args=[1]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
# def test_create_instance_select_type(self):
|
||||
# response = self.client.get(reverse('create_instance_select_type', args=[1]))
|
||||
# self.assertEqual(response.status_code, 200)
|
||||
|
||||
# TODO: create_instance
|
||||
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
from django.urls import path, include
|
||||
from secrets.views import secrets
|
||||
|
||||
from . import views
|
||||
from . import forms
|
||||
from create.views import create_instance, create_instance_select_type
|
||||
from instances.views import instances
|
||||
from django.urls import include, path
|
||||
|
||||
# from instances.views import create_instance, create_instance_select_type
|
||||
from interfaces.views import interface, interfaces
|
||||
from networks.views import network, networks
|
||||
from nwfilters.views import nwfilter, nwfilters
|
||||
from secrets.views import secrets
|
||||
from storages.views import get_volumes, storage, storages
|
||||
from storages.views import create_volume, get_volumes, storage, storages
|
||||
|
||||
from . import forms, views
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.computes, name='computes'),
|
||||
|
@ -23,10 +23,11 @@ urlpatterns = [
|
|||
path('update/', views.compute_update, name='compute_update'),
|
||||
path('delete/', views.compute_delete, name='compute_delete'),
|
||||
path('statistics', views.compute_graph, name='compute_graph'),
|
||||
path('instances/', instances, name='instances'),
|
||||
path('instances/', views.instances, name='instances'),
|
||||
path('storages/', storages, name='storages'),
|
||||
path('storage/<str:pool>/volumes', get_volumes, name='volumes'),
|
||||
path('storage/<str:pool>/volumes/', get_volumes, name='volumes'),
|
||||
path('storage/<str:pool>/', storage, name='storage'),
|
||||
path('storage/<str:pool>/create_volume/', create_volume, name='create_volume'),
|
||||
path('networks/', networks, name='networks'),
|
||||
path('network/<str:pool>/', network, name='network'),
|
||||
path('interfaces/', interfaces, name='interfaces'),
|
||||
|
@ -34,8 +35,8 @@ urlpatterns = [
|
|||
path('nwfilters/', nwfilters, name='nwfilters'),
|
||||
path('nwfilter/<str:nwfltr>/', nwfilter, name='nwfilter'),
|
||||
path('secrets/', secrets, name='secrets'),
|
||||
path('create/', create_instance_select_type, name='create_instance_select_type'),
|
||||
path('create/archs/<str:arch>/machines/<str:machine>/', create_instance, name='create_instance'),
|
||||
# path('create/', create_instance_select_type, name='create_instance_select_type'),
|
||||
# path('create/archs/<str:arch>/machines/<str:machine>/', create_instance, name='create_instance'),
|
||||
path('archs/<str:arch>/machines/', views.get_compute_machine_types, name='machines'),
|
||||
path(
|
||||
'archs/<str:arch>/machines/<str:machine>/disks/<str:disk>/buses/',
|
||||
|
|
13
computes/utils.py
Normal file
13
computes/utils.py
Normal file
|
@ -0,0 +1,13 @@
|
|||
from instances.models import Instance
|
||||
|
||||
|
||||
def refresh_instance_database(compute):
|
||||
domains = compute.proxy.wvm.listAllDomains()
|
||||
domain_names = [d.name() for d in domains]
|
||||
# Delete instances that're not on host from DB
|
||||
Instance.objects.filter(compute=compute).exclude(name__in=domain_names).delete()
|
||||
# Create instances that're on host but not in DB
|
||||
names = Instance.objects.filter(compute=compute).values_list('name', flat=True)
|
||||
for domain in domains:
|
||||
if domain.name() not in names:
|
||||
Instance(compute=compute, name=domain.name(), uuid=domain.UUIDString()).save()
|
|
@ -15,6 +15,8 @@ from instances.models import Instance
|
|||
from vrtManager.connection import (CONN_SOCKET, CONN_SSH, CONN_TCP, CONN_TLS, connection_manager, wvmConnect)
|
||||
from vrtManager.hostdetails import wvmHostDetails
|
||||
|
||||
from . import utils
|
||||
|
||||
|
||||
@superuser_only
|
||||
def computes(request):
|
||||
|
@ -30,17 +32,9 @@ def computes(request):
|
|||
|
||||
@superuser_only
|
||||
def overview(request, compute_id):
|
||||
"""
|
||||
:param request:
|
||||
:param compute_id:
|
||||
:return:
|
||||
"""
|
||||
|
||||
error_messages = []
|
||||
compute = get_object_or_404(Compute, pk=compute_id)
|
||||
status = 'true' if connection_manager.host_is_up(compute.type, compute.hostname) is True else 'false'
|
||||
|
||||
try:
|
||||
conn = wvmHostDetails(
|
||||
compute.hostname,
|
||||
compute.login,
|
||||
|
@ -54,12 +48,20 @@ def overview(request, compute_id):
|
|||
version = conn.get_version()
|
||||
lib_version = conn.get_lib_version()
|
||||
conn.close()
|
||||
except libvirtError as lib_err:
|
||||
error_messages.append(lib_err)
|
||||
|
||||
return render(request, 'overview.html', locals())
|
||||
|
||||
|
||||
@superuser_only
|
||||
def instances(request, compute_id):
|
||||
compute = get_object_or_404(Compute, pk=compute_id)
|
||||
|
||||
utils.refresh_instance_database(compute)
|
||||
instances = Instance.objects.filter(compute=compute).prefetch_related('userinstance_set')
|
||||
|
||||
return render(request, 'computes/instances.html', {'compute': compute, 'instances': instances})
|
||||
|
||||
|
||||
@superuser_only
|
||||
def compute_create(request, FormClass):
|
||||
form = FormClass(request.POST or None)
|
||||
|
|
|
@ -8,7 +8,6 @@ import json
|
|||
import guestfs
|
||||
import re
|
||||
|
||||
|
||||
PORT = 16510
|
||||
ADDRESS = "0.0.0.0"
|
||||
|
||||
|
@ -20,7 +19,8 @@ class MyTCPServer(socketserver.ThreadingTCPServer):
|
|||
class MyTCPServerHandler(socketserver.BaseRequestHandler):
|
||||
def handle(self):
|
||||
# recive data
|
||||
data = json.loads(self.request.recv(1024).strip())
|
||||
d = self.request.recv(1024).strip()
|
||||
data = json.loads(d)
|
||||
|
||||
# GuestFS
|
||||
gfs = guestfs.GuestFS(python_return_dict=True)
|
||||
|
@ -51,8 +51,8 @@ class MyTCPServerHandler(socketserver.BaseRequestHandler):
|
|||
pass
|
||||
gfs.shutdown()
|
||||
gfs.close()
|
||||
except RuntimeError as err:
|
||||
self.request.sendall(json.dumps({'return': 'error', 'message': err}))
|
||||
except Exception as err:
|
||||
self.request.sendall(bytes(json.dumps({'return': 'error', 'message': str(err)}).encode()))
|
||||
|
||||
|
||||
server = MyTCPServer((ADDRESS, PORT), MyTCPServerHandler)
|
||||
|
|
|
@ -26,11 +26,13 @@ def console(request):
|
|||
host = int(temptoken[0])
|
||||
uuid = temptoken[1]
|
||||
instance = Instance.objects.get(compute_id=host, uuid=uuid)
|
||||
conn = wvmInstance(instance.compute.hostname,
|
||||
conn = wvmInstance(
|
||||
instance.compute.hostname,
|
||||
instance.compute.login,
|
||||
instance.compute.password,
|
||||
instance.compute.type,
|
||||
instance.name)
|
||||
instance.name,
|
||||
)
|
||||
console_type = conn.get_console_type()
|
||||
console_websocket_port = conn.get_console_websocket_port()
|
||||
console_passwd = conn.get_console_passwd()
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
# Generated by Django 2.2.10 on 2020-01-28 07:01
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Flavor',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('label', models.CharField(max_length=12)),
|
||||
('memory', models.IntegerField()),
|
||||
('vcpu', models.IntegerField()),
|
||||
('disk', models.IntegerField()),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -1,33 +0,0 @@
|
|||
# Generated by Django 2.2.13 on 2020-06-15 06:37
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('create', '0002_addFlavors'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='flavor',
|
||||
name='disk',
|
||||
field=models.IntegerField(verbose_name='disk'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='flavor',
|
||||
name='label',
|
||||
field=models.CharField(max_length=12, verbose_name='label'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='flavor',
|
||||
name='memory',
|
||||
field=models.IntegerField(verbose_name='memory'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='flavor',
|
||||
name='vcpu',
|
||||
field=models.IntegerField(verbose_name='vcpu'),
|
||||
),
|
||||
]
|
|
@ -1,12 +0,0 @@
|
|||
from django.db.models import Model, CharField, IntegerField
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
class Flavor(Model):
|
||||
label = CharField(_('label'), max_length=12)
|
||||
memory = IntegerField(_('memory'))
|
||||
vcpu = IntegerField(_('vcpu'))
|
||||
disk = IntegerField(_('disk'))
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
|
@ -1,61 +0,0 @@
|
|||
{% load i18n %}
|
||||
{% if request.user.is_superuser %}
|
||||
<button href="#addFlavor" type="button" class="btn btn-success btn-header float-right" data-toggle="modal">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
</button>
|
||||
|
||||
<!-- Modal Flavor -->
|
||||
<div class="modal fade" id="addFlavor" tabindex="-1" role="dialog" aria-labelledby="addFlavorLabel"
|
||||
aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{% trans "Add New Flavor" %}</h4>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form method="post" role="form" aria-label="Flavor selection form">{% csrf_token %}
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">{% trans "Name" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="text" name="label" class="form-control" placeholder="{% trans "Micro" %}" maxlength="20"
|
||||
required pattern="[a-zA-Z0-9]+">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">{% trans "VCPU" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="text" class="form-control" name="vcpu" value="1" maxlength="1" required
|
||||
pattern="[0-9]">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">{% trans "RAM" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="text" class="form-control" name="memory" value="512" maxlength="5" required
|
||||
pattern="[0-9]+">
|
||||
</div>
|
||||
<label class="col-sm-1 col-form-label">{% trans "MB" %}</label>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">{% trans "HDD" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="text" class="form-control" name="disk" value="10" maxlength="3" required
|
||||
pattern="[0-9]+">
|
||||
</div>
|
||||
<label class="col-sm-1 col-form-label">{% trans "GB" %}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">
|
||||
{% trans "Close" %}
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary" name="create_flavor">
|
||||
{% trans "Add" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div> <!-- /.modal-content -->
|
||||
</div> <!-- /.modal-dialog -->
|
||||
</div> <!-- /.modal -->
|
||||
{% endif %}
|
|
@ -1,3 +0,0 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
304
create/views.py
304
create/views.py
|
@ -1,304 +0,0 @@
|
|||
from django.contrib import messages
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.urls import reverse
|
||||
from libvirt import libvirtError
|
||||
from admin.decorators import superuser_only
|
||||
from computes.models import Compute
|
||||
from create.models import Flavor
|
||||
from create.forms import FlavorAddForm, NewVMForm
|
||||
from appsettings.models import AppSettings
|
||||
from instances.models import Instance
|
||||
from vrtManager.create import wvmCreate
|
||||
from vrtManager import util
|
||||
from logs.views import addlogmsg
|
||||
from webvirtcloud.settings import QEMU_CONSOLE_LISTEN_ADDRESSES
|
||||
|
||||
|
||||
@superuser_only
|
||||
def create_instance_select_type(request, compute_id):
|
||||
"""
|
||||
:param request:
|
||||
:param compute_id:
|
||||
:return:
|
||||
"""
|
||||
conn = None
|
||||
error_messages = list()
|
||||
storages = list()
|
||||
networks = list()
|
||||
hypervisors = list()
|
||||
meta_prealloc = False
|
||||
compute = get_object_or_404(Compute, pk=compute_id)
|
||||
appsettings = AppSettings.objects.all()
|
||||
|
||||
try:
|
||||
conn = wvmCreate(compute.hostname, compute.login, compute.password, compute.type)
|
||||
instances = conn.get_instances()
|
||||
all_hypervisors = conn.get_hypervisors_machines()
|
||||
# Supported hypervisors by webvirtcloud: i686, x86_64(for now)
|
||||
supported_arch = ["x86_64", "i686", "aarch64", "armv7l", "ppc64", "ppc64le", "s390x"]
|
||||
hypervisors = [hpv for hpv in all_hypervisors.keys() if hpv in supported_arch]
|
||||
default_machine = appsettings.get(key="INSTANCE_MACHINE_DEFAULT_TYPE").value
|
||||
default_arch = appsettings.get(key="INSTANCE_ARCH_DEFAULT_TYPE").value
|
||||
|
||||
if request.method == 'POST':
|
||||
if 'create_xml' in request.POST:
|
||||
xml = request.POST.get('dom_xml', '')
|
||||
try:
|
||||
name = util.get_xml_path(xml, '/domain/name')
|
||||
except util.etree.Error as err:
|
||||
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('instances:instance', args=[compute_id, name]))
|
||||
except libvirtError as lib_err:
|
||||
error_messages.append(lib_err)
|
||||
|
||||
except libvirtError as lib_err:
|
||||
error_messages.append(lib_err)
|
||||
|
||||
return render(request, 'create_instance_w1.html', locals())
|
||||
|
||||
|
||||
@superuser_only
|
||||
def create_instance(request, compute_id, arch, machine):
|
||||
"""
|
||||
:param request:
|
||||
:param compute_id:
|
||||
:param arch:
|
||||
:param machine:
|
||||
:return:
|
||||
"""
|
||||
|
||||
conn = None
|
||||
error_messages = list()
|
||||
storages = list()
|
||||
networks = list()
|
||||
hypervisors = list()
|
||||
firmwares = list()
|
||||
meta_prealloc = False
|
||||
compute = get_object_or_404(Compute, pk=compute_id)
|
||||
flavors = Flavor.objects.filter().order_by('id')
|
||||
appsettings = AppSettings.objects.all()
|
||||
|
||||
try:
|
||||
conn = wvmCreate(compute.hostname, compute.login, compute.password, compute.type)
|
||||
|
||||
default_firmware = appsettings.get(key="INSTANCE_FIRMWARE_DEFAULT_TYPE").value
|
||||
default_cpu_mode = appsettings.get(key="INSTANCE_CPU_DEFAULT_MODE").value
|
||||
instances = conn.get_instances()
|
||||
videos = conn.get_video_models(arch, machine)
|
||||
cache_modes = sorted(conn.get_cache_modes().items())
|
||||
default_cache = appsettings.get(key="INSTANCE_VOLUME_DEFAULT_CACHE").value
|
||||
default_io = appsettings.get(key="INSTANCE_VOLUME_DEFAULT_IO").value
|
||||
default_zeroes = appsettings.get(key="INSTANCE_VOLUME_DEFAULT_DETECT_ZEROES").value
|
||||
default_discard = appsettings.get(key="INSTANCE_VOLUME_DEFAULT_DISCARD").value
|
||||
default_disk_format = appsettings.get(key="INSTANCE_VOLUME_DEFAULT_FORMAT").value
|
||||
default_disk_owner_uid = int(appsettings.get(key="INSTANCE_VOLUME_DEFAULT_OWNER_UID").value)
|
||||
default_disk_owner_gid = int(appsettings.get(key="INSTANCE_VOLUME_DEFAULT_OWNER_GID").value)
|
||||
default_scsi_disk_model = appsettings.get(key="INSTANCE_VOLUME_DEFAULT_SCSI_CONTROLLER").value
|
||||
listener_addr = QEMU_CONSOLE_LISTEN_ADDRESSES
|
||||
mac_auto = util.randomMAC()
|
||||
disk_devices = conn.get_disk_device_types(arch, machine)
|
||||
disk_buses = conn.get_disk_bus_types(arch, machine)
|
||||
default_bus = appsettings.get(key="INSTANCE_VOLUME_DEFAULT_BUS").value
|
||||
networks = sorted(conn.get_networks())
|
||||
nwfilters = conn.get_nwfilters()
|
||||
storages = sorted(conn.get_storages(only_actives=True))
|
||||
default_graphics = appsettings.get(key="QEMU_CONSOLE_DEFAULT_TYPE").value
|
||||
|
||||
dom_caps = conn.get_dom_capabilities(arch, machine)
|
||||
caps = conn.get_capabilities(arch)
|
||||
|
||||
virtio_support = conn.is_supports_virtio(arch, machine)
|
||||
hv_supports_uefi = conn.supports_uefi_xml(dom_caps["loader_enums"])
|
||||
# Add BIOS
|
||||
label = conn.label_for_firmware_path(arch, None)
|
||||
if label: firmwares.append(label)
|
||||
# Add UEFI
|
||||
loader_path = conn.find_uefi_path_for_arch(arch, dom_caps["loaders"])
|
||||
label = conn.label_for_firmware_path(arch, loader_path)
|
||||
if label: firmwares.append(label)
|
||||
firmwares = list(set(firmwares))
|
||||
|
||||
except libvirtError as lib_err:
|
||||
error_messages.append(lib_err)
|
||||
|
||||
if conn:
|
||||
if not storages:
|
||||
msg = _("You haven't defined any storage pools")
|
||||
error_messages.append(msg)
|
||||
if not networks:
|
||||
msg = _("You haven't defined 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' in request.POST:
|
||||
firmware = dict()
|
||||
volume_list = list()
|
||||
is_disk_created = False
|
||||
clone_path = ""
|
||||
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 Instance.objects.filter(name__exact=data['name']):
|
||||
messages.warning(request, _("There is an instance with same name. Are you sure?"))
|
||||
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'],
|
||||
default_disk_format,
|
||||
meta_prealloc,
|
||||
default_disk_owner_uid,
|
||||
default_disk_owner_gid,
|
||||
)
|
||||
volume = dict()
|
||||
volume['device'] = 'disk'
|
||||
volume['path'] = path
|
||||
volume['type'] = conn.get_volume_type(path)
|
||||
volume['cache_mode'] = data['cache_mode']
|
||||
volume['bus'] = default_bus
|
||||
if volume['bus'] == 'scsi':
|
||||
volume['scsi_model'] = default_scsi_disk_model
|
||||
volume['discard_mode'] = default_discard
|
||||
volume['detect_zeroes_mode'] = default_zeroes
|
||||
volume['io_mode'] = default_io
|
||||
|
||||
volume_list.append(volume)
|
||||
is_disk_created = True
|
||||
|
||||
except libvirtError as lib_err:
|
||||
error_messages.append(lib_err)
|
||||
elif data['template']:
|
||||
templ_path = conn.get_volume_path(data['template'])
|
||||
dest_vol = conn.get_volume_path(data["name"] + ".img", data['storage'])
|
||||
if dest_vol:
|
||||
error_msg = _("Image has already exist. Please check volumes or change instance name")
|
||||
error_messages.append(error_msg)
|
||||
else:
|
||||
clone_path = conn.clone_from_template(
|
||||
data['name'],
|
||||
templ_path,
|
||||
data['storage'],
|
||||
meta_prealloc,
|
||||
default_disk_owner_uid,
|
||||
default_disk_owner_gid,
|
||||
)
|
||||
volume = dict()
|
||||
volume['path'] = clone_path
|
||||
volume['type'] = conn.get_volume_type(clone_path)
|
||||
volume['device'] = 'disk'
|
||||
volume['cache_mode'] = data['cache_mode']
|
||||
volume['bus'] = default_bus
|
||||
if volume['bus'] == 'scsi':
|
||||
volume['scsi_model'] = default_scsi_disk_model
|
||||
volume['discard_mode'] = default_discard
|
||||
volume['detect_zeroes_mode'] = default_zeroes
|
||||
volume['io_mode'] = default_io
|
||||
|
||||
volume_list.append(volume)
|
||||
is_disk_created = True
|
||||
else:
|
||||
if not data['images']:
|
||||
error_msg = _("First you need to create or select an image")
|
||||
error_messages.append(error_msg)
|
||||
else:
|
||||
for idx, vol in enumerate(data['images'].split(',')):
|
||||
try:
|
||||
path = conn.get_volume_path(vol)
|
||||
volume = dict()
|
||||
volume['path'] = path
|
||||
volume['type'] = conn.get_volume_type(path)
|
||||
volume['device'] = request.POST.get('device' + str(idx), '')
|
||||
volume['bus'] = request.POST.get('bus' + str(idx), '')
|
||||
if volume['bus'] == 'scsi':
|
||||
volume['scsi_model'] = default_scsi_disk_model
|
||||
volume['cache_mode'] = data['cache_mode']
|
||||
volume['discard_mode'] = default_discard
|
||||
volume['detect_zeroes_mode'] = default_zeroes
|
||||
volume['io_mode'] = default_io
|
||||
|
||||
volume_list.append(volume)
|
||||
except libvirtError as lib_err:
|
||||
error_messages.append(lib_err)
|
||||
if data['cache_mode'] not in conn.get_cache_modes():
|
||||
error_msg = _("Invalid cache mode")
|
||||
error_messages.append(error_msg)
|
||||
|
||||
if 'UEFI' in data["firmware"]:
|
||||
firmware["loader"] = data["firmware"].split(":")[1].strip()
|
||||
firmware["secure"] = 'no'
|
||||
firmware["readonly"] = 'yes'
|
||||
firmware["type"] = 'pflash'
|
||||
if 'secboot' in firmware["loader"] and machine != 'q35':
|
||||
messages.warning(
|
||||
request, "Changing machine type from '%s' to 'q35' "
|
||||
"which is required for UEFI secure boot." % machine)
|
||||
machine = 'q35'
|
||||
firmware["secure"] = 'yes'
|
||||
|
||||
if not error_messages:
|
||||
uuid = util.randomUUID()
|
||||
try:
|
||||
conn.create_instance(name=data['name'],
|
||||
memory=data['memory'],
|
||||
vcpu=data['vcpu'],
|
||||
vcpu_mode=data['vcpu_mode'],
|
||||
uuid=uuid,
|
||||
arch=arch,
|
||||
machine=machine,
|
||||
firmware=firmware,
|
||||
volumes=volume_list,
|
||||
networks=data['networks'],
|
||||
virtio=data['virtio'],
|
||||
listen_addr=data["listener_addr"],
|
||||
nwfilter=data["nwfilter"],
|
||||
graphics=data["graphics"],
|
||||
video=data["video"],
|
||||
console_pass=data["console_pass"],
|
||||
mac=data['mac'],
|
||||
qemu_ga=data['qemu_ga'])
|
||||
create_instance = Instance(compute_id=compute_id, name=data['name'], uuid=uuid)
|
||||
create_instance.save()
|
||||
msg = _("Instance is created")
|
||||
messages.success(request, msg)
|
||||
addlogmsg(request.user.username, create_instance.name, msg)
|
||||
return HttpResponseRedirect(reverse('instances:instance', args=[compute_id, data['name']]))
|
||||
except libvirtError as lib_err:
|
||||
if data['hdd_size'] or len(volume_list) > 0:
|
||||
if is_disk_created:
|
||||
for vol in volume_list:
|
||||
conn.delete_volume(vol['path'])
|
||||
error_messages.append(lib_err)
|
||||
conn.close()
|
||||
return render(request, 'create_instance_w2.html', locals())
|
|
@ -1,38 +1,40 @@
|
|||
import re
|
||||
|
||||
from django import forms
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from create.models import Flavor
|
||||
from webvirtcloud.settings import QEMU_CONSOLE_LISTEN_ADDRESSES
|
||||
|
||||
from appsettings.models import AppSettings
|
||||
from webvirtcloud.settings import QEMU_CONSOLE_LISTEN_ADDRESSES, QEMU_KEYMAPS
|
||||
|
||||
from .models import Flavor
|
||||
|
||||
|
||||
class FlavorAddForm(forms.Form):
|
||||
label = forms.CharField(label="Name",
|
||||
error_messages={'required': _('No flavor name has been entered')},
|
||||
max_length=64)
|
||||
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')}, )
|
||||
class FlavorForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Flavor
|
||||
fields = '__all__'
|
||||
|
||||
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) > 64:
|
||||
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 ConsoleForm(forms.Form):
|
||||
type = forms.ChoiceField()
|
||||
listen_on = forms.ChoiceField()
|
||||
generate_password = forms.BooleanField(required=False)
|
||||
clear_password = forms.BooleanField(required=False)
|
||||
password = forms.CharField(widget=forms.PasswordInput(render_value=True), required=False)
|
||||
clear_keymap = forms.BooleanField(required=False)
|
||||
keymap = forms.ChoiceField(required=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ConsoleForm, self).__init__(*args, **kwargs)
|
||||
type_choices = ((c, c) for c in AppSettings.objects.get(key="QEMU_CONSOLE_DEFAULT_TYPE").choices_as_list())
|
||||
keymap_choices = [('auto', 'Auto')] + list((c, c) for c in QEMU_KEYMAPS)
|
||||
self.fields['type'] = forms.ChoiceField(choices=type_choices)
|
||||
self.fields['listen_on'] = forms.ChoiceField(choices=QEMU_CONSOLE_LISTEN_ADDRESSES)
|
||||
self.fields['keymap'] = forms.ChoiceField(choices=keymap_choices)
|
||||
|
||||
|
||||
class NewVMForm(forms.Form):
|
||||
name = forms.CharField(error_messages={'required': _('No Virtual Machine name has been entered')},
|
||||
max_length=64)
|
||||
name = forms.CharField(error_messages={'required': _('No Virtual Machine name has been entered')}, max_length=64)
|
||||
firmware = forms.CharField(max_length=50, required=False)
|
||||
vcpu = forms.IntegerField(error_messages={'required': _('No VCPU has been entered')})
|
||||
vcpu_mode = forms.CharField(max_length=20, required=False)
|
23
instances/migrations/0004_auto_20200618_0817.py
Normal file
23
instances/migrations/0004_auto_20200618_0817.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
# Generated by Django 2.2.13 on 2020-06-18 08:17
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('instances', '0003_auto_20200615_0637'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='instance',
|
||||
name='name',
|
||||
field=models.CharField(db_index=True, max_length=120, verbose_name='name'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='instance',
|
||||
name='uuid',
|
||||
field=models.CharField(db_index=True, max_length=36, verbose_name='uuid'),
|
||||
),
|
||||
]
|
23
instances/migrations/0005_flavor.py
Normal file
23
instances/migrations/0005_flavor.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
# Generated by Django 2.2.13 on 2020-06-23 12:12
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('instances', '0004_auto_20200618_0817'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Flavor',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('label', models.CharField(max_length=12, verbose_name='label')),
|
||||
('memory', models.IntegerField(verbose_name='memory')),
|
||||
('vcpu', models.IntegerField(verbose_name='vcpu')),
|
||||
('disk', models.IntegerField(verbose_name='disk')),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -3,8 +3,8 @@
|
|||
from django.db import migrations
|
||||
|
||||
|
||||
def add_favors(apps, schema_editor):
|
||||
Flavor = apps.get_model("create", "Flavor")
|
||||
def add_flavors(apps, schema_editor):
|
||||
Flavor = apps.get_model("instances", "Flavor")
|
||||
add_flavor = Flavor(label="micro", vcpu="1", memory="512", disk="20")
|
||||
add_flavor.save()
|
||||
add_flavor = Flavor(label="mini", vcpu="2", memory="1024", disk="30")
|
||||
|
@ -19,12 +19,17 @@ def add_favors(apps, schema_editor):
|
|||
add_flavor.save()
|
||||
|
||||
|
||||
def del_flavors(apps, schema_editor):
|
||||
Flavor = apps.get_model("instances", "Flavor")
|
||||
Flavor.objects.all().delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('create', '0001_initial'),
|
||||
('instances', '0005_flavor'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(add_favors),
|
||||
migrations.RunPython(add_flavors, del_flavors),
|
||||
]
|
18
instances/migrations/0007_auto_20200624_0821.py
Normal file
18
instances/migrations/0007_auto_20200624_0821.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 2.2.13 on 2020-06-24 08:21
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('instances', '0006_addFlavors'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='flavor',
|
||||
name='label',
|
||||
field=models.CharField(max_length=12, unique=True, verbose_name='label'),
|
||||
),
|
||||
]
|
18
instances/migrations/0008_auto_20200708_0950.py
Normal file
18
instances/migrations/0008_auto_20200708_0950.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 2.2.13 on 2020-07-08 09:50
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('instances', '0007_auto_20200624_0821'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='instance',
|
||||
name='created',
|
||||
field=models.DateTimeField(auto_now_add=True, verbose_name='created'),
|
||||
),
|
||||
]
|
|
@ -1,21 +1,211 @@
|
|||
from django.db.models import (CASCADE, BooleanField, CharField, DateField, ForeignKey, Model)
|
||||
from django.db import models
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from libvirt import VIR_DOMAIN_XML_SECURE
|
||||
|
||||
from computes.models import Compute
|
||||
from vrtManager.instance import wvmInstance
|
||||
|
||||
|
||||
class Instance(Model):
|
||||
compute = ForeignKey(Compute, on_delete=CASCADE)
|
||||
name = CharField(_('name'), max_length=120)
|
||||
uuid = CharField(_('uuid'), max_length=36)
|
||||
is_template = BooleanField(_('is template'), default=False)
|
||||
created = DateField(_('created'), auto_now_add=True)
|
||||
class Flavor(models.Model):
|
||||
label = models.CharField(_('label'), max_length=12, unique=True)
|
||||
memory = models.IntegerField(_('memory'))
|
||||
vcpu = models.IntegerField(_('vcpu'))
|
||||
disk = models.IntegerField(_('disk'))
|
||||
|
||||
def __str__(self):
|
||||
return self.label
|
||||
|
||||
|
||||
class InstanceManager(models.Manager):
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().select_related('compute')
|
||||
|
||||
|
||||
class Instance(models.Model):
|
||||
compute = models.ForeignKey(Compute, on_delete=models.CASCADE)
|
||||
name = models.CharField(_('name'), max_length=120, db_index=True)
|
||||
uuid = models.CharField(_('uuid'), max_length=36, db_index=True)
|
||||
is_template = models.BooleanField(_('is template'), default=False)
|
||||
created = models.DateTimeField(_('created'), auto_now_add=True)
|
||||
|
||||
objects = InstanceManager()
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.compute}/{self.name}'
|
||||
|
||||
@cached_property
|
||||
def proxy(self):
|
||||
return wvmInstance(
|
||||
self.compute.hostname,
|
||||
self.compute.login,
|
||||
self.compute.password,
|
||||
self.compute.type,
|
||||
self.name,
|
||||
)
|
||||
|
||||
class PermissionSet(Model):
|
||||
@cached_property
|
||||
def media(self):
|
||||
return self.proxy.get_media_devices()
|
||||
|
||||
@cached_property
|
||||
def media_iso(self):
|
||||
return sorted(self.proxy.get_iso_media())
|
||||
|
||||
@cached_property
|
||||
def disks(self):
|
||||
return self.proxy.get_disk_devices()
|
||||
|
||||
@cached_property
|
||||
def status(self):
|
||||
return self.proxy.get_status()
|
||||
|
||||
@cached_property
|
||||
def autostart(self):
|
||||
return self.proxy.get_autostart()
|
||||
|
||||
@cached_property
|
||||
def bootmenu(self):
|
||||
return self.proxy.get_bootmenu()
|
||||
|
||||
@cached_property
|
||||
def boot_order(self):
|
||||
return self.proxy.get_bootorder()
|
||||
|
||||
@cached_property
|
||||
def arch(self):
|
||||
return self.proxy.get_arch()
|
||||
|
||||
@cached_property
|
||||
def machine(self):
|
||||
return self.proxy.get_machine_type()
|
||||
|
||||
@cached_property
|
||||
def firmware(self):
|
||||
return self.proxy.get_loader()
|
||||
|
||||
@cached_property
|
||||
def nvram(self):
|
||||
return self.proxy.get_nvram()
|
||||
|
||||
@cached_property
|
||||
def vcpu(self):
|
||||
return self.proxy.get_vcpu()
|
||||
|
||||
@cached_property
|
||||
def vcpu_range(self):
|
||||
return self.proxy.get_max_cpus()
|
||||
|
||||
@cached_property
|
||||
def cur_vcpu(self):
|
||||
return self.proxy.get_cur_vcpu()
|
||||
|
||||
@cached_property
|
||||
def vcpus(self):
|
||||
return self.proxy.get_vcpus()
|
||||
|
||||
@cached_property
|
||||
def get_uuid(self):
|
||||
return self.proxy.get_uuid()
|
||||
|
||||
@cached_property
|
||||
def memory(self):
|
||||
return self.proxy.get_memory()
|
||||
|
||||
@cached_property
|
||||
def cur_memory(self):
|
||||
return self.proxy.get_cur_memory()
|
||||
|
||||
@cached_property
|
||||
def title(self):
|
||||
return self.proxy.get_title()
|
||||
|
||||
@cached_property
|
||||
def description(self):
|
||||
return self.proxy.get_description()
|
||||
|
||||
@cached_property
|
||||
def networks(self):
|
||||
return self.proxy.get_net_devices()
|
||||
|
||||
@cached_property
|
||||
def qos(self):
|
||||
return self.proxy.get_all_qos()
|
||||
|
||||
@cached_property
|
||||
def telnet_port(self):
|
||||
return self.proxy.get_telnet_port()
|
||||
|
||||
@cached_property
|
||||
def console_type(self):
|
||||
return self.proxy.get_console_type()
|
||||
|
||||
@cached_property
|
||||
def console_port(self):
|
||||
return self.proxy.get_console_port()
|
||||
|
||||
@cached_property
|
||||
def console_keymap(self):
|
||||
return self.proxy.get_console_keymap()
|
||||
|
||||
@cached_property
|
||||
def console_listen_address(self):
|
||||
return self.proxy.get_console_listen_addr()
|
||||
|
||||
@cached_property
|
||||
def guest_agent(self):
|
||||
return False if self.proxy.get_guest_agent() is None else True
|
||||
|
||||
@cached_property
|
||||
def guest_agent_ready(self):
|
||||
return self.proxy.is_agent_ready()
|
||||
|
||||
@cached_property
|
||||
def video_model(self):
|
||||
return self.proxy.get_video_model()
|
||||
|
||||
@cached_property
|
||||
def video_models(self):
|
||||
return self.proxy.get_video_models(self.arch, self.machine)
|
||||
|
||||
@cached_property
|
||||
def snapshots(self):
|
||||
return sorted(self.proxy.get_snapshot(), reverse=True, key=lambda k: k['date'])
|
||||
|
||||
@cached_property
|
||||
def inst_xml(self):
|
||||
return self.proxy._XMLDesc(VIR_DOMAIN_XML_SECURE)
|
||||
|
||||
@cached_property
|
||||
def has_managed_save_image(self):
|
||||
return self.proxy.get_managed_save_image()
|
||||
|
||||
@cached_property
|
||||
def console_passwd(self):
|
||||
return self.proxy.get_console_passwd()
|
||||
|
||||
@cached_property
|
||||
def cache_modes(self):
|
||||
return sorted(self.proxy.get_cache_modes().items())
|
||||
|
||||
@cached_property
|
||||
def io_modes(self):
|
||||
return sorted(self.proxy.get_io_modes().items())
|
||||
|
||||
@cached_property
|
||||
def discard_modes(self):
|
||||
return sorted(self.proxy.get_discard_modes().items())
|
||||
|
||||
@cached_property
|
||||
def detect_zeroes_modes(self):
|
||||
return sorted(self.proxy.get_detect_zeroes_modes().items())
|
||||
|
||||
@cached_property
|
||||
def formats(self):
|
||||
return self.proxy.get_image_formats()
|
||||
|
||||
|
||||
class PermissionSet(models.Model):
|
||||
"""
|
||||
Dummy model for holding set of permissions we need to be automatically added by Django
|
||||
"""
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form method="post" action="" role="form" aria-label="Add instance network form">{% csrf_token %}
|
||||
<form action="{% url 'instances:add_network' instance.id %}" method="post" action="" role="form" aria-label="Add instance network form">{% csrf_token %}
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-4 col-form-label">{% trans "MAC" %}</label>
|
||||
<div class="col-sm-6">
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form method="post" action="" role="form" aria-label="Add instance owner form">{% csrf_token %}
|
||||
<form id="add-owner-form" method="post" action="{% url 'instances:add_owner' instance.id %}" role="form" aria-label="Add instance owner form">{% csrf_token %}
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-4 col-form-label">{% trans "User" %}</label>
|
||||
<div class="col-sm-6">
|
||||
|
@ -24,12 +24,12 @@
|
|||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">{% trans "Close" %}</button>
|
||||
<button type="submit" class="btn btn-primary" name="add_owner">{% trans "Add" %}</button>
|
||||
<button type="submit" class="btn btn-primary" form="add-owner-form">{% trans "Add" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div> <!-- /.modal-content -->
|
||||
</div> <!-- /.modal-dialog -->
|
||||
</div> <!-- /.modal -->
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
</ul>
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active" id="NewDisk">
|
||||
<form method="post" role="form" aria-label="Add new volume to disk form">{% csrf_token %}
|
||||
<form action="{% url 'instances:add_new_vol' instance.id %}" method="post" role="form" aria-label="Add new volume to disk form">{% csrf_token %}
|
||||
<div class="modal-body">
|
||||
<p class="font-weight-bold">{% trans "Volume parameters" %}</p>
|
||||
<div class="form-group row">
|
||||
|
@ -46,7 +46,7 @@
|
|||
<label class="col-sm-3 col-form-label">{% trans "Format" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<select name="format" class="custom-select image-format">
|
||||
{% for format in formats %}
|
||||
{% for format in instance.formats %}
|
||||
<option value="{{ format }}" {% if format == default_format %}selected{% endif %}>{% trans format %}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
@ -73,7 +73,7 @@
|
|||
<label class="col-sm-3 col-form-label">{% trans "Cache" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<select name="cache" class="custom-select image-format">
|
||||
{% for mode, name in cache_modes %}
|
||||
{% for mode, name in instance.cache_modes %}
|
||||
<option value="{{ mode }}" {% if mode == default_cache %}selected{% endif %}>{% trans name %}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
@ -93,7 +93,7 @@
|
|||
</div>
|
||||
</div> <!-- /.modal-body -->
|
||||
<div class="tab-pane" id="ExistingDisk">
|
||||
<form method="post" role="form" aria-label="Add existing volume to instance form">{% csrf_token %}
|
||||
<form action="{% url 'instances:add_existing_vol' instance.id %}" method="post" role="form" aria-label="Add existing volume to instance form">{% csrf_token %}
|
||||
<div class="modal-body">
|
||||
<p class="font-weight-bold">{% trans "Volume parameters" %}</p>
|
||||
<div class="form-group row">
|
||||
|
@ -103,7 +103,7 @@
|
|||
<button id="select_storage" class="btn btn-secondary dropdown-toggle form-control" type="button" data-toggle="dropdown">{% trans 'Select Pool' %}...</button>
|
||||
<div class="dropdown-menu">
|
||||
{% for storage in storages_host %}
|
||||
<a class="dropdown-item" href="#" onclick="get_volumes({{ compute_id }}, '{{ storage }}')">{{ storage }}</a>
|
||||
<a class="dropdown-item" href="#" onclick="get_volumes({{ instance.compute.id }}, '{{ storage }}')">{{ storage }}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<input id="selected_storage" name="selected_storage" hidden/>
|
||||
|
@ -133,7 +133,7 @@
|
|||
<label class="col-sm-3 col-form-label">{% trans "Cache" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<select name="cache" class="custom-select image-format">
|
||||
{% for mode, name in cache_modes %}
|
||||
{% for mode, name in instance.cache_modes %}
|
||||
<option value="{{ mode }}" {% if mode == default_cache %}selected{% endif %}>{% trans name %}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
@ -142,7 +142,7 @@
|
|||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">{% trans "Close" %}</button>
|
||||
<button type="submit" class="btn btn-success" name="add_existing_vol">{% trans "Add Volume" %}</button>
|
||||
<button type="submit" class="btn btn-success">{% trans "Add Volume" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -1,92 +1,41 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
{% load icons %}
|
||||
{% load staticfiles %}
|
||||
{% block title %}{% trans "Instances" %}{% endblock %}
|
||||
{% block style %}
|
||||
<link rel="stylesheet" href="{% static "css/sortable-theme-bootstrap.css" %}" />
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<!-- Page Heading -->
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
{% block page_header %}{% trans "Instances" %}{% endblock page_header %}
|
||||
|
||||
{% block page_header_extra %}
|
||||
{% if request.user.is_superuser %}
|
||||
{% include 'create_inst_block.html' %}
|
||||
{% endif %}
|
||||
{% if all_host_vms or all_user_vms %}
|
||||
<div class="float-right search">
|
||||
<input id="filter" class="form-control" type="text" placeholder="{% trans 'Search' %}">
|
||||
</div>
|
||||
{% endblock page_header_extra %}
|
||||
|
||||
{% block content %}
|
||||
{% for compute in computes %}
|
||||
{% if compute.status is not True %}
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
{% trans 'Problem occurred with host' %} {{ compute.name }}: {{ compute.status }}
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
<h2 class="page-header">{% trans "Instances" %}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /.row -->
|
||||
|
||||
{% include 'errors_block.html' %}
|
||||
|
||||
{% endfor %}
|
||||
<div class="col-lg-12">
|
||||
{% if request.user.is_superuser %}
|
||||
{% if not all_host_vms %}
|
||||
<div class="col-lg-12">
|
||||
<div class="alert alert-warning alert-dismissable">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
|
||||
<i class="fa fa-exclamation-triangle"></i> <strong>{% trans "Warning" %}:</strong> {% trans "You don't have any Instance" %}
|
||||
</div>
|
||||
</div>
|
||||
{% if app_settings.VIEW_INSTANCES_LIST_STYLE == 'grouped' and request.user.is_superuser %}
|
||||
{% include 'allinstances_index_grouped.html' %}
|
||||
{% else %}
|
||||
{% if view_style == "nongrouped" %}
|
||||
{% include 'allinstances_index_nongrouped.html' %}
|
||||
{% endif %}
|
||||
{% if view_style == "grouped" %}
|
||||
{% include 'allinstances_index_grouped.html' %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if not all_user_vms %}
|
||||
<div class="col-lg-12">
|
||||
<div class="alert alert-warning alert-dismissable">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
|
||||
<i class="fa fa-exclamation-triangle"></i> <strong>{% trans "Warning" %}:</strong> {% trans "You don't have any Instance" %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<table class="table table-hover table-striped sortable-theme-bootstrap" data-sortable>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{% trans 'Name' %}</th>
|
||||
<th scope="col">{% trans 'Status' %}</th>
|
||||
<th scope="col">{% trans 'VCPU' %}</th>
|
||||
<th scope="col">{% trans 'Memory' %}</th>
|
||||
<th scope="col" data-sortable="false" style="width: 165px;">{% trans 'Actions' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="searchable">
|
||||
{% for inst, vm in all_user_vms.items %}
|
||||
<tr>
|
||||
<td><a href="{% url 'instances:instance' vm.compute_id vm.name %}">{{ vm.name }}</a><br><small><em>{{ vm.title }}</em></small></td>
|
||||
<td>{% if vm.status == 1 %}
|
||||
<span class="text-success">{% trans "Active" %}</span>
|
||||
{% endif %}
|
||||
{% if vm.status == 5 %}
|
||||
<span class="text-danger">{% trans "Off" %}</span>
|
||||
{% endif %}
|
||||
{% if vm.status == 3 %}
|
||||
<span class="text-warning">{% trans "Suspend" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ vm.vcpu }}</td>
|
||||
<td>{{ vm.memory }} {% trans "MB" %}</td>
|
||||
<td>
|
||||
{% include "instance_actions.html" %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endblock content %}
|
||||
{% block script %}
|
||||
<script src="{% static "js/sortable.min.js" %}"></script>
|
||||
<script>
|
||||
|
@ -94,38 +43,13 @@
|
|||
window.open("{% url 'console' %}?token=" + uuid, "", "width=850,height=485");
|
||||
}
|
||||
</script>
|
||||
<script>
|
||||
function filter_table() {
|
||||
var rex = new RegExp($(this).val(), 'i');
|
||||
$('.searchable tr').hide();
|
||||
$('.searchable tr').filter(function () {
|
||||
return rex.test($(this).text());
|
||||
}).show();
|
||||
Cookies.set("instances_filter", $(this).val(), { expires: 1 });
|
||||
}
|
||||
$(document).ready(function () {
|
||||
instances_filter_cookie = Cookies.get("instances_filter");
|
||||
if (instances_filter_cookie) {
|
||||
$('#filter').val(instances_filter_cookie);
|
||||
$('#filter').each(filter_table);
|
||||
}
|
||||
(function ($) {
|
||||
$('#filter').keyup(filter_table)
|
||||
}(jQuery));
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
function goto_instance_clone(compute, instance) {
|
||||
window.location = "/instances/" + compute + "/" + instance + "/#clone";
|
||||
}
|
||||
</script>
|
||||
<script src="{% static 'js/filter-table.js' %}"></script>
|
||||
{% if request.user.is_superuser %}
|
||||
<script>
|
||||
function goto_compute() {
|
||||
let compute = $("#compute_select").val();
|
||||
{#window.location.href = "{% url 'create_instance' 1 %}".replace(1, compute);#}
|
||||
window.location.href = "{% url 'create_instance_select_type' 1 %}".replace(1, compute);
|
||||
window.location.href = "{% url 'instances:create_instance_select_type' 1 %}".replace(1, compute);
|
||||
}
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% endblock script %}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{% load i18n %}
|
||||
{% load icons %}
|
||||
<table class="table table-hover sortable-theme-bootstrap" data-sortable>
|
||||
<thead>
|
||||
<tr style="border: 2px solid transparent; ">
|
||||
|
@ -12,61 +13,65 @@
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody class="searchable">
|
||||
{% for host, insts in all_host_vms.items %}
|
||||
{% for compute in computes %}
|
||||
{% if compute.status is True and compute.instance_set.count > 0 %}
|
||||
<tr class="font-weight-bold active" style="border-bottom: 2px solid darkgray;border-top: 2px solid darkgray;">
|
||||
<td>
|
||||
<span id="collapse_host_instances_{{ host.1 }}" class="fa fa-chevron-up" onclick="hide_host_instances('{{ host.1 }}');"></span>
|
||||
<span id="collapse_host_instances_{{ compute.id }}" class="fa fa-chevron-up" onclick="hide_host_instances('{{ compute.id }}');"></span>
|
||||
</td>
|
||||
<td>
|
||||
<a class="text-secondary" href="{% url 'overview' host.0 %}">{{ host.1 }}</a>
|
||||
<span id="inst_count_badge_{{ host.1 }}" class="badge badge-secondary d-none">{{ insts.items|length }}</span>
|
||||
<a class="text-secondary" href="{% url 'overview' compute.id %}">{{ compute.name }}</a>
|
||||
<span id="inst_count_badge_{{ compute.id }}" class="badge badge-secondary d-none">{{ compute.instance_set.count }}</span>
|
||||
</td>
|
||||
<td class="d-none d-sm-table-cell"></td>
|
||||
<td>
|
||||
{% if host.2 == 1 %}<span class="text-success">{% trans "Active" %}</span>{% endif %}
|
||||
{% if host.2 == 2 %}<span class="text-warning">{% trans "Not Active" %}</span>{% endif %}
|
||||
{% if host.2 == 3 %}<span class="text-danger">{% trans "Connection Failed" %}</span>{% endif %}
|
||||
<span class="text-success">{% trans "Connected" %}</span>
|
||||
</td>
|
||||
<td class="d-none d-sm-table-cell text-center">{{ host.3 }}</td>
|
||||
<td class="d-none d-sm-table-cell text-right">{{ host.4|filesizeformat }}</td>
|
||||
<td class="d-none d-sm-table-cell text-center">{{ compute.cpu_count }}</td>
|
||||
<td class="d-none d-sm-table-cell text-right">{{ compute.ram_size|filesizeformat }}</td>
|
||||
<td>
|
||||
<div class="progress">
|
||||
<div class="progress-bar bg-success" role="progressbar" style="width: {{ host.5 }}%"
|
||||
aria-valuenow="{{ host.5 }}" aria-valuemin="0" aria-valuemax="100">{{ host.5 }}%
|
||||
<div class="progress-bar bg-success" role="progressbar" style="width: {{ compute.ram_usage }}%"
|
||||
aria-valuenow="{{ compute.ram_usage }}" aria-valuemin="0" aria-valuemax="100">{{ compute.ram_usage }}%
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{% for inst, vm in insts.items %}
|
||||
<tr host="{{ host.1 }}">
|
||||
{% for instance in compute.instance_set.all %}
|
||||
<tr host="{{ compute.id }}">
|
||||
<td class="text-right">{{ forloop.counter }} </td>
|
||||
<td> 
|
||||
<a class="text-secondary" href="{% url 'instances:instance' host.0 inst %}">{{ inst }}</a><br>
|
||||
<small><em>{{ vm.title }}</em></small>
|
||||
</td>
|
||||
<td class="d-none d-sm-table-cell">
|
||||
<span class="font-small font-italic">
|
||||
{% if vm.userinstances.count > 0 %} {{ vm.userinstances.first_user.user.username }}
|
||||
{% if vm.userinstances.count > 1 %} (+{{ vm.userinstances.count|add:"-1" }}){% endif %}
|
||||
{% endif %}
|
||||
</span>
|
||||
<td>
|
||||
<a class="text-secondary" href="{% url 'instances:instance' instance.id %}">{{ instance.name }}</a><br>
|
||||
</td>
|
||||
<td>
|
||||
{% if vm.status == 1 %}<span class="text-success">{% trans "Active" %}</span>{% endif %}
|
||||
{% if vm.status == 5 %}<span class="text-danger">{% trans "Off" %}</span>{% endif %}
|
||||
{% if vm.status == 3 %}<span class="text-warning">{% trans "Suspend" %}</span>{% endif %}
|
||||
<em>
|
||||
{% if instance.userinstance_set.all.count > 0 %}
|
||||
{{ instance.userinstance_set.all.0.user }}
|
||||
{% if instance.userinstance_set.all.count > 1 %}
|
||||
(+{{ instance.userinstance_set.all.count|add:"-1" }})
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</em>
|
||||
</td>
|
||||
<td class="d-none d-sm-table-cell text-center">{{ vm.vcpu }}</td>
|
||||
<td class="d-none d-sm-table-cell text-right">{{ vm.memory |filesizeformat }}</td>
|
||||
<td>
|
||||
{% if instance.proxy.instance.info.0 == 1 %}<span class="text-success">{% trans "Active" %}</span>{% endif %}
|
||||
{% if instance.proxy.instance.info.0 == 5 %}<span class="text-danger">{% trans "Off" %}</span>{% endif %}
|
||||
{% if instance.proxy.instance.info.0 == 3 %}
|
||||
<span class="text-warning">{% trans "Suspended" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ instance.proxy.instance.info.3 }}</td>
|
||||
<td>{{ instance.cur_memory }} MB</td>
|
||||
<td class="text-nowrap">
|
||||
{% include 'instance_actions.html' %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% block script %}
|
||||
<script>
|
||||
function hide_all_host_instances() {
|
||||
|
|
|
@ -2,32 +2,54 @@
|
|||
<table class="table table-hover sortable-theme-bootstrap" data-sortable>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{% trans "Name" %}<br>{% trans "Description" %}</th>
|
||||
<th scope="col">{% trans "Host" %}<br>{% trans "User"%}</th>
|
||||
<th scope="col">{% trans "Status" %}</th>
|
||||
<th scope="col">{% trans "VCPU" %}</th>
|
||||
<th scope="col">{% trans "Memory" %}</th>
|
||||
<th scope="col" style="width:200px;" data-sortable="false">{% trans "Actions" %}</th>
|
||||
<th scope="col">{% trans 'Name' %}<br>{% trans 'Description' %}</th>
|
||||
{% if request.user.is_superuser %}
|
||||
<th scope="col">{% trans 'Host' %}<br>{% trans 'User' %}</th>
|
||||
{% endif %}
|
||||
<th scope="col">{% trans 'Status' %}</th>
|
||||
<th scope="col">{% trans 'VCPU' %}</th>
|
||||
<th scope="col">{% trans 'Memory' %}</th>
|
||||
<th scope="col" data-sortable="false">{% trans 'Actions' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="searchable">
|
||||
{% for host, inst in all_host_vms.items %}
|
||||
{% for inst, vm in inst.items %}
|
||||
{% for instance in instances %}
|
||||
{% if instance.compute.status is True %}
|
||||
<tr>
|
||||
<td><a href="{% url 'instances:instance' host.0 inst %}">{{ inst }}</a><br><small><em>{{ info.title }}</em></small></td>
|
||||
<td><a href="{% url 'overview' host.0 %}">{{ host.1 }}</a><br><small><em>{% if info.userinstances.count > 0 %}{{ info.userinstances.first_user.user.username }}{% if info.userinstances.count > 1 %} (+{{ info.userinstances.count|add:"-1" }}){% endif %}{% endif %}</em></small></td>
|
||||
<td>
|
||||
{% if vm.status == 1 %}<span class="text-success">{% trans "Active" %}</span>{% endif %}
|
||||
{% if vm.status == 5 %}<span class="text-danger">{% trans "Off" %}</span>{% endif %}
|
||||
{% if vm.status == 3 %}<span class="text-warning">{% trans "Suspend" %}</span>{% endif %}
|
||||
<a class="text-secondary" href="{% url 'instances:instance' instance.id %}">
|
||||
{{ instance.name }}
|
||||
</a><br>
|
||||
<small><em>{{ instance.title }}</em></small>
|
||||
</td>
|
||||
<td>{{ vm.vcpu }}</td>
|
||||
<td>{{ vm.memory|filesizeformat }}</td>
|
||||
{% if request.user.is_superuser %}
|
||||
<td>
|
||||
<a href="{% url 'overview' instance.compute.id %}">{{ instance.compute.name }}</a><br>
|
||||
<small><em>
|
||||
{% if instance.userinstance_set.all.count > 0 %}
|
||||
{{ instance.userinstance_set.all.0.user }}
|
||||
{% if instance.userinstance_set.all.count > 1 %}
|
||||
(+{{ instance.userinstance_set.all.count|add:"-1" }})
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</em></small>
|
||||
</td>
|
||||
{% endif %}
|
||||
<td>
|
||||
{% if instance.proxy.instance.info.0 == 1 %}<span
|
||||
class="text-success">{% trans "Active" %}</span>{% endif %}
|
||||
{% if instance.proxy.instance.info.0 == 5 %}<span
|
||||
class="text-danger">{% trans "Off" %}</span>{% endif %}
|
||||
{% if instance.proxy.instance.info.0 == 3 %}<span
|
||||
class="text-warning">{% trans "Suspended" %}</span>{% endif %}
|
||||
</td>
|
||||
<td>{{ instance.proxy.instance.info.3 }}</td>
|
||||
<td>{{ instance.cur_memory }} MB</td>
|
||||
<td class="text-nowrap">
|
||||
{% include "instance_actions.html" %}
|
||||
{% include 'instance_actions.html' %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
<a class="nav-link" href="{% url 'instances' compute.id %}"><i class="fa fa-desktop"></i> {% trans "Instances" %}</a>
|
||||
</li>
|
||||
<li class="nav-item active">
|
||||
<a class="nav-link" href="{% url 'instances:instance' compute.id vname %}"><i class="fa fa-hdd-o"></i> {{ vname }}</a>
|
||||
<a class="nav-link" href="{% url 'instances:instance' instance.id %}"><i class="fa fa-hdd-o"></i> {{ instance.name }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
34
instances/templates/create_flav_block.html
Normal file
34
instances/templates/create_flav_block.html
Normal file
|
@ -0,0 +1,34 @@
|
|||
{% load i18n %}
|
||||
{% load bootstrap4 %}
|
||||
{% if request.user.is_superuser %}
|
||||
<button href="#addFlavor" type="button" class="btn btn-success btn-header float-right" data-toggle="modal">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
</button>
|
||||
|
||||
<!-- Modal Flavor -->
|
||||
<div class="modal fade" id="addFlavor" tabindex="-1" role="dialog" aria-labelledby="addFlavorLabel"
|
||||
aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{% trans "Add New Flavor" %}</h4>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form method="post" role="form" action="{% url 'instances:flavor_create' %}" id="flavor-create-form">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form flavor_form layout='horizontal' %}
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">
|
||||
{% trans "Close" %}
|
||||
</button>
|
||||
<button form="flavor-create-form" type="submit" class="btn btn-primary">
|
||||
{% trans "Add" %}
|
||||
</button>
|
||||
</div>
|
||||
</div> <!-- /.modal-content -->
|
||||
</div> <!-- /.modal-dialog -->
|
||||
</div> <!-- /.modal -->
|
||||
{% endif %}
|
|
@ -1,5 +1,4 @@
|
|||
{% load i18n %}
|
||||
{% if request.user.is_superuser %}
|
||||
<a href="#AddInstance" type="button" class="btn btn-success btn-header float-right" data-toggle="modal">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
</a>
|
||||
|
@ -13,13 +12,14 @@
|
|||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form method="post" aria-label="Select compute for instance create form"> {% csrf_token %}
|
||||
<form method="post" aria-label="Select compute for instance create form">
|
||||
{% csrf_token %}
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-4 col-form-label">{% trans "Compute" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<select class="custom-select" id="compute_select">
|
||||
<option>{% trans "Please select" %}</option>
|
||||
{% for compute in computes_data %}
|
||||
{% for compute in computes %}
|
||||
<option {% if compute.status is not True %} class="font-italic text-muted" {% else %} value="{{ compute.id }}" {% endif %}>{{ compute.name }}</option>
|
||||
{% empty %}
|
||||
<option value="None">{% trans "None" %}</option>
|
||||
|
@ -46,4 +46,3 @@
|
|||
</div> <!-- /.modal-content -->
|
||||
</div> <!-- /.modal-dialog -->
|
||||
</div> <!-- /.modal -->
|
||||
{% endif %}
|
||||
|
|
|
@ -143,9 +143,9 @@
|
|||
let compute = '{{ compute.id }}';
|
||||
let arch = $("#select_archs").val();
|
||||
let machine = $("#select_chipset").val();
|
||||
create_machine_url = "/computes/" + compute + "/create/archs/" + arch + "/machines/" + machine;
|
||||
{#url = "{% url 'create_instance' compute.id 'x86_64' 'pc' %}".replace(/x86_64/, arch).replace(/pc/, machine);#}
|
||||
window.location.href = create_machine_url;
|
||||
//create_machine_url = "/computes/" + compute + "/create/archs/" + arch + "/machines/" + machine;
|
||||
url = "{% url 'instances:create_instance' compute.id 'x86_64' 'pc' %}".replace(/x86_64/, arch).replace(/pc/, machine);
|
||||
window.location.href = url;//create_machine_url;
|
||||
}
|
||||
|
||||
</script>
|
|
@ -1,6 +1,7 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
{% load staticfiles %}
|
||||
{% load icons %}
|
||||
{% block title %}{% trans "Create new instance" %}{% endblock %}
|
||||
{% block style %}
|
||||
<link href="{% static "css/bootstrap-multiselect.css" %}" rel="stylesheet">
|
||||
|
@ -74,7 +75,7 @@
|
|||
{% include 'create_flav_block.html' %}
|
||||
<h3 class="page-header">{% trans "Create from flavor" %}</h3>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-hover">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">#</th>
|
||||
|
@ -286,10 +287,10 @@
|
|||
</a>
|
||||
</td>
|
||||
<td style="width:5px;">
|
||||
<form action="" method="post" role="form" aria-label="Delete flavor form">{% csrf_token %}
|
||||
<input type="hidden" name="flavor" value="{{ flavor.id }}">
|
||||
<form action="{% url 'instances:flavor_delete' flavor.id %}" method="post" role="form" aria-label="Delete flavor form">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-sm btn-secondary" name="delete_flavor" onclick="return confirm('{% trans "Are you sure?" %}')">
|
||||
<span class="fa fa-trash"></span>
|
||||
{% icon 'trash' %}
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
|
@ -872,7 +873,7 @@
|
|||
<script>
|
||||
function goto_compute() {
|
||||
let compute = {{ compute.id }}
|
||||
window.location.href = "{% url 'create_instance_select_type' 1 %}".replace(1, compute);
|
||||
window.location.href = "{% url 'instances:create_instance_select_type' 1 %}".replace(1, compute);
|
||||
}
|
||||
</script>
|
||||
{% endif %}
|
|
@ -52,7 +52,7 @@
|
|||
<div class="form-group row">
|
||||
<label class="col-sm-4 col-form-label">{% trans 'Bus' %}</label>
|
||||
<div class="col-sm-8">
|
||||
<select class="custom-select" name="vol_bus" {% if status != 5 %} disabled {% endif %}>
|
||||
<select class="custom-select" name="vol_bus" {% if instance.status != 5 %} disabled {% endif %}>
|
||||
{% for bus in bus_host %}
|
||||
<option value="{{ bus }}" {% if bus == disk.bus %}selected{% endif %}>{{ bus }}</option>
|
||||
{% endfor %}
|
||||
|
@ -78,7 +78,7 @@
|
|||
<label class="col-sm-4 col-form-label">{% trans 'Cache mode' %}</label>
|
||||
<div class="col-sm-8">
|
||||
<select class="custom-select" name="vol_cache">
|
||||
{% for key, val in cache_modes %}
|
||||
{% for key, val in instance.cache_modes %}
|
||||
<option value="{{ key }}" {% if key == disk.cache %}selected{% endif %}>{{ val }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
@ -88,7 +88,7 @@
|
|||
<label class="col-sm-4 col-form-label">{% trans 'IO mode' %}</label>
|
||||
<div class="col-sm-8">
|
||||
<select class="custom-select" name="vol_io_mode">
|
||||
{% for key, val in io_modes %}
|
||||
{% for key, val in instance.io_modes %}
|
||||
<option value="{{ key }}" {% if key == disk.io %}selected{% endif %}>{{ val }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
@ -98,7 +98,7 @@
|
|||
<label class="col-sm-4 col-form-label">{% trans 'Discard mode' %}</label>
|
||||
<div class="col-sm-8">
|
||||
<select class="custom-select" name="vol_discard_mode">
|
||||
{% for key, val in discard_modes %}
|
||||
{% for key, val in instance.discard_modes %}
|
||||
<option value="{{ key }}" {% if key == disk.discard %}selected{% endif %}>{{ val }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
@ -108,7 +108,7 @@
|
|||
<label class="col-sm-4 col-form-label">{% trans 'Detect zeroes' %}</label>
|
||||
<div class="col-sm-8">
|
||||
<select class="custom-select" name="vol_detect_zeroes">
|
||||
{% for key, val in detect_zeroes_modes %}
|
||||
{% for key, val in instance.detect_zeroes_modes %}
|
||||
<option value="{{ key }}" {% if key == disk.detect_zeroes %}selected{% endif %}>{{ val }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,61 +1,44 @@
|
|||
{% load i18n %}
|
||||
{% load icons %}
|
||||
<form action="" method="post" role="form" aria-label="Shortcut instance action form">{% csrf_token %}
|
||||
<input type="hidden" name="name" value="{{ inst }}"/>
|
||||
<input type="hidden" name="compute_id" value="{{ host.0 }}"/>
|
||||
{% if vm.status == 5 %}
|
||||
{% if vm.is_template %}
|
||||
<button class="btn btn-sm btn-secondary" type="button" name="clone" title="{% trans "Clone" %}" onclick="goto_instance_clone({{ host.0 }}, '{{ inst }}');">
|
||||
<span class="fa fa-clone"></span>
|
||||
</button>
|
||||
{% if instance.proxy.instance.info.0 == 5 %}
|
||||
{% if instance.is_template %}
|
||||
<a href="{% url 'instances:instance' instance.id %}#clone" class="btn btn-sm btn-secondary" title="{% trans "Clone" %}">
|
||||
{% icon 'clone' %}
|
||||
</a>
|
||||
{% else %}
|
||||
<button class="btn btn-sm btn-secondary" type="submit" name="poweron" title="{% trans "Power On" %}">
|
||||
<span class="fa fa-play"></span>
|
||||
</button>
|
||||
<a class="btn btn-sm btn-secondary" href="{% url 'instances:poweron' instance.id %}" title="{% trans "Power On" %}">
|
||||
{% icon 'play' %}
|
||||
</a>
|
||||
{% endif %}
|
||||
<button class="btn btn-sm btn-secondary disabled" title="{% trans "Suspend" %}" disabled>
|
||||
<span class="fa fa-pause"></span>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-secondary disabled" title="{% trans "Power Off" %}" disabled>
|
||||
<span class="fa fa-power-off"></span>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-secondary disabled" title="{% trans "Power Cycle" %}" disabled>
|
||||
<span class="fa fa-refresh"></span>
|
||||
</button>
|
||||
<a class="btn btn-sm btn-secondary disabled" title="{% trans "Suspend" %}">{% icon 'pause' %}</a>
|
||||
<a class="btn btn-sm btn-secondary disabled" title="{% trans "Power Off" %}">{% icon 'power-off' %}</a>
|
||||
<a class="btn btn-sm btn-secondary disabled" title="{% trans "Power Cycle" %}">{% icon 'refresh' %}</a>
|
||||
<button class="btn btn-sm btn-secondary disabled" title="{% trans "VNC Console" %}" disabled>
|
||||
<span class="fa fa-eye"></span>
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if vm.status == 3 %}
|
||||
<button class="btn btn-sm btn-secondary" type="submit" name="resume" title="{% trans "Resume" %}">
|
||||
<span class="fa fa-play"></span>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-secondary disabled" title="{% trans "Suspend" %}" disabled>
|
||||
<span class="fa fa-pause"></span>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-secondary" type="submit" name="powerforce" title="{% trans "Force Off" %}" onclick="return confirm('Are you sure to force it down?')">
|
||||
<span class="fa fa-power-off"></span>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-secondary disabled" title="{% trans "Power Cycle" %}" disabled>
|
||||
<span class="fa fa-refresh"></span>
|
||||
</button>
|
||||
{% if instance.proxy.instance.info.0 == 3 %}
|
||||
<a class="btn btn-sm btn-secondary" href="{% url 'instances:resume' instance.id %}" title="{% trans "Resume" %}">
|
||||
{% icon 'play' %}
|
||||
</a>
|
||||
<a class="btn btn-sm btn-secondary disabled" title="{% trans "Suspend" %}">{% icon 'pause' %}</a>
|
||||
<a class="btn btn-sm btn-secondary" href="{% url 'instances:force_off' instance.id %}" title="{% trans "Force Off" %}">
|
||||
{% icon 'power-off' %}
|
||||
</a>
|
||||
<a class="btn btn-sm btn-secondary disabled" title="{% trans "Power Cycle" %}">{% icon 'refresh' %}</a>
|
||||
<button class="btn btn-sm btn-secondary disabled" title="{% trans "VNC Console" %}" disabled>
|
||||
<span class="fa fa-eye"></span>
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if vm.status == 1 %}
|
||||
<button class="btn btn-sm btn-secondary disabled" title="{% trans "Power On" %}" disabled>
|
||||
<span class="fa fa-play"></span>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-secondary" type="submit" name="suspend" title="{% trans "Suspend" %}">
|
||||
<span class="fa fa-pause"></span>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-secondary" type="submit" name="poweroff" title="{% trans "Power Off" %}" onclick="return confirm('Are you sure?')">
|
||||
<span class="fa fa-power-off"></span>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-secondary" type="submit" name="powercycle" title="{% trans "Power Cycle" %}" onclick="return confirm('Are you sure?')">
|
||||
<span class="fa fa-refresh"></span>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-secondary" type="button" onclick='open_console("{{ host.0 }}-{{ vm.uuid }}")' title="{% trans "Console" %}">
|
||||
{% if instance.proxy.instance.info.0 == 1 %}
|
||||
<a class="btn btn-sm btn-secondary disabled" title="{% trans "Power On" %}">{% icon 'play' %}</a>
|
||||
<a class="btn btn-sm btn-secondary" href="{% url 'instances:suspend' instance.id %}"
|
||||
title="{% trans "Suspend" %}">{% icon 'pause' %}</a>
|
||||
<a class="btn btn-sm btn-secondary" href="{% url 'instances:poweroff' instance.id %}">{% icon 'power-off' %}</a>
|
||||
<a class="btn btn-sm btn-secondary" href="{% url 'instances:powercycle' instance.id %}">{% icon 'refresh' %}</a>
|
||||
<button class="btn btn-sm btn-secondary" type="button" onclick='open_console("{{ instance.compute.id }}-{{ instance.get_uuid }}")'
|
||||
title="{% trans "Console" %}">
|
||||
<span class="fa fa-eye"></span>
|
||||
</button>
|
||||
{% endif %}
|
||||
|
|
|
@ -1,135 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
{% load staticfiles %}
|
||||
{% block title %}{% trans "Instances" %} - {{ compute.name }}{% endblock %}
|
||||
{% block style %}
|
||||
<link rel="stylesheet" href="{% static "css/sortable-theme-bootstrap.css" %}" />
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<!-- Page Heading -->
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
{% if request.user.is_superuser %}
|
||||
<a href="{% url 'create_instance_select_type' compute.id %}" type="button" class="btn btn-success btn-header float-right">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if all_host_vms or all_user_vms %}
|
||||
<div class="float-right search">
|
||||
<input id="filter" class="form-control" type="text" placeholder="{% trans 'Search' %}">
|
||||
</div>
|
||||
{% endif %}
|
||||
<h2 class="page-header">{{ compute.name }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb bg-light shadow-sm">
|
||||
<li class="breadcrumb-item active">
|
||||
<a href="{% url 'overview' compute.id %}"><i class="fa fa-dashboard"></i> {% trans "Overview" %}</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item">
|
||||
<span class="font-weight-bold"><i class="fa fa-server"></i> {% trans "Instances" %}</span>
|
||||
</li>
|
||||
<li class="breadcrumb-item">
|
||||
<a href="{% url 'storages' compute.id %}"><i class="fa fa-hdd-o"></i> {% trans "Storages" %}</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item">
|
||||
<a href="{% url 'networks' compute.id %}"><i class="fa fa-sitemap"></i> {% trans "Networks" %}</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item">
|
||||
<a href="{% url 'interfaces' compute.id %}"><i class="fa fa-wifi"></i> {% trans "Interfaces" %}</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item">
|
||||
<a href="{% url 'nwfilters' compute.id %}"><i class="fa fa-filter"></i> {% trans "NWFilters" %}</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item">
|
||||
<a href="{% url 'secrets' compute.id %}"><i class="fa fa-key"></i> {% trans "Secrets" %}</a>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /.row -->
|
||||
{% include 'errors_block.html' %}
|
||||
{% include 'messages_block.html' %}
|
||||
<div class="row">
|
||||
{% if not all_host_vms %}
|
||||
<div class="col-lg-12">
|
||||
<div class="alert alert-warning alert-dismissable fade show">
|
||||
<i class="fa fa-exclamation-triangle"></i> <strong>{% trans "Warning" %}:</strong> {% trans "Hypervisor doesn't have any Instances" %}
|
||||
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="col-lg-12">
|
||||
<table class="table table-hover sortable-theme-bootstrap" data-sortable>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{% trans 'Name' %}<br>{% trans 'Description' %}</th>
|
||||
<th scope="col" class="d-none d-md-table-cell">{% trans 'User' %}</th>
|
||||
<th scope="col">{% trans 'Status' %}</th>
|
||||
<th scope="col">{% trans 'VCPU' %}</th>
|
||||
<th scope="col">{% trans 'Memory' %}</th>
|
||||
<th scope="col" style="width:200px;" data-sortable="false">{% trans 'Actions' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="searchable">
|
||||
{% for host, insts in all_host_vms.items %}
|
||||
{% for inst, vm in insts.items %}
|
||||
<tr>
|
||||
<td><a class="text-secondary" href="{% url 'instances:instance' host.0 inst %}">{{ inst }}</a><br><small><em>{{ vm.title }}</em></small></td>
|
||||
<td class="d-none d-md-table-cell"><small><em>{% if vm.userinstances.count > 0 %}{{ vm.userinstances.first_user.user.username }}{% if vm.userinstances.count > 1 %} (+{{ vm.userinstances.count|add:"-1" }}){% endif %}{% endif %}</em></small></td>
|
||||
<td>
|
||||
{% if vm.status == 1 %}<span class="text-success">{% trans "Active" %}</span>{% endif %}
|
||||
{% if vm.status == 5 %}<span class="text-danger">{% trans "Off" %}</span>{% endif %}
|
||||
{% if vm.status == 3 %}<span class="text-warning">{% trans "Suspend" %}</span>{% endif %}
|
||||
</td>
|
||||
<td>{{ vm.vcpu }}</td>
|
||||
<td>{{ vm.memory|filesizeformat }}</td>
|
||||
<td class="text-nowrap">
|
||||
{% include 'instance_actions.html' %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block script %}
|
||||
<script src="{% static "js/sortable.min.js" %}"></script>
|
||||
<script>
|
||||
function open_console(uuid) {
|
||||
window.open("{% url 'console' %}?token=" + uuid, "", "width=850,height=685");
|
||||
}
|
||||
</script>
|
||||
<script>
|
||||
function filter_table() {
|
||||
var rex = new RegExp($(this).val(), 'i');
|
||||
$('.searchable tr').hide();
|
||||
$('.searchable tr').filter(function () {
|
||||
return rex.test($(this).text());
|
||||
}).show();
|
||||
Cookies.set("instances_filter", $(this).val(), { expires: 1 });
|
||||
}
|
||||
$(document).ready(function () {
|
||||
instances_filter_cookie = Cookies.get("instances_filter");
|
||||
if (instances_filter_cookie) {
|
||||
$('#filter').val(instances_filter_cookie);
|
||||
$('#filter').each(filter_table);
|
||||
}
|
||||
(function ($) {
|
||||
$('#filter').keyup(filter_table)
|
||||
}(jQuery));
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
function goto_instance_clone(compute, instance) {
|
||||
window.location = "/instances/" + compute + "/" + instance + "/#clone";
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
114
instances/templates/instances/access_tab.html
Normal file
114
instances/templates/instances/access_tab.html
Normal file
|
@ -0,0 +1,114 @@
|
|||
{% load i18n %}
|
||||
<div role="tabpanel" class="tab-pane" id="access" aria-label="Instance access options">
|
||||
<div role="tabpanel">
|
||||
<!-- Nav tabs -->
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-secondary active" href="#vnconsole" aria-controls="vnconsole" role="tab" data-toggle="tab">
|
||||
{% trans "Console" %}
|
||||
</a>
|
||||
</li>
|
||||
{% if app_settings.SHOW_ACCESS_ROOT_PASSWORD == 'True' %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-secondary" href="#rootpasswd" aria-controls="rootpasswd" role="tab" data-toggle="tab">
|
||||
{% trans "Root Password" %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if app_settings.SHOW_ACCESS_SSH_KEYS == 'True' %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-secondary" href="#sshkeys" aria-controls="sshkeys" role="tab" data-toggle="tab">
|
||||
{% trans "SSH Keys" %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if instance.status == 1 %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-secondary" href="#vdiconsole" aria-controls="vdiconsole" role="tab" data-toggle="tab">
|
||||
{% trans "VDI" %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
<!-- Tab panes -->
|
||||
<div class="tab-content">
|
||||
<div role="tabpanel" class="tab-pane tab-pane-bordered active" id="vnconsole">
|
||||
<p>{% trans "This action opens a new window with a VNC connection to the console of the instance." %}</p>
|
||||
{% if instance.status == 1 %}
|
||||
<!-- Split button -->
|
||||
<div class="btn-group float-right">
|
||||
<button type="button" id="consoleBtnGroup" class="btn btn-lg btn-success" onclick="open_console('lite')">{% trans 'Console' %}</button>
|
||||
<button type="button" class="btn btn-success dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="sr-only">{% trans 'Toggle Dropdown' %}</span>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item" href="#" title="{% trans "Console port" %}: {{ instance.console_port }}" onclick="open_console('lite')">{% trans "Console" %} - {% trans "Lite" %}</a>
|
||||
<a class="dropdown-item" href="#" title="{% trans "Console port" %}: {{ instance.console_port }}" onclick="open_console('full')">{% trans "Console" %} - {% trans "Full" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<button class="btn btn-lg btn-success float-right disabled">{% trans "Console" %}</button>
|
||||
{% endif %}
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
{% if app_settings.SHOW_ACCESS_SSH_KEYS == 'True' %}
|
||||
<div role="tabpanel" class="tab-pane tab-pane-bordered" id="rootpasswd">
|
||||
<p>{% trans "You need shut down your instance and enter a new root password." %}</p>
|
||||
<form action="{% url 'instances:rootpasswd' instance.id %}" class="form-inline" method="post" role="form" aria-label="Add root password to instance form">
|
||||
{% csrf_token %}
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-12">
|
||||
<input type="text" class="form-control-lg" name="passwd" placeholder="{% trans "Enter Password" %}" maxlength="24">
|
||||
</div>
|
||||
</div>
|
||||
{% if instance.status == 5 %}
|
||||
<input type="submit" class="btn btn-lg btn-success float-right" name="rootpasswd" value="{% trans "Reset Root Password" %}">
|
||||
{% else %}
|
||||
<button class="btn btn-lg btn-success float-right disabled">{% trans "Reset Root Password" %}</button>
|
||||
{% endif %}
|
||||
</form>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if app_settings.SHOW_ACCESS_SSH_KEYS == 'True' %}
|
||||
<div role="tabpanel" class="tab-pane tab-pane-bordered" id="sshkeys">
|
||||
<p>{% trans "You need shut down your instance and choose your public key." %}</p>
|
||||
<form action="{% url 'instances:add_public_key' instance.id %}" class="form-inline" method="post" role="form" aria-label="Add public key to instance form">
|
||||
{% csrf_token %}
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-12">
|
||||
<select name="sshkeyid" class="form-control-lg keyselect">
|
||||
{% if publickeys %}
|
||||
{% for key in publickeys %}
|
||||
<option value="{{ key.id }}">{{ key.keyname }}</option>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<option value="None">{% trans "None" %}</option>
|
||||
{% endif %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{% if instance.status == 5 %}
|
||||
<input type="submit" class="btn btn-lg btn-success float-right" name="addpublickey" value="{% trans "Add Public Key" %}">
|
||||
{% else %}
|
||||
<button class="btn btn-lg btn-success float-right disabled">{% trans "Add Public Key" %}</button>
|
||||
{% endif %}
|
||||
</form>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if instance.status == 1 %}
|
||||
<div role="tabpanel" class="tab-pane tab-pane-bordered" id="vdiconsole">
|
||||
<p>{% trans "This action opens a remote viewer with a connection to the console of the instance." %}</p>
|
||||
<div class="input-group">
|
||||
<input type="text" class="input-lg disabled form-control" disabled id="vdi_url_input"/>
|
||||
<span class="input-group-append">
|
||||
<a href="#" class="btn btn-success" id="vdi_url" >{% trans "VDI" %}</a>
|
||||
</span>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
49
instances/templates/instances/destroy_instance_form.html
Normal file
49
instances/templates/instances/destroy_instance_form.html
Normal file
|
@ -0,0 +1,49 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}
|
||||
{% trans "Confirm Destroy" %}
|
||||
{% endblock title %}
|
||||
|
||||
{% block page_header %}
|
||||
{% trans "Destroy instance" %} {{ instance }}
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block content %}
|
||||
{% if request.user.is_superuser or userinstance.is_delete %}
|
||||
{% if instance.status == 3 %}
|
||||
<div class="alert alert-danger">
|
||||
{% trans "Instance is suspended, cannot destroy!" %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-danger">
|
||||
{% trans "This action is irreversible!" %}
|
||||
</div>
|
||||
<form action="{% url 'instances:destroy' instance.id %}" class="form" method="post" role="form" id="delete_form">
|
||||
{% csrf_token %}
|
||||
<div class="ml-3 form-row">
|
||||
<div class="custom-control custom-switch">
|
||||
<input class="custom-control-input" type="checkbox" name="delete_disk" value="true" checked id="delete_disk">
|
||||
<label class="custom-control-label font-weight-bold" for="delete_disk">{% trans "Remove Instance's data" %}</label>
|
||||
</div>
|
||||
</div>
|
||||
{% if instance.nvram %}
|
||||
<div class="ml-3 form-row">
|
||||
<div class="custom-control custom-switch">
|
||||
<input class="custom-control-input" type="checkbox" name="delete_nvram" value="true" id="delete_nvram" checked>
|
||||
<label class="custom-control-label font-weight-bold" for="delete_nvram">
|
||||
{% trans "Remove Instance's NVRAM" %}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<button type="submit" class="btn btn-lg btn-success float-right" name="delete">
|
||||
{% trans "Destroy" %}
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="alert alert-danger">
|
||||
{% trans "You cannot destroy instance!" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock content %}
|
28
instances/templates/instances/destroy_tab.html
Normal file
28
instances/templates/instances/destroy_tab.html
Normal file
|
@ -0,0 +1,28 @@
|
|||
{% load i18n %}
|
||||
<div role="tabpanel" class="tab-pane" id="undefine">
|
||||
<div role="tabpanel">
|
||||
<!-- Nav tabs -->
|
||||
<ul class="nav nav-tabs" role="tablist" aria-label="Instance destroy menu">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="#destroy" aria-controls="destroy" role="tab" data-toggle="tab">
|
||||
{% trans "Destroy Instance" %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<!-- Tab panes -->
|
||||
<div class="tab-content">
|
||||
<div role="tabpanel" class="tab-pane tab-pane-bordered active" id="destroy">
|
||||
{% if request.user.is_superuser or userinstance.is_delete %}
|
||||
{% if instance.status == 3 %}
|
||||
<a class="btn btn-lg btn-success disabled float-right">{% trans "Destroy" %}</a>
|
||||
{% else %}
|
||||
<a href="{% url 'instances:destroy' instance.id %}" class="btn btn-lg btn-success float-right">{% trans "Destroy" %}</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<button class="btn btn-lg btn-success disabled float-right" name="delete">{% trans "Destroy" %}</button>
|
||||
{% endif %}
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
131
instances/templates/instances/power_tab.html
Normal file
131
instances/templates/instances/power_tab.html
Normal file
|
@ -0,0 +1,131 @@
|
|||
{% load i18n %}
|
||||
<div role="tabpanel" class="tab-pane active" id="power">
|
||||
<div role="tabpanel">
|
||||
<!-- Nav tabs -->
|
||||
<ul class="nav nav-tabs" role="tablist" aria-label="Instance power actions">
|
||||
{% if instance.status == 1 %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-secondary active" href="#poweroff" aria-controls="poweroff" role="tab" data-toggle="tab">
|
||||
{% trans "Power Off" %}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-secondary" href="#powercycle" aria-controls="powercycle" role="tab" data-toggle="tab">
|
||||
{% trans "Power Cycle" %}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-secondary" href="#powerforce" aria-controls="powerforce" role="tab" data-toggle="tab">
|
||||
{% trans "Force Off" %}
|
||||
</a>
|
||||
</li>
|
||||
{% if request.user.is_superuser %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-secondary" href="#suspend" aria-controls="suspend" role="tab" data-toggle="tab">
|
||||
{% trans "Suspend" %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if instance.status == 3 %}
|
||||
{% if request.user.is_superuser %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-secondary" href="#resume" aria-controls="resume" role="tab" data-toggle="tab">
|
||||
{% trans "Resume" %}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-secondary" href="#powerforce" aria-controls="powerforce" role="tab" data-toggle="tab">
|
||||
{% trans "Force Off" %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if instance.status == 5 %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-secondary active" href="#boot" aria-controls="boot" role="tab" data-toggle="tab">
|
||||
{% trans "Power On" %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
<!-- Tab panes -->
|
||||
<div class="tab-content">
|
||||
{% if instance.status == 1 %}
|
||||
<div role="tabpanel" class="tab-pane tab-pane-bordered active" id="poweroff">
|
||||
<p>{% trans "This action sends an ACPI shutdown signal to the instance." %}</p>
|
||||
<form action="{% url 'instances:poweroff' instance.id %}" method="post" role="form" aria-label0="Power off instance form">
|
||||
{% csrf_token %}
|
||||
<input type="submit" name="poweroff" class="btn btn-lg btn-success float-right" value="{% trans "Power Off" %}">
|
||||
<div class="clearfix"></div>
|
||||
</form>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane tab-pane-bordered" id="powercycle">
|
||||
<p>{% trans "This action forcibly powers off and start the instance and may cause data corruption." %}</p>
|
||||
<form action="{% url 'instances:powercycle' instance.id %}" method="post" role="form" aria-label="Power cycle instance form">{% csrf_token %}
|
||||
<input type="submit" name="powercycle" class="btn btn-lg btn-success float-right" value="{% trans "Power Cycle" %}">
|
||||
<div class="clearfix"></div>
|
||||
</form>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane tab-pane-bordered" id="powerforce">
|
||||
<p>{% trans "This action forcibly powers off the instance and may cause data corruption." %}</p>
|
||||
<form action="{% url 'instances:force_off' instance.id %}" method="post" role="form" aria-label="Force to shotdown instance form">
|
||||
{% csrf_token %}
|
||||
<input type="submit" name="powerforce" class="btn btn-lg btn-success float-right" value="{% trans "Force Off" %}">
|
||||
<div class="clearfix"></div>
|
||||
</form>
|
||||
</div>
|
||||
{% if request.user.is_superuser %}
|
||||
<div role="tabpanel" class="tab-pane tab-pane-bordered" id="suspend">
|
||||
<p>{% trans "This action suspends the instance." %}</p>
|
||||
<form action="{% url 'instances:suspend' instance.id %}" method="post" role="form" aria-label="Suspend instance form">{% csrf_token %}
|
||||
<input type="submit" name="suspend" class="btn btn-lg btn-success float-right" value="{% trans "Suspend" %}">
|
||||
<div class="clearfix"></div>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if instance.status == 3 %}
|
||||
{% if request.user.is_superuser %}
|
||||
<div role="tabpanel" class="tab-pane tab-pane-bordered active" id="resume">
|
||||
<p>{% trans "This action restore the instance after suspend." %}</p>
|
||||
<form action="{% url 'instances:resume' instance.id %}" method="post" role="form" aria-label="Resume instance from suspension form">{% csrf_token %}
|
||||
<input type="submit" name="resume" class="btn btn-lg btn-success float-right" value="{% trans "Resume" %}">
|
||||
<div class="clearfix"></div>
|
||||
</form>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane tab-pane-bordered" id="powerforce">
|
||||
<p>{% trans "This action forcibly powers off the instance and may cause data corruption." %}</p>
|
||||
<form action="{% url 'instances:force_off' instance.id %}" method="post" role="form" aria-label="Force to shutdown form">{% csrf_token %}
|
||||
<input type="submit" name="powerforce" class="btn btn-lg btn-success float-right" value="{% trans "Force Off" %}">
|
||||
<div class="clearfix"></div>
|
||||
</form>
|
||||
</div>
|
||||
{% else %}
|
||||
<div role="tabpanel" class="tab-pane tab-pane-bordered active" id="resume">
|
||||
<p>{% trans "Administrator blocked your instance." %}</p>
|
||||
<form action="{% url 'instances:resume' instance.id %}" method="post" role="form" aria-label="Resume instance form">{% csrf_token %}
|
||||
<button class="btn btn-lg btn-success disabled float-right">{% trans "Resume" %}</button>
|
||||
<div class="clearfix"></div>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if instance.status == 5 %}
|
||||
<div role="tabpanel" class="tab-pane tab-pane-bordered active" id="boot">
|
||||
<p>{% trans "Click on Power On button to start this instance." %}</p>
|
||||
<form action="{% url 'instances:poweron' instance.id %}" method="post" role="form" aria-label="Start instance form">
|
||||
{% csrf_token %}
|
||||
{% if instance.is_template %}
|
||||
<p>{% trans "Template instance cannot be started." %}</p>
|
||||
<input type="submit" name="poweron" class="btn btn-lg btn-success float-right disabled" value="{% trans "Power On" %}">
|
||||
{% else %}
|
||||
<input type="submit" name="poweron" class="btn btn-lg btn-success float-right" value="{% trans "Power On" %}">
|
||||
{% endif %}
|
||||
<div class="clearfix"></div>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
164
instances/templates/instances/resize_tab.html
Normal file
164
instances/templates/instances/resize_tab.html
Normal file
|
@ -0,0 +1,164 @@
|
|||
{% load i18n %}
|
||||
<div role="tabpanel" class="tab-pane" id="resize">
|
||||
<div role="tabpanel">
|
||||
<!-- Nav tabs -->
|
||||
<ul class="nav nav-tabs" role="tablist" aria-label="Instance resize options">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-secondary active" href="#resizevm_cpu" aria-controls="resizevm_cpu" role="tab" data-toggle="tab">
|
||||
{% trans "CPU" %}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-secondary" href="#resizevm_mem" aria-controls="resizevm_mem" role="tab" data-toggle="tab">
|
||||
{% trans "Memory" %}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-secondary" href="#resizevm_disk" aria-controls="resizevm_disk" role="tab" data-toggle="tab">
|
||||
{% trans "Disk" %}
|
||||
</a>
|
||||
</li>
|
||||
</li>
|
||||
</ul>
|
||||
<!-- Tab panes -->
|
||||
<div class="tab-content">
|
||||
<div role="tabpanel" class="tab-pane tab-pane-bordered active" id="resizevm_cpu">
|
||||
{% if request.user.is_superuser or request.user.is_staff or userinstance.is_change %}
|
||||
{% if instance.status == 5 or not instance.vcpus %}
|
||||
<form action="{% url 'instances:resizevm_cpu' instance.id %}" method="post" role="form" aria-label="Resize instance cpu form">{% csrf_token %}
|
||||
<p class="font-weight-bold">{% trans "Logical host CPUs" %} : {{ vcpu_host }}</p>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-4 col-form-label"> {% trans "Current Allocation" %}</label>
|
||||
<div class="col-sm-4">
|
||||
<select name="cur_vcpu" class="custom-select">
|
||||
{% for cpu in instance.vcpu_range %}
|
||||
{% if instance.cur_vcpu %}
|
||||
<option value="{{ cpu }}" {% if cpu == instance.cur_vcpu %}selected{% endif %}>{{ cpu }}</option>
|
||||
{% else %}
|
||||
<option value="{{ cpu }}" {% if cpu == instance.vcpu %}selected{% endif %}>{{ cpu }}</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-4 col-form-label">{% trans "Maximum Allocation" %}</label>
|
||||
<div class="col-sm-4">
|
||||
<select name="vcpu" class="custom-select">
|
||||
{% for cpu in instance.vcpu_range %}
|
||||
<option value="{{ cpu }}" {% if cpu == instance.vcpu %}selected{% endif %}>{{ cpu }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if instance.status == 5 %}
|
||||
<button type="submit" class="btn btn-lg btn-success float-right" name="resizevm_cpu">{% trans "Resize" %}</button>
|
||||
{% else %}
|
||||
<button class="btn btn-lg btn-success float-right disabled">{% trans "Resize" %}</button>
|
||||
{% endif %}
|
||||
</form>
|
||||
<div class="clearfix"></div>
|
||||
{% else %}
|
||||
<p class="font-weight-bold">{% trans "Logical Instance Active/Maximum CPUs" %} : {{ instance.cur_vcpu }} / {{ instance.vcpu }} </p>
|
||||
<div class="col-sm-3"></div>
|
||||
<div class="col-sm-6">
|
||||
{% for id, vcpu in instance.vcpus.items %}
|
||||
<form action="{% url 'instances:set_vcpu' instance.id %}" method="post" role="form" aria-label="Resize instance cpu form">{% csrf_token %}
|
||||
<div class="col-sm-3">
|
||||
<input name="id" value="{{ id }}" hidden/>
|
||||
{% if vcpu.enabled == 'yes' and vcpu.hotpluggable == "yes" %}
|
||||
<button type="submit" class="btn btn-block btn-success" value="False" name="set_vcpu" title="{% trans "Disable" %}">{{ id }}</button>
|
||||
{% elif vcpu.enabled == 'yes' and vcpu.hotpluggable == "no" %}
|
||||
<button type="button" class="btn btn btn-block btn-info" title="{% trans "Constant" %}">{{ id }}</button>
|
||||
{% else %}
|
||||
<button type="submit" class="btn btn btn-block btn-secondary" value="True" name="set_vcpu" title="{% trans "Enable" %}">{{ id }}</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="col-sm-3"></div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% trans "You don't have permission for resizing instance" %}
|
||||
<button class="btn btn-lg btn-success float-right disabled">{% trans "Resize" %}</button>
|
||||
{% endif %}
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane tab-pane-bordered" id="resizevm_mem">
|
||||
{% if request.user.is_superuser or request.user.is_staff or userinstance.is_change %}
|
||||
<form action="{% url 'instances:resize_memory' instance.id %}" method="post" role="form" aria-label="Resize instance memory form">
|
||||
{% csrf_token %}
|
||||
<p class="font-weight-bold">{% trans "Total host memory" %}: {{ memory_host|filesizeformat }}</p>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-4 col-form-label">{% trans "Current Allocation" %} ({% trans "MB" %})</label>
|
||||
<div class="col-sm-4 js-custom__container">
|
||||
<select name="cur_memory" class="custom-select js-custom__toggle">
|
||||
{% for mem in memory_range %}
|
||||
<option value="{{ mem }}" {% if mem == instance.cur_memory %}selected{% endif %}>{{ mem }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<input type="text" name="cur_memory_custom" class="custom-select js-custom__toggle" style="display: none" />
|
||||
<small><input type="checkbox" class="js-custom__checkbox" /> {% trans "Custom value" %}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-4 col-form-label">
|
||||
{% trans "Maximum Allocation" %} ({% trans "MB" %})
|
||||
</label>
|
||||
<div class="col-sm-4 js-custom__container">
|
||||
<select name="memory" class="form-control js-custom__toggle">
|
||||
{% for mem in memory_range %}
|
||||
<option value="{{ mem }}"
|
||||
{% if mem == instance.memory %}selected{% endif %}>{{ mem }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<input type="text" name="memory_custom" class="form-control js-custom__toggle" style="display: none" />
|
||||
<small><input type="checkbox" class="js-custom__checkbox" /> {% trans "Custom value" %}</small>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-lg btn-success float-right" name="resizevm_mem">{% trans "Resize" %}</button>
|
||||
</form>
|
||||
{% else %}
|
||||
{% trans "You don't have permission for resizing instance" %}
|
||||
<button class="btn btn-lg btn-success float-right disabled">{% trans "Resize" %}</button>
|
||||
{% endif %}
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane tab-pane-bordered" id="resizevm_disk">
|
||||
{% if request.user.is_superuser or request.user.is_staff or userinstance.is_change %}
|
||||
<form action="{% url 'instances:resize_disk' instance.id %}" method="post" role="form" aria-label="Resize instance disk form">
|
||||
{% csrf_token %}
|
||||
<p class="font-weight-bold">{% trans "Disk allocation (GB)" %}:</p>
|
||||
{% for disk in instance.disks %}
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-4 col-form-label">{% trans "Current Allocation" %} ({{ disk.dev }})</label>
|
||||
{% if disk.storage is None %}
|
||||
<div class="col-sm-4 js-custom__container">
|
||||
<div class="alert alert-danger">
|
||||
{% trans "Error getting disk info" %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="col-sm-4 js-custom__container">
|
||||
<input type="number" name="disk_size_{{ disk.dev }}" class="form-control" value="{% widthratio disk.size 1073741824 1 %}" />
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if instance.status == 5 %}
|
||||
<button type="submit" class="btn btn-lg btn-success float-right" name="resizevm_disk">{% trans "Resize" %}</button>
|
||||
{% else %}
|
||||
<button class="btn btn-lg btn-success float-right disabled">{% trans "Resize" %}</button>
|
||||
{% endif %}
|
||||
</form>
|
||||
{% else %}
|
||||
{% trans "You don't have permission for resizing instance" %}
|
||||
<button class="btn btn-lg btn-success float-right disabled">{% trans "Resize" %}</button>
|
||||
{% endif %}
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
904
instances/templates/instances/settings_tab.html
Normal file
904
instances/templates/instances/settings_tab.html
Normal file
|
@ -0,0 +1,904 @@
|
|||
{% load i18n %}
|
||||
{% load bootstrap4 %}
|
||||
{% load icons %}
|
||||
<div role="tabpanel" class="tab-pane" id="settings">
|
||||
<div role="tabpanel">
|
||||
<!-- Nav tabs -->
|
||||
<ul class="nav nav-tabs" role="tablist" aria-label="Instance settings">
|
||||
{% if request.user.is_superuser %}
|
||||
<li class="nav-item ">
|
||||
<a class="nav-link text-secondary active" href="#boot_opt" aria-controls="boot" role="tab" data-toggle="tab">
|
||||
{% trans "Boot" %}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-secondary" href="#disks" aria-controls="disks" role="tab" data-toggle="tab">
|
||||
{% trans "Disk" %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if request.user.is_superuser or request.user.is_staff or userinstance.is_vnc %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-secondary" href="#vncsettings" aria-controls="vncsettings" role="tab" data-toggle="tab">
|
||||
{% trans "Console" %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if request.user.is_superuser %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-secondary" href="#network" aria-controls="network" role="tab" data-toggle="tab">
|
||||
{% trans "Network" %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if perms.instances.clone_instances %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-secondary" href="#clone" aria-controls="clone" role="tab" data-toggle="tab">
|
||||
{% trans "Clone" %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if request.user.is_superuser %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-secondary" href="#migrate" aria-controls="migrate" role="tab" data-toggle="tab">
|
||||
{% trans "Migrate" %}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-secondary" href="#xmledit" aria-controls="xmledit" role="tab" data-toggle="tab">
|
||||
{% trans "XML" %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if perms.instances.clone_instances %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-secondary" href="#options" aria-controls="options" role="tab" data-toggle="tab">
|
||||
{% trans "Options" %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if request.user.is_superuser %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-secondary" href="#users" aria-controls="users" role="tab" data-toggle="tab">
|
||||
{% trans "Users" %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
<!-- Tab panes -->
|
||||
<div class="tab-content">
|
||||
{% if request.user.is_superuser %}
|
||||
<div role="tabpanel" class="tab-pane tab-pane-bordered active" id="boot_opt">
|
||||
<p class="font-weight-bold">{% trans 'Autostart' %}</p>
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-12 text-center">
|
||||
<p>{% trans "Autostart your instance when host server is power on " %}
|
||||
{% if instance.autostart == 0 %}
|
||||
<a class="btn btn-success" href="{% url 'instances:set_autostart' instance.id %}">{% trans "Enable" %}</a>
|
||||
{% else %}
|
||||
<a class="btn btn-danger" href="{% url 'instances:unset_autostart' instance.id %}">{% trans "Disable" %}</a>
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="font-weight-bold">{% trans 'Boot Order' %}</p>
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-12 text-center">
|
||||
{% if instance.status == 5 %}
|
||||
<p>{% trans "Enable Boot Menu for your instance when it starts up " %}
|
||||
{% if instance.bootmenu == 0 %}
|
||||
<form action="{% url 'instances:set_bootmenu' instance.id %}" method="post" role="form" aria-label="Enable/disable instance boot order form">{% csrf_token %}
|
||||
<input type="submit" class="btn btn-success" name="set_bootmenu" title="{% trans 'Show boot menu' %}" value="{% trans "Enable" %}">
|
||||
</form>
|
||||
{% else %}
|
||||
<form action="{% url 'instances:unset_bootmenu' instance.id %}" method="post" role="form" aria-label="Enable/disable instance boot order form">{% csrf_token %}
|
||||
<input type="submit" class="btn btn-danger" name="unset_bootmenu" title="{% trans 'Hide boot menu' %}" value="{% trans "Disable" %}">
|
||||
</form>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if instance.bootmenu == 0 %}
|
||||
<p>**** {% trans "Please shutdown instance to modify boot menu" %} ****</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if instance.bootmenu == 1 %}
|
||||
<div class="d-flex justify-content-center">
|
||||
<div class="col-sm-6 bg-light rounded shadow-sm">
|
||||
{% for idx, val in instance.boot_order.items %}
|
||||
<label>{{ idx|add:1 }}:{{ val.target }}, </label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<form action="{% url 'instances:set_bootorder' instance.id %}" method="post" role="form" aria-label="Boot order edit form">{% csrf_token %}
|
||||
<input id="bootorder" name="bootorder" hidden>
|
||||
<div class="d-flex justify-content-center">
|
||||
<div id="b_order" class="multipleselect border-0">
|
||||
{% for disk in instance.disks %}
|
||||
<label><input type="checkbox" name="disk:{{ disk.dev }}" value="disk:{{ disk.dev }}" onclick="set_orderlist($('#bootorder'))" />{{ disk.dev }} - {{ disk.image }}</label>
|
||||
{% endfor %}
|
||||
{% for cd in instance.media %}
|
||||
<label><input type="checkbox" name="cdrom:{{ cd.dev }}" value="cdrom:{{ cd.dev }}" onclick="set_orderlist($('#bootorder'))"/>{{ cd.dev }} - {{ cd.image }}</label>
|
||||
{% endfor %}
|
||||
{% for net in instance.networks %}
|
||||
<label><input type="checkbox" name="network:{{ net.mac }}" value="network:{{ net.mac }}" onclick="set_orderlist($('#bootorder'))"/>NIC - {{ net.mac|slice:"9:" }}</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div>
|
||||
<div class="row mt-4">
|
||||
<a href="#" id="boot_order_up" class="btn btn-light shadow-sm"><span class="fa fa-arrow-up" title="{% trans 'up: move selected devices' %}"></span></a>
|
||||
</div>
|
||||
<div class="row mt-2">
|
||||
<a href="#" id="boot_order_down" class="btn btn-light shadow-sm"><span class="fa fa-arrow-down" title="{% trans 'down: move selected devices' %}"></span></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex justify-content-center">
|
||||
<div class="col-sm-6">
|
||||
<input type="submit" class="btn btn-success btn-block" name="set_bootorder" value="{% trans "Apply" %}">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane tab-pane-bordered" id="disks">
|
||||
<form action="{% url 'instances:add_cdrom' instance.id %}" method="post" role="form" aria-label="Add CD-ROM form">{% csrf_token %}
|
||||
<p class="font-weight-bold">
|
||||
{% trans "Instance Media" %}
|
||||
<button class="btn btn-success float-right"
|
||||
type="submit" type="button"
|
||||
title="{% trans 'Add CD-ROM' %}"
|
||||
{% if instance.status != 5 %} disabled {% endif %}>
|
||||
<span class="fa fa-plus"></span>
|
||||
</button>
|
||||
</p>
|
||||
</form>
|
||||
<div class="clearfix"></div>
|
||||
{% for cd in instance.media %}
|
||||
<div class="row mt-2">
|
||||
<a class="col-sm-3 col-form-label"
|
||||
name="details"
|
||||
title="{% trans "Details" %}"
|
||||
tabindex="0"
|
||||
data-trigger="focus"
|
||||
data-toggle="popover"
|
||||
data-html="true"
|
||||
data-content="<strong>{% trans 'Bus' %}:</strong> {{ cd.bus }} <br/>
|
||||
<strong>{% trans 'Device' %}:</strong> {{ cd.dev }}">
|
||||
{% trans "CD-ROM" %} {{ forloop.counter }}
|
||||
</a>
|
||||
<div class="col-sm-9">
|
||||
{% if not cd.image %}
|
||||
<form action="{% url 'instances:mount_iso' instance.id %}" method="post">
|
||||
{% csrf_token %}
|
||||
<div class="input-group">
|
||||
<select name="media" class="form-control">
|
||||
{% if instance.media_iso %}
|
||||
{% for iso in instance.media_iso %}
|
||||
<option value="{{ iso }}">{{ iso }}</option>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<option value="none">{% trans "None" %}</option>
|
||||
{% endif %}
|
||||
</select>
|
||||
<div class="input-group-append">
|
||||
{% if instance.media_iso and allow_admin_or_not_template %}
|
||||
<button type="submit" class="btn btn-sm btn-success float-left" name="mount_iso" value="{{ cd.dev }}">{% trans "Mount" %}</button>
|
||||
{% else %}
|
||||
<button class="btn btn-sm btn-success float-left disabled">{% trans "Mount" %}</button>
|
||||
{% endif %}
|
||||
{% if instance.status == 5 and allow_admin_or_not_template %}
|
||||
<a href="{% url 'instances:detach_cdrom' instance.id cd.dev %}" class="btn btn-sm btn-danger float-right" title="{% trans "Detach CD-ROM (remove device)" %}">
|
||||
{% icon 'remove' %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
<form action="{% url 'instances:unmount_iso' instance.id %}" method="post">
|
||||
{% csrf_token %}
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" value="{{ cd.image }}" disabled/>
|
||||
<div class="input-group-append">
|
||||
<input type="hidden" name="path" value="{{ cd.path }}">
|
||||
{% if allow_admin_or_not_template %}
|
||||
<button type="submit" class="btn btn-sm btn-success float-left" value="{{ cd.dev }}" name="umount_iso">{% trans "Unmount" %}</button>
|
||||
{% else %}
|
||||
<button class="btn btn-sm btn-success float-left disabled" value="{{ cd.dev }}" name="umount_iso">{% trans "Unmount" %}</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="offset-3 col-sm-6">
|
||||
<div class="bg-light rounded shadow-sm">{% trans 'There is not any CD-ROM device.' %}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="clearfix"></div>
|
||||
<p class="font-weight-bold">
|
||||
{% trans "Instance Volume" %}
|
||||
{% include 'add_instance_volume.html' %}
|
||||
</p>
|
||||
|
||||
<div class="col-12 col-sm-12">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mt-3">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{% trans "Device" %}</th>
|
||||
<th scope="col">{% trans "Used" %}</th>
|
||||
<th scope="col">{% trans "Capacity" %}</th>
|
||||
<th scope="col">{% trans "Storage" %}</th>
|
||||
<th scope="col">{% trans "Source" %}</th>
|
||||
<th scope="col">{% trans "Action" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for disk in instance.disks %}
|
||||
<tr>
|
||||
<td>
|
||||
<button type="submit" class="btn btn-sm btn-secondary"
|
||||
name="details{{ forloop.counter0 }}"
|
||||
title="{% trans "Details" %}"
|
||||
tabindex="0"
|
||||
data-trigger="focus"
|
||||
data-toggle="popover"
|
||||
data-html="true"
|
||||
data-content="<strong>Bus:</strong> {{ disk.bus }} <br/>
|
||||
<strong>Format:</strong> {{ disk.format }} <br/>
|
||||
<strong>Cache:</strong> {{ disk.cache }} <br/>
|
||||
<strong>Serial:</strong> {{ disk.serial }} <br/>
|
||||
<strong>Readonly:</strong> {{ disk.readonly }} <br/>
|
||||
<strong>Shareable:</strong> {{ disk.shareable }}</br>
|
||||
<strong>IO Mode:</strong> {{ disk.io }} <br/>
|
||||
<strong>Discard:</strong> {{ disk.discard }} <br/>
|
||||
<strong>Detect Zeroes:</strong> {{ disk.detect_zeroes }}">
|
||||
<i class="fa fa-info"></i>
|
||||
</button>
|
||||
{{ disk.dev }}
|
||||
</td>
|
||||
{% if disk.storage is None %}
|
||||
<td colspan="4">
|
||||
<div class="alert alert-danger">
|
||||
{% trans "Error getting disk info" %}
|
||||
</div>
|
||||
</td>
|
||||
{% else %}
|
||||
<td>{{ disk.used | filesizeformat}}</td>
|
||||
<td>{{ disk.size | filesizeformat }}</td>
|
||||
<td>{{ disk.storage }}</td>
|
||||
<td>{{ disk.path }}</td>
|
||||
{% endif %}
|
||||
<td class="text-nowrap">
|
||||
<form class="d-inline" action="{% url 'instances:edit_volume' instance.id %}" method="post" role="form" aria-label="Edit instance volume form">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="path" value="{{ disk.path }}">
|
||||
<input type="hidden" name="dev" value="{{ disk.dev }}">
|
||||
<input type="hidden" name="storage" value="{{ disk.storage }}">
|
||||
<input type="hidden" name="name" value="{{ disk.image }}">
|
||||
{% include 'edit_instance_volume.html' with id=forloop.counter0 %}
|
||||
</form>
|
||||
<form class="d-inline" action="{% url 'instances:detach_vol' instance.id %}" method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="path" value="{{ disk.path }}">
|
||||
<input type="hidden" name="dev" value="{{ disk.dev }}">
|
||||
<input type="hidden" name="storage" value="{{ disk.storage }}">
|
||||
<input type="hidden" name="name" value="{{ disk.image }}">
|
||||
{% if instance.status == 5 %}
|
||||
<button type="submit" class="btn btn-sm btn-secondary" value="{{ disk.dev }}" title="{% trans "Detach" %}" onclick="return confirm('{% trans "Are you sure to detach volume?" %}')">
|
||||
{% icon 'eject' %}
|
||||
</button>
|
||||
{% else %}
|
||||
<button class="btn btn-sm btn-secondary disabled" value="{{ disk.dev }}" title="{% trans "Detach" %}" onclick="return confirm('{% trans "Are you sure? This may lead data corruption!" %}')">
|
||||
{% icon 'eject' %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</form>
|
||||
<form class="d-inline" action="{% url 'instances:delete_vol' instance.id %}" method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="path" value="{{ disk.path }}">
|
||||
<input type="hidden" name="dev" value="{{ disk.dev }}">
|
||||
<input type="hidden" name="storage" value="{{ disk.storage }}">
|
||||
<input type="hidden" name="name" value="{{ disk.image }}">
|
||||
{% if instance.status == 5 %}
|
||||
<button type="submit" class="btn btn-sm btn-secondary" title="{% trans "Delete" %}" onclick="return confirm('{% trans "Are you sure to delete volume?" %}')">
|
||||
<i class="fa fa-trash"></i>
|
||||
</button>
|
||||
{% else %}
|
||||
<button class="btn btn-sm btn-secondary disabled" title="{% trans "Delete" %}" onclick="return confirm('{% trans "Are you sure? This may lead data corruption!" %}')">
|
||||
<i class="fa fa-trash"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane tab-pane-bordered" id="network">
|
||||
<p>
|
||||
{% trans "Add a network device" %}
|
||||
{% include 'add_instance_network_block.html' %}
|
||||
</p>
|
||||
|
||||
<div class="row mt-3">
|
||||
<div class="col-lg-12 mt-3">
|
||||
<h5 class="font-weight-bold">{% trans "Network Devices" %}</h5>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{% trans 'Name' %}</th>
|
||||
<th scope="col" class="d-none d-table-cell d-sm-table-cell" colspan="6">{% trans 'Info' %}</th>
|
||||
<th scope="colgroup" class="d-none" colspan="2">{% trans 'Info' %}</th>
|
||||
<th scope="colgroup" colspan="2">{% trans 'Actions' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for network in instance.networks %}
|
||||
<tr>
|
||||
<td rowspan="2">eth{{ forloop.counter0 }}({{ network.target|default:"no target" }})
|
||||
<form action="{% url 'instances:set_link_state' instance.id %}" method="post" aria-label="set instance link state form">
|
||||
{% csrf_token %}
|
||||
<input name="mac" value="{{ network.mac }}" hidden/>
|
||||
<input name="set_link_state" value="{{ network.state }}" hidden/>
|
||||
<input type="checkbox" {% if network.state == 'up' %} checked{% endif %} onclick='submit();' />
|
||||
<strong>{% trans 'active' %}</strong>
|
||||
</form>
|
||||
</td>
|
||||
<th class="d-none d-table-cell d-sm-table-cell">{% trans 'MAC' %}</th>
|
||||
<td>{{ network.mac }}</td>
|
||||
<th scope="row" class="d-none d-table-cell d-sm-table-cell">{% trans 'Filter' %}</th>
|
||||
<td class="d-none d-table-cell">{{ network.filterref|default:"None" }}</td>
|
||||
<th scope="row" class="d-none d-table-cell d-sm-table-cell">{% trans 'Source' %}</th>
|
||||
<td>{{ network.nic }}</td>
|
||||
<td>
|
||||
<form action="{% url 'instances:change_network' instance.id %}" method="post" name="edit_network{{ forloop.counter0 }}" role="form">{% csrf_token %}
|
||||
<button data-target="#editInstanceNetwork{{ forloop.counter0 }}" type="button" class="btn btn-sm btn-primary"
|
||||
title="{% trans "Edit NIC" %}" data-toggle="modal">
|
||||
<span class="fa fa-edit" aria-hidden="true"></span>
|
||||
</button>
|
||||
|
||||
<div class="modal fade" id="editInstanceNetwork{{ forloop.counter0 }}" role="dialog" aria-labelledby="editInstanceNetworkLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">{% trans "Edit Instance Network" %}</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="container">
|
||||
<div class="form-group row">
|
||||
<label class="col-form-label">{% trans "MAC" %}</label>
|
||||
<div class="input-group">
|
||||
<input class="form-control" type="text" value="{{ network.mac }}" readonly/>
|
||||
<input class="form-control" type="text" name="net-mac-{{ forloop.counter0 }}" value="{{ network.mac }}"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-form-label">{% trans "Net Source" %}</label>
|
||||
<div class="input-group">
|
||||
<input class="form-control" type="text" value="{{ network.nic }}" readonly/>
|
||||
<select class="form-control" name="net-source-{{ forloop.counter0 }}">
|
||||
{% for c_net in networks_host %}
|
||||
<option value="net:{{ c_net }}" {% if c_net == network.nic %} selected {% endif %}>{% trans 'Network' %} {{ c_net }}</option>
|
||||
{% endfor %}
|
||||
{% for c_iface in interfaces_host %}
|
||||
<option value="iface:{{ c_iface }}" {% if c_iface == network.nic %} selected {% endif %}>{% trans 'Interface' %} {{ c_iface }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-form-label">{% trans "NWFilter" %}</label>
|
||||
<div class="input-group">
|
||||
<input class="form-control" type="text" value="{{ network.filterref }}" readonly/>
|
||||
<select class="form-control" name="net-nwfilter-{{ forloop.counter0 }}">
|
||||
<option value="">{% trans "None" %}</option>
|
||||
{% for c_filters in nwfilters_host %}
|
||||
<option value="{{ c_filters }}" {% if c_filters == network.filterref %} selected {% endif %}>{{ c_filters }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-form-label">{% trans "Model" %} </label>
|
||||
<div class="input-group">
|
||||
<input class="form-control" type="text" value="{{ network.model }}" readonly/>
|
||||
<select class="form-control" name="net-model-{{ forloop.counter0 }}">
|
||||
{% for model in net_models_host %}
|
||||
<option value="{{ model }}" {% if model == network.model %} selected {% endif %}>{{ model }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" data-dismiss="modal">{% trans 'Close' %}</button>
|
||||
<button class="btn btn-success" name="change_network" title="{% trans "Apply network changes" %}">{% trans "Apply" %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</td>
|
||||
<td align="right">
|
||||
<form action="{% url 'instances:delete_network' instance.id %}" method="post" name="delete_network" role="form">{% csrf_token %}
|
||||
<button class="btn btn-sm btn-danger" value="{{ network.mac }}" name="delete_network" title="{% trans "Delete Device" %}"
|
||||
onclick="return confirm('{% trans "Are you sure?" %}')">
|
||||
<i class="fa fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans 'IPv4' %}</th>
|
||||
<td>
|
||||
{% for ipv4 in network.ipv4|default:"unknown" %}{{ ipv4 }}{% endfor %}
|
||||
</td>
|
||||
<th scope="row">{% trans 'IPv6' %}</th>
|
||||
<td>
|
||||
{% for ipv6 in network.ipv6|default:"unknown" %}{{ ipv6 }}{% endfor %}
|
||||
</td>
|
||||
<th scope="row">{% trans 'Model' %}</th>
|
||||
<td>{{ network.model }}</td>
|
||||
<th>{% trans 'QoS' %}</th>
|
||||
<td class="d-flex justify-content-end">
|
||||
<form action="{% url 'instances:set_qos' instance.id %}" method="post" name="add_qos{{ forloop.counter0 }}" role="form" aria-label="Add network qos form">
|
||||
{% csrf_token %}
|
||||
<input type="text" name="net-mac-{{ forloop.counter0 }}" value="{{ network.mac }}" hidden/>
|
||||
{% include 'add_network_qos.html' with id=forloop.counter0 %}
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="bg-primary" colspan="9"></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if instance.qos %}
|
||||
<div class="row mt-3">
|
||||
<div class="col-sm-10">
|
||||
<p><strong>{% trans "QoS Configuration" %}</strong></p>
|
||||
</div>
|
||||
<div class="col-12 col-sm-12">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr class="d-flex">
|
||||
<th scope="col" class="col-2">{% trans "MAC" %}/{% trans "Direction" %}</th>
|
||||
<th scope="col" class="col-2">{% trans "Average" %}</th>
|
||||
<th scope="col" class="col-3">{% trans "Peak" %}</th>
|
||||
<th scope="col" class="col-3">{% trans "Burst" %}</th>
|
||||
<th scope="col" class="col-2">{% trans "Actions" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for q, attrs in instance.qos.items %}
|
||||
{% for att in attrs %}
|
||||
<tr class="d-flex">
|
||||
<form action="{% url 'instances:set_qos' instance.id %}" method="post" role="form" aria-label="Instance QoS configuration form">
|
||||
{% csrf_token %}
|
||||
<td class="col-2"><label class="col-form-label">{{ q }} {{ att.direction | capfirst }}</label></td>
|
||||
<td class="col-2"><input id="qos_average" class="form-control" name="qos_average"
|
||||
value="{{ att.average|default:'' }}"/>
|
||||
</td>
|
||||
<td class="col-3"><input id="qos_peak" class="form-control" name="qos_peak"
|
||||
value="{{ att.peak|default:'' }}"/>
|
||||
</td>
|
||||
<td class="col-3"><input id="qos_burst" class="form-control" name="qos_burst"
|
||||
value="{{ att.burst|default:'' }}"/>
|
||||
</td>
|
||||
<td class="col-sm-2">
|
||||
<input name="qos_direction" value="{{ att.direction }}" hidden/>
|
||||
<input name="net-mac" value="{{ q }}" hidden/>
|
||||
<button type="submit" class="btn btn-sm btn-primary"
|
||||
name="set_qos" data-toggle="modal"
|
||||
title="{% trans "Edit QoS" %}" onclick="return confirm('{% trans "Are you sure?" %}')">
|
||||
<i class="fa fa-save"></i>
|
||||
</button>
|
||||
</form>
|
||||
<form action="{% url 'instances:unset_qos' instance.id %}" method="post" role="form" aria-label="Instance QoS configuration form">
|
||||
{% csrf_token %}
|
||||
<input name="qos_direction" value="{{ att.direction }}" hidden/>
|
||||
<input name="net-mac" value="{{ q }}" hidden/>
|
||||
<button type="submit" class="btn btn-sm btn-danger"
|
||||
name="unset_qos"
|
||||
title="{% trans "Delete QoS" %}" onclick="return confirm('{% trans "Are you sure?" %}')">
|
||||
<i class="fa fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane tab-pane-bordered" id="migrate">
|
||||
<p>{% trans "For migration both host servers must have equal settings and OS type" %}</p>
|
||||
<form action="{% url 'instances:migrate' instance.id %}" class="ml-3 form" method="post" role="form" aria-label="Migrate instance form">
|
||||
{% csrf_token %}
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">{% trans "Original host" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<label class="form-control" readonly="readonly">{{ compute.name }}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">{% trans "Host migration" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<select name="compute_id" class="custom-select">
|
||||
{% if computes_count != 1 %}
|
||||
{% for comp in computes %}
|
||||
{% if comp.id != compute.id %}
|
||||
<option value="{{ comp.id }}">{{ comp.name }}</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-6 offset-3">
|
||||
<div class="custom-control custom-switch">
|
||||
<input class="custom-control-input" type="checkbox" name="live_migrate" value="true" id="vm_live_migrate" {% if instance.status != 5 %}checked{% else %}disabled{% endif %}>
|
||||
<label class="custom-control-label" for="vm_live_migrate">{% trans "Live migration" %}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-6 offset-3">
|
||||
<div class="custom-control custom-switch">
|
||||
<input class="custom-control-input" type="checkbox" name="unsafe_migrate" value="true" id="vm_unsafe_migrate">
|
||||
<label class="custom-control-label" for="vm_unsafe_migrate">{% trans "Unsafe migration" %}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-6 offset-3">
|
||||
<div class="custom-control custom-switch">
|
||||
<input class="custom-control-input" type="checkbox" name="xml_delete" value="true" id="xml_delete" checked>
|
||||
<label class="custom-control-label" for="xml_delete">{% trans "Delete original" %}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-6 offset-3">
|
||||
<div class="custom-control custom-switch">
|
||||
<input class="custom-control-input" type="checkbox" name="offline_migrate" value="true" id="offline_migrate" {% if instance.status == 5 %}checked{% else %}disabled{% endif %}>
|
||||
<label class="custom-control-label" for="offline_migrate">{% trans "Offline migration" %}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-6 offset-3">
|
||||
<div class="custom-control custom-switch">
|
||||
<input class="custom-control-input" type="checkbox" name="postcopy" value="true" id="postcopy" {% if instance.status != 1 %}disabled{% endif %}>
|
||||
<label class="custom-control-label" for="postcopy">{% trans "Post copy" %}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-6 offset-3">
|
||||
<div class="custom-control custom-switch">
|
||||
<input class="custom-control-input" type="checkbox" name="autoconverge" value="true" id="autoconverge" {% if instance.status != 1 %}disabled{% endif %}>
|
||||
<label class="custom-control-label" for="autoconverge" title="{% trans 'Forces CPU convergence during live migration' %}">{% trans "Auto converge" %}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-6 offset-3">
|
||||
<div class="custom-control custom-switch">
|
||||
<input class="custom-control-input" type="checkbox" name="compress" value="true" id="compress" {% if instance.status != 1 %}disabled{% endif %}>
|
||||
<label class="custom-control-label" for="compress" title="{% trans 'Compress instance memory for fast migration' %}">{% trans "Compressed" %}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if computes_count != 1 %}
|
||||
<button type="submit" class="btn btn-lg btn-success float-right" name="migrate" onclick="showPleaseWaitDialog();">{% trans "Migrate" %}</button>
|
||||
{% else %}
|
||||
<button class="btn btn-lg btn-success float-right disabled">{% trans "Migrate" %}</button>
|
||||
{% endif %}
|
||||
</form>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane tab-pane-bordered" id="xmledit">
|
||||
<p>{% trans "If you need to edit XML please Power Off the instance" %}</p>
|
||||
<form action="{% url 'instances:change_xml' instance.id %}" method="post" role="form" aria-label="Edit instance XML form">
|
||||
{% csrf_token %}
|
||||
<div class="col-sm-12" id="xmlheight">
|
||||
<textarea id="editor">{{ instance.inst_xml }}</textarea>
|
||||
</div>
|
||||
{% if instance.status == 5 %}
|
||||
<input type="hidden" name="inst_xml">
|
||||
<button type="submit" class="btn btn-lg btn-success float-right" name="change_xml">
|
||||
{% trans "Change" %}
|
||||
</button>
|
||||
{% else %}
|
||||
<button class="btn btn-lg btn-success float-right disabled">
|
||||
{% trans "Change" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</form>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane tab-pane-bordered" id="users">
|
||||
<div>
|
||||
<p class="font-weight-bold">
|
||||
{% trans "Instance owners" %}
|
||||
{% include 'add_instance_owner_block.html' %}
|
||||
</p>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped sortable-theme-bootstrap mt-3" data-sortable>
|
||||
<tbody class="searchable">
|
||||
{% for userinstance in userinstances %}
|
||||
<tr>
|
||||
<td><a href="{% url 'account' userinstance.user.id %}">{{ userinstance.user }}</a></td>
|
||||
<td style="width:30px;">
|
||||
<a href="{% url 'user_instance_delete' userinstance.id %}?next={% url 'instances:instance' instance.id %}#users">
|
||||
{% icon 'trash' %}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if request.user.is_superuser or request.user.is_staff or userinstance.is_vnc %}
|
||||
<div role="tabpanel" class="tab-pane tab-pane-bordered" id="vncsettings">
|
||||
<form method="post" action="{% url 'instances:update_console' instance.id %}">
|
||||
{% csrf_token %}
|
||||
{% if instance.status != 5 %}
|
||||
<div class="alert alert-warning">
|
||||
{% trans "To change console settings, shutdown the instance." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% bootstrap_form console_form layout='horizontal' %}
|
||||
<div class="float-right">
|
||||
{% if instance.status != 5 %}
|
||||
<button class="btn btn-success" name="set_console_type" disabled>{% trans "Update" %}</button>
|
||||
{% else %}
|
||||
<button type="submit" class="btn btn-success ">{% trans "Update" %}</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if perms.instances.clone_instances %}
|
||||
<div role="tabpanel" class="tab-pane tab-pane-bordered" id="clone">
|
||||
<p class="font-weight-bold">{% trans "Create a clone" %}</p>
|
||||
<form class="form" action="{% url 'instances:clone' instance.id %}" method="post" role="form" aria-label="Create clone form">{% csrf_token %}
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label font-weight-normal">{% trans "Clone Name" %}</label>
|
||||
{% if request.user.is_superuser %}
|
||||
<div class="col-sm-6">
|
||||
<div class="input-group">
|
||||
<input id="clone_name" type="text" class="form-control" name="name" value="{{ instance.name }}-clone"/>
|
||||
<div class="input-group-append">
|
||||
<button type="button" class="btn btn-success" name="guess-clone-name"
|
||||
onclick="guess_clone_name()">{% trans "Guess" %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% elif app_settings.CLONE_INSTANCE_AUTO_NAME == 'True'%}
|
||||
<div class="col-sm-4">
|
||||
<input id="clone_instance_auto_name" type="text" class="form-control" name="auto_name" value="Automatic" disabled/>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="col-sm-4">
|
||||
<select id="select_clone_name" class="form-control" name="name" size="1">
|
||||
{% for name in clone_free_names %}
|
||||
<option value="{{ name }}">{{ name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if request.user.is_superuser %}
|
||||
<label>{% trans "Network devices" %}:</label>
|
||||
{% for network in instance.networks %}
|
||||
<p>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-2 col-form-label offset-1">eth{{ forloop.counter0 }} ({{ network.nic }})</label>
|
||||
<div class="col-sm-6">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" name="clone-net-mac-{{ forloop.counter0 }}" value="{{ network.mac }}"/>
|
||||
<div class="input-group-append">
|
||||
<button type="button" class="btn btn-success" name="random-mac-{{ forloop.counter0 }}"
|
||||
onclick="random_mac('clone-net-mac-{{ forloop.counter0 }}')">{% trans "Random" %}</button>
|
||||
<button type="button" class="btn btn-success" name="guess-mac-{{ forloop.counter0 }}"
|
||||
onclick="guess_mac_address('#clone_name', {{ forloop.counter0 }})">{% trans "Guess" %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</p>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% for network in instance.networks %}
|
||||
<input type="hidden" class="form-control" name="clone-net-mac-{{ forloop.counter0 }}" value="{{ network.mac }}"/>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if request.user.is_superuser %}
|
||||
<label>{% trans "Storage devices" %}:</label>
|
||||
{% for disk in instance.disks %}
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-2 col-form-label offset-1">{{ disk.dev }} ({{ disk.storage }})</label>
|
||||
<div class="col-sm-6">
|
||||
<div class="input-group">
|
||||
<input id="disk_name-{{ disk.dev }}" type="text" class="form-control" name="disk-{{ disk.dev }}" value="{{ disk.image }}"/>
|
||||
{% if disk.format == 'qcow2' %}
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text" >{% trans 'Metadata' %}</span>
|
||||
<div class="input-group-text">
|
||||
<input type="checkbox" name="meta-{{ disk.dev }}" value="true">
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% for disk in instance.disks %}
|
||||
<input id="disk_name-{{ disk.dev }}" type="hidden" class="form-control" name="disk-{{ disk.dev }}" value="{{ disk.image }}"/>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">{% trans "Title" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="text" name="clone-title" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">{% trans "Description" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<textarea name="clone-description" class="form-control"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
{% if instance.status == 5 %}
|
||||
<button type="submit" class="btn btn-lg btn-success float-right" name="clone" onclick="showPleaseWaitDialog();">{% trans "Clone" %}</button>
|
||||
{% else %}
|
||||
<button class="btn btn-lg btn-success float-right disabled" name="clone">{% trans "Clone" %}</button>
|
||||
{% endif %}
|
||||
</form>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane tab-pane-bordered" id="options">
|
||||
<p>{% trans "To set instance template name description, shutdown the instance." %}</p>
|
||||
<form action="{% url 'instances:change_options' instance.id %}" method="post" role="form" aria-label="Set instance template name description form">{% csrf_token %}
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">{% trans "Title" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="text" name="title" class="form-control" value="{{ instance.title }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">{% trans "Description" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<textarea name="description" class="form-control">{{ instance.description }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">{% trans "Is template" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="checkbox"
|
||||
name="is_template"
|
||||
value="True"
|
||||
id="is_template"
|
||||
{% if instance.is_template %}checked{% endif %}
|
||||
{% if not request.user.is_superuser and not request.user.is_staff %}disabled{% endif %}>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="offset-3 col-sm-6">
|
||||
{% if instance.status == 5 %}
|
||||
<button type="submit" class="btn btn-block btn-success" name="change_options">{% trans "Change" %}</button>
|
||||
{% else %}
|
||||
<button class="btn btn-block btn-success disabled" name="change_options">{% trans "Change" %}</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<p class="font-weight-bold">{% trans "To set instance video model, shutdown the instance." %}</p>
|
||||
<form action="{% url 'instances:set_video_model' instance.id %}" method="post" role="form" aria-label="Set instance video model form">{% csrf_token %}
|
||||
<div class="form-group row">
|
||||
<label for="video_model_select" class="col-sm-3 col-form-label">{% trans "Primary Video Model" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<div class="input-group">
|
||||
<select id="video_model_select" class="custom-select" name="video_model">
|
||||
<option class="font-weight-bold" value="">{% trans "please choose" %}</option>
|
||||
{% for vmodel in instance.video_models %}
|
||||
<option value="{{ vmodel }}"{% if vmodel == instance.video_model %} selected{% endif %}>{{ vmodel }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<span class="input-group-btn">
|
||||
{% if instance.status == 5 %}
|
||||
<button type="submit" class="btn btn-success" name="set_video_model">{% trans "Set" %}</button>
|
||||
{% else %}
|
||||
<button class="btn btn-success disabled" name="set_video_model">{% trans "Set" %}</button>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<p class="font-weight-bold">{% trans "To set instance vCPUs hotpluggable" %}</p>
|
||||
<form action="{% url 'instances:set_vcpu_hotplug' instance.id %}" method="post" role="form" aria-label="Set instance vCPUs hotpluggable form">{% csrf_token %}
|
||||
<div class="form-group row">
|
||||
<label for="vcpu_hotplug" class="col-sm-3 col-form-label">{% trans "vCPU Hot Plug" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<div class="input-group">
|
||||
<select id="vcpu_hotplug" class="form-control" name="vcpu_hotplug">
|
||||
<option value="True" {% if instance.vcpus %} selected {% endif %}>{% trans 'Enabled' %}</option>
|
||||
<option value="False" {% if not instance.vcpus %} selected {% endif %}>{% trans 'Disabled' %}</option>
|
||||
</select>
|
||||
<span class="input-group-btn">
|
||||
{% if instance.status == 5 %}
|
||||
<button type="submit" class="btn btn-success" name="set_vcpu_hotplug">{% trans "Set" %}</button>
|
||||
{% else %}
|
||||
<button class="btn btn-success" name="set_vcpu_hotplug" disabled>{% trans "Set" %}</button>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<p class="font-weight-bold">{% trans "To Enable/Disable Qemu Guest Agent. Status" %}:
|
||||
{% if instance.status == 1 %}
|
||||
{% if instance.guest_agent_ready %}
|
||||
<label class="badge badge-success">{% trans 'Connected' %}</label>
|
||||
{% else %}
|
||||
<label class="badge badge-danger">{% trans 'Disconnected' %}</label>
|
||||
{% endif %}</p>
|
||||
{% else %}
|
||||
<label class="badge badge-default">{% trans 'Unknown' %}</label>
|
||||
{% endif %}
|
||||
<form action="{% url 'instances:set_guest_agent' instance.id %}" method="post" role="form" aria-label="Enable/Disable Qemu Guest Agent form">{% csrf_token %}
|
||||
<div class="form-group row">
|
||||
<label for="guest_agent" class="col-sm-3 col-form-label">{% trans "Qemu Guest Agent" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<div class="input-group">
|
||||
<select id="guest_agent" class="custom-select" name="guest_agent">
|
||||
<option value="True" {% if instance.guest_agent %} selected {% endif %}>{% trans 'Enabled' %}</option>
|
||||
<option value="False" {% if not instance.guest_agent %} selected {% endif %}>{% trans 'Disabled' %}</option>
|
||||
</select>
|
||||
<span class="input-group-btn">
|
||||
<button type="submit" class="btn btn-success" name="set_guest_agent">{% trans "Set" %}</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
91
instances/templates/instances/snapshots_tab.html
Normal file
91
instances/templates/instances/snapshots_tab.html
Normal file
|
@ -0,0 +1,91 @@
|
|||
{% load i18n %}
|
||||
{% load icons %}
|
||||
<div role="tabpanel" class="tab-pane" id="snapshots">
|
||||
<div role="tabpanel">
|
||||
<!-- Nav tabs -->
|
||||
<ul class="nav nav-tabs" role="tablist" aria-label="Instance snapshot menu">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-secondary active" href="#takesnapshot" aria-controls="takesnapshot" role="tab" data-toggle="tab">
|
||||
{% trans "Take Snapshot" %}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-secondary" href="#managesnapshot" aria-controls="managesnapshot" role="tab" data-toggle="tab">
|
||||
{% trans "Manage Snapshots" %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<!-- Tab panes -->
|
||||
<div class="tab-content">
|
||||
<div role="tabpanel" class="tab-pane tab-pane-bordered active" id="takesnapshot">
|
||||
{% if instance.status == 5 %}
|
||||
<p>{% trans "This may take more than an hour, depending on how much content is on your droplet and how large the disk is." %}</p>
|
||||
<form action="{% url 'instances:snapshot' instance.id %}" class="form-inline" method="post" role="form" aria-label="Create snapshot form">
|
||||
{% csrf_token %}
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-12">
|
||||
<input type="text" class="form-control form-control-lg" name="name" placeholder="{% trans "Enter Snapshot Name" %}" maxlength="14">
|
||||
</div>
|
||||
</div>
|
||||
{% if instance.status == 5 %}
|
||||
<input type="submit" class="btn btn-lg btn-success float-right" name="snapshot" value="{% trans "Take Snapshot" %}">
|
||||
{% else %}
|
||||
<button class="btn btn-lg btn-success float-right disabled">{% trans "Take Snapshot" %}</button>
|
||||
{% endif %}
|
||||
</form>
|
||||
<div class="clearfix"></div>
|
||||
{% else %}
|
||||
<p>{% trans "To take a snapshot please Power Off the instance." %}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane tab-pane-bordered" id="managesnapshot">
|
||||
{% if instance.snapshots %}
|
||||
<p>{% trans "Choose a snapshot for restore/delete" %}</p>
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<th scope="col">{% trans "Name" %}</th>
|
||||
<th scope="col">{% trans "Date" %}</th>
|
||||
<th scope="colgroup" colspan="2">{% trans "Action" %}</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for snap in instance.snapshots %}
|
||||
<tr>
|
||||
<td><strong>{{ snap.name }}</strong></td>
|
||||
<td>{{ snap.date|date:"M d H:i:s" }}</td>
|
||||
<td style="width:30px;">
|
||||
<form action="{% url 'instances:revert_snapshot' instance.id %}" method="post" role="form" aria-label="Restore snapshot form">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="name" value="{{ snap.name }}">
|
||||
{% if instance.status == 5 %}
|
||||
<button type="submit" class="btn btn-sm btn-secondary" name="revert_snapshot" title="{% trans 'Revert to this Snapshot' %}" onclick="return confirm('Are you sure?')">
|
||||
<span class="fa fa-download"></span>
|
||||
</button>
|
||||
{% else %}
|
||||
<button type="button" class="btn btn-sm btn-secondary disabled"
|
||||
title="{% trans "To restore snapshots you need Power Off the instance." %}">
|
||||
<span class="fa fa-download"></span>
|
||||
</button>
|
||||
{% endif %}
|
||||
</form>
|
||||
</td>
|
||||
<td style="width:30px;">
|
||||
<form action="{% url 'instances:delete_snapshot' instance.id %}" method="post" role="form" aria-label="Delete snapshot form">{% csrf_token %}
|
||||
<input type="hidden" name="name" value="{{ snap.name }}">
|
||||
<button type="submit" class="btn btn-sm btn-danger" title="{% trans 'Delete Snapshot' %}" onclick="return confirm('{% trans "Are you sure?" %}')">
|
||||
{% icon 'trash' %}
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p>{% trans "You do not have any snapshots" %}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
116
instances/templates/instances/stats_tab.html
Normal file
116
instances/templates/instances/stats_tab.html
Normal file
|
@ -0,0 +1,116 @@
|
|||
{% load i18n %}
|
||||
<div role="tabpanel" class="tab-pane" id="graphics">
|
||||
<div role="tabpanel">
|
||||
<!-- Nav tabs -->
|
||||
<ul class="nav nav-tabs" role="tablist" aria-label="Instance graphs and logs menu">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-secondary active" href="#graphs" id="graphtab" aria-controls="graphs" role="tab" data-toggle="tab" aria-controls="graphs" aria-selected="true">
|
||||
{% trans "Real Time" %}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-secondary" href="#logs" id="logtab" aria-controls="logs" role="tab" data-toggle="tab" aria-controls="logs" onclick='update_logs_table("{{ instance.name }}");'>
|
||||
{% trans "Logs" %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<!-- Tab panes -->
|
||||
<div class="tab-content">
|
||||
<div role="tabpanel" class="tab-pane tab-pane-bordered active" id="graphs">
|
||||
<div class="mb-1 card border-success">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title"><i class="fa fa-long-arrow-right"></i>
|
||||
{% trans "CPU Usage" %}</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="flot-chart">
|
||||
<div class="flot-chart-content" id="flot-moving-line-chart">
|
||||
<canvas id="cpuChart" width="735" height="160"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-1 card border-danger">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title"><i class="fa fa-long-arrow-right"></i>
|
||||
{% trans "Memory Usage" %}</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="flot-chart">
|
||||
<div class="flot-chart-content" id="flot-moving-line-chart">
|
||||
<canvas id="memChart" width="735" height="160"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% for net in instance.networks %}
|
||||
<div class="mb-1 card border-info">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title"><i class="fa fa-long-arrow-right"></i>
|
||||
{% trans "Bandwidth Device" %}: eth{{ forloop.counter0 }}</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="flot-chart">
|
||||
<div class="flot-chart-content" id="flot-moving-line-chart">
|
||||
<canvas id="netEth{{ forloop.counter0 }}Chart" width="735" height="160"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% for disk in instance.disks %}
|
||||
<div class="mb-1 card border-warning">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title"><i class="fa fa-long-arrow-right"></i>
|
||||
{% trans "Disk I/O device" %}: {{ disk.dev }}</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="flot-chart">
|
||||
<div class="flot-chart-content" id="flot-moving-line-chart">
|
||||
<canvas id="blk{{ disk.dev }}Chart" width="735" height="160"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane tab-pane-bordered" id="logs">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped sortable-theme-bootstrap" id="logs_table" data-sortable>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{% trans "Date" %}</th>
|
||||
<th scope="col">{% trans "User" %}</th>
|
||||
<th scope="col">{% trans "Message" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="searchable">
|
||||
<tr>
|
||||
<td colspan="3"><i>{% trans 'None' %}...</i></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
function update_logs_table(vname) {
|
||||
// TODO
|
||||
logurl = "{% url 'vm_logs' 1 %}".replace(1, vname);
|
||||
$.getJSON(logurl, function(data) {
|
||||
var logs = "";
|
||||
$.each(data, function(id) {
|
||||
row = data[id];
|
||||
// console.log(row);
|
||||
logs += '<tr><td style="width:150px">'+row['date']+'</td>';
|
||||
logs += '<td>'+row['user']+'</td>';
|
||||
logs += '<td>'+row['message']+'</td></tr>';
|
||||
});
|
||||
$("#logs_table > tbody").html(logs);
|
||||
});
|
||||
}
|
||||
</script>
|
|
@ -1,3 +1,478 @@
|
|||
import tempfile
|
||||
|
||||
from django.shortcuts import reverse
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
from computes.models import Compute
|
||||
|
||||
from .models import Instance
|
||||
|
||||
|
||||
class InstancesTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.client.login(username='admin', password='admin')
|
||||
Compute(
|
||||
name='local',
|
||||
hostname='localhost',
|
||||
login='',
|
||||
password='',
|
||||
details='local',
|
||||
type=4,
|
||||
).save()
|
||||
|
||||
def test_index(self):
|
||||
response = self.client.get(reverse('instances:index'))
|
||||
# with open('index.html', 'wb') as f:
|
||||
# f.write(response.content)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_create_select_type(self):
|
||||
response = self.client.get(reverse('instances:create_instance_select_type', args=[1]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_instance(self):
|
||||
compute = Compute.objects.get(pk=1)
|
||||
|
||||
# create volume
|
||||
response = self.client.post(
|
||||
reverse('create_volume', args=[compute.id, 'default']),
|
||||
{
|
||||
'name': 'test',
|
||||
'format': 'qcow2',
|
||||
'size': '1',
|
||||
'meta_prealloc': False,
|
||||
},
|
||||
)
|
||||
self.assertRedirects(response, reverse('storage', args=[compute.id, 'default']))
|
||||
|
||||
# create instance
|
||||
response = self.client.get(reverse('instances:create_instance', args=[compute.id, 'x86_64', 'q35']))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.post(
|
||||
reverse('instances:create_instance', args=[compute.id, 'x86_64', 'q35']),
|
||||
{
|
||||
'name': 'test',
|
||||
'firmware': 'BIOS',
|
||||
'vcpu': 1,
|
||||
'vcpu_mode': 'host-model',
|
||||
'memory': 512,
|
||||
'device0': 'disk',
|
||||
'bus0': 'virtio',
|
||||
'images': 'test.qcow2',
|
||||
'storage-control': 'default',
|
||||
'image-control': 'test.qcow2',
|
||||
'networks': 'default',
|
||||
'network-control': 'default',
|
||||
'cache_mode': 'directsync',
|
||||
'nwfilter': '',
|
||||
'graphics': 'spice',
|
||||
'video': 'vga',
|
||||
'listener_addr': '0.0.0.0',
|
||||
'console_pass': '',
|
||||
'qemu_ga': False,
|
||||
'virtio': True,
|
||||
'create': True,
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
instance: Instance = Instance.objects.get(pk=1)
|
||||
self.assertEqual(instance.name, 'test')
|
||||
|
||||
# get instance page
|
||||
response = self.client.get(reverse('instances:instance', args=[instance.id]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# resize cpu
|
||||
self.assertEqual(instance.vcpu, 1)
|
||||
self.assertEqual(instance.cur_vcpu, 1)
|
||||
|
||||
response = self.client.post(reverse('instances:resizevm_cpu', args=[instance.id]), {'vcpu': 4, 'cur_vcpu': 2})
|
||||
self.assertRedirects(response, reverse('instances:instance', args=[instance.id]) + '#resize')
|
||||
|
||||
# reset cached properties
|
||||
del instance.vcpu
|
||||
del instance.cur_vcpu
|
||||
self.assertEqual(instance.vcpu, 4)
|
||||
self.assertEqual(instance.cur_vcpu, 2)
|
||||
|
||||
# resize memory
|
||||
self.assertEqual(instance.memory, 512)
|
||||
self.assertEqual(instance.cur_memory, 512)
|
||||
|
||||
response = self.client.post(reverse('instances:resize_memory', args=[instance.id]), {
|
||||
'memory': 2048,
|
||||
'cur_memory': 1024
|
||||
})
|
||||
self.assertRedirects(response, reverse('instances:instance', args=[instance.id]) + '#resize')
|
||||
|
||||
del instance.memory
|
||||
del instance.cur_memory
|
||||
self.assertEqual(instance.memory, 2048)
|
||||
self.assertEqual(instance.cur_memory, 1024)
|
||||
|
||||
# resize disk
|
||||
self.assertEqual(instance.disks[0]['size'], 1024**3)
|
||||
|
||||
response = self.client.post(reverse('instances:resize_disk', args=[instance.id]), {
|
||||
'disk_size_vda': '2.0 GB',
|
||||
})
|
||||
self.assertRedirects(response, reverse('instances:instance', args=[instance.id]) + '#resize')
|
||||
|
||||
del instance.disks
|
||||
self.assertEqual(instance.disks[0]['size'], 2 * 1024**3)
|
||||
|
||||
# add new volume
|
||||
self.assertEqual(len(instance.disks), 1)
|
||||
|
||||
response = self.client.post(
|
||||
reverse('instances:add_new_vol', args=[instance.id]),
|
||||
{
|
||||
'storage': 'default',
|
||||
'name': 'test2',
|
||||
'size': 1,
|
||||
},
|
||||
HTTP_REFERER=reverse('index'),
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
del instance.disks
|
||||
self.assertEqual(len(instance.disks), 2)
|
||||
|
||||
# delete volume from instance
|
||||
response = self.client.post(
|
||||
reverse('instances:delete_vol', args=[instance.id]),
|
||||
{
|
||||
'storage': 'default',
|
||||
'dev': 'vdb',
|
||||
'name': 'test2.qcow2',
|
||||
},
|
||||
HTTP_REFERER=reverse('index'),
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
del instance.disks
|
||||
self.assertEqual(len(instance.disks), 1)
|
||||
|
||||
# detach volume
|
||||
response = self.client.post(
|
||||
reverse('instances:detach_vol', args=[instance.id]),
|
||||
{
|
||||
'dev': 'vda',
|
||||
},
|
||||
HTTP_REFERER=reverse('index'),
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
del instance.disks
|
||||
self.assertEqual(len(instance.disks), 0)
|
||||
|
||||
# add existing volume
|
||||
response = self.client.post(
|
||||
reverse('instances:add_existing_vol', args=[instance.id]),
|
||||
{
|
||||
'selected_storage': 'default',
|
||||
'vols': 'test.qcow2',
|
||||
},
|
||||
HTTP_REFERER=reverse('index'),
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
del instance.disks
|
||||
self.assertEqual(len(instance.disks), 1)
|
||||
|
||||
# edit volume
|
||||
response = self.client.post(
|
||||
reverse('instances:edit_volume', args=[instance.id]),
|
||||
{
|
||||
'vol_path': '/var/lib/libvirt/images/test.qcow2',
|
||||
# 'vol_shareable': False,
|
||||
# 'vol_readonly': False,
|
||||
'vol_bus': 'virtio',
|
||||
'vol_bus_old': 'virtio',
|
||||
'vol_format': 'qcow2',
|
||||
'dev': 'vda',
|
||||
'edit_volume': True
|
||||
},
|
||||
HTTP_REFERER=reverse('index'),
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
# add media device
|
||||
self.assertEqual(len(instance.media), 1)
|
||||
|
||||
response = self.client.post(
|
||||
reverse('instances:add_cdrom', args=[instance.id]),
|
||||
{
|
||||
'bus': 'sata',
|
||||
},
|
||||
HTTP_REFERER=reverse('index'),
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
del instance.media
|
||||
self.assertEqual(len(instance.media), 2)
|
||||
|
||||
# create dummy iso
|
||||
# with tempfile.NamedTemporaryFile() as f:
|
||||
# f.write(b'\x00' * 1024**2)
|
||||
|
||||
# response = self.client.post(
|
||||
# reverse('storage', args=[instance.compute.id, 'default']),
|
||||
# {
|
||||
# 'iso_upload': True,
|
||||
# 'file': f
|
||||
# },
|
||||
# )
|
||||
|
||||
# remove media device
|
||||
response = self.client.post(
|
||||
reverse('instances:detach_cdrom', args=[instance.id, 'sda']),
|
||||
{},
|
||||
HTTP_REFERER=reverse('index'),
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
del instance.media
|
||||
self.assertEqual(len(instance.media), 1)
|
||||
|
||||
# snapshots
|
||||
self.assertEqual(len(instance.snapshots), 0)
|
||||
|
||||
response = self.client.post(
|
||||
reverse('instances:snapshot', args=[instance.id]),
|
||||
{
|
||||
'name': 'test',
|
||||
},
|
||||
HTTP_REFERER=reverse('index'),
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
del instance.snapshots
|
||||
self.assertEqual(len(instance.snapshots), 1)
|
||||
|
||||
response = self.client.post(
|
||||
reverse('instances:revert_snapshot', args=[instance.id]),
|
||||
{
|
||||
'name': 'test',
|
||||
},
|
||||
HTTP_REFERER=reverse('index'),
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
response = self.client.post(
|
||||
reverse('instances:delete_snapshot', args=[instance.id]),
|
||||
{
|
||||
'name': 'test',
|
||||
},
|
||||
HTTP_REFERER=reverse('index'),
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
del instance.snapshots
|
||||
self.assertEqual(len(instance.snapshots), 0)
|
||||
|
||||
# autostart
|
||||
self.assertEqual(instance.autostart, 0)
|
||||
|
||||
response = self.client.post(
|
||||
reverse('instances:set_autostart', args=[instance.id]),
|
||||
{},
|
||||
HTTP_REFERER=reverse('index'),
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
del instance.autostart
|
||||
self.assertEqual(instance.autostart, 1)
|
||||
|
||||
response = self.client.post(
|
||||
reverse('instances:unset_autostart', args=[instance.id]),
|
||||
{},
|
||||
HTTP_REFERER=reverse('index'),
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
del instance.autostart
|
||||
self.assertEqual(instance.autostart, 0)
|
||||
|
||||
# bootmenu
|
||||
self.assertEqual(instance.bootmenu, True)
|
||||
|
||||
response = self.client.post(
|
||||
reverse('instances:unset_bootmenu', args=[instance.id]),
|
||||
{},
|
||||
HTTP_REFERER=reverse('index'),
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
del instance.bootmenu
|
||||
self.assertEqual(instance.bootmenu, False)
|
||||
|
||||
response = self.client.post(
|
||||
reverse('instances:set_bootmenu', args=[instance.id]),
|
||||
{},
|
||||
HTTP_REFERER=reverse('index'),
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
del instance.bootmenu
|
||||
self.assertEqual(instance.bootmenu, True)
|
||||
|
||||
# guest agent
|
||||
|
||||
self.assertEqual(instance.guest_agent, False)
|
||||
|
||||
response = self.client.post(
|
||||
reverse('instances:set_guest_agent', args=[instance.id]),
|
||||
{'guest_agent': True},
|
||||
HTTP_REFERER=reverse('index'),
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
del instance.guest_agent
|
||||
self.assertEqual(instance.guest_agent, True)
|
||||
|
||||
response = self.client.post(
|
||||
reverse('instances:set_guest_agent', args=[instance.id]),
|
||||
{'guest_agent': False},
|
||||
HTTP_REFERER=reverse('index'),
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
del instance.guest_agent
|
||||
self.assertEqual(instance.guest_agent, False)
|
||||
|
||||
# video model
|
||||
self.assertEqual(instance.video_model, 'vga')
|
||||
|
||||
response = self.client.post(
|
||||
reverse('instances:set_video_model', args=[instance.id]),
|
||||
{'video_model': 'virtio'},
|
||||
HTTP_REFERER=reverse('index'),
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
del instance.video_model
|
||||
self.assertEqual(instance.video_model, 'virtio')
|
||||
|
||||
# console
|
||||
response = self.client.post(
|
||||
reverse('instances:update_console', args=[instance.id]),
|
||||
{
|
||||
'type': 'spice',
|
||||
'listen_on': '0.0.0.0',
|
||||
'password': '',
|
||||
'keymap': 'auto'
|
||||
},
|
||||
HTTP_REFERER=reverse('index'),
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
# poweron
|
||||
self.assertEqual(instance.status, 5)
|
||||
|
||||
response = self.client.get(reverse('instances:poweron', args=[instance.id]), HTTP_REFERER=reverse('index'))
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
del instance.status
|
||||
|
||||
self.assertEqual(instance.status, 1)
|
||||
|
||||
# status
|
||||
response = self.client.get(reverse('instances:status', args=[instance.id]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# stats
|
||||
response = self.client.get(reverse('instances:stats', args=[instance.id]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# guess_mac_address
|
||||
response = self.client.get(reverse('instances:guess_mac_address', args=[instance.name]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# random_mac_address
|
||||
response = self.client.get(reverse('instances:random_mac_address'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# random_mac_address
|
||||
response = self.client.get(reverse('instances:guess_clone_name'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# guess_mac_address
|
||||
response = self.client.get(reverse('instances:check_instance', args=[instance.name]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# sshkeys
|
||||
response = self.client.get(reverse('instances:sshkeys', args=[instance.name]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# suspend
|
||||
response = self.client.get(reverse('instances:suspend', args=[instance.id]), HTTP_REFERER=reverse('index'))
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
del instance.status
|
||||
self.assertEqual(instance.status, 3)
|
||||
|
||||
# resume
|
||||
response = self.client.get(reverse('instances:resume', args=[instance.id]), HTTP_REFERER=reverse('index'))
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
del instance.status
|
||||
self.assertEqual(instance.status, 1)
|
||||
|
||||
# poweroff
|
||||
response = self.client.get(reverse('instances:poweroff', args=[instance.id]), HTTP_REFERER=reverse('index'))
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
# as no OS is installed ACPI won't work
|
||||
del instance.status
|
||||
self.assertEqual(instance.status, 1)
|
||||
|
||||
# powercycle
|
||||
response = self.client.get(reverse('instances:powercycle', args=[instance.id]), HTTP_REFERER=reverse('index'))
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(instance.status, 1)
|
||||
|
||||
# force_off
|
||||
response = self.client.get(reverse('instances:force_off', args=[instance.id]), HTTP_REFERER=reverse('index'))
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
del instance.status
|
||||
self.assertEqual(instance.status, 5)
|
||||
|
||||
# delete started instance with disks
|
||||
instance.proxy.start()
|
||||
del instance.status
|
||||
self.assertEqual(instance.status, 1)
|
||||
|
||||
response = self.client.post(
|
||||
reverse('instances:destroy', args=[instance.id]),
|
||||
{'delete_disk': True},
|
||||
HTTP_REFERER=reverse('index'),
|
||||
)
|
||||
self.assertRedirects(response, reverse('instances', args=[compute.id]))
|
||||
|
||||
# # create volume
|
||||
# response = self.client.post(
|
||||
# reverse('create_volume', args=[compute.id, 'default']),
|
||||
# {
|
||||
# 'name': 'test3',
|
||||
# 'format': 'qcow2',
|
||||
# 'size': '1',
|
||||
# 'meta_prealloc': False,
|
||||
# },
|
||||
# )
|
||||
# self.assertRedirects(response, reverse('storage', args=[compute.id, 'default']))
|
||||
|
||||
# # delete volume
|
||||
# response = self.client.post(
|
||||
# reverse('instances:delete_vol', args=[instance.id]),
|
||||
# {
|
||||
# 'storage': 'default',
|
||||
# 'dev': 'vdb',
|
||||
# 'name': 'test3.qcow2',
|
||||
# },
|
||||
# HTTP_REFERER=reverse('index'),
|
||||
# )
|
||||
# self.assertEqual(response.status_code, 302)
|
||||
|
|
|
@ -5,10 +5,62 @@ from . import views
|
|||
app_name = 'instances'
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.allinstances, name='index'),
|
||||
path('<int:compute_id>/<vname>/', views.instance, name='instance'),
|
||||
path('statistics/<int:compute_id>/<vname>/', views.inst_graph, name='inst_graph'),
|
||||
path('status/<int:compute_id>/<vname>/', views.inst_status, name='inst_status'),
|
||||
path('', views.index, name='index'),
|
||||
path('flavor/create/', views.flavor_create, name='flavor_create'),
|
||||
path('flavor/<int:pk>/update/', views.flavor_update, name='flavor_update'),
|
||||
path('flavor/<int:pk>/delete/', views.flavor_delete, name='flavor_delete'),
|
||||
path('<int:pk>/', views.instance, name='instance'),
|
||||
path('<int:pk>/poweron/', views.poweron, name='poweron'),
|
||||
path('<int:pk>/powercycle/', views.powercycle, name='powercycle'),
|
||||
path('<int:pk>/poweroff/', views.poweroff, name='poweroff'),
|
||||
path('<int:pk>/suspend/', views.suspend, name='suspend'),
|
||||
path('<int:pk>/resume/', views.resume, name='resume'),
|
||||
path('<int:pk>/force_off/', views.force_off, name='force_off'),
|
||||
path('<int:pk>/destroy/', views.destroy, name='destroy'),
|
||||
path('<int:pk>/migrate/', views.migrate, name='migrate'),
|
||||
path('<int:pk>/status/', views.status, name='status'),
|
||||
path('<int:pk>/stats/', views.stats, name='stats'),
|
||||
path('<int:pk>/rootpasswd/', views.set_root_pass, name='rootpasswd'),
|
||||
path('<int:pk>/add_public_key/', views.add_public_key, name='add_public_key'),
|
||||
path('<int:pk>/resizevm_cpu/', views.resizevm_cpu, name='resizevm_cpu'),
|
||||
path('<int:pk>/resize_memory/', views.resize_memory, name='resize_memory'),
|
||||
path('<int:pk>/resize_disk/', views.resize_disk, name='resize_disk'),
|
||||
path('<int:pk>/add_new_vol/', views.add_new_vol, name='add_new_vol'),
|
||||
path('<int:pk>/delete_vol/', views.delete_vol, name='delete_vol'),
|
||||
path('<int:pk>/add_owner/', views.add_owner, name='add_owner'),
|
||||
path('<int:pk>/add_existing_vol/', views.add_existing_vol, name='add_existing_vol'),
|
||||
path('<int:pk>/edit_volume/', views.edit_volume, name='edit_volume'),
|
||||
path('<int:pk>/detach_vol/', views.detach_vol, name='detach_vol'),
|
||||
path('<int:pk>/add_cdrom/', views.add_cdrom, name='add_cdrom'),
|
||||
path('<int:pk>/detach_cdrom/<str:dev>/', views.detach_cdrom, name='detach_cdrom'),
|
||||
path('<int:pk>/unmount_iso/', views.unmount_iso, name='unmount_iso'),
|
||||
path('<int:pk>/mount_iso/', views.mount_iso, name='mount_iso'),
|
||||
path('<int:pk>/snapshot/', views.snapshot, name='snapshot'),
|
||||
path('<int:pk>/delete_snapshot/', views.delete_snapshot, name='delete_snapshot'),
|
||||
path('<int:pk>/revert_snapshot/', views.revert_snapshot, name='revert_snapshot'),
|
||||
path('<int:pk>/set_vcpu/', views.set_vcpu, name='set_vcpu'),
|
||||
path('<int:pk>/set_vcpu_hotplug/', views.set_vcpu_hotplug, name='set_vcpu_hotplug'),
|
||||
path('<int:pk>/set_autostart/', views.set_autostart, name='set_autostart'),
|
||||
path('<int:pk>/unset_autostart/', views.unset_autostart, name='unset_autostart'),
|
||||
path('<int:pk>/set_bootmenu/', views.set_bootmenu, name='set_bootmenu'),
|
||||
path('<int:pk>/unset_bootmenu/', views.unset_bootmenu, name='unset_bootmenu'),
|
||||
path('<int:pk>/set_bootorder/', views.set_bootorder, name='set_bootorder'),
|
||||
path('<int:pk>/change_xml/', views.change_xml, name='change_xml'),
|
||||
path('<int:pk>/set_guest_agent/', views.set_guest_agent, name='set_guest_agent'),
|
||||
path('<int:pk>/set_video_model/', views.set_video_model, name='set_video_model'),
|
||||
path('<int:pk>/change_network/', views.change_network, name='change_network'),
|
||||
path('<int:pk>/add_network/', views.add_network, name='add_network'),
|
||||
path('<int:pk>/delete_network/', views.delete_network, name='delete_network'),
|
||||
path('<int:pk>/set_link_state/', views.set_link_state, name='set_link_state'),
|
||||
path('<int:pk>/set_qos/', views.set_qos, name='set_qos'),
|
||||
path('<int:pk>/unset_qos/', views.unset_qos, name='unset_qos'),
|
||||
path('<int:pk>/del_owner/', views.del_owner, name='del_owner'), # no links to this one???
|
||||
path('<int:pk>/clone/', views.clone, name='clone'),
|
||||
path('<int:pk>/update_console/', views.update_console, name='update_console'),
|
||||
path('<int:pk>/change_options/', views.change_options, name='change_options'),
|
||||
path('<int:pk>/getvvfile/', views.getvvfile, name='getvvfile'), # no links to this one???
|
||||
path('create/<int:compute_id>/', views.create_instance_select_type, name='create_instance_select_type'),
|
||||
path('create/<int:compute_id>/<str:arch>/<str:machine>/', views.create_instance, name='create_instance'),
|
||||
path('guess_mac_address/<vname>/', views.guess_mac_address, name='guess_mac_address'),
|
||||
path('guess_clone_name/', views.guess_clone_name, name='guess_clone_name'),
|
||||
path('random_mac_address/', views.random_mac_address, name='random_mac_address'),
|
||||
|
|
361
instances/utils.py
Normal file
361
instances/utils.py
Normal file
|
@ -0,0 +1,361 @@
|
|||
import os
|
||||
import random
|
||||
import string
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from accounts.models import UserInstance
|
||||
from appsettings.settings import app_settings
|
||||
from logs.views import addlogmsg
|
||||
from vrtManager.connection import connection_manager
|
||||
from vrtManager.hostdetails import wvmHostDetails
|
||||
from vrtManager.instance import wvmInstance, wvmInstances
|
||||
|
||||
from .models import Instance
|
||||
|
||||
|
||||
def filesizefstr(size_str):
|
||||
if size_str == '':
|
||||
return 0
|
||||
size_str = size_str.upper().replace("B", "")
|
||||
if size_str[-1] == 'K':
|
||||
return int(float(size_str[:-1])) << 10
|
||||
elif size_str[-1] == 'M':
|
||||
return int(float(size_str[:-1])) << 20
|
||||
elif size_str[-1] == 'G':
|
||||
return int(float(size_str[:-1])) << 30
|
||||
elif size_str[-1] == 'T':
|
||||
return int(float(size_str[:-1])) << 40
|
||||
elif size_str[-1] == 'P':
|
||||
return int(float(size_str[:-1])) << 50
|
||||
else:
|
||||
return int(float(size_str))
|
||||
|
||||
|
||||
def get_clone_free_names(size=10):
|
||||
prefix = app_settings.CLONE_INSTANCE_DEFAULT_PREFIX
|
||||
free_names = []
|
||||
existing_names = [i.name for i in Instance.objects.filter(name__startswith=prefix)]
|
||||
index = 1
|
||||
while len(free_names) < size:
|
||||
new_name = prefix + str(index)
|
||||
if new_name not in existing_names:
|
||||
free_names.append(new_name)
|
||||
index += 1
|
||||
return free_names
|
||||
|
||||
|
||||
def check_user_quota(user, instance, cpu, memory, disk_size):
|
||||
ua = user.userattributes
|
||||
msg = ""
|
||||
|
||||
if user.is_superuser:
|
||||
return msg
|
||||
|
||||
quota_debug = app_settings.QUOTA_DEBUG
|
||||
|
||||
user_instances = UserInstance.objects.filter(user=user, instance__is_template=False)
|
||||
instance += user_instances.count()
|
||||
for usr_inst in user_instances:
|
||||
if connection_manager.host_is_up(
|
||||
usr_inst.instance.compute.type,
|
||||
usr_inst.instance.compute.hostname,
|
||||
):
|
||||
conn = wvmInstance(
|
||||
usr_inst.instance.compute.hostname,
|
||||
usr_inst.instance.compute.login,
|
||||
usr_inst.instance.compute.password,
|
||||
usr_inst.instance.compute.type,
|
||||
usr_inst.instance.name,
|
||||
)
|
||||
cpu += int(conn.get_vcpu())
|
||||
memory += int(conn.get_memory())
|
||||
for disk in conn.get_disk_devices():
|
||||
if disk['size']:
|
||||
disk_size += int(disk['size']) >> 30
|
||||
|
||||
if ua.max_instances > 0 and instance > ua.max_instances:
|
||||
msg = "instance"
|
||||
if quota_debug:
|
||||
msg += f" ({instance} > {ua.max_instances})"
|
||||
if ua.max_cpus > 0 and cpu > ua.max_cpus:
|
||||
msg = "cpu"
|
||||
if quota_debug:
|
||||
msg += f" ({cpu} > {ua.max_cpus})"
|
||||
if ua.max_memory > 0 and memory > ua.max_memory:
|
||||
msg = "memory"
|
||||
if quota_debug:
|
||||
msg += f" ({memory} > {ua.max_memory})"
|
||||
if ua.max_disk_size > 0 and disk_size > ua.max_disk_size:
|
||||
msg = "disk"
|
||||
if quota_debug:
|
||||
msg += f" ({disk_size} > {ua.max_disk_size})"
|
||||
return msg
|
||||
|
||||
|
||||
def get_new_disk_dev(media, disks, bus):
|
||||
existing_disk_devs = []
|
||||
existing_media_devs = []
|
||||
if bus == "virtio":
|
||||
dev_base = "vd"
|
||||
elif bus == "ide":
|
||||
dev_base = "hd"
|
||||
elif bus == "fdc":
|
||||
dev_base = "fd"
|
||||
else:
|
||||
dev_base = "sd"
|
||||
|
||||
if disks:
|
||||
existing_disk_devs = [disk['dev'] for disk in disks]
|
||||
|
||||
# cd-rom bus could be virtio/sata, because of that we should check it also
|
||||
if media:
|
||||
existing_media_devs = [m['dev'] for m in media]
|
||||
|
||||
for al in string.ascii_lowercase:
|
||||
dev = dev_base + al
|
||||
if dev not in existing_disk_devs and dev not in existing_media_devs:
|
||||
return dev
|
||||
raise Exception(_('None available device name'))
|
||||
|
||||
|
||||
def get_network_tuple(network_source_str):
|
||||
network_source_pack = network_source_str.split(":", 1)
|
||||
if len(network_source_pack) > 1:
|
||||
return network_source_pack[1], network_source_pack[0]
|
||||
else:
|
||||
return network_source_pack[0], 'net'
|
||||
|
||||
|
||||
def migrate_instance(
|
||||
new_compute,
|
||||
instance,
|
||||
user,
|
||||
live=False,
|
||||
unsafe=False,
|
||||
xml_del=False,
|
||||
offline=False,
|
||||
autoconverge=False,
|
||||
compress=False,
|
||||
postcopy=False,
|
||||
):
|
||||
status = connection_manager.host_is_up(new_compute.type, new_compute.hostname)
|
||||
if not status:
|
||||
return
|
||||
if new_compute == instance.compute:
|
||||
return
|
||||
try:
|
||||
conn_migrate = wvmInstances(
|
||||
new_compute.hostname,
|
||||
new_compute.login,
|
||||
new_compute.password,
|
||||
new_compute.type,
|
||||
)
|
||||
|
||||
autostart = instance.autostart
|
||||
conn_migrate.moveto(
|
||||
instance.proxy,
|
||||
instance.name,
|
||||
live,
|
||||
unsafe,
|
||||
xml_del,
|
||||
offline,
|
||||
autoconverge,
|
||||
compress,
|
||||
postcopy,
|
||||
)
|
||||
|
||||
conn_new = wvmInstance(
|
||||
new_compute.hostname,
|
||||
new_compute.login,
|
||||
new_compute.password,
|
||||
new_compute.type,
|
||||
instance.name,
|
||||
)
|
||||
|
||||
if autostart:
|
||||
conn_new.set_autostart(1)
|
||||
finally:
|
||||
conn_migrate.close()
|
||||
conn_new.close()
|
||||
|
||||
instance.compute = new_compute
|
||||
instance.save()
|
||||
|
||||
|
||||
def get_hosts_status(computes):
|
||||
"""
|
||||
Function return all hosts all vds on host
|
||||
"""
|
||||
compute_data = []
|
||||
for compute in computes:
|
||||
compute_data.append({
|
||||
'id': compute.id,
|
||||
'name': compute.name,
|
||||
'hostname': compute.hostname,
|
||||
'status': connection_manager.host_is_up(compute.type, compute.hostname),
|
||||
'type': compute.type,
|
||||
'login': compute.login,
|
||||
'password': compute.password,
|
||||
'details': compute.details,
|
||||
})
|
||||
return compute_data
|
||||
|
||||
|
||||
def get_userinstances_info(instance):
|
||||
info = {}
|
||||
uis = UserInstance.objects.filter(instance=instance)
|
||||
info['count'] = uis.count()
|
||||
if info['count'] > 0:
|
||||
info['first_user'] = uis[0]
|
||||
else:
|
||||
info['first_user'] = None
|
||||
return info
|
||||
|
||||
|
||||
def refr(compute):
|
||||
if compute.status is True:
|
||||
domains = compute.proxy.wvm.listAllDomains()
|
||||
domain_names = [d.name() for d in domains]
|
||||
# Delete instances that're not on host
|
||||
Instance.objects.filter(compute=compute).exclude(name__in=domain_names).delete()
|
||||
# Create instances that're not in DB
|
||||
names = Instance.objects.filter(compute=compute).values_list('name', flat=True)
|
||||
for domain in domains:
|
||||
if domain.name() not in names:
|
||||
Instance(compute=compute, name=domain.name(), uuid=domain.UUIDString()).save()
|
||||
|
||||
|
||||
def refresh_instance_database(comp, inst_name, info, all_host_vms, user):
|
||||
# Multiple Instance Name Check
|
||||
instances = Instance.objects.filter(name=inst_name)
|
||||
if instances.count() > 1:
|
||||
for i in instances:
|
||||
user_instances_count = UserInstance.objects.filter(instance=i).count()
|
||||
if user_instances_count == 0:
|
||||
addlogmsg(user.username, i.name, _("Deleting due to multiple(Instance Name) records."))
|
||||
i.delete()
|
||||
|
||||
# Multiple UUID Check
|
||||
instances = Instance.objects.filter(uuid=info['uuid'])
|
||||
if instances.count() > 1:
|
||||
for i in instances:
|
||||
if i.name != inst_name:
|
||||
addlogmsg(user.username, i.name, _("Deleting due to multiple(UUID) records."))
|
||||
i.delete()
|
||||
|
||||
try:
|
||||
inst_on_db = Instance.objects.get(compute_id=comp["id"], name=inst_name)
|
||||
if inst_on_db.uuid != info['uuid']:
|
||||
inst_on_db.save()
|
||||
|
||||
all_host_vms[comp["id"], comp["name"], comp["status"], comp["cpu"], comp["mem_size"],
|
||||
comp["mem_perc"]][inst_name]['is_template'] = inst_on_db.is_template
|
||||
all_host_vms[comp["id"], comp["name"], comp["status"], comp["cpu"], comp["mem_size"],
|
||||
comp["mem_perc"]][inst_name]['userinstances'] = get_userinstances_info(inst_on_db)
|
||||
except Instance.DoesNotExist:
|
||||
inst_on_db = Instance(compute_id=comp["id"], name=inst_name, uuid=info['uuid'])
|
||||
inst_on_db.save()
|
||||
|
||||
|
||||
def get_user_instances(user):
|
||||
all_user_vms = {}
|
||||
user_instances = UserInstance.objects.filter(user=user)
|
||||
for usr_inst in user_instances:
|
||||
if connection_manager.host_is_up(usr_inst.instance.compute.type, usr_inst.instance.compute.hostname):
|
||||
conn = wvmHostDetails(
|
||||
usr_inst.instance.compute.hostname,
|
||||
usr_inst.instance.compute.login,
|
||||
usr_inst.instance.compute.password,
|
||||
usr_inst.instance.compute.type,
|
||||
)
|
||||
all_user_vms[usr_inst] = conn.get_user_instances(usr_inst.instance.name)
|
||||
all_user_vms[usr_inst].update({'compute_id': usr_inst.instance.compute.id})
|
||||
return all_user_vms
|
||||
|
||||
|
||||
def get_host_instances(compute):
|
||||
all_host_vms = OrderedDict()
|
||||
|
||||
# if compute.status:
|
||||
comp_node_info = compute.proxy.get_node_info()
|
||||
comp_mem = compute.proxy.get_memory_usage()
|
||||
comp_instances = compute.proxy.get_host_instances(True)
|
||||
|
||||
# if comp_instances:
|
||||
comp_info = {
|
||||
"id": compute.id,
|
||||
"name": compute.name,
|
||||
"status": compute.status,
|
||||
"cpu": comp_node_info[3],
|
||||
"mem_size": comp_node_info[2],
|
||||
"mem_perc": comp_mem['percent'],
|
||||
}
|
||||
# refr(compute)
|
||||
all_host_vms[comp_info["id"], comp_info["name"], comp_info["status"], comp_info["cpu"], comp_info["mem_size"],
|
||||
comp_info["mem_perc"]] = comp_instances
|
||||
for vm, info in comp_instances.items():
|
||||
# TODO: Delete this function completely
|
||||
refresh_instance_database(comp_info, vm, info, all_host_vms, User.objects.get(pk=1))
|
||||
|
||||
# else:
|
||||
# raise libvirtError(_(f"Problem occurred with host: {compute.name} - {status}"))
|
||||
return all_host_vms
|
||||
|
||||
|
||||
def get_dhcp_mac_address(vname):
|
||||
dhcp_file = settings.BASE_DIR + '/dhcpd.conf'
|
||||
mac = ''
|
||||
if os.path.isfile(dhcp_file):
|
||||
with open(dhcp_file, 'r') as f:
|
||||
name_found = False
|
||||
for line in f:
|
||||
if "host %s." % vname in line:
|
||||
name_found = True
|
||||
if name_found and "hardware ethernet" in line:
|
||||
mac = line.split(' ')[-1].strip().strip(';')
|
||||
break
|
||||
return mac
|
||||
|
||||
|
||||
def get_random_mac_address():
|
||||
mac = '52:54:00:%02x:%02x:%02x' % (
|
||||
random.randint(0x00, 0xff),
|
||||
random.randint(0x00, 0xff),
|
||||
random.randint(0x00, 0xff),
|
||||
)
|
||||
return mac
|
||||
|
||||
|
||||
def get_clone_disk_name(disk, prefix, clone_name=''):
|
||||
if not disk['image']:
|
||||
return None
|
||||
if disk['image'].startswith(prefix) and clone_name:
|
||||
suffix = disk['image'][len(prefix):]
|
||||
image = f"{clone_name}{suffix}"
|
||||
elif "." in disk['image'] and len(disk['image'].rsplit(".", 1)[1]) <= 7:
|
||||
name, suffix = disk['image'].rsplit(".", 1)
|
||||
image = f"{name}-clone.{suffix}"
|
||||
else:
|
||||
image = f"{disk['image']}-clone"
|
||||
return image
|
||||
|
||||
|
||||
# TODO: this function is not used
|
||||
def _get_clone_disks(disks, vname=''):
|
||||
clone_disks = []
|
||||
for disk in disks:
|
||||
new_image = get_clone_disk_name(disk, vname)
|
||||
if not new_image:
|
||||
continue
|
||||
new_disk = {
|
||||
'dev': disk['dev'],
|
||||
'storage': disk['storage'],
|
||||
'image': new_image,
|
||||
'format': disk['format'],
|
||||
}
|
||||
clone_disks.append(new_disk)
|
||||
return clone_disks
|
2629
instances/views.py
2629
instances/views.py
File diff suppressed because it is too large
Load diff
|
@ -4,8 +4,14 @@ function filter_table() {
|
|||
$('.searchable tr').filter(function () {
|
||||
return rex.test($(this).text());
|
||||
}).show();
|
||||
Cookies.set("instances_filter", $(this).val(), { expires: 1 });
|
||||
}
|
||||
$(document).ready(function () {
|
||||
instances_filter_cookie = Cookies.get("instances_filter");
|
||||
if (instances_filter_cookie) {
|
||||
$('#filter').val(instances_filter_cookie);
|
||||
$('#filter').each(filter_table);
|
||||
}
|
||||
(function ($) {
|
||||
$('#filter').keyup(filter_table)
|
||||
}(jQuery));
|
||||
|
|
|
@ -4,12 +4,9 @@ from django.utils.translation import ugettext_lazy as _
|
|||
|
||||
|
||||
class AddStgPool(forms.Form):
|
||||
name = forms.CharField(error_messages={'required': _('No pool name has been entered')},
|
||||
max_length=20)
|
||||
name = forms.CharField(error_messages={'required': _('No pool name has been entered')}, max_length=20)
|
||||
stg_type = forms.CharField(max_length=10)
|
||||
target = forms.CharField(error_messages={'required': _('No path has been entered')},
|
||||
max_length=100,
|
||||
required=False)
|
||||
target = forms.CharField(error_messages={'required': _('No path has been entered')}, max_length=100, required=False)
|
||||
source = forms.CharField(max_length=100, required=False)
|
||||
ceph_user = forms.CharField(required=False)
|
||||
ceph_host = forms.CharField(required=False)
|
||||
|
@ -51,11 +48,9 @@ class AddStgPool(forms.Form):
|
|||
return source
|
||||
|
||||
|
||||
class AddImage(forms.Form):
|
||||
class CreateVolumeForm(forms.Form):
|
||||
name = forms.CharField(max_length=120)
|
||||
format = forms.ChoiceField(required=True, choices=(('qcow2', 'qcow2 (recommended)'),
|
||||
('qcow', 'qcow'),
|
||||
('raw', 'raw')))
|
||||
format = forms.ChoiceField(required=True, choices=(('qcow2', 'qcow2 (recommended)'), ('qcow', 'qcow'), ('raw', 'raw')))
|
||||
size = forms.IntegerField()
|
||||
meta_prealloc = forms.BooleanField(required=False)
|
||||
|
||||
|
@ -64,8 +59,6 @@ class AddImage(forms.Form):
|
|||
have_symbol = re.match('^[a-zA-Z0-9._-]+$', name)
|
||||
if not have_symbol:
|
||||
raise forms.ValidationError(_('The image name must not contain any special characters'))
|
||||
elif len(name) > 120:
|
||||
raise forms.ValidationError(_('The image name must not exceed 120 characters'))
|
||||
return name
|
||||
|
||||
|
||||
|
@ -73,9 +66,7 @@ class CloneImage(forms.Form):
|
|||
name = forms.CharField(max_length=120)
|
||||
image = forms.CharField(max_length=120)
|
||||
convert = forms.BooleanField(required=False)
|
||||
format = forms.ChoiceField(required=False, choices=(('qcow2', 'qcow2 (recommended)'),
|
||||
('qcow', 'qcow'),
|
||||
('raw', 'raw')))
|
||||
format = forms.ChoiceField(required=False, choices=(('qcow2', 'qcow2 (recommended)'), ('qcow', 'qcow'), ('raw', 'raw')))
|
||||
meta_prealloc = forms.BooleanField(required=False)
|
||||
|
||||
def clean_name(self):
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{% load i18n %}
|
||||
{% load bootstrap4 %}
|
||||
{% if request.user.is_superuser %}
|
||||
{% if state != 0 %}
|
||||
{% if pool == "iso" %}
|
||||
|
@ -46,42 +47,15 @@
|
|||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form method="post" role="form" aria-label="Create volume to storage form">{% csrf_token %}
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">{% trans "Name" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="text" class="form-control" name="name" placeholder="{% trans "Name" %}" required pattern="[a-zA-Z0-9\.\-_]+">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row" id="image_format">
|
||||
<label class="col-sm-3 col-form-label">{% trans "Format" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<select name="format" class="form-control image-format">
|
||||
<option value="qcow2">{% trans "qcow2" %}</option>
|
||||
<option value="qcow">{% trans "qcow" %}</option>
|
||||
<option value="raw">{% trans "raw" %}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">{% trans "Size" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="text" class="form-control" name="size" value="10" maxlength="3" required pattern="[0-9]+">
|
||||
</div>
|
||||
<label class="col-sm-1 col-form-label">{% trans "GB" %}</label>
|
||||
</div>
|
||||
<div class="form-group row meta-prealloc">
|
||||
<label class="col-sm-3 col-form-label">{% trans "Metadata" %}</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="checkbox" name="meta_prealloc" value="true">
|
||||
</div>
|
||||
</div>
|
||||
<form action="{% url 'create_volume' compute_id pool %}" id="create-volume" method="post" role="form" aria-label="Create volume to storage form">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form layout='horizontal' %}
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">{% trans "Close" %}</button>
|
||||
<button type="submit" class="btn btn-primary" name="add_volume">{% trans "Create" %}</button>
|
||||
<button type="submit" form="create-volume" class="btn btn-primary">{% trans "Create" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div> <!-- /.modal-content -->
|
||||
</div> <!-- /.modal-dialog -->
|
||||
</div> <!-- /.modal -->
|
||||
|
|
|
@ -40,7 +40,6 @@
|
|||
<!-- /.row -->
|
||||
|
||||
{% include 'errors_block.html' %}
|
||||
{% include 'messages_block.html' %}
|
||||
|
||||
<dl class="ml-3 row">
|
||||
<dt class="col-6">{% trans "Pool name" %}</dt>
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
|
||||
class StoragesTestCase(TestCase):
|
||||
def setUp(self):
|
||||
pass
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
import json
|
||||
|
||||
from django.contrib import messages
|
||||
from django.http import HttpResponseRedirect, HttpResponse
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.http import HttpResponse, HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from libvirt import libvirtError
|
||||
|
||||
from admin.decorators import superuser_only
|
||||
from computes.models import Compute
|
||||
from appsettings.models import AppSettings
|
||||
from storages.forms import AddStgPool, AddImage, CloneImage
|
||||
from appsettings.settings import app_settings
|
||||
from computes.models import Compute
|
||||
from storages.forms import AddStgPool, CloneImage, CreateVolumeForm
|
||||
from vrtManager.storage import wvmStorage, wvmStorages
|
||||
|
||||
|
||||
|
@ -79,11 +81,10 @@ def storage(request, compute_id, pool):
|
|||
destination.write(chunk)
|
||||
destination.close()
|
||||
|
||||
error_messages = []
|
||||
compute = get_object_or_404(Compute, pk=compute_id)
|
||||
meta_prealloc = False
|
||||
form = CreateVolumeForm()
|
||||
|
||||
try:
|
||||
conn = wvmStorage(compute.hostname, compute.login, compute.password, compute.type, pool)
|
||||
|
||||
storages = conn.get_storages()
|
||||
|
@ -104,72 +105,34 @@ def storage(request, compute_id, pool):
|
|||
volumes = conn.update_volumes()
|
||||
else:
|
||||
volumes = None
|
||||
except libvirtError as lib_err:
|
||||
error_messages.append(lib_err)
|
||||
|
||||
if request.method == 'POST':
|
||||
if 'start' in request.POST:
|
||||
try:
|
||||
conn.start()
|
||||
return HttpResponseRedirect(request.get_full_path())
|
||||
except libvirtError as lib_err:
|
||||
error_messages.append(lib_err)
|
||||
if 'stop' in request.POST:
|
||||
try:
|
||||
conn.stop()
|
||||
return HttpResponseRedirect(request.get_full_path())
|
||||
except libvirtError as lib_err:
|
||||
error_messages.append(lib_err)
|
||||
if 'delete' in request.POST:
|
||||
try:
|
||||
conn.delete()
|
||||
return HttpResponseRedirect(reverse('storages', args=[compute_id]))
|
||||
except libvirtError as lib_err:
|
||||
error_messages.append(lib_err)
|
||||
if 'set_autostart' in request.POST:
|
||||
try:
|
||||
conn.set_autostart(1)
|
||||
return HttpResponseRedirect(request.get_full_path())
|
||||
except libvirtError as lib_err:
|
||||
error_messages.append(lib_err)
|
||||
if 'unset_autostart' in request.POST:
|
||||
try:
|
||||
conn.set_autostart(0)
|
||||
return HttpResponseRedirect(request.get_full_path())
|
||||
except libvirtError as lib_err:
|
||||
error_messages.append(lib_err)
|
||||
if 'add_volume' in request.POST:
|
||||
form = AddImage(request.POST)
|
||||
if form.is_valid():
|
||||
data = form.cleaned_data
|
||||
if data['meta_prealloc'] and data['format'] == 'qcow2':
|
||||
meta_prealloc = True
|
||||
try:
|
||||
disk_owner = AppSettings.objects.filter(key__startswith="INSTANCE_VOLUME_DEFAULT_OWNER")
|
||||
disk_owner_uid = int(disk_owner.get(key="INSTANCE_VOLUME_DEFAULT_OWNER_UID").value)
|
||||
disk_owner_gid = int(disk_owner.get(key="INSTANCE_VOLUME_DEFAULT_OWNER_GID").value)
|
||||
|
||||
name = conn.create_volume(data['name'], data['size'], data['format'], meta_prealloc, disk_owner_uid, disk_owner_gid)
|
||||
messages.success(request, _(f"Image file {name} is created successfully"))
|
||||
return HttpResponseRedirect(request.get_full_path())
|
||||
except libvirtError as lib_err:
|
||||
error_messages.append(lib_err)
|
||||
else:
|
||||
for msg_err in form.errors.values():
|
||||
error_messages.append(msg_err.as_text())
|
||||
if 'del_volume' in request.POST:
|
||||
volname = request.POST.get('volname', '')
|
||||
try:
|
||||
vol = conn.get_volume(volname)
|
||||
vol.delete(0)
|
||||
messages.success(request, _(f"Volume: {volname} is deleted."))
|
||||
return HttpResponseRedirect(request.get_full_path())
|
||||
except libvirtError as lib_err:
|
||||
error_messages.append(lib_err)
|
||||
return redirect(reverse('storage', args=[compute.id, pool]))
|
||||
# return HttpResponseRedirect(request.get_full_path())
|
||||
if 'iso_upload' in request.POST:
|
||||
if str(request.FILES['file']) in conn.update_volumes():
|
||||
error_msg = _("ISO image already exist")
|
||||
error_messages.append(error_msg)
|
||||
messages.error(request, error_msg)
|
||||
else:
|
||||
handle_uploaded_file(path, request.FILES['file'])
|
||||
messages.success(request, _(f"ISO: {request.FILES['file']} is uploaded."))
|
||||
|
@ -182,8 +145,7 @@ def storage(request, compute_id, pool):
|
|||
meta_prealloc = 0
|
||||
if img_name in conn.update_volumes():
|
||||
msg = _("Name of volume already in use")
|
||||
error_messages.append(msg)
|
||||
if not error_messages:
|
||||
messages.error(request, msg)
|
||||
if data['convert']:
|
||||
format = data['format']
|
||||
if data['meta_prealloc'] and data['format'] == 'qcow2':
|
||||
|
@ -195,16 +157,50 @@ def storage(request, compute_id, pool):
|
|||
messages.success(request, _(f"{data['image']} image cloned as {name} successfully"))
|
||||
return HttpResponseRedirect(request.get_full_path())
|
||||
except libvirtError as lib_err:
|
||||
error_messages.append(lib_err)
|
||||
messages.error(request, lib_err)
|
||||
else:
|
||||
for msg_err in form.errors.values():
|
||||
error_messages.append(msg_err.as_text())
|
||||
messages.error(request, msg_err.as_text())
|
||||
|
||||
conn.close()
|
||||
|
||||
return render(request, 'storage.html', locals())
|
||||
|
||||
|
||||
@superuser_only
|
||||
def create_volume(request, compute_id, pool):
|
||||
compute = get_object_or_404(Compute, pk=compute_id)
|
||||
meta_prealloc = False
|
||||
|
||||
conn = wvmStorage(compute.hostname, compute.login, compute.password, compute.type, pool)
|
||||
|
||||
storages = conn.get_storages()
|
||||
|
||||
form = CreateVolumeForm(request.POST or None)
|
||||
if form.is_valid():
|
||||
data = form.cleaned_data
|
||||
if data['meta_prealloc'] and data['format'] == 'qcow2':
|
||||
meta_prealloc = True
|
||||
|
||||
disk_owner_uid = int(app_settings.INSTANCE_VOLUME_DEFAULT_OWNER_UID)
|
||||
disk_owner_gid = int(app_settings.INSTANCE_VOLUME_DEFAULT_OWNER_GID)
|
||||
|
||||
name = conn.create_volume(
|
||||
data['name'],
|
||||
data['size'],
|
||||
data['format'],
|
||||
meta_prealloc,
|
||||
disk_owner_uid,
|
||||
disk_owner_gid,
|
||||
)
|
||||
messages.success(request, _(f"Image file {name} is created successfully"))
|
||||
else:
|
||||
for msg_err in form.errors.values():
|
||||
messages.error(request, msg_err.as_text())
|
||||
|
||||
return redirect(reverse('storage', args=[compute.id, pool]))
|
||||
|
||||
|
||||
def get_volumes(request, compute_id, pool):
|
||||
"""
|
||||
:param request:
|
||||
|
|
|
@ -37,11 +37,21 @@
|
|||
{% include 'navbar.html' %}
|
||||
|
||||
<div role="main" class="container">
|
||||
<div class"row">
|
||||
<div class="col-sm-12">
|
||||
<div class="float-right">
|
||||
{% block page_header_extra %}
|
||||
{% endblock page_header_extra %}
|
||||
</div>
|
||||
<h3 class="page-header">
|
||||
{% block page_header %}
|
||||
{% endblock page_header %}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
{% bootstrap_messages %}
|
||||
{% block content %}{% endblock %}
|
||||
|
||||
</div> <!-- /container -->
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Bootstrap core JavaScript
|
||||
================================================== -->
|
||||
|
|
|
@ -5,20 +5,18 @@
|
|||
|
||||
{% block title %}{{ title }}{% endblock %}
|
||||
|
||||
{% block page_header %}{{ title }}{% endblock page_header %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<h2 class="page-header">{{ title }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
{% bootstrap_messages %}
|
||||
<div class="row">
|
||||
<div class="thumbnail col-sm-10 offset-1">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form id="create-update" action="" method="post">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form layout='horizontal' %}
|
||||
</form>
|
||||
<div class="form-group float-right">
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<div class="form-group mb-0 float-right">
|
||||
<a class="btn btn-primary" href="javascript:history.back()">{% icon 'times' %} {% trans "Cancel" %}</a>
|
||||
<button type="submit" form="create-update" class="btn btn-success">
|
||||
{% icon 'check' %} {% trans "Save" %}
|
||||
|
|
|
@ -7,7 +7,6 @@ from vrtManager.rwlock import ReadWriteLock
|
|||
from django.conf import settings
|
||||
from libvirt import libvirtError
|
||||
|
||||
|
||||
CONN_SOCKET = 4
|
||||
CONN_TLS = 3
|
||||
CONN_SSH = 2
|
||||
|
@ -19,7 +18,6 @@ TCP_PORT = 16509
|
|||
|
||||
class wvmEventLoop(threading.Thread):
|
||||
""" Event Loop Class"""
|
||||
|
||||
def __init__(self, group=None, target=None, name=None, args=(), kwargs={}):
|
||||
# register the default event implementation
|
||||
# of libvirt, as we do not have an existing
|
||||
|
@ -48,7 +46,6 @@ class wvmConnection(object):
|
|||
class representing a single connection stored in the Connection Manager
|
||||
# to-do: may also need some locking to ensure to not connect simultaniously in 2 threads
|
||||
"""
|
||||
|
||||
def __init__(self, host, login, passwd, conn):
|
||||
"""
|
||||
Sets all class attributes and tries to open the connection
|
||||
|
@ -208,7 +205,7 @@ class wvmConnection(object):
|
|||
except:
|
||||
pass
|
||||
|
||||
def __unicode__(self):
|
||||
def __str__(self):
|
||||
if self.type == CONN_TCP:
|
||||
type_str = 'tcp'
|
||||
elif self.type == CONN_SSH:
|
||||
|
@ -332,7 +329,7 @@ class wvmConnectionManager(object):
|
|||
|
||||
connection_manager = wvmConnectionManager(
|
||||
settings.LIBVIRT_KEEPALIVE_INTERVAL if hasattr(settings, 'LIBVIRT_KEEPALIVE_INTERVAL') else 5,
|
||||
settings.LIBVIRT_KEEPALIVE_COUNT if hasattr(settings, 'LIBVIRT_KEEPALIVE_COUNT') else 5
|
||||
settings.LIBVIRT_KEEPALIVE_COUNT if hasattr(settings, 'LIBVIRT_KEEPALIVE_COUNT') else 5,
|
||||
)
|
||||
|
||||
|
||||
|
@ -374,9 +371,11 @@ class wvmConnect(object):
|
|||
|
||||
result["machines"] = []
|
||||
for m in arch_el.xpath("machine"):
|
||||
result["machines"].append({"machine": m.text,
|
||||
result["machines"].append({
|
||||
"machine": m.text,
|
||||
"max_cpu": m.get("maxCpus"),
|
||||
"canonical": m.get("canonical")})
|
||||
"canonical": m.get("canonical")
|
||||
})
|
||||
|
||||
guest_el = arch_el.getparent()
|
||||
for f in guest_el.xpath("features"):
|
||||
|
@ -385,6 +384,7 @@ class wvmConnect(object):
|
|||
result["os_type"] = guest_el.find("os_type").text
|
||||
|
||||
return result
|
||||
|
||||
return util.get_xml_path(self.get_cap_xml(), func=guests)
|
||||
|
||||
def get_dom_capabilities(self, arch, machine):
|
||||
|
@ -560,6 +560,7 @@ class wvmConnect(object):
|
|||
arch_name = arch.xpath('@name')[0]
|
||||
result[arch_name] = domain_types
|
||||
return result
|
||||
|
||||
return util.get_xml_path(self.get_cap_xml(), func=hypervisors)
|
||||
|
||||
def get_hypervisors_machines(self):
|
||||
|
@ -573,6 +574,7 @@ class wvmConnect(object):
|
|||
|
||||
result[arch] = self.get_machine_types(arch)
|
||||
return result
|
||||
|
||||
return util.get_xml_path(self.get_cap_xml(), func=machines)
|
||||
|
||||
def get_emulator(self, arch):
|
||||
|
@ -607,6 +609,7 @@ class wvmConnect(object):
|
|||
arch_name = arch.xpath('@name')[0]
|
||||
result[arch_name] = emulator
|
||||
return result
|
||||
|
||||
return util.get_xml_path(self.get_cap_xml(), func=emulators)
|
||||
|
||||
def get_os_loaders(self, arch='x86_64', machine='pc'):
|
||||
|
@ -617,6 +620,7 @@ class wvmConnect(object):
|
|||
"""
|
||||
def get_os_loaders(ctx):
|
||||
return [v.text for v in ctx.xpath("/domainCapabilities/os/loader[@supported='yes']/value")]
|
||||
|
||||
return util.get_xml_path(self.get_dom_cap_xml(arch, machine), func=get_os_loaders)
|
||||
|
||||
def get_os_loader_enums(self, arch, machine):
|
||||
|
@ -632,6 +636,7 @@ class wvmConnect(object):
|
|||
path = "/domainCapabilities/os/loader[@supported='yes']/enum[@name='{}']/value".format(enum)
|
||||
result[enum] = [v.text for v in ctx.xpath(path)]
|
||||
return result
|
||||
|
||||
return util.get_xml_path(self.get_dom_cap_xml(arch, machine), func=get_os_loader_enums)
|
||||
|
||||
def get_disk_bus_types(self, arch, machine):
|
||||
|
@ -642,6 +647,7 @@ class wvmConnect(object):
|
|||
"""
|
||||
def get_bus_list(ctx):
|
||||
return [v.text for v in ctx.xpath("/domainCapabilities/devices/disk/enum[@name='bus']/value")]
|
||||
|
||||
# return [ 'ide', 'scsi', 'usb', 'virtio' ]
|
||||
return util.get_xml_path(self.get_dom_cap_xml(arch, machine), func=get_bus_list)
|
||||
|
||||
|
@ -653,6 +659,7 @@ class wvmConnect(object):
|
|||
"""
|
||||
def get_device_list(ctx):
|
||||
return [v.text for v in ctx.xpath("/domainCapabilities/devices/disk/enum[@name='diskDevice']/value")]
|
||||
|
||||
# return [ 'disk', 'cdrom', 'floppy', 'lun' ]
|
||||
return util.get_xml_path(self.get_dom_cap_xml(arch, machine), func=get_device_list)
|
||||
|
||||
|
@ -664,6 +671,7 @@ class wvmConnect(object):
|
|||
"""
|
||||
def get_graphics_list(ctx):
|
||||
return [v.text for v in ctx.xpath("/domainCapabilities/devices/graphics/enum[@name='type']/value")]
|
||||
|
||||
return util.get_xml_path(self.get_dom_cap_xml(arch, machine), func=get_graphics_list)
|
||||
|
||||
def get_cpu_modes(self, arch, machine):
|
||||
|
@ -674,6 +682,7 @@ class wvmConnect(object):
|
|||
"""
|
||||
def get_cpu_modes(ctx):
|
||||
return [v for v in ctx.xpath("/domainCapabilities/cpu/mode[@supported='yes']/@name")]
|
||||
|
||||
return util.get_xml_path(self.get_dom_cap_xml(arch, machine), func=get_cpu_modes)
|
||||
|
||||
def get_cpu_custom_types(self, arch, machine):
|
||||
|
@ -688,6 +697,7 @@ class wvmConnect(object):
|
|||
result = [v.text for v in ctx.xpath(usable_yes)]
|
||||
result += [v.text for v in ctx.xpath(usable_unknown)]
|
||||
return result
|
||||
|
||||
return util.get_xml_path(self.get_dom_cap_xml(arch, machine), func=get_custom_list)
|
||||
|
||||
def get_hostdev_modes(self, arch, machine):
|
||||
|
@ -698,6 +708,7 @@ class wvmConnect(object):
|
|||
"""
|
||||
def get_hostdev_list(ctx):
|
||||
return [v.text for v in ctx.xpath("/domainCapabilities/devices/hostdev/enum[@name='mode']/value")]
|
||||
|
||||
return util.get_xml_path(self.get_dom_cap_xml(arch, machine), func=get_hostdev_list)
|
||||
|
||||
def get_hostdev_startup_policies(self, arch, machine):
|
||||
|
@ -708,6 +719,7 @@ class wvmConnect(object):
|
|||
"""
|
||||
def get_hostdev_list(ctx):
|
||||
return [v.text for v in ctx.xpath("/domainCapabilities/devices/hostdev/enum[@name='startupPolicy']/value")]
|
||||
|
||||
return util.get_xml_path(self.get_dom_cap_xml(arch, machine), func=get_hostdev_list)
|
||||
|
||||
def get_hostdev_subsys_types(self, arch, machine):
|
||||
|
@ -718,6 +730,7 @@ class wvmConnect(object):
|
|||
"""
|
||||
def get_hostdev_list(ctx):
|
||||
return [v.text for v in ctx.xpath("/domainCapabilities/devices/hostdev/enum[@name='subsysType']/value")]
|
||||
|
||||
return util.get_xml_path(self.get_dom_cap_xml(arch, machine), func=get_hostdev_list)
|
||||
|
||||
def get_network_models(self):
|
||||
|
@ -751,6 +764,7 @@ class wvmConnect(object):
|
|||
for values in video_enum:
|
||||
result.append(values.text)
|
||||
return result
|
||||
|
||||
return util.get_xml_path(self.get_dom_cap_xml(arch, machine), func=get_video_list)
|
||||
|
||||
def get_iface(self, name):
|
||||
|
@ -840,6 +854,7 @@ class wvmConnect(object):
|
|||
description = util.get_xpath(doc, "/domain/description")
|
||||
description = description if description else ''
|
||||
return mem, vcpu, title, description
|
||||
|
||||
for name in self.get_instances():
|
||||
dom = self.get_instance(name)
|
||||
xml = dom.XMLDesc(0)
|
||||
|
@ -871,6 +886,7 @@ class wvmConnect(object):
|
|||
description = util.get_xpath(ctx, "/domain/description")
|
||||
description = description if description else ''
|
||||
return mem, vcpu, title, description
|
||||
|
||||
(mem, vcpu, title, description) = util.get_xml_path(xml, func=get_info)
|
||||
return {
|
||||
'name': dom.name(),
|
||||
|
@ -929,8 +945,7 @@ class wvmConnect(object):
|
|||
"""
|
||||
Return True if libvirt advertises support for proper UEFI setup
|
||||
"""
|
||||
return ("readonly" in loader_enums and
|
||||
"yes" in loader_enums.get("readonly"))
|
||||
return ("readonly" in loader_enums and "yes" in loader_enums.get("readonly"))
|
||||
|
||||
def is_supports_virtio(self, arch, machine):
|
||||
if not self.is_qemu():
|
||||
|
|
|
@ -206,6 +206,8 @@ class wvmInstance(wvmConnect):
|
|||
cur_vcpu = util.get_xml_path(self._XMLDesc(0), "/domain/vcpu/@current")
|
||||
if cur_vcpu:
|
||||
return int(cur_vcpu)
|
||||
else:
|
||||
return self.get_vcpu()
|
||||
|
||||
def get_arch(self):
|
||||
return util.get_xml_path(self._XMLDesc(0), "/domain/os/type/@arch")
|
||||
|
@ -251,7 +253,6 @@ class wvmInstance(wvmConnect):
|
|||
return title if title else ''
|
||||
|
||||
def get_filterrefs(self):
|
||||
|
||||
def filterrefs(ctx):
|
||||
result = []
|
||||
for net in ctx.xpath('/domain/devices/interface'):
|
||||
|
@ -295,8 +296,7 @@ class wvmInstance(wvmConnect):
|
|||
for addr in addrs["addrs"]:
|
||||
if addr["type"] == 0:
|
||||
ipv4.append(addr["addr"])
|
||||
elif (addr["type"] == 1 and
|
||||
not str(addr["addr"]).startswith("fe80")):
|
||||
elif (addr["type"] == 1 and not str(addr["addr"]).startswith("fe80")):
|
||||
ipv6.append(addr["addr"] + "/" + str(addr["prefix"]))
|
||||
return ipv4, ipv6
|
||||
|
||||
|
@ -335,14 +335,12 @@ class wvmInstance(wvmConnect):
|
|||
return
|
||||
|
||||
if self.is_agent_ready():
|
||||
self._ip_cache["qemuga"] = self._get_interface_addresses(
|
||||
VIR_DOMAIN_INTERFACE_ADDRESSES_SRC_AGENT)
|
||||
self._ip_cache["qemuga"] = self._get_interface_addresses(VIR_DOMAIN_INTERFACE_ADDRESSES_SRC_AGENT)
|
||||
|
||||
arp_flag = 3 # libvirt."VIR_DOMAIN_INTERFACE_ADDRESSES_SRC_ARP"
|
||||
self._ip_cache["arp"] = self._get_interface_addresses(arp_flag)
|
||||
|
||||
def get_net_devices(self):
|
||||
|
||||
def networks(ctx):
|
||||
result = []
|
||||
inbound = outbound = []
|
||||
|
@ -370,7 +368,8 @@ class wvmInstance(wvmConnect):
|
|||
ipv4, ipv6 = self.get_interface_addresses(mac_inst)
|
||||
except libvirtError:
|
||||
ipv4, ipv6 = None, None
|
||||
result.append({'mac': mac_inst,
|
||||
result.append({
|
||||
'mac': mac_inst,
|
||||
'nic': nic_inst,
|
||||
'target': target_inst,
|
||||
'state': link_state,
|
||||
|
@ -400,7 +399,13 @@ class wvmInstance(wvmConnect):
|
|||
try:
|
||||
dev = disk.xpath('target/@dev')[0]
|
||||
bus = disk.xpath('target/@bus')[0]
|
||||
src_file = disk.xpath('source/@file|source/@dev|source/@name|source/@volume')[0]
|
||||
try:
|
||||
src_file = disk.xpath('source/@file|source/@dev|source/@name')[0]
|
||||
except Exception as e:
|
||||
v = disk.xpath('source/@volume')[0]
|
||||
s_name = disk.xpath('source/@pool')[0]
|
||||
s = self.wvm.storagePoolLookupByName(s_name)
|
||||
src_file = s.storageVolLookupByName(v).path()
|
||||
try:
|
||||
disk_format = disk.xpath('driver/@type')[0]
|
||||
except:
|
||||
|
@ -436,14 +441,26 @@ class wvmInstance(wvmConnect):
|
|||
storage = stg.name()
|
||||
except libvirtError:
|
||||
volume = src_file
|
||||
except:
|
||||
pass
|
||||
except Exception as e:
|
||||
print(f'Exception: {e}')
|
||||
finally:
|
||||
result.append(
|
||||
{'dev': dev, 'bus': bus, 'image': volume, 'storage': storage, 'path': src_file,
|
||||
'format': disk_format, 'size': disk_size, 'used': used_size,
|
||||
'cache': disk_cache, 'io': disk_io, 'discard': disk_discard, 'detect_zeroes': disk_zeroes,
|
||||
'readonly': readonly, 'shareable': shareable, 'serial': serial})
|
||||
result.append({
|
||||
'dev': dev,
|
||||
'bus': bus,
|
||||
'image': volume,
|
||||
'storage': storage,
|
||||
'path': src_file,
|
||||
'format': disk_format,
|
||||
'size': disk_size,
|
||||
'used': used_size,
|
||||
'cache': disk_cache,
|
||||
'io': disk_io,
|
||||
'discard': disk_discard,
|
||||
'detect_zeroes': disk_zeroes,
|
||||
'readonly': readonly,
|
||||
'shareable': shareable,
|
||||
'serial': serial
|
||||
})
|
||||
return result
|
||||
|
||||
return util.get_xml_path(self._XMLDesc(0), func=disks)
|
||||
|
@ -642,10 +659,21 @@ class wvmInstance(wvmConnect):
|
|||
xmldom = ElementTree.tostring(tree).decode()
|
||||
self._defineXML(xmldom)
|
||||
|
||||
def attach_disk(self, target_dev, source, target_bus='ide', disk_type='file',
|
||||
disk_device='disk', driver_name='qemu', driver_type='raw',
|
||||
readonly=False, shareable=False, serial=None,
|
||||
cache_mode=None, io_mode=None, discard_mode=None, detect_zeroes_mode=None):
|
||||
def attach_disk(self,
|
||||
target_dev,
|
||||
source,
|
||||
target_bus='ide',
|
||||
disk_type='file',
|
||||
disk_device='disk',
|
||||
driver_name='qemu',
|
||||
driver_type='raw',
|
||||
readonly=False,
|
||||
shareable=False,
|
||||
serial=None,
|
||||
cache_mode=None,
|
||||
io_mode=None,
|
||||
discard_mode=None,
|
||||
detect_zeroes_mode=None):
|
||||
|
||||
additionals = ''
|
||||
if cache_mode is not None and cache_mode != 'default' and disk_device != 'cdrom':
|
||||
|
@ -691,7 +719,8 @@ class wvmInstance(wvmConnect):
|
|||
if self.get_status() == 5:
|
||||
self.instance.detachDeviceFlags(xml_disk, VIR_DOMAIN_AFFECT_CONFIG)
|
||||
|
||||
def edit_disk(self, target_dev, source, readonly, shareable, target_bus, serial, format, cache_mode, io_mode, discard_mode, detect_zeroes_mode):
|
||||
def edit_disk(self, target_dev, source, readonly, shareable, target_bus, serial, format, cache_mode, io_mode, discard_mode,
|
||||
detect_zeroes_mode):
|
||||
tree = etree.fromstring(self._XMLDesc(0))
|
||||
disk_el = tree.xpath("./devices/disk/target[@dev='{}']".format(target_dev))[0].getparent()
|
||||
old_disk_type = disk_el.get('type')
|
||||
|
@ -928,8 +957,7 @@ class wvmInstance(wvmConnect):
|
|||
|
||||
def get_console_websocket_port(self):
|
||||
console_type = self.get_console_type()
|
||||
websocket_port = util.get_xml_path(self._XMLDesc(0),
|
||||
"/domain/devices/graphics[@type='%s']/@websocket" % console_type)
|
||||
websocket_port = util.get_xml_path(self._XMLDesc(0), "/domain/devices/graphics[@type='%s']/@websocket" % console_type)
|
||||
return websocket_port
|
||||
|
||||
def get_console_passwd(self):
|
||||
|
@ -1421,7 +1449,13 @@ class wvmInstance(wvmConnect):
|
|||
in_peak = in_qos.get('peak')
|
||||
in_burst = in_qos.get('burst')
|
||||
in_floor = in_qos.get('floor')
|
||||
bound_list.append({'direction': 'inbound', 'average': in_av, 'peak': in_peak, 'floor': in_floor, 'burst': in_burst})
|
||||
bound_list.append({
|
||||
'direction': 'inbound',
|
||||
'average': in_av,
|
||||
'peak': in_peak,
|
||||
'floor': in_floor,
|
||||
'burst': in_burst
|
||||
})
|
||||
|
||||
out_qos = band.find('outbound')
|
||||
if out_qos is not None:
|
||||
|
|
24
webvirtcloud/middleware.py
Normal file
24
webvirtcloud/middleware.py
Normal file
|
@ -0,0 +1,24 @@
|
|||
import json
|
||||
|
||||
from django.contrib import messages
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import render
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from libvirt import libvirtError
|
||||
|
||||
|
||||
class ExceptionMiddleware:
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
return self.get_response(request)
|
||||
|
||||
def process_exception(self, request, exception):
|
||||
if isinstance(exception, libvirtError):
|
||||
messages.error(
|
||||
request,
|
||||
_('libvirt Error - %(exception)s') % {'exception': exception},
|
||||
)
|
||||
return render(request, '500.html', status=500)
|
||||
# TODO: check connecting to host via VPN
|
|
@ -28,7 +28,6 @@ INSTALLED_APPS = [
|
|||
'appsettings',
|
||||
'computes',
|
||||
'console',
|
||||
'create',
|
||||
'datasource',
|
||||
'networks',
|
||||
'instances',
|
||||
|
@ -51,6 +50,7 @@ MIDDLEWARE = [
|
|||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
'appsettings.middleware.AppSettingsMiddleware',
|
||||
'webvirtcloud.middleware.ExceptionMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'webvirtcloud.urls'
|
||||
|
|
Loading…
Reference in a new issue