1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2024-12-25 15:45:23 +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.forms import ModelForm, ValidationError
from django import forms
from django.utils.translation import ugettext_lazy as _ 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): class UserInstanceForm(ModelForm):
name = forms.CharField(label="Name", def __init__(self, *args, **kwargs):
error_messages={'required': _('No username has been entered')}, super(UserInstanceForm, self).__init__(*args, **kwargs)
max_length=20)
password = forms.CharField(required=not settings.ALLOW_EMPTY_PASSWORD,
error_messages={'required': _('No password has been entered')},)
def clean_name(self): # Make user and instance fields not editable after creation
name = self.cleaned_data['name'] instance = getattr(self, 'instance', None)
have_symbol = re.match('^[a-z0-9]+$', name) if instance and instance.id is not None:
if not have_symbol: self.fields['user'].disabled = True
raise forms.ValidationError(_('The flavor name must not contain any special characters')) self.fields['instance'].disabled = True
elif len(name) > 20:
raise forms.ValidationError(_('The flavor name must not exceed 20 characters')) def clean_instance(self):
try: instance = self.cleaned_data['instance']
User.objects.get(username=name) if AppSettings.objects.get(key="ALLOW_INSTANCE_MULTIPLE_OWNER").value == 'False':
except User.DoesNotExist: exists = UserInstance.objects.filter(instance=instance)
return name if exists:
raise forms.ValidationError(_('Flavor name is already use')) 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.contrib.auth.models import User
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator
from django.db import models from django.db import models
@ -13,8 +14,11 @@ class UserInstance(models.Model):
is_delete = models.BooleanField(default=False) is_delete = models.BooleanField(default=False)
is_vnc = models.BooleanField(default=False) is_vnc = models.BooleanField(default=False)
def __unicode__(self): def __str__(self):
return self.instance.name return _('Instance "%(inst)s" of user %(user)s') % {'inst': self.instance, 'user': self.user}
class Meta:
unique_together = ['user', 'instance']
class UserSSHKey(models.Model): class UserSSHKey(models.Model):
@ -22,7 +26,7 @@ class UserSSHKey(models.Model):
keyname = models.CharField(_('key name'), max_length=25) keyname = models.CharField(_('key name'), max_length=25)
keypublic = models.CharField(_('public key'), max_length=500) keypublic = models.CharField(_('public key'), max_length=500)
def __unicode__(self): def __str__(self):
return self.keyname return self.keyname
@ -54,29 +58,7 @@ class UserAttributes(models.Model):
validators=[MinValueValidator(-1)], validators=[MinValueValidator(-1)],
) )
@staticmethod def __str__(self):
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):
return self.user.username return self.user.username

View file

