diff --git a/accounts/forms.py b/accounts/forms.py index 2e04118..8c05b75 100644 --- a/accounts/forms.py +++ b/accounts/forms.py @@ -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__' diff --git a/accounts/migrations/0004_auto_20200615_0637.py b/accounts/migrations/0004_auto_20200615_0637.py new file mode 100644 index 0000000..c24bd98 --- /dev/null +++ b/accounts/migrations/0004_auto_20200615_0637.py @@ -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'), + ), + ] diff --git a/accounts/migrations/0005_auto_20200616_1039.py b/accounts/migrations/0005_auto_20200616_1039.py new file mode 100644 index 0000000..bf48ddc --- /dev/null +++ b/accounts/migrations/0005_auto_20200616_1039.py @@ -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')}, + ), + ] diff --git a/accounts/models.py b/accounts/models.py index 0f96879..072d9f2 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -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 diff --git a/accounts/templates/account.html b/accounts/templates/account.html index 087f431..13d8f69 100644 --- a/accounts/templates/account.html +++ b/accounts/templates/account.html @@ -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 %}
- {% include 'create_user_inst_block.html' %} - + + {% icon 'plus' %} + +
{% include 'errors_block.html' %} - {% if request.user.is_superuser and publickeys %} -
-
-
- - + + +
+
+
+ + + + + + + + + + + + {% for inst in user_insts %} - - - - - - {% for publickey in publickeys %} - - - + + + + + + + {% endfor %} - -
#{% trans "Instance" %}{% trans "VNC" %}{% trans "Resize" %}{% trans "Delete" %}{% trans "Action" %}
{% trans "Key name" %}{% trans "Public key" %}
{{ publickey.keyname }}{{ publickey.keypublic|truncatechars:64 }}{{ forloop.counter }}{{ inst.instance.name }}{{ inst.is_vnc }}{{ inst.is_change }}{{ inst.is_delete }} + + {% icon 'pencil' %} + + + + {% icon 'trash' %} + +
-
+ + +
+
+ + + + + + + + + {% for publickey in publickeys %} + + + + + {% endfor %} + +
{% trans "Key name" %}{% trans "Public key" %}
{{ publickey.keyname }}{{ publickey.keypublic|truncatechars:64 }}
- {% endif %} - -
-
- {% if not user_insts %} -
-
- - {% trans "Warning" %}: {% trans "User doesn't have any Instance" %} -
-
- {% else %} -
- - - - - - - - - - - - - {% for inst in user_insts %} - - - - - - - - - - {% endfor %} - -
#{% trans "Instance" %}{% trans "VNC" %}{% trans "Resize" %}{% trans "Delete" %}{% trans "Action" %}
{{ forloop.counter }}{{ inst.instance.name }}{{ inst.is_vnc }}{{ inst.is_change }}{{ inst.is_delete }} - - - - - - - -
{% csrf_token %} - - -
-
-
- {% endif %} -
-
-{% endblock %} +{% endblock content %} diff --git a/accounts/templates/accounts/change_password_form.html b/accounts/templates/accounts/change_password_form.html new file mode 100644 index 0000000..9dfb6a3 --- /dev/null +++ b/accounts/templates/accounts/change_password_form.html @@ -0,0 +1,29 @@ +{% extends "base.html" %} + +{% load bootstrap4 %} +{% load i18n %} +{% load icons %} + +{% block title %}{%trans "Change Password" %}{% endblock title %} + +{% block content %} +
+
+

{%trans "Change Password" %}: {{ user }}

