mirror of
https://github.com/retspen/webvirtcloud
synced 2024-12-24 15:15:22 +00:00
Merge pull request #315 from Real-Gecko/master
Fixed migrations for new deployments, added unit tests
This commit is contained in:
commit
37022df459
40 changed files with 505 additions and 571 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -12,3 +12,5 @@ tags
|
||||||
dhcpd.*
|
dhcpd.*
|
||||||
webvirtcloud/settings.py
|
webvirtcloud/settings.py
|
||||||
*migrations/*
|
*migrations/*
|
||||||
|
.coverage
|
||||||
|
htmlcov
|
|
@ -0,0 +1 @@
|
||||||
|
default_app_config = 'accounts.apps.AccountsConfig'
|
|
@ -1,3 +0,0 @@
|
||||||
from django.contrib import admin
|
|
||||||
|
|
||||||
# Register your models here.
|
|
51
accounts/apps.py
Normal file
51
accounts/apps.py
Normal file
|
@ -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)
|
|
@ -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
|
|
||||||
|
|
|
@ -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),
|
|
||||||
]
|
|
|
@ -6,7 +6,7 @@ from django.db import migrations, models
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('accounts', '0002_addAdmin'),
|
('accounts', '0001_initial'),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
@ -16,7 +16,7 @@ class Migration(migrations.Migration):
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'permissions': (('change_password', 'Can change password'),),
|
'permissions': (('change_password', 'Can change password'), ),
|
||||||
'managed': False,
|
'managed': False,
|
||||||
'default_permissions': (),
|
'default_permissions': (),
|
||||||
},
|
},
|
|
@ -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),
|
|
||||||
]
|
|
|
@ -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',
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -29,6 +29,7 @@ class UserSSHKey(models.Model):
|
||||||
|
|
||||||
class UserAttributes(models.Model):
|
class UserAttributes(models.Model):
|
||||||
user = models.OneToOneField(User, on_delete=models.CASCADE)
|
user = models.OneToOneField(User, on_delete=models.CASCADE)
|
||||||
|
can_clone_instances = models.BooleanField(default=True)
|
||||||
max_instances = models.IntegerField(default=1,
|
max_instances = models.IntegerField(default=1,
|
||||||
help_text="-1 for unlimited. Any integer value",
|
help_text="-1 for unlimited. Any integer value",
|
||||||
validators=[
|
validators=[
|
||||||
|
|
|
@ -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'))
|
||||||
|
|
|
@ -91,4 +91,4 @@ class UserCreateForm(UserForm):
|
||||||
class UserAttributesForm(forms.ModelForm):
|
class UserAttributesForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = UserAttributes
|
model = UserAttributes
|
||||||
exclude = ['user']
|
exclude = ['user', 'can_clone_instances']
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
<h2 class="page-header">{{ title }}</h2>
|
<h2 class="page-header">{{ title }}</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% bootstrap_messages %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="thumbnail col-sm-10 col-sm-offset-1">
|
<div class="thumbnail col-sm-10 col-sm-offset-1">
|
||||||
<form id="create-update" action="" method="post" class="form-horizontal">
|
<form id="create-update" action="" method="post" class="form-horizontal">
|
||||||
|
|
120
admin/tests.py
Normal file
120
admin/tests.py
Normal file
|
@ -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)
|
|
@ -1,5 +1,4 @@
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.decorators import user_passes_test
|
|
||||||
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
|
||||||
|
@ -30,6 +29,7 @@ def group_create(request):
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
form.save()
|
form.save()
|
||||||
return redirect('admin:group_list')
|
return redirect('admin:group_list')
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
'admin/common/form.html',
|
'admin/common/form.html',
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
from django.contrib import admin
|
|
||||||
|
|
||||||
# Register your models here.
|
|
|
@ -2,131 +2,51 @@ import re
|
||||||
from django import forms
|
from django import forms
|
||||||
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
|
||||||
|
from vrtManager.connection import CONN_TCP, CONN_SSH, CONN_TLS, CONN_SOCKET
|
||||||
|
from .validators import validate_hostname
|
||||||
|
|
||||||
|
|
||||||
class ComputeAddTcpForm(forms.Form):
|
class TcpComputeForm(forms.ModelForm):
|
||||||
name = forms.CharField(error_messages={'required': _('No hostname has been entered')},
|
hostname = forms.CharField(validators=[validate_hostname])
|
||||||
max_length=64)
|
type = forms.IntegerField(widget=forms.HiddenInput, initial=CONN_TCP)
|
||||||
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)
|
|
||||||
|
|
||||||
def clean_name(self):
|
class Meta:
|
||||||
name = self.cleaned_data['name']
|
model = Compute
|
||||||
have_symbol = re.match('[^a-zA-Z0-9._-]+', name)
|
fields = '__all__'
|
||||||
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 ComputeAddSshForm(forms.Form):
|
class SshComputeForm(forms.ModelForm):
|
||||||
name = forms.CharField(error_messages={'required': _('No hostname has been entered')},
|
hostname = forms.CharField(validators=[validate_hostname], label=_("FQDN/IP"))
|
||||||
max_length=64)
|
type = forms.IntegerField(widget=forms.HiddenInput, initial=CONN_SSH)
|
||||||
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)
|
|
||||||
|
|
||||||
def clean_name(self):
|
class Meta:
|
||||||
name = self.cleaned_data['name']
|
model = Compute
|
||||||
have_symbol = re.match('[^a-zA-Z0-9._-]+', name)
|
exclude = ['password']
|
||||||
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 ComputeAddTlsForm(forms.Form):
|
class TlsComputeForm(forms.ModelForm):
|
||||||
name = forms.CharField(error_messages={'required': _('No hostname has been entered')},
|
hostname = forms.CharField(validators=[validate_hostname])
|
||||||
max_length=64)
|
type = forms.IntegerField(widget=forms.HiddenInput, initial=CONN_TLS)
|
||||||
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)
|
|
||||||
|
|
||||||
def clean_name(self):
|
class Meta:
|
||||||
name = self.cleaned_data['name']
|
model = Compute
|
||||||
have_symbol = re.match('[^a-zA-Z0-9._-]+', name)
|
fields = '__all__'
|
||||||
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']
|
class SocketComputeForm(forms.ModelForm):
|
||||||
have_symbol = re.match('[^a-z0-9.-]+', hostname)
|
hostname = forms.CharField(widget=forms.HiddenInput, initial='localhost')
|
||||||
wrong_ip = re.match('^0.|^255.', hostname)
|
type = forms.IntegerField(widget=forms.HiddenInput, initial=CONN_SOCKET)
|
||||||
if have_symbol:
|
|
||||||
raise forms.ValidationError(_('Hostname must contain only numbers, or the domain name separated by "."'))
|
class Meta:
|
||||||
elif wrong_ip:
|
model = Compute
|
||||||
raise forms.ValidationError(_('Wrong IP address'))
|
fields = ['name', 'details', 'hostname', 'type']
|
||||||
try:
|
|
||||||
Compute.objects.get(hostname=hostname)
|
|
||||||
except Compute.DoesNotExist:
|
|
||||||
return hostname
|
|
||||||
raise forms.ValidationError(_('This host is already connected'))
|
|
||||||
|
|
||||||
|
|
||||||
class ComputeEditHostForm(forms.Form):
|
class ComputeEditHostForm(forms.Form):
|
||||||
host_id = forms.CharField()
|
host_id = forms.CharField()
|
||||||
name = forms.CharField(error_messages={'required': _('No hostname has been entered')},
|
name = forms.CharField(error_messages={'required': _('No hostname has been entered')}, max_length=64)
|
||||||
max_length=64)
|
hostname = forms.CharField(error_messages={'required': _('No IP / Domain name has been entered')}, max_length=100)
|
||||||
hostname = forms.CharField(error_messages={'required': _('No IP / Domain name has been entered')},
|
login = forms.CharField(error_messages={'required': _('No login has been entered')}, max_length=100)
|
||||||
max_length=100)
|
|
||||||
login = forms.CharField(error_messages={'required': _('No login has been entered')},
|
|
||||||
max_length=100)
|
|
||||||
password = forms.CharField(max_length=100)
|
password = forms.CharField(max_length=100)
|
||||||
details = forms.CharField(max_length=50, required=False)
|
details = forms.CharField(max_length=50, required=False)
|
||||||
|
|
||||||
|
@ -148,21 +68,3 @@ class ComputeEditHostForm(forms.Form):
|
||||||
elif wrong_ip:
|
elif wrong_ip:
|
||||||
raise forms.ValidationError(_('Wrong IP address'))
|
raise forms.ValidationError(_('Wrong IP address'))
|
||||||
return hostname
|
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'))
|
|
||||||
|
|
18
computes/migrations/0002_auto_20200529_1320.py
Normal file
18
computes/migrations/0002_auto_20200529_1320.py
Normal file
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
|
@ -2,7 +2,7 @@ from django.db.models import Model, CharField, IntegerField
|
||||||
|
|
||||||
|
|
||||||
class Compute(Model):
|
class Compute(Model):
|
||||||
name = CharField(max_length=64)
|
name = CharField(max_length=64, unique=True)
|
||||||
hostname = CharField(max_length=64)
|
hostname = CharField(max_length=64)
|
||||||
login = CharField(max_length=20)
|
login = CharField(max_length=20)
|
||||||
password = CharField(max_length=14, blank=True, null=True)
|
password = CharField(max_length=14, blank=True, null=True)
|
||||||
|
|
29
computes/templates/computes/form.html
Normal file
29
computes/templates/computes/form.html
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load bootstrap3 %}
|
||||||
|
{% load font_awesome %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Add Compute" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-12">
|
||||||
|
<h2 class="page-header">{% trans "Create Compute" %}</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% bootstrap_messages %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="thumbnail col-sm-10 col-sm-offset-1">
|
||||||
|
<form id="create-update" action="" method="post" class="form-horizontal">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% bootstrap_form form layout='horizontal' %}
|
||||||
|
</form>
|
||||||
|
<div class="form-group pull-right">
|
||||||
|
<a class="btn btn-primary" href="javascript:history.back()">{% icon 'times' %} {% trans "Cancel" %}</a>
|
||||||
|
<button type="submit" form="create-update" class="btn btn-success">
|
||||||
|
{% icon 'check' %} {% trans "Save" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock content %}
|
|
@ -1,185 +1,9 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% if request.user.is_superuser %}
|
{% load bootstrap3 %}
|
||||||
<a href="#addHost" type="button" class="btn btn-success pull-right" data-toggle="modal">
|
<div class"pull-right">{% trans "Add Connection" %}</div>
|
||||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
<div class="btn-group pull-right">
|
||||||
</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>
|
||||||
<!-- Modal -->
|
<a href="{% url 'add_tls_host' %}" class="btn btn-success">{% trans "TLS" %}</a>
|
||||||
<div class="modal fade" id="addHost" tabindex="-1" role="dialog" aria-labelledby="addHostLabel" aria-hidden="true">
|
<a href="{% url 'add_socket_host' %}" class="btn btn-success">{% trans "Local" %}</a>
|
||||||
<div class="modal-dialog">
|
</div>
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
|
||||||
<h4 class="modal-title">{% trans "Add Connection" %}</h4>
|
|
||||||
</div>
|
|
||||||
<div class="tabbable">
|
|
||||||
<ul class="nav nav-tabs">
|
|
||||||
<li class="active">
|
|
||||||
<a href="#1" data-toggle="tab">{% trans "TCP Connections" %}</a>
|
|
||||||
</li>
|
|
||||||
<li><a href="#2" data-toggle="tab">{% trans "SSH Connections" %}</a></li>
|
|
||||||
<li><a href="#3" data-toggle="tab">{% trans "TLS Connection" %}</a></li>
|
|
||||||
<li><a href="#4" data-toggle="tab">{% trans "Local Socket" %}</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="tab-content">
|
|
||||||
<div class="tab-pane active" id="1">
|
|
||||||
<div class="modal-body">
|
|
||||||
<form class="form-horizontal" method="post" role="form">{% csrf_token %}
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="col-sm-4 control-label">{% trans "Label" %}</label>
|
|
||||||
<div class="col-sm-6">
|
|
||||||
<input type="text" name="name" class="form-control" placeholder="Label Name" maxlength="20" required pattern="[a-z0-9\.\-_]+">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="col-sm-4 control-label">{% trans "FQDN / IP" %}</label>
|
|
||||||
<div class="col-sm-6">
|
|
||||||
<input type="text" name="hostname" class="form-control" placeholder="{% trans "FQDN or IP Address" %}" required pattern="[a-z0-9\.\-_]+">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="col-sm-4 control-label">{% trans "Username" %}</label>
|
|
||||||
<div class="col-sm-6">
|
|
||||||
<input type="text" name="login" class="form-control" placeholder="{% trans "Username" %}">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="col-sm-4 control-label">{% trans "Password" %}</label>
|
|
||||||
<div class="col-sm-6">
|
|
||||||
<input type="password" name="password" class="form-control" placeholder="{% trans "Password" %}">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="col-sm-4 control-label">{% trans "Details" %}</label>
|
|
||||||
<div class="col-sm-6">
|
|
||||||
<input type="text" name="details" class="form-control" placeholder="{% trans "Details" %}">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-default" data-dismiss="modal">
|
|
||||||
{% trans "Close" %}
|
|
||||||
</button>
|
|
||||||
<button type="submit" class="btn btn-primary" name="host_tcp_add">
|
|
||||||
{% trans "Add" %}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div class="tab-pane" id="2">
|
|
||||||
<div class="modal-body">
|
|
||||||
<form class="form-horizontal" method="post" role="form">{% csrf_token %}
|
|
||||||
<p class="modal-body">{% trans "You must 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">
|
|
||||||
<label class="col-sm-4 control-label">{% trans "Label" %}</label>
|
|
||||||
<div class="col-sm-6">
|
|
||||||
<input type="text" name="name" class="form-control" placeholder="Label Name" maxlength="20" required pattern="[a-z0-9\.\-_]+">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="col-sm-4 control-label">{% trans "FQDN / IP" %}</label>
|
|
||||||
<div class="col-sm-6">
|
|
||||||
<input type="text" name="hostname" class="form-control" placeholder="{% trans "FQDN or IP Address" %}" required pattern="[a-z0-9\:\.\-_]+">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="col-sm-4 control-label">{% trans "Username" %}</label>
|
|
||||||
<div class="col-sm-6">
|
|
||||||
<input type="text" name="login" class="form-control" placeholder="{% trans "Username" %}">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="col-sm-4 control-label">{% trans "Details" %}</label>
|
|
||||||
<div class="col-sm-6">
|
|
||||||
<input type="text" name="details" class="form-control" placeholder="{% trans "Details" %}">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-default" data-dismiss="modal">
|
|
||||||
{% trans "Close" %}
|
|
||||||
</button>
|
|
||||||
<button type="submit" class="btn btn-primary" name="host_ssh_add">
|
|
||||||
{% trans "Add" %}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div class="tab-pane" id="3">
|
|
||||||
<div class="modal-body">
|
|
||||||
<form class="form-horizontal" method="post" role="form">{% csrf_token %}
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="col-sm-4 control-label">{% trans "Label" %}</label>
|
|
||||||
<div class="col-sm-6">
|
|
||||||
<input type="text" name="name" class="form-control" placeholder="Label Name" maxlength="20" required pattern="[a-z0-9\.\-_]+">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="col-sm-4 control-label">{% trans "FQDN / IP" %}</label>
|
|
||||||
<div class="col-sm-6">
|
|
||||||
<input type="text" name="hostname" class="form-control" placeholder="{% trans "FQDN or IP Address" %}" required pattern="[a-z0-9\.\-_]+">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="col-sm-4 control-label">{% trans "Username" %}</label>
|
|
||||||
<div class="col-sm-6">
|
|
||||||
<input type="text" name="login" class="form-control" placeholder="{% trans "Username" %}">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="col-sm-4 control-label">{% trans "Password" %}</label>
|
|
||||||
<div class="col-sm-6">
|
|
||||||
<input type="password" name="password" class="form-control" placeholder="{% trans "Password" %}">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="col-sm-4 control-label">{% trans "Details" %}</label>
|
|
||||||
<div class="col-sm-6">
|
|
||||||
<input type="text" name="details" class="form-control" placeholder="{% trans "Details" %}">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-default" data-dismiss="modal">
|
|
||||||
{% trans "Close" %}
|
|
||||||
</button>
|
|
||||||
<button type="submit" class="btn btn-primary" name="host_tls_add">
|
|
||||||
{% trans "Add" %}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div class="tab-pane" id="4">
|
|
||||||
<div class="modal-body">
|
|
||||||
<form class="form-horizontal" method="post" role="form">{% csrf_token %}
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="col-sm-4 control-label">{% trans "Label" %}</label>
|
|
||||||
<div class="col-sm-6">
|
|
||||||
<input type="text" name="name" class="form-control" placeholder="Label Name" maxlength="20" required pattern="[a-z0-9\.\-_]+">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="col-sm-4 control-label">{% trans "Details" %}</label>
|
|
||||||
<div class="col-sm-6">
|
|
||||||
<input type="text" name="details" class="form-control" placeholder="{% trans "Details" %}">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-default" data-dismiss="modal">
|
|
||||||
{% trans "Close" %}
|
|
||||||
</button>
|
|
||||||
<button type="submit" class="btn btn-primary" name="host_socket_add">
|
|
||||||
{% trans "Add" %}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div> <!-- /.tab-content -->
|
|
||||||
</div> <!-- /.modal-content -->
|
|
||||||
</div> <!-- /.modal-dialog -->
|
|
||||||
</div><!-- /.modal -->
|
|
||||||
{% endif %}
|
|
||||||
|
|
|
@ -1,3 +1,83 @@
|
||||||
|
from django.shortcuts import reverse
|
||||||
from django.test import TestCase
|
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
|
||||||
|
|
|
@ -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 secrets.views import secrets
|
||||||
|
|
||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from . import views
|
||||||
from create.views import create_instance, create_instance_select_type
|
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 instances.views import instances
|
||||||
|
from interfaces.views import interface, interfaces
|
||||||
|
from networks.views import network, networks
|
||||||
from nwfilters.views import nwfilter, nwfilters
|
from nwfilters.views import nwfilter, nwfilters
|
||||||
|
from storages.views import get_volumes, storage, storages
|
||||||
|
from . import forms
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', computes, name='computes'),
|
path('', views.computes, name='computes'),
|
||||||
re_path(r'^(?P<compute_id>[0-9]+)/$', overview, name='overview'),
|
path('add_tcp_host/', views.add_host, {'FormClass': forms.TcpComputeForm}, name='add_tcp_host'),
|
||||||
re_path(r'^(?P<compute_id>[0-9]+)/statistics$', compute_graph, name='compute_graph'),
|
path('add_ssh_host/', views.add_host, {'FormClass': forms.SshComputeForm}, name='add_ssh_host'),
|
||||||
re_path(r'^(?P<compute_id>[0-9]+)/instances/$', instances, name='instances'),
|
path('add_tls_host/', views.add_host, {'FormClass': forms.TlsComputeForm}, name='add_tls_host'),
|
||||||
re_path(r'^(?P<compute_id>[0-9]+)/storages/$', storages, name='storages'),
|
path('add_socket_host/', views.add_host, {'FormClass': forms.SocketComputeForm}, name='add_socket_host'),
|
||||||
re_path(r'^(?P<compute_id>[0-9]+)/storage/(?P<pool>[\w\-\.\/]+)/volumes$', get_volumes, name='volumes'),
|
path('<int:compute_id>/', views.overview, name='overview'),
|
||||||
re_path(r'^(?P<compute_id>[0-9]+)/storage/(?P<pool>[\w\-\.\/]+)/$', storage, name='storage'),
|
path('<int:compute_id>/statistics/', views.compute_graph, name='compute_graph'),
|
||||||
re_path(r'^(?P<compute_id>[0-9]+)/networks/$', networks, name='networks'),
|
path('<int:compute_id>/instances/', instances, name='instances'),
|
||||||
re_path(r'^(?P<compute_id>[0-9]+)/network/(?P<pool>[\w\-\.]+)/$', network, name='network'),
|
path('<int:compute_id>/storages/', storages, name='storages'),
|
||||||
re_path(r'^(?P<compute_id>[0-9]+)/interfaces/$', interfaces, name='interfaces'),
|
path('<int:compute_id>/storage/<str:pool>/volumes/', get_volumes, name='volumes'),
|
||||||
re_path(r'^(?P<compute_id>[0-9]+)/interface/(?P<iface>[\w\-\.\:]+)/$', interface, name='interface'),
|
path('<int:compute_id>/storage/<str:pool>/', storage, name='storage'),
|
||||||
re_path(r'^(?P<compute_id>[0-9]+)/nwfilters/$', nwfilters, name='nwfilters'),
|
path('<int:compute_id>/networks/', networks, name='networks'),
|
||||||
re_path(r'^(?P<compute_id>[0-9]+)/nwfilter/(?P<nwfltr>[\w\-\.\:]+)/$', nwfilter, name='nwfilter'),
|
path('<int:compute_id>/network/<str:pool>/', network, name='network'),
|
||||||
re_path(r'^(?P<compute_id>[0-9]+)/secrets/$', secrets, name='secrets'),
|
path('<int:compute_id>/interfaces/', interfaces, name='interfaces'),
|
||||||
re_path(r'^(?P<compute_id>[0-9]+)/create/$', create_instance_select_type, name='create_instance_select_type'),
|
path('<int:compute_id>/interface/<str:iface>/', interface, name='interface'),
|
||||||
re_path(r'^(?P<compute_id>[0-9]+)/create/archs/(?P<arch>[\w\-\.\/]+)/machines/(?P<machine>[\w\-\.\/]+)$', create_instance, name='create_instance'),
|
path('<int:compute_id>/nwfilters/', nwfilters, name='nwfilters'),
|
||||||
re_path(r'^(?P<compute_id>[0-9]+)/archs/(?P<arch>[\w\-\.\/]+)/machines$', get_compute_machine_types, name='machines'),
|
path('<int:compute_id>/nwfilter/<str:nwfltr>/', nwfilter, name='nwfilter'),
|
||||||
re_path(r'^(?P<compute_id>[0-9]+)/archs/(?P<arch>[\w\-\.\/]+)/machines/(?P<machine>[\w\-\.\/]+)/disks/(?P<disk>[\w\-\.\/]+)/buses$', get_compute_disk_buses, name='buses'),
|
path('<int:compute_id>/secrets/', secrets, name='secrets'),
|
||||||
re_path(r'^(?P<compute_id>[0-9]+)/archs/(?P<arch>[\w\-\.\/]+)/machines/(?P<machine>[\w\-\.\/]+)/capabilities$', get_dom_capabilities, name='domcaps'),
|
path('<int:compute_id>/create/', create_instance_select_type, name='create_instance_select_type'),
|
||||||
|
path('<int:compute_id>/create/archs/<str:arch>/machines/<str:machine>/', create_instance, name='create_instance'),
|
||||||
|
path('<int:compute_id>/archs/<str:arch>/machines/', views.get_compute_machine_types, name='machines'),
|
||||||
|
path(
|
||||||
|
'<int:compute_id>/archs/<str:arch>/machines/<str:machine>/disks/<str:disk>/buses/',
|
||||||
|
views.get_compute_disk_buses,
|
||||||
|
name='buses',
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
'<int:compute_id>/archs/<str:arch>/machines/<str:machine>/capabilities/',
|
||||||
|
views.get_dom_capabilities,
|
||||||
|
name='domcaps',
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
24
computes/validators.py
Normal file
24
computes/validators.py
Normal file
|
@ -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'))
|
|
@ -1,16 +1,19 @@
|
||||||
import json
|
import json
|
||||||
from django.utils import timezone
|
|
||||||
|
from django.contrib import messages
|
||||||
from django.http import HttpResponse, HttpResponseRedirect
|
from django.http import HttpResponse, HttpResponseRedirect
|
||||||
|
from django.shortcuts import get_object_or_404, redirect, render, reverse
|
||||||
from django.urls import 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 computes.models import Compute
|
||||||
from instances.models import Instance
|
from instances.models import Instance
|
||||||
from accounts.models import UserInstance
|
from vrtManager.connection import (CONN_SOCKET, CONN_SSH, CONN_TCP, CONN_TLS, connection_manager, wvmConnect)
|
||||||
from computes.forms import ComputeAddTcpForm, ComputeAddSshForm, ComputeEditHostForm, ComputeAddTlsForm, ComputeAddSocketForm
|
|
||||||
from vrtManager.hostdetails import wvmHostDetails
|
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
|
@superuser_only
|
||||||
|
@ -55,65 +58,6 @@ def computes(request):
|
||||||
del_host = Compute.objects.get(id=compute_id)
|
del_host = Compute.objects.get(id=compute_id)
|
||||||
del_host.delete()
|
del_host.delete()
|
||||||
return HttpResponseRedirect(request.get_full_path())
|
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:
|
if 'host_edit' in request.POST:
|
||||||
form = ComputeEditHostForm(request.POST)
|
form = ComputeEditHostForm(request.POST)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
|
@ -274,3 +218,13 @@ 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})
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
coverage==5.1
|
||||||
Django==2.2.12
|
Django==2.2.12
|
||||||
django-bootstrap3==12.1.0
|
django-bootstrap3==12.1.0
|
||||||
django-fa==1.0.0
|
django-fa==1.0.0
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
from django.contrib import admin
|
|
||||||
|
|
||||||
# Register your models here.
|
|
|
@ -1,3 +0,0 @@
|
||||||
from django.contrib import admin
|
|
||||||
|
|
||||||
# Register your models here.
|
|
|
@ -1,3 +0,0 @@
|
||||||
from django.contrib import admin
|
|
||||||
|
|
||||||
# Register your models here.
|
|
|
@ -0,0 +1 @@
|
||||||
|
default_app_config = 'instances.apps.InstancesConfig'
|
|
@ -1,3 +0,0 @@
|
||||||
from django.contrib import admin
|
|
||||||
|
|
||||||
# Register your models here.
|
|
31
instances/apps.py
Normal file
31
instances/apps.py
Normal file
|
@ -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)
|
|
@ -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),
|
|
||||||
]
|
|
|
@ -1,3 +0,0 @@
|
||||||
from django.contrib import admin
|
|
||||||
|
|
||||||
# Register your models here.
|
|
|
@ -1,3 +0,0 @@
|
||||||
from django.contrib import admin
|
|
||||||
|
|
||||||
# Register your models here.
|
|
|
@ -1,3 +0,0 @@
|
||||||
from django.contrib import admin
|
|
||||||
|
|
||||||
# Register your models here.
|
|
|
@ -1,6 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.contrib import admin
|
|
||||||
|
|
||||||
# Register your models here.
|
|
|
@ -1,3 +0,0 @@
|
||||||
from django.contrib import admin
|
|
||||||
|
|
||||||
# Register your models here.
|
|
|
@ -1,3 +0,0 @@
|
||||||
from django.contrib import admin
|
|
||||||
|
|
||||||
# Register your models here.
|
|
|
@ -15,7 +15,6 @@ DEBUG = True
|
||||||
ALLOWED_HOSTS = ['*']
|
ALLOWED_HOSTS = ['*']
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
'django.contrib.auth',
|
'django.contrib.auth',
|
||||||
'django.contrib.contenttypes',
|
'django.contrib.contenttypes',
|
||||||
|
@ -91,9 +90,9 @@ AUTHENTICATION_BACKENDS = [
|
||||||
'django.contrib.auth.backends.ModelBackend',
|
'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'
|
LANGUAGE_CODE = 'en-us'
|
||||||
|
|
||||||
|
@ -114,14 +113,23 @@ STATICFILES_DIRS = [
|
||||||
LOGGING = {
|
LOGGING = {
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"disable_existing_loggers": False,
|
"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": {
|
"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
|
# Websock port
|
||||||
|
|
Loading…
Reference in a new issue