diff --git a/.gitignore b/.gitignore index 289210e..f90e8ec 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ tags dhcpd.* webvirtcloud/settings.py *migrations/* +.coverage +htmlcov \ No newline at end of file diff --git a/accounts/__init__.py b/accounts/__init__.py index e69de29..8319823 100644 --- a/accounts/__init__.py +++ b/accounts/__init__.py @@ -0,0 +1 @@ +default_app_config = 'accounts.apps.AccountsConfig' diff --git a/accounts/admin.py b/accounts/admin.py deleted file mode 100644 index 8c38f3f..0000000 --- a/accounts/admin.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.contrib import admin - -# Register your models here. diff --git a/accounts/apps.py b/accounts/apps.py new file mode 100644 index 0000000..94f4d27 --- /dev/null +++ b/accounts/apps.py @@ -0,0 +1,51 @@ +from django.apps import AppConfig +from django.db.models.signals import post_migrate + + +def apply_change_password(sender, **kwargs): + ''' + Apply new change_password permission for all users + Depending on settings SHOW_PROFILE_EDIT_PASSWORD + ''' + from django.conf import settings + from django.contrib.auth.models import User, Permission + if hasattr(settings, 'SHOW_PROFILE_EDIT_PASSWORD'): + print('\033[92mSHOW_PROFILE_EDIT_PASSWORD is found inside settings.py\033[0m') + print('\033[92mApplying permission can_change_password for all users\033[0m') + users = User.objects.all() + permission = Permission.objects.get(codename='change_password') + if settings.SHOW_PROFILE_EDIT_PASSWORD: + print('\033[91mWarning!!! Setting to True for all users\033[0m') + for user in users: + user.user_permissions.add(permission) + else: + print('\033[91mWarning!!! Setting to False for all users\033[0m') + for user in users: + user.user_permissions.remove(permission) + print('\033[1mDon`t forget to remove the option from settings.py\033[0m') + + +def create_admin(sender, **kwargs): + ''' + Create initial admin user + ''' + from django.contrib.auth.models import User + from accounts.models import UserAttributes + + plan = kwargs['plan'] + for migration, rolled_back in plan: + if migration.app_label == 'accounts' and migration.name == '0001_initial' and not rolled_back: + if User.objects.count() == 0: + print('\033[92mCreating default admin user\033[0m') + admin = User.objects.create_superuser('admin', None, 'admin') + UserAttributes(user=admin, max_instances=-1, max_cpus=-1, max_memory=-1, max_disk_size=-1).save() + break + + +class AccountsConfig(AppConfig): + name = 'accounts' + verbose_name = 'Accounts' + + def ready(self): + post_migrate.connect(apply_change_password, sender=self) + post_migrate.connect(create_admin, sender=self) diff --git a/accounts/backends.py b/accounts/backends.py deleted file mode 100644 index e66b94a..0000000 --- a/accounts/backends.py +++ /dev/null @@ -1,13 +0,0 @@ -from django.contrib.auth.backends import RemoteUserBackend -from accounts.models import UserInstance, UserAttributes -from instances.models import Instance - -class MyRemoteUserBackend(RemoteUserBackend): - - #create_unknown_user = True - - def configure_user(self, user): - #user.is_superuser = True - UserAttributes.configure_user(user) - return user - diff --git a/accounts/migrations/0002_addAdmin.py b/accounts/migrations/0002_addAdmin.py deleted file mode 100644 index 09a91fc..0000000 --- a/accounts/migrations/0002_addAdmin.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 2.2.10 on 2020-01-28 07:01 - -from django.db import migrations - - -def add_useradmin(apps, schema_editor): - from django.utils import timezone - from django.contrib.auth.models import User - from accounts.models import UserAttributes - - admin = User.objects.create_superuser('admin', None, 'admin', last_login=timezone.now()) - UserAttributes(user=admin, max_instances=-1, max_cpus=-1, max_memory=-1, max_disk_size=-1).save() - - -class Migration(migrations.Migration): - - dependencies = [ - ('accounts', '0001_initial'), - ] - - operations = [ - migrations.RunPython(add_useradmin), - ] diff --git a/accounts/migrations/0003_permissionset.py b/accounts/migrations/0002_permissionset.py similarity index 91% rename from accounts/migrations/0003_permissionset.py rename to accounts/migrations/0002_permissionset.py index d7cf465..5b36210 100644 --- a/accounts/migrations/0003_permissionset.py +++ b/accounts/migrations/0002_permissionset.py @@ -6,7 +6,7 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('accounts', '0002_addAdmin'), + ('accounts', '0001_initial'), ] operations = [ @@ -16,7 +16,7 @@ class Migration(migrations.Migration): ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ], options={ - 'permissions': (('change_password', 'Can change password'),), + 'permissions': (('change_password', 'Can change password'), ), 'managed': False, 'default_permissions': (), }, diff --git a/accounts/migrations/0004_apply_change_password.py b/accounts/migrations/0004_apply_change_password.py deleted file mode 100644 index f313b7e..0000000 --- a/accounts/migrations/0004_apply_change_password.py +++ /dev/null @@ -1,25 +0,0 @@ -from django.db import migrations - - -def apply_change_password(apps, schema_editor): - from django.conf import settings - from django.contrib.auth.models import User, Permission - - if hasattr(settings, 'SHOW_PROFILE_EDIT_PASSWORD'): - if settings.SHOW_PROFILE_EDIT_PASSWORD: - permission = Permission.objects.get(codename='change_password') - users = User.objects.all() - user: User - for user in users: - user.user_permissions.add(permission) - - -class Migration(migrations.Migration): - - dependencies = [ - ('accounts', '0003_permissionset'), - ] - - operations = [ - migrations.RunPython(apply_change_password), - ] diff --git a/accounts/migrations/0005_remove_userattributes_can_clone_instances.py b/accounts/migrations/0005_remove_userattributes_can_clone_instances.py deleted file mode 100644 index 76e0458..0000000 --- a/accounts/migrations/0005_remove_userattributes_can_clone_instances.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.2.12 on 2020-05-28 04:24 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('accounts', '0004_apply_change_password'), - ('instances', '0003_migrate_can_clone_instances'), - ] - - operations = [ - migrations.RemoveField( - model_name='userattributes', - name='can_clone_instances', - ), - ] diff --git a/accounts/models.py b/accounts/models.py index 8484a7e..33b6251 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -29,6 +29,7 @@ class UserSSHKey(models.Model): class UserAttributes(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE) + can_clone_instances = models.BooleanField(default=True) max_instances = models.IntegerField(default=1, help_text="-1 for unlimited. Any integer value", validators=[ diff --git a/accounts/tests.py b/accounts/tests.py index 7ce503c..4d6c01e 100644 --- a/accounts/tests.py +++ b/accounts/tests.py @@ -1,3 +1,28 @@ -from django.test import TestCase +from django.contrib.auth.models import User +from django.shortcuts import reverse +from django.test import Client, TestCase -# Create your tests here. + +class AccountsTestCase(TestCase): + def setUp(self): + self.client.login(username='admin', password='admin') + User.objects.create_user(username='test', password='test') + + def test_profile(self): + response = self.client.get(reverse('profile')) + self.assertEqual(response.status_code, 200) + + response = self.client.get(reverse('account', args=[2])) + self.assertEqual(response.status_code, 200) + + def test_login_logout(self): + user = User.objects.get(username='test') + self.assertEqual(user.id, 2) + + client = Client() + + response = client.post(reverse('login'), {'username': 'test', 'password': 'test'}) + self.assertRedirects(response, reverse('profile')) + + response = client.get(reverse('logout')) + self.assertRedirects(response, reverse('login')) diff --git a/admin/forms.py b/admin/forms.py index 29c87b8..5c658aa 100644 --- a/admin/forms.py +++ b/admin/forms.py @@ -91,4 +91,4 @@ class UserCreateForm(UserForm): class UserAttributesForm(forms.ModelForm): class Meta: model = UserAttributes - exclude = ['user'] + exclude = ['user', 'can_clone_instances'] diff --git a/admin/templates/admin/common/form.html b/admin/templates/admin/common/form.html index 4ff3fd3..ced001e 100644 --- a/admin/templates/admin/common/form.html +++ b/admin/templates/admin/common/form.html @@ -11,6 +11,7 @@ +{% bootstrap_messages %}
diff --git a/admin/tests.py b/admin/tests.py new file mode 100644 index 0000000..b030436 --- /dev/null +++ b/admin/tests.py @@ -0,0 +1,120 @@ +from django.contrib.auth.models import Group, User +from django.core.exceptions import ObjectDoesNotExist +from django.shortcuts import reverse +from django.test import TestCase + +from accounts.models import UserAttributes + + +class AdminTestCase(TestCase): + def setUp(self): + self.client.login(username='admin', password='admin') + + def test_group_list(self): + response = self.client.get(reverse('admin:group_list')) + self.assertEqual(response.status_code, 200) + + def test_groups(self): + response = self.client.get(reverse('admin:group_create')) + self.assertEqual(response.status_code, 200) + + response = self.client.post(reverse('admin:group_create'), {'name': 'Test Group'}) + self.assertRedirects(response, reverse('admin:group_list')) + + group = Group.objects.get(name='Test Group') + self.assertEqual(group.id, 1) + + response = self.client.get(reverse('admin:group_update', args=[1])) + self.assertEqual(response.status_code, 200) + + response = self.client.post(reverse('admin:group_update', args=[1]), {'name': 'Updated Group Test'}) + self.assertRedirects(response, reverse('admin:group_list')) + + group = Group.objects.get(id=1) + self.assertEqual(group.name, 'Updated Group Test') + + response = self.client.get(reverse('admin:group_delete', args=[1])) + self.assertEqual(response.status_code, 200) + + response = self.client.post(reverse('admin:group_delete', args=[1])) + self.assertRedirects(response, reverse('admin:group_list')) + + with self.assertRaises(ObjectDoesNotExist): + Group.objects.get(id=1) + + def test_user_list(self): + response = self.client.get(reverse('admin:user_list')) + self.assertEqual(response.status_code, 200) + + def test_users(self): + response = self.client.get(reverse('admin:user_create')) + self.assertEqual(response.status_code, 200) + + response = self.client.post( + reverse('admin:user_create'), + { + 'username': 'test', + 'password': 'test', + 'max_instances': 1, + 'max_cpus': 1, + 'max_memory': 1024, + 'max_disk_size': 4, + }, + ) + self.assertRedirects(response, reverse('admin:user_list')) + + user = User.objects.get(username='test') + self.assertEqual(user.id, 2) + + ua: UserAttributes = UserAttributes.objects.get(id=2) + self.assertEqual(ua.user_id, 2) + self.assertEqual(ua.max_instances, 1) + self.assertEqual(ua.max_cpus, 1) + self.assertEqual(ua.max_memory, 1024) + self.assertEqual(ua.max_disk_size, 4) + + response = self.client.get(reverse('admin:user_update', args=[2])) + self.assertEqual(response.status_code, 200) + + response = self.client.post( + reverse('admin:user_update', args=[2]), + { + 'username': 'utest', + 'max_instances': 2, + 'max_cpus': 2, + 'max_memory': 2048, + 'max_disk_size': 8, + }, + ) + self.assertRedirects(response, reverse('admin:user_list')) + + user = User.objects.get(id=2) + self.assertEqual(user.username, 'utest') + + ua: UserAttributes = UserAttributes.objects.get(id=2) + self.assertEqual(ua.user_id, 2) + self.assertEqual(ua.max_instances, 2) + self.assertEqual(ua.max_cpus, 2) + self.assertEqual(ua.max_memory, 2048) + self.assertEqual(ua.max_disk_size, 8) + + response = self.client.get(reverse('admin:user_block', args=[2])) + user = User.objects.get(id=2) + self.assertFalse(user.is_active) + + response = self.client.get(reverse('admin:user_unblock', args=[2])) + user = User.objects.get(id=2) + self.assertTrue(user.is_active) + + response = self.client.get(reverse('admin:user_delete', args=[2])) + self.assertEqual(response.status_code, 200) + + response = self.client.post(reverse('admin:user_delete', args=[2])) + self.assertRedirects(response, reverse('admin:user_list')) + + with self.assertRaises(ObjectDoesNotExist): + User.objects.get(id=2) + + def test_logs(self): + response = self.client.get(reverse('admin:logs')) + self.assertEqual(response.status_code, 200) diff --git a/admin/views.py b/admin/views.py index 85a4370..955f23f 100644 --- a/admin/views.py +++ b/admin/views.py @@ -1,5 +1,4 @@ from django.conf import settings -from django.contrib.auth.decorators import user_passes_test from django.contrib.auth.models import Group, User from django.core.paginator import Paginator from django.shortcuts import get_object_or_404, redirect, render @@ -30,6 +29,7 @@ def group_create(request): if form.is_valid(): form.save() return redirect('admin:group_list') + return render( request, 'admin/common/form.html', diff --git a/computes/admin.py b/computes/admin.py deleted file mode 100644 index 8c38f3f..0000000 --- a/computes/admin.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.contrib import admin - -# Register your models here. diff --git a/computes/forms.py b/computes/forms.py index bbde4d4..3a27a39 100644 --- a/computes/forms.py +++ b/computes/forms.py @@ -2,131 +2,51 @@ import re from django import forms from django.utils.translation import ugettext_lazy as _ from computes.models import Compute +from vrtManager.connection import CONN_TCP, CONN_SSH, CONN_TLS, CONN_SOCKET +from .validators import validate_hostname -class ComputeAddTcpForm(forms.Form): - 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(error_messages={'required': _('No password has been entered')}, - max_length=100) - details = forms.CharField(max_length=50, required=False) +class TcpComputeForm(forms.ModelForm): + hostname = forms.CharField(validators=[validate_hostname]) + type = forms.IntegerField(widget=forms.HiddenInput, initial=CONN_TCP) - 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 host name must not contain any special characters')) - elif len(name) > 20: - raise forms.ValidationError(_('The host name must not exceed 20 characters')) - try: - Compute.objects.get(name=name) - except Compute.DoesNotExist: - return name - raise forms.ValidationError(_('This host is already connected')) - - def clean_hostname(self): - hostname = self.cleaned_data['hostname'] - have_symbol = re.match('[^a-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')) - try: - Compute.objects.get(hostname=hostname) - except Compute.DoesNotExist: - return hostname - raise forms.ValidationError(_('This host is already connected')) + class Meta: + model = Compute + fields = '__all__' -class ComputeAddSshForm(forms.Form): - 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=20) - details = forms.CharField(max_length=50, required=False) +class SshComputeForm(forms.ModelForm): + hostname = forms.CharField(validators=[validate_hostname], label=_("FQDN/IP")) + type = forms.IntegerField(widget=forms.HiddenInput, initial=CONN_SSH) - 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')) - try: - Compute.objects.get(name=name) - except Compute.DoesNotExist: - return name - raise forms.ValidationError(_('This host is already connected')) - - 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')) - try: - Compute.objects.get(hostname=hostname) - except Compute.DoesNotExist: - return hostname - raise forms.ValidationError(_('This host is already connected')) + class Meta: + model = Compute + exclude = ['password'] -class ComputeAddTlsForm(forms.Form): - 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(error_messages={'required': _('No password has been entered')}, - max_length=100) - details = forms.CharField(max_length=50, required=False) +class TlsComputeForm(forms.ModelForm): + hostname = forms.CharField(validators=[validate_hostname]) + type = forms.IntegerField(widget=forms.HiddenInput, initial=CONN_TLS) - 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 host name must not contain any special characters')) - elif len(name) > 20: - raise forms.ValidationError(_('The host name must not exceed 20 characters')) - try: - Compute.objects.get(name=name) - except Compute.DoesNotExist: - return name - raise forms.ValidationError(_('This host is already connected')) + class Meta: + model = Compute + fields = '__all__' - def clean_hostname(self): - hostname = self.cleaned_data['hostname'] - have_symbol = re.match('[^a-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')) - try: - Compute.objects.get(hostname=hostname) - except Compute.DoesNotExist: - return hostname - raise forms.ValidationError(_('This host is already connected')) + +class SocketComputeForm(forms.ModelForm): + hostname = forms.CharField(widget=forms.HiddenInput, initial='localhost') + type = forms.IntegerField(widget=forms.HiddenInput, initial=CONN_SOCKET) + + 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) + 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) @@ -148,21 +68,3 @@ class ComputeEditHostForm(forms.Form): elif wrong_ip: raise forms.ValidationError(_('Wrong IP address')) return hostname - - -class ComputeAddSocketForm(forms.Form): - name = forms.CharField(error_messages={'required': _('No hostname has been entered')}, max_length=64) - details = forms.CharField(error_messages={'required': _('No details has been entred')}, max_length=50) - - 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 host name must not contain any special characters')) - elif len(name) > 20: - raise forms.ValidationError(_('The host name must not exceed 20 characters')) - try: - Compute.objects.get(name=name) - except Compute.DoesNotExist: - return name - raise forms.ValidationError(_('This host is already connected')) diff --git a/computes/migrations/0002_auto_20200529_1320.py b/computes/migrations/0002_auto_20200529_1320.py new file mode 100644 index 0000000..194d885 --- /dev/null +++ b/computes/migrations/0002_auto_20200529_1320.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.12 on 2020-05-29 13:20 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('computes', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='compute', + name='name', + field=models.CharField(max_length=64, unique=True), + ), + ] diff --git a/computes/models.py b/computes/models.py index db224a3..bda751b 100644 --- a/computes/models.py +++ b/computes/models.py @@ -2,7 +2,7 @@ from django.db.models import Model, CharField, IntegerField class Compute(Model): - name = CharField(max_length=64) + name = CharField(max_length=64, unique=True) hostname = CharField(max_length=64) login = CharField(max_length=20) password = CharField(max_length=14, blank=True, null=True) diff --git a/computes/templates/computes/form.html b/computes/templates/computes/form.html new file mode 100644 index 0000000..7b3489e --- /dev/null +++ b/computes/templates/computes/form.html @@ -0,0 +1,29 @@ +{% extends "base.html" %} +{% load bootstrap3 %} +{% load font_awesome %} +{% load i18n %} + +{% block title %}{% trans "Add Compute" %}{% endblock %} + +{% block content %} +
+
+ +
+
+{% bootstrap_messages %} +
+
+ + {% csrf_token %} + {% bootstrap_form form layout='horizontal' %} + +
+ {% icon 'times' %} {% trans "Cancel" %} + +
+
+
+{% endblock content %} \ No newline at end of file diff --git a/computes/templates/create_comp_block.html b/computes/templates/create_comp_block.html index 734e4c1..4bcf7dd 100644 --- a/computes/templates/create_comp_block.html +++ b/computes/templates/create_comp_block.html @@ -1,185 +1,9 @@ {% load i18n %} -{% if request.user.is_superuser %} - - - - - - -{% endif %} +{% load bootstrap3 %} +
{% trans "Add Connection" %}
+ diff --git a/computes/tests.py b/computes/tests.py index 7ce503c..ca4cb4c 100644 --- a/computes/tests.py +++ b/computes/tests.py @@ -1,3 +1,83 @@ +from django.shortcuts import reverse from django.test import TestCase -# Create your tests here. +from .models import Compute + + +class ComputesTestCase(TestCase): + def setUp(self): + self.client.login(username='admin', password='admin') + Compute( + name='local', + hostname='localhost', + login='', + password='', + details='local', + type=4, + ).save() + + def test_index(self): + response = self.client.get(reverse('computes')) + self.assertEqual(response.status_code, 200) + + def test_overview(self): + response = self.client.get(reverse('overview', args=[1])) + self.assertEqual(response.status_code, 200) + + def test_graph(self): + response = self.client.get(reverse('compute_graph', args=[1])) + self.assertEqual(response.status_code, 200) + + def test_instances(self): + response = self.client.get(reverse('instances', args=[1])) + self.assertEqual(response.status_code, 200) + + def test_storages(self): + response = self.client.get(reverse('storages', args=[1])) + self.assertEqual(response.status_code, 200) + + def test_default_storage_volumes(self): + response = self.client.get(reverse('volumes', kwargs={'compute_id': 1, 'pool': 'default'})) + self.assertEqual(response.status_code, 200) + + def test_default_storage(self): + response = self.client.get(reverse('storage', kwargs={'compute_id': 1, 'pool': 'default'})) + self.assertEqual(response.status_code, 200) + + def test_networks(self): + response = self.client.get(reverse('networks', args=[1])) + self.assertEqual(response.status_code, 200) + + def test_default_network(self): + response = self.client.get(reverse('network', kwargs={'compute_id': 1, 'pool': 'default'})) + self.assertEqual(response.status_code, 200) + + def test_interfaces(self): + response = self.client.get(reverse('interfaces', args=[1])) + self.assertEqual(response.status_code, 200) + + # TODO: add test for single interface + + def test_nwfilters(self): + response = self.client.get(reverse('nwfilters', args=[1])) + self.assertEqual(response.status_code, 200) + + # TODO: add test for single nwfilter + + def test_secrets(self): + response = self.client.get(reverse('secrets', args=[1])) + self.assertEqual(response.status_code, 200) + + def test_create_instance_select_type(self): + response = self.client.get(reverse('create_instance_select_type', args=[1])) + self.assertEqual(response.status_code, 200) + + # TODO: create_instance + + def test_machines(self): + response = self.client.get(reverse('machines', kwargs={'compute_id': 1, 'arch': 'x86_64'})) + self.assertEqual(response.status_code, 200) + + # TODO: get_compute_disk_buses + + # TODO: domcaps diff --git a/computes/urls.py b/computes/urls.py index be5f09b..fa77f79 100644 --- a/computes/urls.py +++ b/computes/urls.py @@ -1,32 +1,46 @@ -from django.urls import path, re_path -from storages.views import storages, storage, get_volumes -from networks.views import networks, network from secrets.views import secrets + +from django.urls import path + +from . import views from create.views import create_instance, create_instance_select_type -from interfaces.views import interfaces, interface -from computes.views import overview, compute_graph, computes, get_compute_disk_buses, get_compute_machine_types, get_dom_capabilities from instances.views import instances +from interfaces.views import interface, interfaces +from networks.views import network, networks from nwfilters.views import nwfilter, nwfilters +from storages.views import get_volumes, storage, storages +from . import forms urlpatterns = [ - path('', computes, name='computes'), - re_path(r'^(?P[0-9]+)/$', overview, name='overview'), - re_path(r'^(?P[0-9]+)/statistics$', compute_graph, name='compute_graph'), - re_path(r'^(?P[0-9]+)/instances/$', instances, name='instances'), - re_path(r'^(?P[0-9]+)/storages/$', storages, name='storages'), - re_path(r'^(?P[0-9]+)/storage/(?P[\w\-\.\/]+)/volumes$', get_volumes, name='volumes'), - re_path(r'^(?P[0-9]+)/storage/(?P[\w\-\.\/]+)/$', storage, name='storage'), - re_path(r'^(?P[0-9]+)/networks/$', networks, name='networks'), - re_path(r'^(?P[0-9]+)/network/(?P[\w\-\.]+)/$', network, name='network'), - re_path(r'^(?P[0-9]+)/interfaces/$', interfaces, name='interfaces'), - re_path(r'^(?P[0-9]+)/interface/(?P[\w\-\.\:]+)/$', interface, name='interface'), - re_path(r'^(?P[0-9]+)/nwfilters/$', nwfilters, name='nwfilters'), - re_path(r'^(?P[0-9]+)/nwfilter/(?P[\w\-\.\:]+)/$', nwfilter, name='nwfilter'), - re_path(r'^(?P[0-9]+)/secrets/$', secrets, name='secrets'), - re_path(r'^(?P[0-9]+)/create/$', create_instance_select_type, name='create_instance_select_type'), - re_path(r'^(?P[0-9]+)/create/archs/(?P[\w\-\.\/]+)/machines/(?P[\w\-\.\/]+)$', create_instance, name='create_instance'), - re_path(r'^(?P[0-9]+)/archs/(?P[\w\-\.\/]+)/machines$', get_compute_machine_types, name='machines'), - re_path(r'^(?P[0-9]+)/archs/(?P[\w\-\.\/]+)/machines/(?P[\w\-\.\/]+)/disks/(?P[\w\-\.\/]+)/buses$', get_compute_disk_buses, name='buses'), - re_path(r'^(?P[0-9]+)/archs/(?P[\w\-\.\/]+)/machines/(?P[\w\-\.\/]+)/capabilities$', get_dom_capabilities, name='domcaps'), + 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('/', 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', + ), ] - diff --git a/computes/validators.py b/computes/validators.py new file mode 100644 index 0000000..abdc4b8 --- /dev/null +++ b/computes/validators.py @@ -0,0 +1,24 @@ +from django.core.exceptions import ValidationError + +from django.utils.translation import ugettext_lazy as _ +import re + +have_symbol = re.compile('[^a-zA-Z0-9._-]+') +wrong_ip = re.compile('^0.|^255.') +wrong_name = re.compile('[^a-zA-Z0-9._-]+') + + +def validate_hostname(value): + sym = have_symbol.match(value) + wip = wrong_ip.match(value) + + if sym: + raise ValidationError(_('Hostname must contain only numbers, or the domain name separated by "."')) + elif wip: + raise ValidationError(_('Wrong IP address')) + + +def validate_name(value): + have_symbol = wrong_name.match('[^a-zA-Z0-9._-]+') + if have_symbol: + raise ValidationError(_('The host name must not contain any special characters')) diff --git a/computes/views.py b/computes/views.py index d6b5c73..6d7a4bd 100644 --- a/computes/views.py +++ b/computes/views.py @@ -1,16 +1,19 @@ import json -from django.utils import timezone + +from django.contrib import messages from django.http import HttpResponse, HttpResponseRedirect +from django.shortcuts import get_object_or_404, redirect, render, reverse from django.urls import reverse -from django.shortcuts import render, get_object_or_404 +from django.utils import timezone +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.models import Compute from instances.models import Instance -from accounts.models import UserInstance -from computes.forms import ComputeAddTcpForm, ComputeAddSshForm, ComputeEditHostForm, ComputeAddTlsForm, ComputeAddSocketForm +from vrtManager.connection import (CONN_SOCKET, CONN_SSH, CONN_TCP, CONN_TLS, connection_manager, wvmConnect) from vrtManager.hostdetails import wvmHostDetails -from vrtManager.connection import CONN_SSH, CONN_TCP, CONN_TLS, CONN_SOCKET, connection_manager, wvmConnect -from libvirt import libvirtError -from admin.decorators import superuser_only @superuser_only @@ -55,65 +58,6 @@ def computes(request): del_host = Compute.objects.get(id=compute_id) del_host.delete() return HttpResponseRedirect(request.get_full_path()) - if 'host_tcp_add' in request.POST: - form = ComputeAddTcpForm(request.POST) - if form.is_valid(): - data = form.cleaned_data - new_tcp_host = Compute(name=data['name'], - hostname=data['hostname'], - type=CONN_TCP, - login=data['login'], - password=data['password'], - details=data['details']) - new_tcp_host.save() - return HttpResponseRedirect(request.get_full_path()) - else: - for msg_err in form.errors.values(): - error_messages.append(msg_err.as_text()) - if 'host_ssh_add' in request.POST: - form = ComputeAddSshForm(request.POST) - if form.is_valid(): - data = form.cleaned_data - new_ssh_host = Compute(name=data['name'], - hostname=data['hostname'], - type=CONN_SSH, - login=data['login'], - details=data['details']) - new_ssh_host.save() - return HttpResponseRedirect(request.get_full_path()) - else: - for msg_err in form.errors.values(): - error_messages.append(msg_err.as_text()) - if 'host_tls_add' in request.POST: - form = ComputeAddTlsForm(request.POST) - if form.is_valid(): - data = form.cleaned_data - new_tls_host = Compute(name=data['name'], - hostname=data['hostname'], - type=CONN_TLS, - login=data['login'], - password=data['password'], - details=data['details']) - new_tls_host.save() - return HttpResponseRedirect(request.get_full_path()) - else: - for msg_err in form.errors.values(): - error_messages.append(msg_err.as_text()) - if 'host_socket_add' in request.POST: - form = ComputeAddSocketForm(request.POST) - if form.is_valid(): - data = form.cleaned_data - new_socket_host = Compute(name=data['name'], - details=data['details'], - hostname='localhost', - type=CONN_SOCKET, - login='', - password='') - new_socket_host.save() - return HttpResponseRedirect(request.get_full_path()) - else: - for msg_err in form.errors.values(): - error_messages.append(msg_err.as_text()) if 'host_edit' in request.POST: form = ComputeEditHostForm(request.POST) if form.is_valid(): @@ -274,3 +218,13 @@ 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.txt b/conf/requirements.txt index af0e93a..bbddbd3 100644 --- a/conf/requirements.txt +++ b/conf/requirements.txt @@ -1,3 +1,4 @@ +coverage==5.1 Django==2.2.12 django-bootstrap3==12.1.0 django-fa==1.0.0 diff --git a/console/admin.py b/console/admin.py deleted file mode 100644 index 8c38f3f..0000000 --- a/console/admin.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.contrib import admin - -# Register your models here. diff --git a/create/admin.py b/create/admin.py deleted file mode 100644 index 8c38f3f..0000000 --- a/create/admin.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.contrib import admin - -# Register your models here. diff --git a/datasource/admin.py b/datasource/admin.py deleted file mode 100644 index 8c38f3f..0000000 --- a/datasource/admin.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.contrib import admin - -# Register your models here. diff --git a/instances/__init__.py b/instances/__init__.py index e69de29..bf9df05 100644 --- a/instances/__init__.py +++ b/instances/__init__.py @@ -0,0 +1 @@ +default_app_config = 'instances.apps.InstancesConfig' diff --git a/instances/admin.py b/instances/admin.py deleted file mode 100644 index 8c38f3f..0000000 --- a/instances/admin.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.contrib import admin - -# Register your models here. diff --git a/instances/apps.py b/instances/apps.py new file mode 100644 index 0000000..8b67b98 --- /dev/null +++ b/instances/apps.py @@ -0,0 +1,31 @@ +from django.apps import AppConfig +from django.db.models.signals import post_migrate + + +def migrate_can_clone_instances(sender, **kwargs): + ''' + Migrate can clone instances user attribute to permission + ''' + from django.conf import settings + from django.contrib.auth.models import User, Permission + from accounts.models import UserAttributes + + plan = kwargs['plan'] + for migration, rolled_back in plan: + if migration.app_label == 'instances' and migration.name == '0002_permissionset' and not rolled_back: + users = User.objects.all() + permission = Permission.objects.get(codename='clone_instances') + print('\033[92mMigrating can_clone_instaces user attribute to permission\033[0m') + for user in users: + if user.userattributes: + if user.userattributes.can_clone_instances: + user.user_permissions.add(permission) + break + + +class InstancesConfig(AppConfig): + name = 'instances' + verbose_name = 'instances' + + def ready(self): + post_migrate.connect(migrate_can_clone_instances, sender=self) diff --git a/instances/migrations/0003_migrate_can_clone_instances.py b/instances/migrations/0003_migrate_can_clone_instances.py deleted file mode 100644 index 6b7cc16..0000000 --- a/instances/migrations/0003_migrate_can_clone_instances.py +++ /dev/null @@ -1,35 +0,0 @@ -from django.db import migrations - - -def migrate_can_clone_instances(apps, schema_editor): - from django.contrib.auth.models import User, Permission - user: User - users = User.objects.all() - - permission = Permission.objects.get(codename='clone_instances') - - for user in users: - if user.userattributes.can_clone_instances: - user.user_permissions.add(permission) - - -def reverse_can_clone_instances(apps, schema_editor): - from django.contrib.auth.models import User, Permission - user: User - users = User.objects.all() - - permission = Permission.objects.get(codename='clone_instances') - - for user in users: - user.user_permissions.remove(permission) - - -class Migration(migrations.Migration): - - dependencies = [ - ('instances', '0002_permissionset'), - ] - - operations = [ - migrations.RunPython(migrate_can_clone_instances, reverse_can_clone_instances), - ] diff --git a/interfaces/admin.py b/interfaces/admin.py deleted file mode 100644 index 8c38f3f..0000000 --- a/interfaces/admin.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.contrib import admin - -# Register your models here. diff --git a/logs/admin.py b/logs/admin.py deleted file mode 100644 index 8c38f3f..0000000 --- a/logs/admin.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.contrib import admin - -# Register your models here. diff --git a/networks/admin.py b/networks/admin.py deleted file mode 100644 index 8c38f3f..0000000 --- a/networks/admin.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.contrib import admin - -# Register your models here. diff --git a/nwfilters/admin.py b/nwfilters/admin.py deleted file mode 100644 index 13be29d..0000000 --- a/nwfilters/admin.py +++ /dev/null @@ -1,6 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.contrib import admin - -# Register your models here. diff --git a/secrets/admin.py b/secrets/admin.py deleted file mode 100644 index 8c38f3f..0000000 --- a/secrets/admin.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.contrib import admin - -# Register your models here. diff --git a/storages/admin.py b/storages/admin.py deleted file mode 100644 index 8c38f3f..0000000 --- a/storages/admin.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.contrib import admin - -# Register your models here. diff --git a/webvirtcloud/settings.py.template b/webvirtcloud/settings.py.template index e6d57e7..8809811 100644 --- a/webvirtcloud/settings.py.template +++ b/webvirtcloud/settings.py.template @@ -15,7 +15,6 @@ DEBUG = True ALLOWED_HOSTS = ['*'] # Application definition - INSTALLED_APPS = [ 'django.contrib.auth', 'django.contrib.contenttypes', @@ -91,9 +90,9 @@ AUTHENTICATION_BACKENDS = [ 'django.contrib.auth.backends.ModelBackend', ] -LOGIN_URL = '/accounts/login' +LOGIN_URL = '/accounts/login/' -LOGOUT_REDIRECT_URL = '/accounts/login' +LOGOUT_REDIRECT_URL = '/accounts/login/' LANGUAGE_CODE = 'en-us' @@ -114,14 +113,23 @@ STATICFILES_DIRS = [ LOGGING = { "version": 1, "disable_existing_loggers": False, - "handlers": {"mail_admins": {"level": "ERROR", "class": "django.utils.log.AdminEmailHandler"}}, + "handlers": { + "mail_admins": { + "level": "ERROR", + "class": "django.utils.log.AdminEmailHandler" + } + }, "loggers": { - "django.request": {"handlers": ["mail_admins"], "level": "ERROR", "propagate": True} + "django.request": { + "handlers": ["mail_admins"], + "level": "ERROR", + "propagate": True + } }, } # -## WebVirtCloud settings +# WebVirtCloud settings # # Websock port