1
0
Fork 0
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:
Anatoliy Guskov 2020-06-03 10:25:53 +03:00 committed by GitHub
commit 37022df459
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 505 additions and 571 deletions

2
.gitignore vendored
View file

@ -12,3 +12,5 @@ tags
dhcpd.* dhcpd.*
webvirtcloud/settings.py webvirtcloud/settings.py
*migrations/* *migrations/*
.coverage
htmlcov

View file

@ -0,0 +1 @@
default_app_config = 'accounts.apps.AccountsConfig'

View file

@ -1,3 +0,0 @@
from django.contrib import admin
# Register your models here.

51
accounts/apps.py Normal file
View 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)

View file

@ -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

View file

@ -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),
]

View file

@ -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': (),
}, },

View file

@ -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),
]

View file

@ -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',
),
]

View file

@ -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=[

View file

@ -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'))

View file

@ -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']

View file

@ -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
View 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)

View file

@ -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',

View file

@ -1,3 +0,0 @@
from django.contrib import admin
# Register your models here.

View file

@ -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'))

View 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),
),
]

View file

@ -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)

View 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 %}

View file

@ -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">&times</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 %}

View file

@ -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

View file

@ -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
View 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'))

View file

@ -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})

View file

@ -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

View file

@ -1,3 +0,0 @@
from django.contrib import admin
# Register your models here.

View file

@ -1,3 +0,0 @@
from django.contrib import admin
# Register your models here.

View file

@ -1,3 +0,0 @@
from django.contrib import admin
# Register your models here.

View file

@ -0,0 +1 @@
default_app_config = 'instances.apps.InstancesConfig'

View file

@ -1,3 +0,0 @@
from django.contrib import admin
# Register your models here.

31
instances/apps.py Normal file
View 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)

View file

@ -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),
]

View file

@ -1,3 +0,0 @@
from django.contrib import admin
# Register your models here.

View file

@ -1,3 +0,0 @@
from django.contrib import admin
# Register your models here.

View file

@ -1,3 +0,0 @@
from django.contrib import admin
# Register your models here.

View file

@ -1,6 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.contrib import admin
# Register your models here.

View file

@ -1,3 +0,0 @@
from django.contrib import admin
# Register your models here.

View file

@ -1,3 +0,0 @@
from django.contrib import admin
# Register your models here.

View file

@ -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