1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2024-12-24 15:15:22 +00:00

Merge pull request #322 from Real-Gecko/master

Minor fixes and more tests
This commit is contained in:
Anatoliy Guskov 2020-06-17 12:01:58 +03:00 committed by GitHub
commit c791f582af
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
50 changed files with 808 additions and 712 deletions

View file

@ -1,26 +1,30 @@
import re
from django import forms
from django.forms import ModelForm, ValidationError
from django.utils.translation import ugettext_lazy as _
from django.contrib.auth.models import User
from django.conf import settings
from appsettings.models import AppSettings
from .models import UserInstance
class UserAddForm(forms.Form):
name = forms.CharField(label="Name",
error_messages={'required': _('No username has been entered')},
max_length=20)
password = forms.CharField(required=not settings.ALLOW_EMPTY_PASSWORD,
error_messages={'required': _('No password has been entered')},)
def clean_name(self):
name = self.cleaned_data['name']
have_symbol = re.match('^[a-z0-9]+$', name)
if not have_symbol:
raise forms.ValidationError(_('The flavor name must not contain any special characters'))
elif len(name) > 20:
raise forms.ValidationError(_('The flavor name must not exceed 20 characters'))
try:
User.objects.get(username=name)
except User.DoesNotExist:
return name
raise forms.ValidationError(_('Flavor name is already use'))
class UserInstanceForm(ModelForm):
def __init__(self, *args, **kwargs):
super(UserInstanceForm, self).__init__(*args, **kwargs)
# Make user and instance fields not editable after creation
instance = getattr(self, 'instance', None)
if instance and instance.id is not None:
self.fields['user'].disabled = True
self.fields['instance'].disabled = True
def clean_instance(self):
instance = self.cleaned_data['instance']
if AppSettings.objects.get(key="ALLOW_INSTANCE_MULTIPLE_OWNER").value == 'False':
exists = UserInstance.objects.filter(instance=instance)
if exists:
raise ValidationError(_('Instance owned by another user'))
return instance
class Meta:
model = UserInstance
fields = '__all__'

View file

@ -0,0 +1,44 @@
# Generated by Django 2.2.13 on 2020-06-15 06:37
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0003_auto_20200604_0930'),
]
operations = [
migrations.AlterField(
model_name='userattributes',
name='max_cpus',
field=models.IntegerField(default=2, help_text='-1 for unlimited. Any integer value', validators=[django.core.validators.MinValueValidator(-1)], verbose_name='max CPUs'),
),
migrations.AlterField(
model_name='userattributes',
name='max_disk_size',
field=models.IntegerField(default=20, help_text='-1 for unlimited. Any integer value', validators=[django.core.validators.MinValueValidator(-1)], verbose_name='max disk size'),
),
migrations.AlterField(
model_name='userattributes',
name='max_instances',
field=models.IntegerField(default=2, help_text='-1 for unlimited. Any integer value', validators=[django.core.validators.MinValueValidator(-1)], verbose_name='max instances'),
),
migrations.AlterField(
model_name='userattributes',
name='max_memory',
field=models.IntegerField(default=2048, help_text='-1 for unlimited. Any integer value', validators=[django.core.validators.MinValueValidator(-1)], verbose_name='max memory'),
),
migrations.AlterField(
model_name='usersshkey',
name='keyname',
field=models.CharField(max_length=25, verbose_name='key name'),
),
migrations.AlterField(
model_name='usersshkey',
name='keypublic',
field=models.CharField(max_length=500, verbose_name='public key'),
),
]

View file

@ -0,0 +1,20 @@
# Generated by Django 2.2.13 on 2020-06-16 10:39
from django.conf import settings
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('instances', '0003_auto_20200615_0637'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('accounts', '0004_auto_20200615_0637'),
]
operations = [
migrations.AlterUniqueTogether(
name='userinstance',
unique_together={('user', 'instance')},
),
]

View file

@ -1,3 +1,4 @@
from django.conf import settings
from django.contrib.auth.models import User
from django.core.validators import MinValueValidator
from django.db import models
@ -13,8 +14,11 @@ class UserInstance(models.Model):
is_delete = models.BooleanField(default=False)
is_vnc = models.BooleanField(default=False)
def __unicode__(self):
return self.instance.name
def __str__(self):
return _('Instance "%(inst)s" of user %(user)s') % {'inst': self.instance, 'user': self.user}
class Meta:
unique_together = ['user', 'instance']
class UserSSHKey(models.Model):
@ -22,7 +26,7 @@ class UserSSHKey(models.Model):
keyname = models.CharField(_('key name'), max_length=25)
keypublic = models.CharField(_('public key'), max_length=500)
def __unicode__(self):
def __str__(self):
return self.keyname
@ -54,29 +58,7 @@ class UserAttributes(models.Model):
validators=[MinValueValidator(-1)],
)
@staticmethod
def create_missing_userattributes(user):
try:
userattributes = user.userattributes
except UserAttributes.DoesNotExist:
userattributes = UserAttributes(user=user)
userattributes.save()
@staticmethod
def add_default_instances(user):
existing_instances = UserInstance.objects.filter(user=user)
if not existing_instances:
for instance_name in settings.NEW_USER_DEFAULT_INSTANCES:
instance = Instance.objects.get(name=instance_name)
user_instance = UserInstance(user=user, instance=instance)
user_instance.save()
@staticmethod
def configure_user(user):
UserAttributes.create_missing_userattributes(user)
UserAttributes.add_default_instances(user)
def __unicode__(self):
def __str__(self):
return self.user.username

View file