+
+
+
+ {% csrf_token %} + {% bootstrap_form form layout='horizontal' %} +
+
+ +
+{% endblock content %} diff --git a/accounts/templates/create_user_inst_block.html b/accounts/templates/create_user_inst_block.html deleted file mode 100644 index c496c01..0000000 --- a/accounts/templates/create_user_inst_block.html +++ /dev/null @@ -1,36 +0,0 @@ -{% load i18n %} -{% if request.user.is_superuser %} - - - - - - -{% endif %} \ No newline at end of file diff --git a/accounts/templates/profile.html b/accounts/templates/profile.html index 92b0ef6..54a8bd0 100644 --- a/accounts/templates/profile.html +++ b/accounts/templates/profile.html @@ -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 @@
+ {% if perms.accounts.change_password %} + {% icon 'lock' %} {% trans "Change Password" %} + {% endif %}
{% csrf_token %}
@@ -41,34 +45,6 @@
- {% if perms.accounts.change_password %} - -
{% csrf_token %} -
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
-
- -
-
-
- {% endif %} {% if publickeys %}
diff --git a/accounts/tests.py b/accounts/tests.py index 4d6c01e..fba29bf 100644 --- a/accounts/tests.py +++ b/accounts/tests.py @@ -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) diff --git a/accounts/urls.py b/accounts/urls.py index e4306df..6e170ae 100644 --- a/accounts/urls.py +++ b/accounts/urls.py @@ -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//', views.account, name='account'), + path('change_password/', views.change_password, name='change_password'), + path('user_instance/create//', views.user_instance_create, name='user_instance_create'), + path('user_instance//update/', views.user_instance_update, name='user_instance_update'), + path('user_instance//delete/', views.user_instance_delete, name='user_instance_delete'), ] diff --git a/accounts/views.py b/accounts/views.py index 9b8d5a1..2bd765c 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -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}, + ) diff --git a/admin/templates/admin/common/list.html b/admin/templates/admin/common/list.html index 8d76c0b..d267636 100644 --- a/admin/templates/admin/common/list.html +++ b/admin/templates/admin/common/list.html @@ -1,5 +1,5 @@ {% extends "base.html" %} -{% load font_awesome %} +{% load icons %} {% load i18n %} {% block title %}{{ title }}{% endblock %} diff --git a/admin/templates/admin/group_list.html b/admin/templates/admin/group_list.html index f58614d..82b8890 100644 --- a/admin/templates/admin/group_list.html +++ b/admin/templates/admin/group_list.html @@ -1,7 +1,7 @@ {% extends "base.html" %} {% load i18n %} {% load static %} -{% load font_awesome %} +{% load icons %} {% block title %}{% trans "Users" %}{% endblock %} {% block content %}
diff --git a/admin/templates/admin/logs.html b/admin/templates/admin/logs.html index 0cbdede..c2db0c4 100644 --- a/admin/templates/admin/logs.html +++ b/admin/templates/admin/logs.html @@ -1,5 +1,6 @@ {% extends "base.html" %} {% load i18n %} +{% load bootstrap4 %} {% block title %}{% trans "Logs" %}{% endblock %} {% block content %} @@ -47,7 +48,7 @@
- {% include "paging.html" %} + {% bootstrap_pagination logs %} {% endif %}
diff --git a/admin/templates/admin/user_form.html b/admin/templates/admin/user_form.html index b6d7919..db77710 100644 --- a/admin/templates/admin/user_form.html +++ b/admin/templates/admin/user_form.html @@ -1,6 +1,6 @@ {% extends "base.html" %} {% load bootstrap4 %} -{% load font_awesome %} +{% load icons %} {% load i18n %} {% block title %}{% trans "User" %}{% endblock %} diff --git a/admin/templates/admin/user_list.html b/admin/templates/admin/user_list.html index 03863ef..1e544f1 100644 --- a/admin/templates/admin/user_list.html +++ b/admin/templates/admin/user_list.html @@ -2,7 +2,7 @@ {% load i18n %} {% load static %} {% load common_tags %} -{% load font_awesome %} +{% load icons %} {% block title %}{% trans "Users" %}{% endblock %} {% block content %}
diff --git a/admin/views.py b/admin/views.py index a7fbed9..3a5f72e 100644 --- a/admin/views.py +++ b/admin/views.py @@ -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() diff --git a/appsettings/migrations/0003_auto_20200615_0637.py b/appsettings/migrations/0003_auto_20200615_0637.py new file mode 100644 index 0000000..54cefed --- /dev/null +++ b/appsettings/migrations/0003_auto_20200615_0637.py @@ -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'), + ), + ] diff --git a/computes/forms.py b/computes/forms.py index 3a27a39..8bf6d07 100644 --- a/computes/forms.py +++ b/computes/forms.py @@ -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 diff --git a/computes/migrations/0003_auto_20200615_0637.py b/computes/migrations/0003_auto_20200615_0637.py new file mode 100644 index 0000000..34cd59a --- /dev/null +++ b/computes/migrations/0003_auto_20200615_0637.py @@ -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'), + ), + ] diff --git a/computes/models.py b/computes/models.py index c2ab052..cfe0317 100644 --- a/computes/models.py +++ b/computes/models.py @@ -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 diff --git a/computes/templates/computes.html b/computes/templates/computes.html deleted file mode 100644 index 3e7f98b..0000000 --- a/computes/templates/computes.html +++ /dev/null @@ -1,246 +0,0 @@ -{% extends "base.html" %} -{% load i18n %} -{% block title %}{% trans "Computes" %}{% endblock %} -{% block content %} - -
-
- {% include 'create_comp_block.html' %} - -
-
- - - {% include 'errors_block.html' %} - -
- {% if computes_info %} - {% for compute in computes_info %} -
- {% if compute.status is True %} -
-
- {% else %} -
-
- {% endif %} -
- {% if compute.status is True %} - {{ compute.name }} - {% else %} - {{ compute.name }} - {% endif %} - - - -
-
-
-
-
{% trans "Status" %}
- {% if compute.status %} -
{% trans "Connected" %}
- {% else %} -
{% trans "Not Connected" %}
- {% endif %} -
{% trans "Details" %}
- {% if compute.details %} -
{% trans compute.details %}
- {% else %} -
{% trans "No details available" %}
- {% endif %} -
- - - -
-
-
- {% endfor %} - {% else %} -
-
- - {% trans "Warning" %}: {% trans "Hypervisor doesn't have any Computes" %} -
-
- {% endif %} -
-{% endblock %} diff --git a/computes/templates/computes/form.html b/computes/templates/computes/form.html index 35c5da7..483a3d1 100644 --- a/computes/templates/computes/form.html +++ b/computes/templates/computes/form.html @@ -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 @@
-{% bootstrap_messages %}
diff --git a/computes/templates/computes/list.html b/computes/templates/computes/list.html new file mode 100644 index 0000000..18a2886 --- /dev/null +++ b/computes/templates/computes/list.html @@ -0,0 +1,69 @@ +{% extends "base.html" %} +{% load i18n %} +{% load static %} +{% load common_tags %} +{% load icons %} +{% block title %}{% trans "Computes" %}{% endblock %} +{% block content %} +
+
+ {% include 'create_comp_block.html' %} + {% include 'search_block.html' %} + +
+
+{% include 'errors_block.html' %} +
+ {% if not computes %} +
+
+ + {% icon 'exclamation-triangle '%} {% trans "Warning" %}: {% trans "You don't have any computes" %} +
+
+ {% else %} +
+ + + + + + + + + + + {% for compute in computes %} + + + + + + + {% endfor %} + +
{% trans "Name" %}{% trans "Status" %}{% trans "Details" %}{% trans "Actions" %}
+ {{ compute.name }} + + {% if compute.status is True %}{% trans "Connected" %}{% else %}{% trans "Not Connected" %}{% endif %} + + {{ compute.details|default:"" }} + +
+ {% if compute.status is True %} + {% icon 'eye' %} + {% else %} + {% icon 'eye' %} + {% endif %} + {% icon 'pencil' %} + {% icon 'times' %} +
+
+
+ {% endif %} +
+{% endblock content %} + +{% block script %} + +{% endblock script %} diff --git a/computes/templates/create_comp_block.html b/computes/templates/create_comp_block.html index b552d67..478fea4 100644 --- a/computes/templates/create_comp_block.html +++ b/computes/templates/create_comp_block.html @@ -1,7 +1,7 @@ {% load i18n %} {% load bootstrap4 %} -{% load font_awesome %} -
+{% load icons %} +
{% trans "TCP" %} {% trans "SSH" %} {% trans "TLS" %} diff --git a/computes/tests.py b/computes/tests.py index ca4cb4c..4197be3 100644 --- a/computes/tests.py +++ b/computes/tests.py @@ -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) diff --git a/computes/urls.py b/computes/urls.py index 54f67ff..0649cf4 100644 --- a/computes/urls.py +++ b/computes/urls.py @@ -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('/', 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//volumes', get_volumes, name='volumes'), - path('storage//', storage, name='storage'), - path('networks/', networks, name='networks'), - path('network//', network, name='network'), - path('interfaces/', interfaces, name='interfaces'), - path('interface//', interface, name='interface'), - path('nwfilters/', nwfilters, name='nwfilters'), - path('nwfilter//', nwfilter, name='nwfilter'), - path('secrets/', secrets, name='secrets'), - path('create/', create_instance_select_type, name='create_instance_select_type'), - path('create/archs//machines//', create_instance, name='create_instance'), - path('archs//machines', views.get_compute_machine_types, name='machines'), - path('archs//machines//disks//buses', views.get_compute_disk_buses, name='buses'), - path('archs//machines//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( + '/', + 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//volumes', get_volumes, name='volumes'), + path('storage//', storage, name='storage'), + path('networks/', networks, name='networks'), + path('network//', network, name='network'), + path('interfaces/', interfaces, name='interfaces'), + path('interface//', interface, name='interface'), + path('nwfilters/', nwfilters, name='nwfilters'), + path('nwfilter//', nwfilter, name='nwfilter'), + path('secrets/', secrets, name='secrets'), + path('create/', create_instance_select_type, name='create_instance_select_type'), + path('create/archs//machines//', create_instance, name='create_instance'), + path('archs//machines/', views.get_compute_machine_types, name='machines'), + path( + 'archs//machines//disks//buses/', + views.get_compute_disk_buses, + name='buses', + ), + path('archs//machines//capabilities/', views.get_dom_capabilities, name='domcaps'), + ])), ] diff --git a/computes/views.py b/computes/views.py index 2685760..d832c0e 100644 --- a/computes/views.py +++ b/computes/views.py @@ -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}) diff --git a/conf/requirements-dev.txt b/conf/requirements-dev.txt new file mode 100644 index 0000000..de27d31 --- /dev/null +++ b/conf/requirements-dev.txt @@ -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 diff --git a/conf/requirements.txt b/conf/requirements.txt index 82b52c5..f55bd1f 100644 --- a/conf/requirements.txt +++ b/conf/requirements.txt @@ -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 diff --git a/create/migrations/0003_auto_20200615_0637.py b/create/migrations/0003_auto_20200615_0637.py new file mode 100644 index 0000000..e4cf66d --- /dev/null +++ b/create/migrations/0003_auto_20200615_0637.py @@ -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'), + ), + ] diff --git a/create/models.py b/create/models.py index 4f504a4..202c0e9 100644 --- a/create/models.py +++ b/create/models.py @@ -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 diff --git a/instances/migrations/0003_auto_20200615_0637.py b/instances/migrations/0003_auto_20200615_0637.py new file mode 100644 index 0000000..d8638a5 --- /dev/null +++ b/instances/migrations/0003_auto_20200615_0637.py @@ -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'), + ), + ] diff --git a/instances/models.py b/instances/models.py index b4163fc..cec6d39 100644 --- a/instances/models.py +++ b/instances/models.py @@ -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 diff --git a/instances/templates/allinstances.html b/instances/templates/allinstances.html index 8e6b342..bf03976 100644 --- a/instances/templates/allinstances.html +++ b/instances/templates/allinstances.html @@ -63,7 +63,7 @@ {% for inst, vm in all_user_vms.items %} - {{ vm.name }}
{{ vm.title }} + {{ vm.name }}
{{ vm.title }} {% if vm.status == 1 %} {% trans "Active" %} {% endif %} diff --git a/instances/templates/allinstances_index_grouped.html b/instances/templates/allinstances_index_grouped.html index 40ed8b0..be5bbcf 100644 --- a/instances/templates/allinstances_index_grouped.html +++ b/instances/templates/allinstances_index_grouped.html @@ -42,7 +42,7 @@ {{ forloop.counter }}   - {{ inst }}
+ {{ inst }}
{{ vm.title }} diff --git a/instances/templates/allinstances_index_nongrouped.html b/instances/templates/allinstances_index_nongrouped.html index a8f9da4..6d45116 100644 --- a/instances/templates/allinstances_index_nongrouped.html +++ b/instances/templates/allinstances_index_nongrouped.html @@ -14,7 +14,7 @@ {% for host, inst in all_host_vms.items %} {% for inst, vm in inst.items %} - {{ inst }}
{{ info.title }} + {{ inst }}
{{ info.title }} {{ host.1 }}
{% if info.userinstances.count > 0 %}{{ info.userinstances.first_user.user.username }}{% if info.userinstances.count > 1 %} (+{{ info.userinstances.count|add:"-1" }}){% endif %}{% endif %} {% if vm.status == 1 %}{% trans "Active" %}{% endif %} diff --git a/instances/templates/bottom_bar.html b/instances/templates/bottom_bar.html index 040af6c..81a9e39 100644 --- a/instances/templates/bottom_bar.html +++ b/instances/templates/bottom_bar.html @@ -17,7 +17,7 @@ {% trans "Instances" %}
diff --git a/instances/templates/instance.html b/instances/templates/instance.html index a7b8b8f..d25ce0b 100644 --- a/instances/templates/instance.html +++ b/instances/templates/instance.html @@ -52,7 +52,7 @@ {{ ipv4 }} | {% endfor %} {% endfor %} - +
{% if user_quota_msg %}
@@ -1749,7 +1749,7 @@