@ -1,23 +1,68 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load i18n %} {% load i18n %}
{% block title %}{% trans "User" %} - {{ user }}{% endblock %} {% load icons %}
{% block title %}{% trans "User Profile" %} - {{ user }}{% endblock %}
{% block content %} {% block content %}
<!-- Page Heading --> <!-- Page Heading -->
<div class="row"> <div class="row">
<div class="col-lg-12"> <div class="col-lg-12">
{% include 'create_user_inst_block.html' %} <a href="{% url 'user_instance_create' user.id %}" class="btn btn-success btn-header float-right">
<h2 class="page-header">{{ user }}</h2> {% icon 'plus' %}
</a>
<h2 class="page-header">{% trans "User Profile" %}: {{ user }}</h2>
</div> </div>
</div> </div>
<!-- /.row --> <!-- /.row -->
{% include 'errors_block.html' %} {% include 'errors_block.html' %}
{% if request.user.is_superuser and publickeys %} <ul class="nav nav-tabs">
<div class="row"> <li class="nav-item">
<div class="col-lg-12"> <a class="nav-link active" data-toggle="tab" href="#instances">{% trans "Instances" %}</a>
<div class="table-responsive"> </li>
<table class="table table-bordered table-hover"> <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>
<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>
<div class="tab-pane fade" id="public-keys">
<table class="table table-striped table-hover">
<thead> <thead>
<tr> <tr>
<th scope="col">{% trans "Key name" %}</th> <th scope="col">{% trans "Key name" %}</th>
@ -35,106 +80,4 @@
</table> </table>
</div> </div>
</div> </div>
</div> {% endblock content %}
{% 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 %}

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" %} {% extends "base.html" %}
{% load i18n %} {% load i18n %}
{% load icons %}
{% load tags_fingerprint %} {% load tags_fingerprint %}
{% block title %}{% trans "Profile" %}{% endblock %} {% block title %}{% trans "Profile" %}{% endblock %}
{% block content %} {% block content %}
@ -16,6 +17,9 @@
<div class="row"> <div class="row">
<div class="col-lg-12"> <div class="col-lg-12">
<h3 class="page-header">{% trans "Edit Profile" %}</h3> <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 %} <form method="post" action="" role="form" aria-label="Edit user info form">{% csrf_token %}
<div class="form-group"> <div class="form-group">
<label class="col-sm-2 col-form-label">{% trans "Login" %}</label> <label class="col-sm-2 col-form-label">{% trans "Login" %}</label>
@ -41,34 +45,6 @@
</div> </div>
</div> </div>
</form> </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> <h3 class="page-header">{% trans "SSH Keys" %}</h3>
{% if publickeys %} {% if publickeys %}
<div class="col-lg-12"> <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.shortcuts import reverse
from django.test import Client, TestCase from django.test import Client, TestCase
@ -6,7 +6,9 @@ from django.test import Client, TestCase
class AccountsTestCase(TestCase): class AccountsTestCase(TestCase):
def setUp(self): def setUp(self):
self.client.login(username='admin', password='admin') 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): def test_profile(self):
response = self.client.get(reverse('profile')) response = self.client.get(reverse('profile'))
@ -26,3 +28,37 @@ class AccountsTestCase(TestCase):
response = client.get(reverse('logout')) response = client.get(reverse('logout'))
self.assertRedirects(response, reverse('login')) 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.contrib.auth import views as auth_views
from django.urls import path
from . import views from . import views
urlpatterns = [ urlpatterns = [
@ -7,4 +8,8 @@ urlpatterns = [
path('logout/', auth_views.LogoutView.as_view(template_name='logout.html'), name='logout'), path('logout/', auth_views.LogoutView.as_view(template_name='logout.html'), name='logout'),
path('profile/', views.profile, name='profile'), path('profile/', views.profile, name='profile'),
path('profile/<int:user_id>/', views.account, name='account'), 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.core.validators import ValidationError
from django.http import HttpResponseRedirect 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.urls import reverse
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from accounts.forms import UserAddForm
from accounts.models import * from accounts.models import *
from admin.decorators import superuser_only from admin.decorators import superuser_only
from appsettings.models import AppSettings from appsettings.models import AppSettings
from instances.models import Instance from instances.models import Instance
import sass from . import forms
import os
def profile(request): def profile(request):
"""
:param request:
:return:
"""
error_messages = [] error_messages = []
# user = User.objects.get(id=request.user.id)
publickeys = UserSSHKey.objects.filter(user_id=request.user.id) publickeys = UserSSHKey.objects.filter(user_id=request.user.id)
if request.method == 'POST': if request.method == 'POST':
@ -30,20 +31,6 @@ def profile(request):
user.email = email user.email = email
request.user.save() request.user.save()
return HttpResponseRedirect(request.get_full_path()) 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: if 'keyname' in request.POST:
keyname = request.POST.get('keyname', '') keyname = request.POST.get('keyname', '')
keypublic = request.POST.get('keypublic', '') keypublic = request.POST.get('keypublic', '')
@ -71,49 +58,78 @@ def profile(request):
@superuser_only @superuser_only
def account(request, user_id): def account(request, user_id):
"""
:param request:
:param user_id:
:return:
"""
error_messages = [] error_messages = []
user = User.objects.get(id=user_id) user = User.objects.get(id=user_id)
user_insts = UserInstance.objects.filter(user_id=user_id) user_insts = UserInstance.objects.filter(user_id=user_id)
instances = Instance.objects.all().order_by('name') instances = Instance.objects.all().order_by('name')
publickeys = UserSSHKey.objects.filter(user_id=user_id) 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()) 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" %} {% extends "base.html" %}
{% load font_awesome %} {% load icons %}
{% load i18n %} {% load i18n %}
{% block title %}{{ title }}{% endblock %} {% block title %}{{ title }}{% endblock %}

View file

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

View file

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

View file

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

View file

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

View file

@ -1,9 +1,10 @@
from django.conf import settings
from django.contrib.auth.models import Group, User from django.contrib.auth.models import Group, User
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.shortcuts import get_object_or_404, redirect, render from django.shortcuts import get_object_or_404, redirect, render
from django.utils.translation import ugettext_lazy as _ 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 appsettings.models import AppSettings
from logs.models import Logs from logs.models import Logs
@ -32,7 +33,7 @@ def group_create(request):
return render( return render(
request, request,
'admin/common/form.html', 'common/form.html',
{ {
'form': form, 'form': form,
'title': _('Create Group'), 'title': _('Create Group'),
@ -50,7 +51,7 @@ def group_update(request, pk):
return render( return render(
request, request,
'admin/common/form.html', 'common/form.html',
{ {
'form': form, 'form': form,
'title': _('Update Group'), 'title': _('Update Group'),
@ -67,7 +68,7 @@ def group_delete(request, pk):
return render( return render(
request, request,
'admin/common/confirm_delete.html', 'common/confirm_delete.html',
{'object': group}, {'object': group},
) )
@ -97,6 +98,7 @@ def user_create(request):
attributes = attributes_form.save(commit=False) attributes = attributes_form.save(commit=False)
attributes.user = user attributes.user = user
attributes.save() attributes.save()
add_default_instances(user)
return redirect('admin:user_list') return redirect('admin:user_list')
return render( return render(
@ -141,7 +143,7 @@ def user_delete(request, pk):
return render( return render(
request, request,
'admin/common/confirm_delete.html', 'common/confirm_delete.html',
{'object': user}, {'object': user},
) )
@ -169,3 +171,15 @@ def logs(request):
page = request.GET.get('page', 1) page = request.GET.get('page', 1)
logs = paginator.page(page) logs = paginator.page(page)
return render(request, 'admin/logs.html', {'logs': logs}) 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: class Meta:
model = Compute model = Compute
fields = ['name', 'details', 'hostname', 'type'] 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 django.utils.translation import ugettext_lazy as _
from vrtManager.connection import connection_manager
class Compute(Model): class Compute(Model):
name = CharField(_('name'), max_length=64, unique=True) name = CharField(_('name'), max_length=64, unique=True)
hostname = CharField(_('hostname'), max_length=64) hostname = CharField(_('hostname'), max_length=64)
@ -9,5 +13,9 @@ class Compute(Model):
details = CharField(_('details'), max_length=64, null=True, blank=True) details = CharField(_('details'), max_length=64, null=True, blank=True)
type = IntegerField() type = IntegerField()
def __unicode__(self): @cached_property
return self.hostname 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" %} {% extends "base.html" %}
{% load bootstrap4 %} {% load bootstrap4 %}
{% load font_awesome %} {% load icons %}
{% load i18n %} {% load i18n %}
{% block title %}{% trans "Add Compute" %}{% endblock %} {% block title %}{% trans "Add Compute" %}{% endblock %}
@ -11,7 +11,6 @@
<h2 class="page-header">{% trans "Create Compute" %}</h2> <h2 class="page-header">{% trans "Create Compute" %}</h2>
</div> </div>
</div> </div>
{% bootstrap_messages %}
<div class="row"> <div class="row">
<div class="thumbnail col-sm-10 offset-1"> <div class="thumbnail col-sm-10 offset-1">
<form id="create-update" action="" method="post"> <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 i18n %}
{% load bootstrap4 %} {% load bootstrap4 %}
{% load font_awesome %} {% load icons %}
<div class="btn-group float-right" role="group" aria-label="Add host button group"> <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_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_ssh_host' %}" class="btn btn-success">{% trans "SSH" %}</a>
<a href="{% url 'add_tls_host' %}" class="btn btn-success">{% trans "TLS" %}</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.shortcuts import reverse
from django.test import TestCase from django.test import TestCase
@ -20,6 +21,52 @@ class ComputesTestCase(TestCase):
response = self.client.get(reverse('computes')) response = self.client.get(reverse('computes'))
self.assertEqual(response.status_code, 200) 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): def test_overview(self):
response = self.client.get(reverse('overview', args=[1])) response = self.client.get(reverse('overview', args=[1]))
self.assertEqual(response.status_code, 200) 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'})) response = self.client.get(reverse('machines', kwargs={'compute_id': 1, 'arch': 'x86_64'}))
self.assertEqual(response.status_code, 200) 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,12 +12,16 @@ from storages.views import get_volumes, storage, storages
urlpatterns = [ urlpatterns = [
path('', views.computes, name='computes'), path('', views.computes, name='computes'),
path('add_tcp_host/', views.add_host, {'FormClass': forms.TcpComputeForm}, name='add_tcp_host'), path('add_tcp_host/', views.compute_create, {'FormClass': forms.TcpComputeForm}, name='add_tcp_host'),
path('add_ssh_host/', views.add_host, {'FormClass': forms.SshComputeForm}, name='add_ssh_host'), path('add_ssh_host/', views.compute_create, {'FormClass': forms.SshComputeForm}, name='add_ssh_host'),
path('add_tls_host/', views.add_host, {'FormClass': forms.TlsComputeForm}, name='add_tls_host'), path('add_tls_host/', views.compute_create, {'FormClass': forms.TlsComputeForm}, name='add_tls_host'),
path('add_socket_host/', views.add_host, {'FormClass': forms.SocketComputeForm}, name='add_socket_host'), path('add_socket_host/', views.compute_create, {'FormClass': forms.SocketComputeForm}, name='add_socket_host'),
path('<int:compute_id>/', include([ path(
'<int:compute_id>/',
include([
path('', views.overview, name='overview'), 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('statistics', views.compute_graph, name='compute_graph'),
path('instances/', instances, name='instances'), path('instances/', instances, name='instances'),
path('storages/', storages, name='storages'), path('storages/', storages, name='storages'),
@ -32,8 +36,12 @@ urlpatterns = [
path('secrets/', secrets, name='secrets'), path('secrets/', secrets, name='secrets'),
path('create/', create_instance_select_type, name='create_instance_select_type'), 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/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/', 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(
path('archs/<str:arch>/machines/<str:machine>/capabilities', views.get_dom_capabilities, name='domcaps'), '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 accounts.models import UserInstance
from admin.decorators import superuser_only 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 computes.models import Compute
from instances.models import Instance from instances.models import Instance
from vrtManager.connection import (CONN_SOCKET, CONN_SSH, CONN_TCP, CONN_TLS, connection_manager, wvmConnect) from vrtManager.connection import (CONN_SOCKET, CONN_SSH, CONN_TCP, CONN_TLS, connection_manager, wvmConnect)
@ -22,58 +22,10 @@ def computes(request):
:param request: :param request:
:return: :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 = Compute.objects.filter().order_by('name')
computes_info = get_hosts_status(computes)
if request.method == 'POST': return render(request, 'computes/list.html', {'computes': computes})
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())
@superuser_only @superuser_only
@ -108,6 +60,51 @@ def overview(request, compute_id):
return render(request, 'overview.html', locals()) 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): def compute_graph(request, compute_id):
""" """
:param request: :param request:
@ -248,13 +245,3 @@ def get_dom_capabilities(request, compute_id, arch, machine):
pass pass
return HttpResponse(json.dumps(data)) 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==2.2.13
django-bootstrap4==2.0.1 django-bootstrap4==2.0.1
django-fa==1.0.0 django-icons==2.0.0
django-login-required-middleware==0.5.0 django-login-required-middleware==0.5.0
gunicorn==20.0.4 gunicorn==20.0.4
libvirt-python==6.3.0
libsass==0.20.0 libsass==0.20.0
libvirt-python==6.3.0
lxml==4.5.0 lxml==4.5.0
numpy==1.18.4 numpy==1.18.4
pytz==2020.1 pytz==2020.1
rwlock==0.0.7 rwlock==0.0.7
six==1.15.0 six==1.15.0
soupsieve==2.0.1
sqlparse==0.3.1 sqlparse==0.3.1
websockify==0.9.0 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.db.models import Model, CharField, IntegerField
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
class Flavor(Model): class Flavor(Model):
label = CharField(_('label'), max_length=12) label = CharField(_('label'), max_length=12)
memory = IntegerField(_('memory')) memory = IntegerField(_('memory'))
vcpu = IntegerField(_('vcpu')) vcpu = IntegerField(_('vcpu'))
disk = IntegerField(_('disk')) disk = IntegerField(_('disk'))
def __unicode__(self): def __str__(self):
return self.name 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, from django.db.models import (CASCADE, BooleanField, CharField, DateField, ForeignKey, Model)
ForeignKey, Model)
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from computes.models import Compute from computes.models import Compute
@ -12,8 +11,9 @@ class Instance(Model):
is_template = BooleanField(_('is template'), default=False) is_template = BooleanField(_('is template'), default=False)
created = DateField(_('created'), auto_now_add=True) created = DateField(_('created'), auto_now_add=True)
def __unicode__(self): def __str__(self):
return self.name return f'{self.compute}/{self.name}'
class PermissionSet(Model): class PermissionSet(Model):
""" """
@ -21,8 +21,6 @@ class PermissionSet(Model):
""" """
class Meta: class Meta:
default_permissions = () default_permissions = ()
permissions = ( permissions = (('clone_instances', _('Can clone instances')), )
('clone_instances', _('Can clone instances')),
)
managed = False managed = False

View file

@ -63,7 +63,7 @@
<tbody class="searchable"> <tbody class="searchable">
{% for inst, vm in all_user_vms.items %} {% for inst, vm in all_user_vms.items %}
<tr> <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 %} <td>{% if vm.status == 1 %}
<span class="text-success">{% trans "Active" %}</span> <span class="text-success">{% trans "Active" %}</span>
{% endif %} {% endif %}

View file

@ -42,7 +42,7 @@
<tr host="{{ host.1 }}"> <tr host="{{ host.1 }}">
<td class="text-right">{{ forloop.counter }} </td> <td class="text-right">{{ forloop.counter }} </td>
<td>&emsp; <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> <small><em>{{ vm.title }}</em></small>
</td> </td>
<td class="d-none d-sm-table-cell"> <td class="d-none d-sm-table-cell">

View file

@ -14,7 +14,7 @@
{% for host, inst in all_host_vms.items %} {% for host, inst in all_host_vms.items %}
{% for inst, vm in inst.items %} {% for inst, vm in inst.items %}
<tr> <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><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> <td>
{% if vm.status == 1 %}<span class="text-success">{% trans "Active" %}</span>{% endif %} {% 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> <a class="nav-link" href="{% url 'instances' compute.id %}"><i class="fa fa-desktop"></i> {% trans "Instances" %}</a>
</li> </li>
<li class="nav-item active"> <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> </li>
</ul> </ul>
</div> </div>

View file

@ -52,7 +52,7 @@
{{ ipv4 }} | {{ ipv4 }} |
{% endfor %} {% endfor %}
{% 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> </div>
{% if user_quota_msg %} {% if user_quota_msg %}
<div class="alert alert-warning fade show"> <div class="alert alert-warning fade show">
@ -1749,7 +1749,7 @@
</script> </script>
<script> <script>
function random_mac(net) { 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']); $('input[name="' + net + '"]').val(data['mac']);
}); });
} }
@ -1766,7 +1766,7 @@
<script> <script>
function guess_mac_address(src_elem, net) { function guess_mac_address(src_elem, net) {
new_vname = $(src_elem).val(); 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) { $.getJSON(guess_mac_address_url, function(data) {
$('input[name="clone-net-mac-'+net+'"]').val(data['mac']); $('input[name="clone-net-mac-'+net+'"]').val(data['mac']);
}); });
@ -1774,7 +1774,7 @@
</script> </script>
<script> <script>
function guess_clone_name() { 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]; guessed_name = data['name'].split(".")[0];
$('#clone_name').val(guessed_name); $('#clone_name').val(guessed_name);
update_clone_disk_name(guessed_name); update_clone_disk_name(guessed_name);
@ -2181,7 +2181,7 @@
{% endfor %} {% endfor %}
var graph_interval = window.setInterval(function graph_usage() { 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.labels.push(data.timeline);
cpuChart.data.datasets[0].data.push(data.cpudata); cpuChart.data.datasets[0].data.push(data.cpudata);
@ -2234,7 +2234,7 @@
backgroundJobRunning = false; backgroundJobRunning = false;
var status_interval = window.setInterval(function get_status() { var status_interval = window.setInterval(function get_status() {
var status = {{ status|lower }}; 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) { if (data['status'] != status && !backgroundJobRunning) {
window.location.reload() window.location.reload()
} }

View file

@ -79,7 +79,7 @@
{% for host, insts in all_host_vms.items %} {% for host, insts in all_host_vms.items %}
{% for inst, vm in insts.items %} {% for inst, vm in insts.items %}
<tr> <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 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> <td>
{% if vm.status == 1 %}<span class="text-success">{% trans "Active" %}</span>{% endif %} {% if vm.status == 1 %}<span class="text-success">{% trans "Active" %}</span>{% endif %}

View file

@ -1,8 +1,11 @@
from django.urls import path from django.urls import path
from . import views from . import views
app_name = 'instances'
urlpatterns = [ urlpatterns = [
path('', views.allinstances, name='allinstances'), path('', views.allinstances, name='index'),
path('<int:compute_id>/<vname>/', views.instance, name='instance'), path('<int:compute_id>/<vname>/', views.instance, name='instance'),
path('statistics/<int:compute_id>/<vname>/', views.inst_graph, name='inst_graph'), 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('status/<int:compute_id>/<vname>/', views.inst_status, name='inst_status'),

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.db.models import Model, CharField, DateTimeField
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
class Logs(Model): class Logs(Model):
user = CharField(_('user'), max_length=50) user = CharField(_('user'), max_length=50)
instance = CharField(_('instance'), max_length=50) instance = CharField(_('instance'), max_length=50)
message = CharField(_('message'), max_length=255) message = CharField(_('message'), max_length=255)
date = DateTimeField(_('date'), auto_now=True) date = DateTimeField(_('date'), auto_now=True)
def __unicode__(self): def __str__(self):
return self.instance return self.instance

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,5 @@
{% load i18n %} {% load i18n %}
{% load font_awesome %} {% load icons %}
{% load common_tags %} {% load common_tags %}
<!-- Fixed navbar --> <!-- Fixed navbar -->
<nav class="navbar navbar-expand-md navbar-dark bg-primary mb-3" aria-label="Main top 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"> <div id="navbar" class="navbar-collapse collapse">
<ul class="navbar-nav mr-auto mt-2 mt-md-0"> <ul class="navbar-nav mr-auto mt-2 mt-md-0">
<li class="nav-item {% class_active request '^/instances' %}"> <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> </li>
{% if request.user.is_superuser %} {% if request.user.is_superuser %}
<li class="nav-item {% class_active request '^/computes' %}"> <li class="nav-item {% class_active request '^/computes' %}">
@ -42,7 +42,7 @@
<a class="dropdown-item disabled" href="#"> <a class="dropdown-item disabled" href="#">
{% trans "Language" %}: <span class="badge badge-secondary">{{ LANGUAGE_CODE }}</span> {% trans "Language" %}: <span class="badge badge-secondary">{{ LANGUAGE_CODE }}</span>
</a> </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> <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> <a class="dropdown-item" href="{% url 'logout' %}"><i class="fa fa-fw fa-power-off"></i> {% trans "Log Out" %}</a>
</div> </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.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'bootstrap4', 'bootstrap4',
'fa', 'django_icons',
'accounts', 'accounts',
'admin', 'admin',
'appsettings', 'appsettings',