@ -1,140 +1,83 @@
{% extends "base.html" %}
{% load i18n %}
{% block title %}{% trans "User" %} - {{ user }}{% endblock %}
{% load icons %}
{% block title %}{% trans "User Profile" %} - {{ user }}{% endblock %}
{% block content %}
<!-- Page Heading -->
<div class="row">
<div class="col-lg-12">
{% include 'create_user_inst_block.html' %}
<h2 class="page-header">{{ user }}</h2>
<a href="{% url 'user_instance_create' user.id %}" class="btn btn-success btn-header float-right">
{% icon 'plus' %}
</a>
<h2 class="page-header">{% trans "User Profile" %}: {{ user }}</h2>
</div>
</div>
<!-- /.row -->
{% include 'errors_block.html' %}
{% if request.user.is_superuser and publickeys %}
<div class="row">
<div class="col-lg-12">
<div class="table-responsive">
<table class="table table-bordered table-hover">
<thead>
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link active" data-toggle="tab" href="#instances">{% trans "Instances" %}</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#public-keys">{% trans "Public Keys" %}</a>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane active" id="instances">
<table class="table table-striped table-hover">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">{% trans "Instance" %}</th>
<th scope="col">{% trans "VNC" %}</th>
<th scope="col">{% trans "Resize" %}</th>
<th scope="col">{% trans "Delete" %}</th>
<th scope="colgroup" colspan="2">{% trans "Action" %}</th>
</tr>
</thead>
<tbody>
{% for inst in user_insts %}
<tr>
<th scope="col">{% trans "Key name" %}</th>
<th scope="col">{% trans "Public key" %}</th>
</tr>
</thead>
<tbody>
{% for publickey in publickeys %}
<tr>
<td>{{ publickey.keyname }}</td>
<td title="{{ publickey.keypublic }}">{{ publickey.keypublic|truncatechars:64 }}</td>
<td>{{ forloop.counter }}</td>
<td><a href="{% url 'instances:instance' inst.instance.compute.id inst.instance.name %}">{{ inst.instance.name }}</a></td>
<td>{{ inst.is_vnc }}</td>
<td>{{ inst.is_change }}</td>
<td>{{ inst.is_delete }}</td>
<td style="width:5px;">
<a href="{% url 'user_instance_update' inst.id %}" class="btn btn-sm btn-secondary" title="{% trans "edit" %}">
{% icon 'pencil' %}
</a>
</td>
<td style="width:5px;">
<a class="btn btn-sm btn-secondary" href="{% url 'user_instance_delete' inst.id %}" title="{% trans "Delete" %}">
{% icon 'trash' %}
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</tbody>
</table>
</div>
<div class="tab-pane fade" id="public-keys">
<table class="table table-striped table-hover">
<thead>
<tr>
<th scope="col">{% trans "Key name" %}</th>
<th scope="col">{% trans "Public key" %}</th>
</tr>
</thead>
<tbody>
{% for publickey in publickeys %}
<tr>
<td>{{ publickey.keyname }}</td>
<td title="{{ publickey.keypublic }}">{{ publickey.keypublic|truncatechars:64 }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
<div class="row">
<div class="col-lg-12">
{% if not user_insts %}
<div class="col-lg-12">
<div class="alert alert-warning alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
<i class="fa fa-exclamation-triangle"></i> <strong>{% trans "Warning" %}:</strong> {% trans "User doesn't have any Instance" %}
</div>
</div>
{% else %}
<div class="table-responsive">
<table class="table table-bordered table-hover">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">{% trans "Instance" %}</th>
<th scope="col">{% trans "VNC" %}</th>
<th scope="col">{% trans "Resize" %}</th>
<th scope="col">{% trans "Delete" %}</th>
<th scope="colgroup" colspan="2">{% trans "Action" %}</th>
</tr>
</thead>
<tbody>
{% for inst in user_insts %}
<tr>
<td>{{ forloop.counter }}</td>
<td><a href="{% url 'instance' inst.instance.compute.id inst.instance.name %}">{{ inst.instance.name }}</a></td>
<td>{{ inst.is_vnc }}</td>
<td>{{ inst.is_change }}</td>
<td>{{ inst.is_delete }}</td>
<td style="width:5px;">
<a href="#editPriv{{ forloop.counter }}" type="button" class="btn btn-sm btn-secondary" data-toggle="modal">
<span class="fa fa-pencil" aria-hidden="true"></span>
</a>
<!-- Modal pool -->
<div class="modal fade" id="editPriv{{ forloop.counter }}" tabindex="-1" role="dialog" aria-labelledby="editPrivLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{% trans "Edit privilegies for" %} {{ inst.instance.name }}</h5>
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
</div>
<div class="modal-body">
<form method="post" action="" role="form" aria-label="Edit privileges form">{% csrf_token %}
<input type="hidden" name="user_inst" value="{{ inst.id }}">
<div class="form-group row">
<label class="col-sm-4 col-form-label">{% trans "VNC" %}</label>
<div class="col-sm-6">
<select class="form-control" name="inst_vnc">
<option value="">{% trans 'False' %}</option>
<option value="1" {% if inst.is_vnc %}selected{% endif %}>True</option>
</select>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">{% trans "Resize" %}</label>
<div class="col-sm-6">
<select class="form-control" name="inst_change">
<option value="">{% trans 'False' %}</option>
<option value="1" {% if inst.is_change %}selected{% endif %}>True</option>
</select>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">{% trans "Delete" %}</label>
<div class="col-sm-6">
<select class="form-control" name="inst_delete">
<option value="">{% trans 'False' %}</option>
<option value="1" {% if inst.is_delete %}selected{% endif %}>True</option>
</select>
</div>
</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="permission">{% trans "Edit" %}</button>
</div>
</form>
</div> <!-- /.modal-content -->
</div> <!-- /.modal-dialog -->
</div> <!-- /.modal -->
</td>
<td style="width:5px;">
<form action="" method="post" role="form" aria-label="Delete user form">{% csrf_token %}
<input type="hidden" name="user_inst" value="{{ inst.id }}">
<button type="submit" class="btn btn-sm btn-secondary" name="delete" title="{% trans "Delete" %}" onclick="return confirm('{% trans "Are you sure?" %}')">
<span class="fa fa-trash" aria-hidden="true"></span>
</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</div>
</div>
{% endblock %}
{% endblock content %}

View file

@ -0,0 +1,29 @@
{% extends "base.html" %}
{% load bootstrap4 %}
{% load i18n %}
{% load icons %}
{% block title %}{%trans "Change Password" %}{% endblock title %}
{% block content %}
<div class="card">
<div class="card-header">
<h4 class="card-title">{%trans "Change Password" %}: {{ user }}</h4>
</div>
<div class="card-body">
<form method="post" id="password-change">
{% csrf_token %}
{% bootstrap_form form layout='horizontal' %}
</form>
</div>
<div class="card-footer">
<div class="float-right">
<a class="btn btn-primary" href="javascript:history.back()">{% icon 'times' %} {% trans "Cancel" %}</a>
<button type="submit" form="password-change" class="btn btn-success">
{% icon 'check' %} {% trans "Change" %}
</button>
</div>
</div>
</div>
{% endblock content %}

View file

@ -1,36 +0,0 @@
{% load i18n %}
{% if request.user.is_superuser %}
<a href="#addUserInst" type="button" class="btn btn-success btn-header float-right" data-toggle="modal">
<span class="fa fa-plus" aria-hidden="true"></span>
</a>
<!-- Modal pool -->
<div class="modal fade" id="addUserInst" tabindex="-1" role="dialog" aria-labelledby="addUserInstLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{% trans "Add Instance for User" %}</h5>
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
</div>
<div class="modal-body">
<form method="post" action="" role="form" aria-label="Add user instance form">{% csrf_token %}
<div class="form-group row">
<label class="col-sm-4 col-form-label">{% trans "Host" %} / {% trans "Instance" %}</label>
<div class="col-sm-6">
<select class="custom-select" name="inst_id">
{% for inst in instances %}
<option value="{{ inst.id }}">{{ inst.compute.name }} / {{ inst.name }}</option>
{% endfor %}
</select>
</div>
</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="add">{% trans "Add" %}</button>
</div>
</form>
</div> <!-- /.modal-content -->
</div> <!-- /.modal-dialog -->
</div> <!-- /.modal -->
{% endif %}

View file

@ -1,5 +1,6 @@
{% extends "base.html" %}
{% load i18n %}
{% load icons %}
{% load tags_fingerprint %}
{% block title %}{% trans "Profile" %}{% endblock %}
{% block content %}
@ -16,6 +17,9 @@
<div class="row">
<div class="col-lg-12">
<h3 class="page-header">{% trans "Edit Profile" %}</h3>
{% if perms.accounts.change_password %}
<a href="{% url 'change_password' %}" class="btn btn-primary">{% icon 'lock' %} {% trans "Change Password" %}</a>
{% endif %}
<form method="post" action="" role="form" aria-label="Edit user info form">{% csrf_token %}
<div class="form-group">
<label class="col-sm-2 col-form-label">{% trans "Login" %}</label>
@ -41,34 +45,6 @@
</div>
</div>
</form>
{% if perms.accounts.change_password %}
<h3 class="page-header">{% trans "Edit Password" %}</h3>
<form method="post" action="" role="form" aria-label="Edit user password form">{% csrf_token %}
<div class="form-group">
<label class="col-sm-2 col-form-label">{% trans "Old" %}</label>
<div class="col-sm-4">
<input type="password" class="form-control" name="oldpasswd" value="">
</div>
</div>
<div class="form-group bridge_name_form_group_dhcp">
<label class="col-sm-2 col-form-label">{% trans "New" %}</label>
<div class="col-sm-4">
<input type="password" class="form-control" name="passwd1" value="">
</div>
</div>
<div class="form-group bridge_name_form_group_dhcp">
<label class="col-sm-2 col-form-label">{% trans "Retry" %}</label>
<div class="col-sm-4">
<input type="password" class="form-control" name="passwd2" value="">
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit" class="btn btn-primary">{% trans "Change" %}</button>
</div>
</div>
</form>
{% endif %}
<h3 class="page-header">{% trans "SSH Keys" %}</h3>
{% if publickeys %}
<div class="col-lg-12">

View file

@ -1,4 +1,4 @@
from django.contrib.auth.models import User
from django.contrib.auth.models import Permission, User
from django.shortcuts import reverse
from django.test import Client, TestCase
@ -6,7 +6,9 @@ from django.test import Client, TestCase
class AccountsTestCase(TestCase):
def setUp(self):
self.client.login(username='admin', password='admin')
User.objects.create_user(username='test', password='test')
user = User.objects.create_user(username='test', password='test')
permission = Permission.objects.get(codename='change_password')
user.user_permissions.add(permission)
def test_profile(self):
response = self.client.get(reverse('profile'))
@ -26,3 +28,37 @@ class AccountsTestCase(TestCase):
response = client.get(reverse('logout'))
self.assertRedirects(response, reverse('login'))
def test_password_change(self):
client = Client()
logged_in = client.login(username='test', password='test')
self.assertTrue(logged_in)
response = client.get(reverse('change_password'))
self.assertEqual(response.status_code, 200)
response = client.post(
reverse('change_password'),
{
'old_password': 'wrongpass',
'new_password1': 'newpw',
'new_password2': 'newpw',
},
)
self.assertEqual(response.status_code, 200)
response = client.post(
reverse('change_password'),
{
'old_password': 'test',
'new_password1': 'newpw',
'new_password2': 'newpw',
},
)
self.assertRedirects(response, reverse('profile'))
client.logout()
logged_in = client.login(username='test', password='newpw')
self.assertTrue(logged_in)

View file

@ -1,5 +1,6 @@
from django.urls import path
from django.contrib.auth import views as auth_views
from django.urls import path
from . import views
urlpatterns = [
@ -7,4 +8,8 @@ urlpatterns = [
path('logout/', auth_views.LogoutView.as_view(template_name='logout.html'), name='logout'),
path('profile/', views.profile, name='profile'),
path('profile/<int:user_id>/', views.account, name='account'),
path('change_password/', views.change_password, name='change_password'),
path('user_instance/create/<int:user_id>/', views.user_instance_create, name='user_instance_create'),
path('user_instance/<int:pk>/update/', views.user_instance_update, name='user_instance_update'),
path('user_instance/<int:pk>/delete/', views.user_instance_delete, name='user_instance_delete'),
]

View file

@ -1,25 +1,26 @@
import os
import sass
from django.contrib import messages
from django.contrib.auth import update_session_auth_hash
from django.contrib.auth.decorators import permission_required
from django.contrib.auth.forms import PasswordChangeForm
from django.core.validators import ValidationError
from django.http import HttpResponseRedirect
from django.shortcuts import render
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils.translation import ugettext_lazy as _
from accounts.forms import UserAddForm
from accounts.models import *
from admin.decorators import superuser_only
from appsettings.models import AppSettings
from instances.models import Instance
import sass
import os
from . import forms
def profile(request):
"""
:param request:
:return:
"""
error_messages = []
# user = User.objects.get(id=request.user.id)
publickeys = UserSSHKey.objects.filter(user_id=request.user.id)
if request.method == 'POST':
@ -30,20 +31,6 @@ def profile(request):
user.email = email
request.user.save()
return HttpResponseRedirect(request.get_full_path())
if 'oldpasswd' in request.POST:
oldpasswd = request.POST.get('oldpasswd', '')
password1 = request.POST.get('passwd1', '')
password2 = request.POST.get('passwd2', '')
if not password1 or not password2:
error_messages.append("Passwords didn't enter")
if password1 and password2 and password1 != password2:
error_messages.append("Passwords don't match")
if not request.user.check_password(oldpasswd):
error_messages.append("Old password is wrong!")
if not error_messages:
request.user.set_password(password1)
request.user.save()
return HttpResponseRedirect(request.get_full_path())
if 'keyname' in request.POST:
keyname = request.POST.get('keyname', '')
keypublic = request.POST.get('keypublic', '')
@ -71,49 +58,78 @@ def profile(request):
@superuser_only
def account(request, user_id):
"""
:param request:
:param user_id:
:return:
"""
error_messages = []
user = User.objects.get(id=user_id)
user_insts = UserInstance.objects.filter(user_id=user_id)
instances = Instance.objects.all().order_by('name')
publickeys = UserSSHKey.objects.filter(user_id=user_id)
if request.method == 'POST':
if 'delete' in request.POST:
user_inst = request.POST.get('user_inst', '')
del_user_inst = UserInstance.objects.get(id=user_inst)
del_user_inst.delete()
return HttpResponseRedirect(request.get_full_path())
if 'permission' in request.POST:
user_inst = request.POST.get('user_inst', '')
inst_vnc = request.POST.get('inst_vnc', '')
inst_change = request.POST.get('inst_change', '')
inst_delete = request.POST.get('inst_delete', '')
edit_user_inst = UserInstance.objects.get(id=user_inst)
edit_user_inst.is_change = bool(inst_change)
edit_user_inst.is_delete = bool(inst_delete)
edit_user_inst.is_vnc = bool(inst_vnc)
edit_user_inst.save()
return HttpResponseRedirect(request.get_full_path())
if 'add' in request.POST:
inst_id = request.POST.get('inst_id', '')
if AppSettings.objects.get(key="ALLOW_INSTANCE_MULTIPLE_OWNER").value == 'True':
check_inst = UserInstance.objects.filter(instance_id=int(inst_id), user_id=int(user_id))
else:
check_inst = UserInstance.objects.filter(instance_id=int(inst_id))
if check_inst:
msg = _("Instance already added")
error_messages.append(msg)
else:
add_user_inst = UserInstance(instance_id=int(inst_id), user_id=int(user_id))
add_user_inst.save()
return HttpResponseRedirect(request.get_full_path())
return render(request, 'account.html', locals())
@permission_required('accounts.change_password', raise_exception=True)
def change_password(request):
if request.method == 'POST':
form = PasswordChangeForm(request.user, request.POST)
if form.is_valid():
user = form.save()
update_session_auth_hash(request, user) # Important!
messages.success(request, _('Password Changed'))
return redirect('profile')
else:
messages.error(request, _('Wrong Data Provided'))
else:
form = PasswordChangeForm(request.user)
return render(request, 'accounts/change_password_form.html', {'form': form})
@superuser_only
def user_instance_create(request, user_id):
user = get_object_or_404(User, pk=user_id)
form = forms.UserInstanceForm(request.POST or None, initial={'user': user})
if form.is_valid():
form.save()
return redirect(reverse('account', args=[user.id]))
return render(
request,
'common/form.html',
{
'form': form,
'title': _('Create User Instance'),
},
)
@superuser_only
def user_instance_update(request, pk):
user_instance = get_object_or_404(UserInstance, pk=pk)
form = forms.UserInstanceForm(request.POST or None, instance=user_instance)
if form.is_valid():
form.save()
return redirect(reverse('account', args=[user_instance.user.id]))
return render(
request,
'common/form.html',
{
'form': form,
'title': _('Update User Instance'),
},
)
@superuser_only
def user_instance_delete(request, pk):
user_instance = get_object_or_404(UserInstance, pk=pk)
if request.method == 'POST':
user = user_instance.user
user_instance.delete()
return redirect(reverse('account', args=[user.id]))
return render(
request,
'common/confirm_delete.html',
{'object': user_instance},
)

View file

@ -1,5 +1,5 @@
{% extends "base.html" %}
{% load font_awesome %}
{% load icons %}
{% load i18n %}
{% block title %}{{ title }}{% endblock %}

View file

@ -1,7 +1,7 @@
{% extends "base.html" %}
{% load i18n %}
{% load static %}
{% load font_awesome %}
{% load icons %}
{% block title %}{% trans "Users" %}{% endblock %}
{% block content %}
<div class="row">

View file

@ -1,5 +1,6 @@
{% extends "base.html" %}
{% load i18n %}
{% load bootstrap4 %}
{% block title %}{% trans "Logs" %}{% endblock %}
{% block content %}
<!-- Page Heading -->
@ -47,7 +48,7 @@
</tbody>
</table>
</div>
{% include "paging.html" %}
{% bootstrap_pagination logs %}
{% endif %}
</div>
</div>

View file

@ -1,6 +1,6 @@
{% extends "base.html" %}
{% load bootstrap4 %}
{% load font_awesome %}
{% load icons %}
{% load i18n %}
{% block title %}{% trans "User" %}{% endblock %}

View file

@ -2,7 +2,7 @@
{% load i18n %}
{% load static %}
{% load common_tags %}
{% load font_awesome %}
{% load icons %}
{% block title %}{% trans "Users" %}{% endblock %}
{% block content %}
<div class="row">

View file

@ -1,9 +1,10 @@
from django.conf import settings
from django.contrib.auth.models import Group, User
from django.core.paginator import Paginator
from django.shortcuts import get_object_or_404, redirect, render
from django.utils.translation import ugettext_lazy as _
from accounts.models import UserAttributes
from accounts.models import UserAttributes, UserInstance, Instance
from appsettings.models import AppSettings
from logs.models import Logs
@ -32,7 +33,7 @@ def group_create(request):
return render(
request,
'admin/common/form.html',
'common/form.html',
{
'form': form,
'title': _('Create Group'),
@ -50,7 +51,7 @@ def group_update(request, pk):
return render(
request,
'admin/common/form.html',
'common/form.html',
{
'form': form,
'title': _('Update Group'),
@ -67,7 +68,7 @@ def group_delete(request, pk):
return render(
request,
'admin/common/confirm_delete.html',
'common/confirm_delete.html',
{'object': group},
)
@ -97,6 +98,7 @@ def user_create(request):
attributes = attributes_form.save(commit=False)
attributes.user = user
attributes.save()
add_default_instances(user)
return redirect('admin:user_list')
return render(
@ -141,7 +143,7 @@ def user_delete(request, pk):
return render(
request,
'admin/common/confirm_delete.html',
'common/confirm_delete.html',
{'object': user},
)
@ -169,3 +171,15 @@ def logs(request):
page = request.GET.get('page', 1)
logs = paginator.page(page)
return render(request, 'admin/logs.html', {'logs': logs})
def add_default_instances(user):
"""
Adds instances listed in NEW_USER_DEFAULT_INSTANCES to user
"""
existing_instances = UserInstance.objects.filter(user=user)
if not existing_instances:
for instance_name in settings.NEW_USER_DEFAULT_INSTANCES:
instance = Instance.objects.get(name=instance_name)
user_instance = UserInstance(user=user, instance=instance)
user_instance.save()

View file

@ -0,0 +1,38 @@
# Generated by Django 2.2.13 on 2020-06-15 06:37
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('appsettings', '0002_auto_20200527_1603'),
]
operations = [
migrations.AlterField(
model_name='appsettings',
name='choices',
field=models.CharField(max_length=50, verbose_name='choices'),
),
migrations.AlterField(
model_name='appsettings',
name='description',
field=models.CharField(max_length=100, null=True, verbose_name='description'),
),
migrations.AlterField(
model_name='appsettings',
name='key',
field=models.CharField(db_index=True, max_length=50, unique=True, verbose_name='key'),
),
migrations.AlterField(
model_name='appsettings',
name='name',
field=models.CharField(max_length=25, verbose_name='name'),
),
migrations.AlterField(
model_name='appsettings',
name='value',
field=models.CharField(max_length=25, verbose_name='value'),
),
]

View file

@ -40,31 +40,3 @@ class SocketComputeForm(forms.ModelForm):
class Meta:
model = Compute
fields = ['name', 'details', 'hostname', 'type']
class ComputeEditHostForm(forms.Form):
host_id = forms.CharField()
name = forms.CharField(error_messages={'required': _('No hostname has been entered')}, max_length=64)
hostname = forms.CharField(error_messages={'required': _('No IP / Domain name has been entered')}, max_length=100)
login = forms.CharField(error_messages={'required': _('No login has been entered')}, max_length=100)
password = forms.CharField(max_length=100)
details = forms.CharField(max_length=50, required=False)
def clean_name(self):
name = self.cleaned_data['name']
have_symbol = re.match('[^a-zA-Z0-9._-]+', name)
if have_symbol:
raise forms.ValidationError(_('The name of the host must not contain any special characters'))
elif len(name) > 20:
raise forms.ValidationError(_('The name of the host must not exceed 20 characters'))
return name
def clean_hostname(self):
hostname = self.cleaned_data['hostname']
have_symbol = re.match('[^a-zA-Z0-9._-]+', hostname)
wrong_ip = re.match('^0.|^255.', hostname)
if have_symbol:
raise forms.ValidationError(_('Hostname must contain only numbers, or the domain name separated by "."'))
elif wrong_ip:
raise forms.ValidationError(_('Wrong IP address'))
return hostname

View file

@ -0,0 +1,38 @@
# Generated by Django 2.2.13 on 2020-06-15 06:37
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('computes', '0002_auto_20200529_1320'),
]
operations = [
migrations.AlterField(
model_name='compute',
name='details',
field=models.CharField(blank=True, max_length=64, null=True, verbose_name='details'),
),
migrations.AlterField(
model_name='compute',
name='hostname',
field=models.CharField(max_length=64, verbose_name='hostname'),
),
migrations.AlterField(
model_name='compute',
name='login',
field=models.CharField(max_length=20, verbose_name='login'),
),
migrations.AlterField(
model_name='compute',
name='name',
field=models.CharField(max_length=64, unique=True, verbose_name='name'),
),
migrations.AlterField(
model_name='compute',
name='password',
field=models.CharField(blank=True, max_length=14, null=True, verbose_name='password'),
),
]

View file

@ -1,6 +1,10 @@
from django.db.models import Model, CharField, IntegerField
from django.db.models import CharField, IntegerField, Model
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
from vrtManager.connection import connection_manager
class Compute(Model):
name = CharField(_('name'), max_length=64, unique=True)
hostname = CharField(_('hostname'), max_length=64)
@ -9,5 +13,9 @@ class Compute(Model):
details = CharField(_('details'), max_length=64, null=True, blank=True)
type = IntegerField()
def __unicode__(self):
return self.hostname
@cached_property
def status(self):
return connection_manager.host_is_up(self.type, self.hostname)
def __str__(self):
return self.name

View file

@ -1,246 +0,0 @@
{% extends "base.html" %}
{% load i18n %}
{% block title %}{% trans "Computes" %}{% endblock %}
{% block content %}
<!-- Page Heading -->
<div class="row">
<div class="col-lg-12">
{% include 'create_comp_block.html' %}
<h2 class="page-header">{% trans "Computes" %}</h2>
</div>
</div>
<!-- /.row -->
{% include 'errors_block.html' %}
<div class="row">
{% if computes_info %}
{% for compute in computes_info %}
<div id="{{ compute.name }}" class="mb-3 col-12 col-sm-4">
{% if compute.status is True %}
<div class="card border-success shadow h-100">
<div class="card-header bg-success">
{% else %}
<div class="card border-danger shadow h-100">
<div class="card-header bg-danger">
{% endif %}
<h5 class="my-0 card-title">
{% if compute.status is True %}
<a class="card-link text-light" href="{% url 'overview' compute.id %}"><strong>{{ compute.name }}</strong></a>
{% else %}
<span class="card-link text-light" href="#"><strong>{{ compute.name }}</strong></span>
{% endif %}
<a class="card-link text-light float-right" data-toggle="modal" href="#editHost{{ compute.id }}" title="{% trans "Edit" %}">
<i class="fa fa-cog"></i>
</a>
</h5>
</div>
<div class="card-body">
<dl class="row">
<dt class="col-5">{% trans "Status" %}</dt>
{% if compute.status %}
<dd class="col-7">{% trans "Connected" %}</dd>
{% else %}
<dd class="col-7">{% trans "Not Connected" %}</dd>
{% endif %}
<dt class="col-5">{% trans "Details" %}</dt>
{% if compute.details %}
<dd class="col-7">{% trans compute.details %}</dd>
{% else %}
<dd class="col-7">{% trans "No details available" %}</dd>
{% endif %}
</dl>
<!-- Modal Edit -->
<div class="modal fade" id="editHost{{ compute.id }}" tabindex="-1" role="dialog" aria-labelledby="editHostLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{% trans "Edit connection" %}</h5>
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
</div>
{% if compute.type == 1 %}
<form method="post" role="form" aria-label="Edit tcp host form">{% csrf_token %}
<div class="modal-body">
<div class="form-group row">
<label class="col-sm-4 col-form-label">{% trans "Name" %}</label>
<div class="col-sm-6">
<input type="hidden" name="host_id" value="{{ compute.id }}">
<input type="text" name="name" class="form-control" value="{{ compute.name }}" maxlength="20" required pattern="[a-zA-Z0-9\.\-_]+">
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">{% trans "FQDN / IP" %}</label>
<div class="col-sm-6">
<input type="text" name="hostname" class="form-control" value="{{ compute.hostname }}" required pattern="[a-z0-9\.\-_]+">
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">{% trans "Username" %}</label>
<div class="col-sm-6">
<input type="text" name="login" class="form-control" value="{{ compute.login }}">
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">{% trans "Password" %}</label>
<div class="col-sm-6">
<input type="password" name="password" class="form-control" value="{{ compute.password }}">
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">{% trans "Details" %}</label>
<div class="col-sm-6">
<input type="text" name="details" class="form-control" placeholder="{% trans "Details" %}" value="{{ compute.details }}">
</div>
</div></div>
<div class="modal-footer">
<button type="submit" class="btn btn-danger mr-auto" name="host_del">
{% trans "Delete" %}
</button>
<button type="button" class="btn btn-secondary" data-dismiss="modal">
{% trans "Close" %}
</button>
<button type="submit" class="btn btn-primary" name="host_edit" autofocus>
{% trans "Change" %}
</button>
</div>
</form>
{% endif %}
{% if compute.type == 2 %}
<form method="post" role="form" aria-label="Edit ssh host form">{% csrf_token %}
<div class="modal-body">
<p class="modal-body">{% trans "Need create ssh <a href='https://github.com/retspen/webvirtmgr/wiki/Setup-SSH-Authorization'>authorization key</a>. If you have another SSH port on your server, you can add IP:PORT like '192.168.1.1:2222'." %}</p>
<div class="form-group row">
<label class="col-sm-4 col-form-label">{% trans "Name" %}</label>
<div class="col-sm-6">
<input type="hidden" name="host_id" value="{{ compute.id }}">
<input type="text" name="name" class="form-control" value="{{ compute.name }}" maxlength="20" required pattern="[a-z0-9\.\-_]+">
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">{% trans "FQDN / IP" %}</label>
<div class="col-sm-6">
<input type="text" name="hostname" class="form-control" value="{{ compute.hostname }}" required pattern="[a-z0-9\:\.\-_]+">
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">{% trans "Username" %}</label>
<div class="col-sm-6">
<input type="text" name="login" class="form-control" value="{{ compute.login }}">
<input type="hidden" name="password" value="{{ compute.password }}">
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">{% trans "Details" %}</label>
<div class="col-sm-6">
<input type="text" name="details" class="form-control" placeholder="{% trans "Details" %}" value="{{ compute.details }}">
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-danger mr-auto" name="host_del">
{% trans "Delete" %}
</button>
<button type="button" class="btn btn-secondary" data-dismiss="modal">
{% trans "Close" %}
</button>
<button type="submit" class="btn btn-primary" name="host_edit">
{% trans "Change" %}
</button>
</div>
</form>
{% endif %}
{% if compute.type == 3 %}
<form method="post" role="form" aria-label="Edit tls host form">{% csrf_token %}
<div class="modal-body">
<div class="form-group row">
<label class="col-sm-4 col-form-label">{% trans "Name" %}</label>
<div class="col-sm-6">
<input type="hidden" name="host_id" value="{{ compute.id }}">
<input type="text" name="name" class="form-control" value="{{ compute.name }}" maxlength="20" required pattern="[a-z0-9\.\-_]+">
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">{% trans "FQDN / IP" %}</label>
<div class="col-sm-6">
<input type="text" name="hostname" class="form-control" value="{{ compute.hostname }}" required pattern="[a-z0-9\:\.\-_]+">
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">{% trans "Username" %}</label>
<div class="col-sm-6">
<input type="text" name="login" class="form-control" placeholder="{% trans "Name" %}">
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">{% trans "Password" %}</label>
<div class="col-sm-6">
<input type="password" name="password" class="form-control" value="{{ compute.password }}">
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">{% trans "Details" %}</label>
<div class="col-sm-6">
<input type="text" name="details" class="form-control" placeholder="{% trans "Details" %}" value="{{ compute.details }}">
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-danger mr-auto" name="host_del">
{% trans "Delete" %}
</button>
<button type="button" class="btn btn-secondary" data-dismiss="modal">
{% trans "Close" %}
</button>
<button type="submit" class="btn btn-primary" name="host_edit">
{% trans "Change" %}
</button>
</div>
</form>
{% endif %}
{% if compute.type == 4 %}
<form method="post" role="form" aria-label="Edit/delete host form">{% csrf_token %}
<div class="modal-body">
<div class="form-group row">
<label class="col-sm-4 col-form-label">{% trans "Name" %}</label>
<div class="col-sm-6">
<input type="hidden" name="host_id" value="{{ compute.id }}">
<input type="text" name="name" class="form-control" value="{{ compute.name }}" maxlength="20" required pattern="[a-z0-9\.\-_]+">
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">{% trans "Details" %}</label>
<div class="col-sm-6">
<input type="text" name="details" class="form-control" placeholder="{% trans 'Details' %}" value="{{ compute.details }}">
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-danger mr-auto" name="host_del">
{% trans "Delete" %}
</button>
<button type="button" class="btn btn-secondary" data-dismiss="modal">
{% trans "Close" %}
<button type="submit" class="btn btn-primary" name="host_edit">
{% trans "Change" %}
</button>
</div>
</form>
{% endif %}
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
</div>
</div>
</div>
{% endfor %}
{% else %}
<div class="col-lg-12">
<div class="alert alert-warning alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
<i class="fa fa-exclamation-triangle"></i> <strong>{% trans "Warning" %}:</strong> {% trans "Hypervisor doesn't have any Computes" %}
</div>
</div>
{% endif %}
</div>
{% endblock %}

View file

@ -1,6 +1,6 @@
{% extends "base.html" %}
{% load bootstrap4 %}
{% load font_awesome %}
{% load icons %}
{% load i18n %}
{% block title %}{% trans "Add Compute" %}{% endblock %}
@ -11,7 +11,6 @@
<h2 class="page-header">{% trans "Create Compute" %}</h2>
</div>
</div>
{% bootstrap_messages %}
<div class="row">
<div class="thumbnail col-sm-10 offset-1">
<form id="create-update" action="" method="post">

View file

@ -0,0 +1,69 @@
{% extends "base.html" %}
{% load i18n %}
{% load static %}
{% load common_tags %}
{% load icons %}
{% block title %}{% trans "Computes" %}{% endblock %}
{% block content %}
<div class="row">
<div class="col-lg-12">
{% include 'create_comp_block.html' %}
{% include 'search_block.html' %}
<h3 class="page-header">{% trans "Computes" %}</h3>
</div>
</div>
{% include 'errors_block.html' %}
<div class="row">
{% if not computes %}
<div class="col-lg-12">
<div class="alert alert-warning alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
{% icon 'exclamation-triangle '%} <strong>{% trans "Warning" %}:</strong> {% trans "You don't have any computes" %}
</div>
</div>
{% else %}
<div class="col-lg-12">
<table class="table table-striped table-hover">
<thead>
<tr>
<th span="col">{% trans "Name" %}</th>
<th span="col">{% trans "Status" %}</th>
<th span="col">{% trans "Details" %}</th>
<th span="col">{% trans "Actions" %}</th>
</tr>
</thead>
<tbody class="searchable">
{% for compute in computes %}
<tr>
<td>
{{ compute.name }}
</td>
<td>
{% if compute.status is True %}{% trans "Connected" %}{% else %}{% trans "Not Connected" %}{% endif %}
</td>
<td>
{{ compute.details|default:"" }}
</td>
<td>
<div class="float-right btn-group">
{% if compute.status is True %}
<a class="btn btn-success" title="{%trans "Overview" %}" href="{% url 'overview' compute.id %}">{% icon 'eye' %}</a>
{% else %}
<a class="btn btn-light" title="{%trans "Overview" %}">{% icon 'eye' %}</a>
{% endif %}
<a class="btn btn-primary" title="{%trans "Edit" %}" href="{% url 'compute_update' compute.id %}">{% icon 'pencil' %}</a>
<a class="btn btn-danger" title="{%trans "Delete" %}" href="{% url 'compute_delete' compute.id %}">{% icon 'times' %}</a>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</div>
{% endblock content %}
{% block script %}
<script src="{% static "js/filter-table.js" %}"></script>
{% endblock script %}

View file

@ -1,7 +1,7 @@
{% load i18n %}
{% load bootstrap4 %}
{% load font_awesome %}
<div class="btn-group float-right" role="group" aria-label="Add host button group">
{% load icons %}
<div class="btn-group float-right mt-1" role="group" aria-label="Add host button group">
<a href="{% url 'add_tcp_host' %}" class="btn btn-success">{% trans "TCP" %}</a>
<a href="{% url 'add_ssh_host' %}" class="btn btn-success">{% trans "SSH" %}</a>
<a href="{% url 'add_tls_host' %}" class="btn btn-success">{% trans "TLS" %}</a>

View file

@ -1,3 +1,4 @@
from django.core.exceptions import ObjectDoesNotExist
from django.shortcuts import reverse
from django.test import TestCase
@ -20,6 +21,52 @@ class ComputesTestCase(TestCase):
response = self.client.get(reverse('computes'))
self.assertEqual(response.status_code, 200)
def test_create_update_delete(self):
response = self.client.get(reverse('add_socket_host'))
self.assertEqual(response.status_code, 200)
response = self.client.post(
reverse('add_socket_host'),
{
'name': 'l1',
'details': 'Created',
'hostname': 'localhost',
'type': 4,
},
)
self.assertRedirects(response, reverse('computes'))
compute = Compute.objects.get(pk=2)
self.assertEqual(compute.name, 'l1')
self.assertEqual(compute.details, 'Created')
response = self.client.get(reverse('compute_update', args=[2]))
self.assertEqual(response.status_code, 200)
response = self.client.post(
reverse('compute_update', args=[2]),
{
'name': 'l2',
'details': 'Updated',
'hostname': 'localhost',
'type': 4,
},
)
self.assertRedirects(response, reverse('computes'))
compute = Compute.objects.get(pk=2)
self.assertEqual(compute.name, 'l2')
self.assertEqual(compute.details, 'Updated')
response = self.client.get(reverse('compute_delete', args=[2]))
self.assertEqual(response.status_code, 200)
response = self.client.post(reverse('compute_delete', args=[2]))
self.assertRedirects(response, reverse('computes'))
with self.assertRaises(ObjectDoesNotExist):
Compute.objects.get(id=2)
def test_overview(self):
response = self.client.get(reverse('overview', args=[1]))
self.assertEqual(response.status_code, 200)
@ -78,6 +125,16 @@ class ComputesTestCase(TestCase):
response = self.client.get(reverse('machines', kwargs={'compute_id': 1, 'arch': 'x86_64'}))
self.assertEqual(response.status_code, 200)
# TODO: get_compute_disk_buses
def test_compute_disk_buses(self):
response = self.client.get(
reverse('buses', kwargs={
'compute_id': 1,
'arch': 'x86_64',
'machine': 'pc',
'disk': 'disk',
}))
self.assertEqual(response.status_code, 200)
# TODO: domcaps
def test_dom_capabilities(self):
response = self.client.get(reverse('domcaps', kwargs={'compute_id': 1, 'arch': 'x86_64', 'machine': 'pc'}))
self.assertEqual(response.status_code, 200)

View file

@ -12,28 +12,36 @@ from storages.views import get_volumes, storage, storages
urlpatterns = [
path('', views.computes, name='computes'),
path('add_tcp_host/', views.add_host, {'FormClass': forms.TcpComputeForm}, name='add_tcp_host'),
path('add_ssh_host/', views.add_host, {'FormClass': forms.SshComputeForm}, name='add_ssh_host'),
path('add_tls_host/', views.add_host, {'FormClass': forms.TlsComputeForm}, name='add_tls_host'),
path('add_socket_host/', views.add_host, {'FormClass': forms.SocketComputeForm}, name='add_socket_host'),
path('<int:compute_id>/', include([
path('', views.overview, name='overview'),
path('statistics', views.compute_graph, name='compute_graph'),
path('instances/', instances, name='instances'),
path('storages/', storages, name='storages'),
path('storage/<str:pool>/volumes', get_volumes, name='volumes'),
path('storage/<str:pool>/', storage, name='storage'),
path('networks/', networks, name='networks'),
path('network/<str:pool>/', network, name='network'),
path('interfaces/', interfaces, name='interfaces'),
path('interface/<str:iface>/', interface, name='interface'),
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('archs/<str:arch>/machines', views.get_compute_machine_types, name='machines'),
path('archs/<str:arch>/machines/<str:machine>/disks/<str:disk>/buses', views.get_compute_disk_buses, name='buses'),
path('archs/<str:arch>/machines/<str:machine>/capabilities', views.get_dom_capabilities, name='domcaps'),
])),
path('add_tcp_host/', views.compute_create, {'FormClass': forms.TcpComputeForm}, name='add_tcp_host'),
path('add_ssh_host/', views.compute_create, {'FormClass': forms.SshComputeForm}, name='add_ssh_host'),
path('add_tls_host/', views.compute_create, {'FormClass': forms.TlsComputeForm}, name='add_tls_host'),
path('add_socket_host/', views.compute_create, {'FormClass': forms.SocketComputeForm}, name='add_socket_host'),
path(
'<int:compute_id>/',
include([
path('', views.overview, name='overview'),
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('storages/', storages, name='storages'),
path('storage/<str:pool>/volumes', get_volumes, name='volumes'),
path('storage/<str:pool>/', storage, name='storage'),
path('networks/', networks, name='networks'),
path('network/<str:pool>/', network, name='network'),
path('interfaces/', interfaces, name='interfaces'),
path('interface/<str:iface>/', interface, name='interface'),
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('archs/<str:arch>/machines/', views.get_compute_machine_types, name='machines'),
path(
'archs/<str:arch>/machines/<str:machine>/disks/<str:disk>/buses/',
views.get_compute_disk_buses,
name='buses',
),
path('archs/<str:arch>/machines/<str:machine>/capabilities/', views.get_dom_capabilities, name='domcaps'),
])),
]

View file

@ -9,7 +9,7 @@ from libvirt import libvirtError
from accounts.models import UserInstance
from admin.decorators import superuser_only
from computes.forms import (ComputeEditHostForm, SocketComputeForm, SshComputeForm, TcpComputeForm, TlsComputeForm)
from computes.forms import (SocketComputeForm, SshComputeForm, TcpComputeForm, TlsComputeForm)
from computes.models import Compute
from instances.models import Instance
from vrtManager.connection import (CONN_SOCKET, CONN_SSH, CONN_TCP, CONN_TLS, connection_manager, wvmConnect)
@ -22,58 +22,10 @@ def computes(request):
:param request:
:return:
"""
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
error_messages = []
computes = Compute.objects.filter().order_by('name')
computes_info = get_hosts_status(computes)
if request.method == 'POST':
if 'host_del' in request.POST:
compute_id = request.POST.get('host_id', '')
try:
del_user_inst_on_host = UserInstance.objects.filter(instance__compute_id=compute_id)
del_user_inst_on_host.delete()
finally:
try:
del_inst_on_host = Instance.objects.filter(compute_id=compute_id)
del_inst_on_host.delete()
finally:
del_host = Compute.objects.get(id=compute_id)
del_host.delete()
return HttpResponseRedirect(request.get_full_path())
if 'host_edit' in request.POST:
form = ComputeEditHostForm(request.POST)
if form.is_valid():
data = form.cleaned_data
compute_edit = Compute.objects.get(id=data['host_id'])
compute_edit.name = data['name']
compute_edit.hostname = data['hostname']
compute_edit.login = data['login']
compute_edit.password = data['password']
compute_edit.details = data['details']
compute_edit.save()
return HttpResponseRedirect(request.get_full_path())
else:
for msg_err in form.errors.values():
error_messages.append(msg_err.as_text())
return render(request, 'computes.html', locals())
return render(request, 'computes/list.html', {'computes': computes})
@superuser_only
@ -87,7 +39,7 @@ def overview(request, compute_id):
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,
@ -108,6 +60,51 @@ def overview(request, compute_id):
return render(request, 'overview.html', locals())
@superuser_only
def compute_create(request, FormClass):
form = FormClass(request.POST or None)
if form.is_valid():
form.save()
return redirect(reverse('computes'))
return render(request, 'computes/form.html', {'form': form})
@superuser_only
def compute_update(request, compute_id):
compute = get_object_or_404(Compute, pk=compute_id)
if compute.type == 1:
FormClass = TcpComputeForm
elif compute.type == 2:
FormClass = SshComputeForm
elif compute.type == 3:
FormClass = TlsComputeForm
elif compute.type == 4:
FormClass = SocketComputeForm
form = FormClass(request.POST or None, instance=compute)
if form.is_valid():
form.save()
return redirect(reverse('computes'))
return render(request, 'computes/form.html', {'form': form})
@superuser_only
def compute_delete(request, compute_id):
compute = get_object_or_404(Compute, pk=compute_id)
if request.method == 'POST':
compute.delete()
return redirect('computes')
return render(
request,
'common/confirm_delete.html',
{'object': compute},
)
def compute_graph(request, compute_id):
"""
:param request:
@ -248,13 +245,3 @@ def get_dom_capabilities(request, compute_id, arch, machine):
pass
return HttpResponse(json.dumps(data))
@superuser_only
def add_host(request, FormClass):
form = FormClass(request.POST or None)
if form.is_valid():
form.save()
return redirect(reverse('computes'))
return render(request, 'computes/form.html', {'form': form})

19
conf/requirements-dev.txt Normal file
View file

@ -0,0 +1,19 @@
beautifulsoup4==4.9.1
coverage==5.1
Django==2.2.13
django-bootstrap4==2.0.1
django-debug-toolbar==2.2
django-icons==2.0.0
django-login-required-middleware==0.5.0
gunicorn==20.0.4
libsass==0.20.0
libvirt-python==6.3.0
lxml==4.5.0
numpy==1.18.4
pytz==2020.1
rwlock==0.0.7
six==1.15.0
soupsieve==2.0.1
sqlparse==0.3.1
websockify==0.9.0
yapf==0.30.0

View file

@ -1,14 +1,16 @@
beautifulsoup4==4.9.1
Django==2.2.13
django-bootstrap4==2.0.1
django-fa==1.0.0
django-icons==2.0.0
django-login-required-middleware==0.5.0
gunicorn==20.0.4
libvirt-python==6.3.0
libsass==0.20.0
libvirt-python==6.3.0
lxml==4.5.0
numpy==1.18.4
pytz==2020.1
rwlock==0.0.7
six==1.15.0
soupsieve==2.0.1
sqlparse==0.3.1
websockify==0.9.0

View file

@ -0,0 +1,33 @@
# 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'),
),
]

View file

@ -1,11 +1,12 @@
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 __unicode__(self):
def __str__(self):
return self.name

View file

@ -0,0 +1,33 @@
# Generated by Django 2.2.13 on 2020-06-15 06:37
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('instances', '0002_permissionset'),
]
operations = [
migrations.AlterField(
model_name='instance',
name='created',
field=models.DateField(auto_now_add=True, verbose_name='created'),
),
migrations.AlterField(
model_name='instance',
name='is_template',
field=models.BooleanField(default=False, verbose_name='is template'),
),
migrations.AlterField(
model_name='instance',
name='name',
field=models.CharField(max_length=120, verbose_name='name'),
),
migrations.AlterField(
model_name='instance',
name='uuid',
field=models.CharField(max_length=36, verbose_name='uuid'),
),
]

View file

@ -1,5 +1,4 @@
from django.db.models import (CASCADE, BooleanField, CharField, DateField,
ForeignKey, Model)
from django.db.models import (CASCADE, BooleanField, CharField, DateField, ForeignKey, Model)
from django.utils.translation import ugettext_lazy as _
from computes.models import Compute
@ -12,8 +11,9 @@ class Instance(Model):
is_template = BooleanField(_('is template'), default=False)
created = DateField(_('created'), auto_now_add=True)
def __unicode__(self):
return self.name
def __str__(self):
return f'{self.compute}/{self.name}'
class PermissionSet(Model):
"""
@ -21,8 +21,6 @@ class PermissionSet(Model):
"""
class Meta:
default_permissions = ()
permissions = (
('clone_instances', _('Can clone instances')),
)
permissions = (('clone_instances', _('Can clone instances')), )
managed = False

View file

@ -63,7 +63,7 @@
<tbody class="searchable">
{% for inst, vm in all_user_vms.items %}
<tr>
<td><a href="{% url 'instance' vm.compute_id vm.name %}">{{ vm.name }}</a><br><small><em>{{ vm.title }}</em></small></td>
<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 %}

View file

@ -42,7 +42,7 @@
<tr host="{{ host.1 }}">
<td class="text-right">{{ forloop.counter }} </td>
<td>&emsp;
<a class="text-secondary" href="{% url 'instance' host.0 inst %}">{{ inst }}</a><br>
<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">

View file

@ -14,7 +14,7 @@
{% for host, inst in all_host_vms.items %}
{% for inst, vm in inst.items %}
<tr>
<td><a href="{% url 'instance' host.0 inst %}">{{ inst }}</a><br><small><em>{{ info.title }}</em></small></td>
<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 %}

View file

@ -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 'instance' compute.id vname %}"><i class="fa fa-hdd-o"></i> {{ vname }}</a>
<a class="nav-link" href="{% url 'instances:instance' compute.id vname %}"><i class="fa fa-hdd-o"></i> {{ vname }}</a>
</li>
</ul>
</div>

View file

@ -52,7 +52,7 @@
{{ ipv4 }} |
{% endfor %}
{% endfor %}
<a class="text-secondary" href="{% url 'instance' compute.id vname %}" title="{% trans 'Refresh instance info' %}"><span class="fa fa-refresh"></span></a>
<a class="text-secondary" href="{% url 'instances:instance' compute.id vname %}" title="{% trans 'Refresh instance info' %}"><span class="fa fa-refresh"></span></a>
</div>
{% if user_quota_msg %}
<div class="alert alert-warning fade show">
@ -1749,7 +1749,7 @@
</script>
<script>
function random_mac(net) {
$.getJSON('{% url 'random_mac_address' %}', function (data) {
$.getJSON('{% url 'instances:random_mac_address' %}', function (data) {
$('input[name="' + net + '"]').val(data['mac']);
});
}
@ -1766,7 +1766,7 @@
<script>
function guess_mac_address(src_elem, net) {
new_vname = $(src_elem).val();
guess_mac_address_url = "{% url 'guess_mac_address' 1 %}".replace(1, new_vname);
guess_mac_address_url = "{% url 'instances:guess_mac_address' 1 %}".replace(1, new_vname);
$.getJSON(guess_mac_address_url, function(data) {
$('input[name="clone-net-mac-'+net+'"]').val(data['mac']);
});
@ -1774,7 +1774,7 @@
</script>
<script>
function guess_clone_name() {
$.getJSON('{% url 'guess_clone_name' %}', function(data) {
$.getJSON('{% url 'instances:guess_clone_name' %}', function(data) {
guessed_name = data['name'].split(".")[0];
$('#clone_name').val(guessed_name);
update_clone_disk_name(guessed_name);
@ -2181,7 +2181,7 @@
{% endfor %}
var graph_interval = window.setInterval(function graph_usage() {
$.getJSON('{% url 'inst_graph' compute_id vname %}', function (data) {
$.getJSON('{% url 'instances:inst_graph' compute_id vname %}', function (data) {
cpuChart.data.labels.push(data.timeline);
cpuChart.data.datasets[0].data.push(data.cpudata);
@ -2234,7 +2234,7 @@
backgroundJobRunning = false;
var status_interval = window.setInterval(function get_status() {
var status = {{ status|lower }};
$.getJSON('{% url 'inst_status' compute_id vname %}', function (data) {
$.getJSON('{% url 'instances:inst_status' compute_id vname %}', function (data) {
if (data['status'] != status && !backgroundJobRunning) {
window.location.reload()
}

View file

@ -79,7 +79,7 @@
{% for host, insts in all_host_vms.items %}
{% for inst, vm in insts.items %}
<tr>
<td><a class="text-secondary" href="{% url 'instance' host.0 inst %}">{{ inst }}</a><br><small><em>{{ vm.title }}</em></small></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-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 %}

View file

@ -1,9 +1,12 @@
from django.urls import path
from . import views
app_name = 'instances'
urlpatterns = [
path('', views.allinstances, name='allinstances'),
path('<int:compute_id>/<vname>/', views.instance, name='instance'),
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('guess_mac_address/<vname>/', views.guess_mac_address, name='guess_mac_address'),

View file

@ -0,0 +1,33 @@
# Generated by Django 2.2.13 on 2020-06-15 06:37
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('logs', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='logs',
name='date',
field=models.DateTimeField(auto_now=True, verbose_name='date'),
),
migrations.AlterField(
model_name='logs',
name='instance',
field=models.CharField(max_length=50, verbose_name='instance'),
),
migrations.AlterField(
model_name='logs',
name='message',
field=models.CharField(max_length=255, verbose_name='message'),
),
migrations.AlterField(
model_name='logs',
name='user',
field=models.CharField(max_length=50, verbose_name='user'),
),
]

View file

@ -1,11 +1,12 @@
from django.db.models import Model, CharField, DateTimeField
from django.utils.translation import ugettext_lazy as _
class Logs(Model):
user = CharField(_('user'), max_length=50)
instance = CharField(_('instance'), max_length=50)
message = CharField(_('message'), max_length=255)
date = DateTimeField(_('date'), auto_now=True)
def __unicode__(self):
def __str__(self):
return self.instance

View file

@ -169,3 +169,7 @@ p {
vertical-align: middle;
}
/* remove ugly dotted outline on tabs */
a {
outline: 0;
}

View file

@ -1,4 +1,5 @@
{% load static %}
{% load bootstrap4 %}
<!DOCTYPE html>
<html lang="en">
<head>
@ -36,7 +37,7 @@
{% include 'navbar.html' %}
<div role="main" class="container">
{% bootstrap_messages %}
{% block content %}{% endblock %}
</div> <!-- /container -->

View file

@ -1,6 +1,6 @@
{% extends "base.html" %}
{% load bootstrap4 %}
{% load font_awesome %}
{% load icons %}
{% load i18n %}
{% block title %}{%trans "Delete" %}{% endblock %}

View file

@ -1,9 +1,9 @@
{% extends "base.html" %}
{% load bootstrap4 %}
{% load font_awesome %}
{% load icons %}
{% load i18n %}
{% block title %}{% trans "User" %}{% endblock %}
{% block title %}{{ title }}{% endblock %}
{% block content %}
<div class="row">

View file

@ -1,5 +1,5 @@
{% load i18n %}
{% load font_awesome %}
{% load icons %}
{% load common_tags %}
<!-- Fixed navbar -->
<nav class="navbar navbar-expand-md navbar-dark bg-primary mb-3" aria-label="Main top navbar">
@ -11,7 +11,7 @@
<div id="navbar" class="navbar-collapse collapse">
<ul class="navbar-nav mr-auto mt-2 mt-md-0">
<li class="nav-item {% class_active request '^/instances' %}">
<a class="nav-link" href="{% url 'allinstances' %}"><i class="fa fa-fw fa-desktop"></i> {% trans "Instances" %}</a>
<a class="nav-link" href="{% url 'instances:index' %}"><i class="fa fa-fw fa-desktop"></i> {% trans "Instances" %}</a>
</li>
{% if request.user.is_superuser %}
<li class="nav-item {% class_active request '^/computes' %}">
@ -42,7 +42,7 @@
<a class="dropdown-item disabled" href="#">
{% trans "Language" %}: <span class="badge badge-secondary">{{ LANGUAGE_CODE }}</span>
</a>
<a class="dropdown-item {% view_active request 'profile' %}" href="{% url 'profile' %}"><i class="fa fa-fw fa-pencil-square-o"></i> {% trans "Profile" %}</a>
<a class="dropdown-item {% view_active request 'profile' %}" href="{% url 'profile' %}">{% icon 'vcard' %} {% trans "Profile" %}</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="{% url 'logout' %}"><i class="fa fa-fw fa-power-off"></i> {% trans "Log Out" %}</a>
</div>

View file

@ -0,0 +1,4 @@
{% load i18n %}
<div class="float-right search">
<input id="filter" class="form-control" type="text" placeholder="{% trans "Search" %}">
</div>

View file

@ -22,7 +22,7 @@ INSTALLED_APPS = [
'django.contrib.messages',
'django.contrib.staticfiles',
'bootstrap4',
'fa',
'django_icons',
'accounts',
'admin',
'appsettings',