From 618d88f1c4253b1b7133cdd697729e32c67fb8fe Mon Sep 17 00:00:00 2001 From: Real-Gecko Date: Mon, 25 May 2020 16:39:34 +0600 Subject: [PATCH 1/8] Fix default admin user - Fix error: RelatedObjectDoesNotExist: User has no userattributes. "instances/views.py", line 162, in check_user_quota --- accounts/migrations/0002_addAdmin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/accounts/migrations/0002_addAdmin.py b/accounts/migrations/0002_addAdmin.py index c0286cf..09a91fc 100644 --- a/accounts/migrations/0002_addAdmin.py +++ b/accounts/migrations/0002_addAdmin.py @@ -6,8 +6,10 @@ 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 - User.objects.create_superuser('admin', None, 'admin', last_login=timezone.now()) + 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): From 6d82c2820b96757b933321d86b70e7ce23da447e Mon Sep 17 00:00:00 2001 From: Real-Gecko Date: Mon, 25 May 2020 17:32:36 +0600 Subject: [PATCH 2/8] Fix instance bottom bar overlapping main content --- instances/templates/bottom_bar.html | 31 +++++++++++++++++++++++++ instances/templates/instance.html | 36 ++++------------------------- static/css/webvirtcloud.css | 4 ++++ 3 files changed, 40 insertions(+), 31 deletions(-) create mode 100644 instances/templates/bottom_bar.html diff --git a/instances/templates/bottom_bar.html b/instances/templates/bottom_bar.html new file mode 100644 index 0000000..0678129 --- /dev/null +++ b/instances/templates/bottom_bar.html @@ -0,0 +1,31 @@ +{% load i18n %} + +
diff --git a/instances/templates/instance.html b/instances/templates/instance.html index f644e5a..a93dbf8 100644 --- a/instances/templates/instance.html +++ b/instances/templates/instance.html @@ -4,37 +4,6 @@ {% block title %}{% trans "Instance" %} - {{ vname }}{% endblock %} {% block content %} {% include 'pleasewaitdialog.html' %} - {% if bottom_bar %} - - {% endif %}
@@ -1690,6 +1659,11 @@
+ + {% if bottom_bar %} + {% include 'bottom_bar.html' %} + {% endif %} + {% endblock %} {% block script %} diff --git a/static/css/webvirtcloud.css b/static/css/webvirtcloud.css index 7789169..0bdbba5 100644 --- a/static/css/webvirtcloud.css +++ b/static/css/webvirtcloud.css @@ -150,4 +150,8 @@ p { .multipleselect-on { color:#ffffff; background-color:#000099; +} + +.bottom-bar-margin { + margin-top: 65px; } \ No newline at end of file From 38befa43627b020b61d10b4739c7a41598fb4b8b Mon Sep 17 00:00:00 2001 From: Real-Gecko Date: Wed, 27 May 2020 12:43:29 +0600 Subject: [PATCH 3/8] Added django-login-required-middleware Thus eliminating need for login_requred decorator on every view --- accounts/templates/login.html | 6 +++--- accounts/views.py | 4 ---- computes/views.py | 8 -------- conf/requirements.txt | 13 ++++++++----- console/views.py | 2 -- create/views.py | 3 --- instances/views.py | 11 ----------- interfaces/views.py | 3 --- logs/views.py | 3 --- networks/views.py | 3 --- nwfilters/views.py | 3 --- secrets/views.py | 2 -- storages/views.py | 4 ---- webvirtcloud/settings.py.template | 1 + 14 files changed, 12 insertions(+), 54 deletions(-) diff --git a/accounts/templates/login.html b/accounts/templates/login.html index c55a07a..d8281b0 100644 --- a/accounts/templates/login.html +++ b/accounts/templates/login.html @@ -15,9 +15,9 @@ {% endif %} diff --git a/accounts/views.py b/accounts/views.py index d109d31..56f2c02 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -2,7 +2,6 @@ from django.shortcuts import render from django.http import HttpResponseRedirect from django.urls import reverse from django.utils.translation import ugettext_lazy as _ -from django.contrib.auth.decorators import login_required from accounts.models import * from instances.models import Instance from accounts.forms import UserAddForm @@ -10,7 +9,6 @@ from django.conf import settings from django.core.validators import ValidationError -@login_required def profile(request): """ :param request: @@ -69,7 +67,6 @@ def profile(request): return render(request, 'profile.html', locals()) -@login_required def accounts(request): """ :param request: @@ -149,7 +146,6 @@ def accounts(request): return render(request, accounts_template_file, locals()) -@login_required def account(request, user_id): """ :param request: diff --git a/computes/views.py b/computes/views.py index 90e6569..77c9d05 100644 --- a/computes/views.py +++ b/computes/views.py @@ -3,7 +3,6 @@ from django.utils import timezone from django.http import HttpResponse, HttpResponseRedirect from django.urls import reverse from django.shortcuts import render, get_object_or_404 -from django.contrib.auth.decorators import login_required from computes.models import Compute from instances.models import Instance from accounts.models import UserInstance @@ -13,7 +12,6 @@ from vrtManager.connection import CONN_SSH, CONN_TCP, CONN_TLS, CONN_SOCKET, con from libvirt import libvirtError -@login_required def computes(request): """ :param request: @@ -135,7 +133,6 @@ def computes(request): return render(request, 'computes.html', locals()) -@login_required def overview(request, compute_id): """ :param request: @@ -167,7 +164,6 @@ def overview(request, compute_id): return render(request, 'overview.html', locals()) -@login_required def compute_graph(request, compute_id): """ :param request: @@ -197,7 +193,6 @@ def compute_graph(request, compute_id): return response -@login_required def get_compute_disk_buses(request, compute_id, arch, machine, disk): data = dict() compute = get_object_or_404(Compute, pk=compute_id) @@ -224,7 +219,6 @@ def get_compute_disk_buses(request, compute_id, arch, machine, disk): return HttpResponse(json.dumps(data)) -@login_required def get_compute_machine_types(request, compute_id, arch): data = dict() try: @@ -240,7 +234,6 @@ def get_compute_machine_types(request, compute_id, arch): return HttpResponse(json.dumps(data)) -@login_required def get_compute_video_models(request, compute_id, arch, machine): data = dict() try: @@ -256,7 +249,6 @@ def get_compute_video_models(request, compute_id, arch, machine): return HttpResponse(json.dumps(data)) -@login_required def get_dom_capabilities(request, compute_id, arch, machine): data = dict() try: diff --git a/conf/requirements.txt b/conf/requirements.txt index 18cdc3c..b831e22 100644 --- a/conf/requirements.txt +++ b/conf/requirements.txt @@ -1,8 +1,11 @@ Django==2.2.12 -websockify==0.9.0 +django-login-required-middleware==0.5.0 gunicorn==20.0.4 -lxml==4.5.0 libvirt-python==6.1.0 -six -pytz -rwlock +lxml==4.5.0 +numpy==1.18.4 +pytz==2020.1 +rwlock==0.0.7 +six==1.15.0 +sqlparse==0.3.1 +websockify==0.9.0 diff --git a/console/views.py b/console/views.py index a27a562..cc4358a 100644 --- a/console/views.py +++ b/console/views.py @@ -1,6 +1,5 @@ import re from django.shortcuts import render -from django.contrib.auth.decorators import login_required from instances.models import Instance from vrtManager.instance import wvmInstance from webvirtcloud.settings import WS_PUBLIC_PORT @@ -8,7 +7,6 @@ from webvirtcloud.settings import WS_PUBLIC_HOST from libvirt import libvirtError -@login_required def console(request): """ :param request: diff --git a/create/views.py b/create/views.py index e2e81fb..384d6eb 100644 --- a/create/views.py +++ b/create/views.py @@ -2,7 +2,6 @@ from django.shortcuts import render, get_object_or_404 from django.http import HttpResponseRedirect from django.utils.translation import ugettext_lazy as _ from django.urls import reverse -from django.contrib.auth.decorators import login_required from computes.models import Compute from create.models import Flavor from create.forms import FlavorAddForm, NewVMForm @@ -26,7 +25,6 @@ from django.contrib import messages from logs.views import addlogmsg -@login_required def create_instance_select_type(request, compute_id): if not request.user.is_superuser: @@ -76,7 +74,6 @@ def create_instance_select_type(request, compute_id): return render(request, 'create_instance_w1.html', locals()) -@login_required def create_instance(request, compute_id, arch, machine): """ :param request: diff --git a/instances/views.py b/instances/views.py index 7cf56da..28e78e4 100644 --- a/instances/views.py +++ b/instances/views.py @@ -11,7 +11,6 @@ from django.http import HttpResponse, HttpResponseRedirect from django.urls import reverse from django.shortcuts import render, get_object_or_404 from django.utils.translation import ugettext_lazy as _ -from django.contrib.auth.decorators import login_required from computes.models import Compute from instances.models import Instance from django.contrib.auth.models import User @@ -29,7 +28,6 @@ from django.contrib import messages from collections import OrderedDict -@login_required def index(request): """ :param request: @@ -38,7 +36,6 @@ def index(request): return HttpResponseRedirect(reverse('allinstances')) -@login_required def allinstances(request): """ INSTANCES LIST FOR ALL HOSTS @@ -70,7 +67,6 @@ def allinstances(request): return render(request, 'allinstances.html', locals()) -@login_required def instances(request, compute_id): """ :param request: @@ -99,7 +95,6 @@ def instances(request, compute_id): return render(request, 'instances.html', locals()) -@login_required def instance(request, compute_id, vname): """ :param request: @@ -1081,7 +1076,6 @@ def instance(request, compute_id, vname): return render(request, 'instance.html', locals()) -@login_required def inst_status(request, compute_id, vname): """ :param request: @@ -1276,7 +1270,6 @@ def instances_actions(request): return HttpResponseRedirect(request.get_full_path()) -@login_required def inst_graph(request, compute_id, vname): """ :param request: @@ -1339,7 +1332,6 @@ def _get_dhcp_mac_address(vname): return mac -@login_required def guess_mac_address(request, vname): data = {'vname': vname} mac = _get_dhcp_mac_address(vname) @@ -1358,14 +1350,12 @@ def _get_random_mac_address(): return mac -@login_required def random_mac_address(request): data = dict() data['mac'] = _get_random_mac_address() return HttpResponse(json.dumps(data)) -@login_required def guess_clone_name(request): dhcp_file = '/srv/webvirtcloud/dhcpd.conf' prefix = settings.CLONE_INSTANCE_DEFAULT_PREFIX @@ -1382,7 +1372,6 @@ def guess_clone_name(request): return HttpResponse(json.dumps({})) -@login_required def check_instance(request, vname): instance = Instance.objects.filter(name=vname) data = {'vname': vname, 'exists': False} diff --git a/interfaces/views.py b/interfaces/views.py index 13b65e5..cd9f43f 100644 --- a/interfaces/views.py +++ b/interfaces/views.py @@ -1,14 +1,12 @@ from django.shortcuts import render, get_object_or_404 from django.http import HttpResponseRedirect from django.urls import reverse -from django.contrib.auth.decorators import login_required from computes.models import Compute from interfaces.forms import AddInterface from vrtManager.interface import wvmInterface, wvmInterfaces from libvirt import libvirtError -@login_required def interfaces(request, compute_id): """ :param request: @@ -57,7 +55,6 @@ def interfaces(request, compute_id): return render(request, 'interfaces.html', locals()) -@login_required def interface(request, compute_id, iface): """ :param request: diff --git a/logs/views.py b/logs/views.py index a276226..45569b0 100644 --- a/logs/views.py +++ b/logs/views.py @@ -1,7 +1,6 @@ from django.shortcuts import render from django.http import HttpResponse, HttpResponseRedirect from django.urls import reverse -from django.contrib.auth.decorators import login_required from instances.models import Instance from logs.models import Logs from django.conf import settings @@ -19,7 +18,6 @@ def addlogmsg(user, instance, message): add_log_msg.save() -@login_required def showlogs(request, page=1): """ :param request: @@ -40,7 +38,6 @@ def showlogs(request, page=1): return render(request, 'showlogs.html', locals()) -@login_required def vm_logs(request, vname): """ :param request: diff --git a/networks/views.py b/networks/views.py index a883be8..39b648d 100644 --- a/networks/views.py +++ b/networks/views.py @@ -2,7 +2,6 @@ from django.shortcuts import render, get_object_or_404 from django.http import HttpResponseRedirect from django.utils.translation import ugettext_lazy as _ from django.urls import reverse -from django.contrib.auth.decorators import login_required from computes.models import Compute from networks.forms import AddNetPool from vrtManager.network import wvmNetwork, wvmNetworks @@ -11,7 +10,6 @@ from libvirt import libvirtError from django.contrib import messages -@login_required def networks(request, compute_id): """ :param request: @@ -70,7 +68,6 @@ def networks(request, compute_id): return render(request, 'networks.html', locals()) -@login_required def network(request, compute_id, pool): """ :param request: diff --git a/nwfilters/views.py b/nwfilters/views.py index 3ee9163..897db7e 100644 --- a/nwfilters/views.py +++ b/nwfilters/views.py @@ -5,7 +5,6 @@ from django.http import HttpResponseRedirect from django.shortcuts import render, get_object_or_404 from django.utils.translation import ugettext_lazy as _ from django.urls import reverse -from django.contrib.auth.decorators import login_required from computes.models import Compute from vrtManager import util from vrtManager.nwfilters import wvmNWFilters, wvmNWFilter @@ -14,7 +13,6 @@ from libvirt import libvirtError from logs.views import addlogmsg -@login_required def nwfilters(request, compute_id): """ :param request: @@ -112,7 +110,6 @@ def nwfilters(request, compute_id): 'compute': compute}) -@login_required def nwfilter(request, compute_id, nwfltr): error_messages = [] diff --git a/secrets/views.py b/secrets/views.py index d519ad9..d40eda0 100644 --- a/secrets/views.py +++ b/secrets/views.py @@ -1,14 +1,12 @@ from django.shortcuts import render, get_object_or_404 from django.http import HttpResponseRedirect from django.urls import reverse -from django.contrib.auth.decorators import login_required from computes.models import Compute from secrets.forms import AddSecret from vrtManager.secrets import wvmSecrets from libvirt import libvirtError -@login_required def secrets(request, compute_id): """ :param request: diff --git a/storages/views.py b/storages/views.py index b7f6c25..76749a4 100644 --- a/storages/views.py +++ b/storages/views.py @@ -2,7 +2,6 @@ from django.shortcuts import render, get_object_or_404 from django.http import HttpResponseRedirect, HttpResponse from django.utils.translation import ugettext_lazy as _ from django.urls import reverse -from django.contrib.auth.decorators import login_required from computes.models import Compute from storages.forms import AddStgPool, AddImage, CloneImage from vrtManager.storage import wvmStorage, wvmStorages @@ -11,7 +10,6 @@ from django.contrib import messages import json -@login_required def storages(request, compute_id): """ :param request: @@ -70,7 +68,6 @@ def storages(request, compute_id): return render(request, 'storages.html', locals()) -@login_required def storage(request, compute_id, pool): """ :param request: @@ -215,7 +212,6 @@ def storage(request, compute_id, pool): return render(request, 'storage.html', locals()) -@login_required def get_volumes(request, compute_id, pool): data = {} compute = get_object_or_404(Compute, pk=compute_id) diff --git a/webvirtcloud/settings.py.template b/webvirtcloud/settings.py.template index 8e93ac9..ad2b200 100644 --- a/webvirtcloud/settings.py.template +++ b/webvirtcloud/settings.py.template @@ -44,6 +44,7 @@ MIDDLEWARE = [ 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'login_required.middleware.LoginRequiredMiddleware', 'django.contrib.auth.middleware.RemoteUserMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', From 27f62dff6c8c1e2f5cbcdb49204e96022a6e5fb2 Mon Sep 17 00:00:00 2001 From: Real-Gecko Date: Wed, 27 May 2020 18:24:06 +0600 Subject: [PATCH 4/8] Added admin application - Manage users - Manage groups - Manage logs --- accounts/templates/accounts-list.html | 175 ------------------ accounts/templates/accounts.html | 144 -------------- accounts/templates/create_user_block.html | 38 ---- accounts/urls.py | 4 +- accounts/views.py | 113 ++--------- {instances/templatetags => admin}/__init__.py | 0 admin/apps.py | 5 + admin/decorators.py | 10 + admin/forms.py | 94 ++++++++++ admin/migrations/0001_initial.py | 30 +++ admin/migrations/__init__.py | 0 admin/models.py | 11 ++ .../admin/common/confirm_delete.html | 19 ++ admin/templates/admin/common/form.html | 28 +++ admin/templates/admin/common/list.html | 28 +++ admin/templates/admin/group_list.html | 63 +++++++ .../templates/admin/logs.html | 0 admin/templates/admin/user_form.html | 29 +++ admin/templates/admin/user_list.html | 79 ++++++++ admin/urls.py | 18 ++ admin/views.py | 171 +++++++++++++++++ computes/views.py | 99 +++++----- conf/requirements.txt | 2 + create/views.py | 65 ++++--- instances/templatetags/tags_active.py | 11 -- interfaces/views.py | 27 +-- logs/urls.py | 2 - logs/views.py | 35 +--- networks/views.py | 78 ++++---- nwfilters/views.py | 42 ++--- secrets/views.py | 33 ++-- static/css/webvirtcloud.css | 11 ++ static/js/filter-table.js | 12 ++ storages/views.py | 33 +--- templates/403.html | 15 ++ templates/navbar.html | 105 ++++++----- webvirtcloud/common_tags.py | 26 +++ webvirtcloud/settings.py.template | 20 +- 38 files changed, 933 insertions(+), 742 deletions(-) delete mode 100644 accounts/templates/accounts-list.html delete mode 100644 accounts/templates/accounts.html delete mode 100644 accounts/templates/create_user_block.html rename {instances/templatetags => admin}/__init__.py (100%) create mode 100644 admin/apps.py create mode 100644 admin/decorators.py create mode 100644 admin/forms.py create mode 100644 admin/migrations/0001_initial.py create mode 100644 admin/migrations/__init__.py create mode 100644 admin/models.py create mode 100644 admin/templates/admin/common/confirm_delete.html create mode 100644 admin/templates/admin/common/form.html create mode 100644 admin/templates/admin/common/list.html create mode 100644 admin/templates/admin/group_list.html rename logs/templates/showlogs.html => admin/templates/admin/logs.html (100%) create mode 100644 admin/templates/admin/user_form.html create mode 100644 admin/templates/admin/user_list.html create mode 100644 admin/urls.py create mode 100644 admin/views.py delete mode 100644 instances/templatetags/tags_active.py create mode 100644 static/js/filter-table.js create mode 100644 templates/403.html create mode 100644 webvirtcloud/common_tags.py diff --git a/accounts/templates/accounts-list.html b/accounts/templates/accounts-list.html deleted file mode 100644 index d6630cc..0000000 --- a/accounts/templates/accounts-list.html +++ /dev/null @@ -1,175 +0,0 @@ -{% extends "base.html" %} -{% load i18n %} -{% load staticfiles %} -{% block title %}{% trans "Users" %}{% endblock %} -{% block content %} - -
-
- {% include 'create_user_block.html' %} - -

{% trans "Users" %}

-
-
- - - {% include 'errors_block.html' %} - -
- {% if not users %} -
-
- - {% trans "Warning:" %} {% trans "You don't have any User" %} -
-
- {% else %} -
- - - - - - - - - - - - {% for user in users %} - - - - - - - - {% endfor %} - -
{% trans "Username" %}{% trans "Status" %}{% trans "Staff" %}{% trans "Superuser" %}{% trans "Clone" %}
- {{ user.username }} - - - - - {% if user.is_active %} - {% trans "Active" %} - {% else %} - {% trans "Blocked" %} - {% endif %} - {% if user.is_staff %}{% endif %}{% if user.is_superuser %}{% endif %}{% if user.userattributes.can_clone_instances %}{% endif %}
- - {% for user in users %} - - - {% endfor %} -
- {% endif %} -
-{% endblock %} -{% block script %} - -{% endblock %} diff --git a/accounts/templates/accounts.html b/accounts/templates/accounts.html deleted file mode 100644 index 59077fc..0000000 --- a/accounts/templates/accounts.html +++ /dev/null @@ -1,144 +0,0 @@ -{% extends "base.html" %} -{% load i18n %} -{% block title %}{% trans "Users" %}{% endblock %} -{% block content %} - -
-
- {% include 'create_user_block.html' %} -

{% trans "Users" %}

-
-
- - - {% include 'errors_block.html' %} - -
- {% if not users %} -
-
- - {% trans "Warning:" %} {% trans "You don't have any User" %} -
-
- {% else %} - {% for user in users %} -
-
- -
-
-

{% trans "Status:" %}

-
-
- {% if user.is_active %} -

{% trans "Active" %}

- {% else %} -

{% trans "Blocked" %}

- {% endif %} -
-
-
-
- - - - {% endfor %} - {% endif %} -
-{% endblock %} diff --git a/accounts/templates/create_user_block.html b/accounts/templates/create_user_block.html deleted file mode 100644 index 55e5c2f..0000000 --- a/accounts/templates/create_user_block.html +++ /dev/null @@ -1,38 +0,0 @@ -{% load i18n %} -{% if request.user.is_superuser %} - - - - - - -{% endif %} diff --git a/accounts/urls.py b/accounts/urls.py index 6b49b70..21b7264 100644 --- a/accounts/urls.py +++ b/accounts/urls.py @@ -5,6 +5,6 @@ from . import views urlpatterns = [ path('login/', auth_views.LoginView.as_view(template_name='login.html'), name='login'), path('logout/', auth_views.LogoutView.as_view(template_name='logout.html'), name='logout'), - path('profile/', views.profile, name='profile'), path('', views.accounts, name='accounts'), - re_path(r'^profile/(?P[0-9]+)/$', views.account, name='account'), + path('profile/', views.profile, name='profile'), + path('profile//', views.account, name='account'), ] diff --git a/accounts/views.py b/accounts/views.py index 56f2c02..0664741 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -1,12 +1,14 @@ -from django.shortcuts import render -from django.http import HttpResponseRedirect -from django.urls import reverse -from django.utils.translation import ugettext_lazy as _ -from accounts.models import * -from instances.models import Instance -from accounts.forms import UserAddForm from django.conf import settings from django.core.validators import ValidationError +from django.http import HttpResponseRedirect +from django.shortcuts import render +from django.urls import reverse +from django.utils.translation import ugettext_lazy as _ + +from accounts.forms import UserAddForm +from accounts.models import * +from admin.decorators import superuser_only +from instances.models import Instance def profile(request): @@ -16,7 +18,7 @@ def profile(request): """ error_messages = [] - user = User.objects.get(id=request.user.id) + # user = User.objects.get(id=request.user.id) publickeys = UserSSHKey.objects.filter(user_id=request.user.id) show_profile_edit_password = settings.SHOW_PROFILE_EDIT_PASSWORD @@ -26,7 +28,7 @@ def profile(request): email = request.POST.get('email', '') user.first_name = username user.email = email - user.save() + request.user.save() return HttpResponseRedirect(request.get_full_path()) if 'oldpasswd' in request.POST: oldpasswd = request.POST.get('oldpasswd', '') @@ -36,11 +38,11 @@ def profile(request): error_messages.append("Passwords didn't enter") if password1 and password2 and password1 != password2: error_messages.append("Passwords don't match") - if not user.check_password(oldpasswd): + if not request.user.check_password(oldpasswd): error_messages.append("Old password is wrong!") if not error_messages: - user.set_password(password1) - user.save() + request.user.set_password(password1) + request.user.save() return HttpResponseRedirect(request.get_full_path()) if 'keyname' in request.POST: keyname = request.POST.get('keyname', '') @@ -67,85 +69,7 @@ def profile(request): return render(request, 'profile.html', locals()) -def accounts(request): - """ - :param request: - :return: - """ - if not request.user.is_superuser: - return HttpResponseRedirect(reverse('index')) - - error_messages = [] - users = User.objects.all().order_by('username') - allow_empty_password = settings.ALLOW_EMPTY_PASSWORD - - if request.method == 'POST': - if 'create' in request.POST: - form = UserAddForm(request.POST) - if form.is_valid(): - data = form.cleaned_data - else: - for msg_err in form.errors.values(): - error_messages.append(msg_err.as_text()) - if not error_messages: - new_user = User.objects.create_user(data['name'], None, data['password']) - new_user.save() - UserAttributes.configure_user(new_user) - return HttpResponseRedirect(request.get_full_path()) - if 'edit' in request.POST: - CHECKBOX_MAPPING = {'on': True, 'off': False, } - - user_id = request.POST.get('user_id', '') - user_pass = request.POST.get('user_pass', '') - user_edit = User.objects.get(id=user_id) - - if user_pass != '': user_edit.set_password(user_pass) - user_edit.is_staff = CHECKBOX_MAPPING.get(request.POST.get('user_is_staff', 'off')) - user_edit.is_superuser = CHECKBOX_MAPPING.get(request.POST.get('user_is_superuser', 'off')) - user_edit.save() - - UserAttributes.create_missing_userattributes(user_edit) - user_edit.userattributes.can_clone_instances = CHECKBOX_MAPPING.get(request.POST.get('userattributes_can_clone_instances', 'off')) - user_edit.userattributes.max_instances = request.POST.get('userattributes_max_instances', 0) - user_edit.userattributes.max_cpus = request.POST.get('userattributes_max_cpus', 0) - user_edit.userattributes.max_memory = request.POST.get('userattributes_max_memory', 0) - user_edit.userattributes.max_disk_size = request.POST.get('userattributes_max_disk_size', 0) - - try: - user_edit.userattributes.clean_fields() - except ValidationError as exc: - error_messages.append(exc) - else: - user_edit.userattributes.save() - return HttpResponseRedirect(request.get_full_path()) - if 'block' in request.POST: - user_id = request.POST.get('user_id', '') - user_block = User.objects.get(id=user_id) - user_block.is_active = False - user_block.save() - return HttpResponseRedirect(request.get_full_path()) - if 'unblock' in request.POST: - user_id = request.POST.get('user_id', '') - user_unblock = User.objects.get(id=user_id) - user_unblock.is_active = True - user_unblock.save() - return HttpResponseRedirect(request.get_full_path()) - if 'delete' in request.POST: - user_id = request.POST.get('user_id', '') - try: - del_user_inst = UserInstance.objects.filter(user_id=user_id) - del_user_inst.delete() - finally: - user_delete = User.objects.get(id=user_id) - user_delete.delete() - return HttpResponseRedirect(request.get_full_path()) - - accounts_template_file = 'accounts.html' - if settings.VIEW_ACCOUNTS_STYLE == "list": - accounts_template_file = 'accounts-list.html' - return render(request, accounts_template_file, locals()) - - +@superuser_only def account(request, user_id): """ :param request: @@ -153,9 +77,6 @@ def account(request, user_id): :return: """ - if not request.user.is_superuser: - return HttpResponseRedirect(reverse('index')) - error_messages = [] user = User.objects.get(id=user_id) user_insts = UserInstance.objects.filter(user_id=user_id) @@ -181,12 +102,12 @@ def account(request, user_id): return HttpResponseRedirect(request.get_full_path()) if 'add' in request.POST: inst_id = request.POST.get('inst_id', '') - + if settings.ALLOW_INSTANCE_MULTIPLE_OWNER: check_inst = UserInstance.objects.filter(instance_id=int(inst_id), user_id=int(user_id)) else: check_inst = UserInstance.objects.filter(instance_id=int(inst_id)) - + if check_inst: msg = _("Instance already added") error_messages.append(msg) diff --git a/instances/templatetags/__init__.py b/admin/__init__.py similarity index 100% rename from instances/templatetags/__init__.py rename to admin/__init__.py diff --git a/admin/apps.py b/admin/apps.py new file mode 100644 index 0000000..5bbf122 --- /dev/null +++ b/admin/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class AdminConfig(AppConfig): + name = 'admin' diff --git a/admin/decorators.py b/admin/decorators.py new file mode 100644 index 0000000..ebe901f --- /dev/null +++ b/admin/decorators.py @@ -0,0 +1,10 @@ +from django.core.exceptions import PermissionDenied + + +def superuser_only(function): + def _inner(request, *args, **kwargs): + if not request.user.is_superuser: + raise PermissionDenied + return function(request, *args, **kwargs) + + return _inner diff --git a/admin/forms.py b/admin/forms.py new file mode 100644 index 0000000..29c87b8 --- /dev/null +++ b/admin/forms.py @@ -0,0 +1,94 @@ +from django import forms +from django.contrib.auth.models import Group, User +from django.utils.translation import ugettext_lazy as _ + +from accounts.models import UserAttributes + +from .models import Permission + + +class GroupForm(forms.ModelForm): + permissions = forms.ModelMultipleChoiceField( + widget=forms.CheckboxSelectMultiple, + queryset=Permission.objects.filter(content_type__model='permissionset'), + required=False, + ) + + users = forms.ModelMultipleChoiceField( + widget=forms.CheckboxSelectMultiple, + queryset=User.objects.all(), + required=False, + ) + + def __init__(self, *args, **kwargs): + super(GroupForm, self).__init__(*args, **kwargs) + instance = getattr(self, 'instance', None) + if instance and instance.id: + self.fields['users'].initial = self.instance.user_set.all() + + def save_m2m(self): + self.instance.user_set.set(self.cleaned_data['users']) + + def save(self, *args, **kwargs): + instance = super(GroupForm, self).save() + self.save_m2m() + return instance + + class Meta: + model = Group + fields = '__all__' + + +class UserForm(forms.ModelForm): + user_permissions = forms.ModelMultipleChoiceField( + widget=forms.CheckboxSelectMultiple, + queryset=Permission.objects.filter(content_type__model='permissionset'), + label=_('Permissions'), + required=False, + ) + + groups = forms.ModelMultipleChoiceField( + widget=forms.CheckboxSelectMultiple, + queryset=Group.objects.all(), + label=_('Groups'), + required=False, + ) + + class Meta: + model = User + fields = [ + 'username', + 'groups', + 'first_name', + 'last_name', + 'email', + 'user_permissions', + 'is_staff', + 'is_active', + 'is_superuser', + ] + + +class UserCreateForm(UserForm): + password = forms.CharField(widget=forms.PasswordInput) + + class Meta: + model = User + fields = [ + 'username', + 'password', + 'groups', + 'first_name', + 'last_name', + 'email', + 'user_permissions', + 'is_staff', + 'is_active', + 'is_superuser', + ] + + +class UserAttributesForm(forms.ModelForm): + class Meta: + model = UserAttributes + exclude = ['user'] diff --git a/admin/migrations/0001_initial.py b/admin/migrations/0001_initial.py new file mode 100644 index 0000000..9a54081 --- /dev/null +++ b/admin/migrations/0001_initial.py @@ -0,0 +1,30 @@ +# Generated by Django 2.2.12 on 2020-05-27 07:01 + +import django.contrib.auth.models +from django.db import migrations + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0011_update_proxy_permissions'), + ] + + operations = [ + migrations.CreateModel( + name='Permission', + fields=[ + ], + options={ + 'proxy': True, + 'indexes': [], + 'constraints': [], + }, + bases=('auth.permission',), + managers=[ + ('objects', django.contrib.auth.models.PermissionManager()), + ], + ), + ] diff --git a/admin/migrations/__init__.py b/admin/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/admin/models.py b/admin/models.py new file mode 100644 index 0000000..73ddb5d --- /dev/null +++ b/admin/models.py @@ -0,0 +1,11 @@ +from django.contrib.auth.models import Permission as P + +class Permission(P): + """ + Proxy model to Django Permissions model allows us to override __str__ + """ + def __str__(self): + return f'{self.content_type.app_label}: {self.name}' + + class Meta: + proxy = True diff --git a/admin/templates/admin/common/confirm_delete.html b/admin/templates/admin/common/confirm_delete.html new file mode 100644 index 0000000..6ad93d4 --- /dev/null +++ b/admin/templates/admin/common/confirm_delete.html @@ -0,0 +1,19 @@ +{% extends "base.html" %} +{% load bootstrap3 %} +{% load font_awesome %} +{% load i18n %} + +{% block title %}{%trans "Delete" %}{% endblock %} + +{% block content %} +
+ {% csrf_token %} +
+ {%trans "Are you sure you want to delete" %} "{{ object }}"? +
+ {% icon 'times' %} {% trans "Cancel" %} + +
+{% endblock %} \ No newline at end of file diff --git a/admin/templates/admin/common/form.html b/admin/templates/admin/common/form.html new file mode 100644 index 0000000..4ff3fd3 --- /dev/null +++ b/admin/templates/admin/common/form.html @@ -0,0 +1,28 @@ +{% extends "base.html" %} +{% load bootstrap3 %} +{% load font_awesome %} +{% load i18n %} + +{% block title %}{% trans "User" %}{% endblock %} + +{% block content %} +
+
+ +
+
+
+
+
+ {% csrf_token %} + {% bootstrap_form form layout='horizontal' %} +
+
+ {% icon 'times' %} {% trans "Cancel" %} + +
+
+
+{% endblock content %} \ No newline at end of file diff --git a/admin/templates/admin/common/list.html b/admin/templates/admin/common/list.html new file mode 100644 index 0000000..a7a8e50 --- /dev/null +++ b/admin/templates/admin/common/list.html @@ -0,0 +1,28 @@ +{% extends "base.html" %} +{% load font_awesome %} +{% load i18n %} + +{% block title %}{{ title }}{% endblock %} + +{% block content %} +{% if create_url %} +{% icon 'plus' %} {%trans "Create New" %} +{% endif %} + + {% for object in object_list %} + + + + {% endfor %} +
{{ object }} +
+ {% icon 'edit' %} {%trans "Edit"%} + {% if extra_urls %} + {% for url in extra_urls %} + {{ url.1 }} + {% endfor %} + {% endif %} + {% icon 'times' %} {%trans "Delete" %} +
+
+{% endblock %} \ No newline at end of file diff --git a/admin/templates/admin/group_list.html b/admin/templates/admin/group_list.html new file mode 100644 index 0000000..4ec6a19 --- /dev/null +++ b/admin/templates/admin/group_list.html @@ -0,0 +1,63 @@ +{% extends "base.html" %} +{% load i18n %} +{% load static %} +{% load font_awesome %} +{% block title %}{% trans "Users" %}{% endblock %} +{% block content %} +
+
+ + {% icon 'plus' %} + + +

{% trans "Groups" %}

+
+
+{% include 'errors_block.html' %} +
+ {% if not groups %} +
+
+ + {% icon 'exclamation-triangle '%} {% trans "Warning" %}: {% trans "You don't have any groups" %} +
+
+ {% else %} +
+ + + + + + + + + {% for group in groups %} + + + + + {% endfor %} + +
{% trans "Group Name" %}{% trans "Actions" %}
+ {{ group.name }} + + +
+
+ {% endif %} +
+{% endblock content %} + +{% block script %} + +{% endblock script %} diff --git a/logs/templates/showlogs.html b/admin/templates/admin/logs.html similarity index 100% rename from logs/templates/showlogs.html rename to admin/templates/admin/logs.html diff --git a/admin/templates/admin/user_form.html b/admin/templates/admin/user_form.html new file mode 100644 index 0000000..ff54117 --- /dev/null +++ b/admin/templates/admin/user_form.html @@ -0,0 +1,29 @@ +{% extends "base.html" %} +{% load bootstrap3 %} +{% load font_awesome %} +{% load i18n %} + +{% block title %}{% trans "User" %}{% endblock %} + +{% block content %} +
+
+ +
+
+
+
+
+ {% csrf_token %} + {% bootstrap_form user_form layout='horizontal' %} + {% bootstrap_form attributes_form layout='horizontal' %} +
+
+ {% icon 'times' %} {% trans "Cancel" %} + +
+
+
+{% endblock content %} \ No newline at end of file diff --git a/admin/templates/admin/user_list.html b/admin/templates/admin/user_list.html new file mode 100644 index 0000000..bba9a58 --- /dev/null +++ b/admin/templates/admin/user_list.html @@ -0,0 +1,79 @@ +{% extends "base.html" %} +{% load i18n %} +{% load static %} +{% load font_awesome %} +{% block title %}{% trans "Users" %}{% endblock %} +{% block content %} +
+
+ + {% icon 'plus' %} + + +

{% trans "Users" %}

+
+
+{% include 'errors_block.html' %} +
+ {% if not users %} +
+
+ + {% icon 'exclamation-triangle '%} {% trans "Warning" %}: {% trans "You don't have any users" %} +
+
+ {% else %} +
+ + + + + + + + + + + + + {% for user in users %} + + + + + + + + + {% endfor %} + +
{% trans "Username" %}{% trans "Status" %}{% trans "Staff" %}{% trans "Superuser" %}{% trans "Clone" %}{% trans "" %}
+ {{ user.username }} + + {% if user.is_active %} + {% trans "Active" %} + {% else %} + {% trans "Blocked" %} + {% endif %} + {% if user.is_staff %}{% icon 'check' %}{% endif %}{% if user.is_superuser %}{% icon 'check' %}{% endif %}{% if user.userattributes.can_clone_instances %}{% icon 'check' %}{% endif %} +
+ {% icon 'eye' %} + {% icon 'pencil' %} + {% if user.is_active %} + {% icon 'stop' %} + {% else %} + {% icon 'play' %} + {% endif %} + {% icon 'times' %} +
+
+
+ {% endif %} +
+{% endblock content %} + +{% block script %} + +{% endblock script %} diff --git a/admin/urls.py b/admin/urls.py new file mode 100644 index 0000000..5e934c0 --- /dev/null +++ b/admin/urls.py @@ -0,0 +1,18 @@ +from django.urls import path +from django.contrib.auth.views import PasswordChangeView, PasswordChangeDoneView + +from . import views + +urlpatterns = [ + path('groups/', views.group_list, name='group_list'), + path('groups/create/', views.group_create, name='group_create'), + path('groups//update/', views.group_update, name='group_update'), + path('groups//delete/', views.group_delete, name='group_delete'), + path('users/', views.user_list, name='user_list'), + path('users/create/', views.user_create, name='user_create'), + path('users//update/', views.user_update, name='user_update'), + path('users//delete/', views.user_delete, name='user_delete'), + path('users//block/', views.user_block, name='user_block'), + path('users//unblock/', views.user_unblock, name='user_unblock'), + path('logs/', views.logs, name='logs'), +] diff --git a/admin/views.py b/admin/views.py new file mode 100644 index 0000000..85a4370 --- /dev/null +++ b/admin/views.py @@ -0,0 +1,171 @@ +from django.conf import settings +from django.contrib.auth.decorators import user_passes_test +from django.contrib.auth.models import Group, User +from django.core.paginator import Paginator +from django.shortcuts import get_object_or_404, redirect, render +from django.utils.translation import ugettext_lazy as _ + +from accounts.models import UserAttributes +from logs.models import Logs + +from . import forms +from .decorators import superuser_only + + +@superuser_only +def group_list(request): + groups = Group.objects.all() + return render( + request, + 'admin/group_list.html', + { + 'groups': groups, + }, + ) + + +@superuser_only +def group_create(request): + form = forms.GroupForm(request.POST or None) + if form.is_valid(): + form.save() + return redirect('admin:group_list') + return render( + request, + 'admin/common/form.html', + { + 'form': form, + 'title': _('Create Group'), + }, + ) + + +@superuser_only +def group_update(request, pk): + group = get_object_or_404(Group, pk=pk) + form = forms.GroupForm(request.POST or None, instance=group) + if form.is_valid(): + form.save() + return redirect('admin:group_list') + + return render( + request, + 'admin/common/form.html', + { + 'form': form, + 'title': _('Update Group'), + }, + ) + + +@superuser_only +def group_delete(request, pk): + group = get_object_or_404(Group, pk=pk) + if request.method == 'POST': + group.delete() + return redirect('admin:group_list') + + return render( + request, + 'admin/common/confirm_delete.html', + {'object': group}, + ) + + +@superuser_only +def user_list(request): + users = User.objects.all() + return render( + request, + 'admin/user_list.html', + { + 'users': users, + 'title': _('Users'), + }, + ) + + +@superuser_only +def user_create(request): + user_form = forms.UserCreateForm(request.POST or None) + attributes_form = forms.UserAttributesForm(request.POST or None) + if user_form.is_valid() and attributes_form.is_valid(): + user = user_form.save() + password = user_form.cleaned_data['password'] + user.set_password(password) + user.save() + attributes = attributes_form.save(commit=False) + attributes.user = user + attributes.save() + return redirect('admin:user_list') + + return render( + request, + 'admin/user_form.html', + { + 'user_form': user_form, + 'attributes_form': attributes_form, + 'title': _('Create User') + }, + ) + + +@superuser_only +def user_update(request, pk): + user = get_object_or_404(User, pk=pk) + attributes = UserAttributes.objects.get(user=user) + user_form = forms.UserForm(request.POST or None, instance=user) + attributes_form = forms.UserAttributesForm(request.POST or None, instance=attributes) + if user_form.is_valid() and attributes_form.is_valid(): + user_form.save() + attributes_form.save() + return redirect('admin:user_list') + + return render( + request, + 'admin/user_form.html', + { + 'user_form': user_form, + 'attributes_form': attributes_form, + 'title': _('Update User') + }, + ) + + +@superuser_only +def user_delete(request, pk): + user = get_object_or_404(User, pk=pk) + if request.method == 'POST': + user.delete() + return redirect('admin:user_list') + + return render( + request, + 'admin/common/confirm_delete.html', + {'object': user}, + ) + + +@superuser_only +def user_block(request, pk): + user: User = get_object_or_404(User, pk=pk) + user.is_active = False + user.save() + return redirect('admin:user_list') + + +@superuser_only +def user_unblock(request, pk): + user: User = get_object_or_404(User, pk=pk) + user.is_active = True + user.save() + return redirect('admin:user_list') + + +@superuser_only +def logs(request): + l = Logs.objects.order_by('-date') + paginator = Paginator(l, settings.LOGS_PER_PAGE) + page = request.GET.get('page', 1) + logs = paginator.page(page) + return render(request, 'admin/logs.html', {'logs': logs}) diff --git a/computes/views.py b/computes/views.py index 77c9d05..d6b5c73 100644 --- a/computes/views.py +++ b/computes/views.py @@ -10,38 +10,37 @@ from computes.forms import ComputeAddTcpForm, ComputeAddSshForm, ComputeEditHost 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 def computes(request): """ :param request: :return: """ - - if not request.user.is_superuser: - return HttpResponseRedirect(reverse('index')) - def get_hosts_status(computes): """ Function return all hosts all vds on host """ compute_data = [] for compute in computes: - compute_data.append({'id': compute.id, - 'name': compute.name, - 'hostname': compute.hostname, - 'status': connection_manager.host_is_up(compute.type, compute.hostname), - 'type': compute.type, - 'login': compute.login, - 'password': compute.password, - 'details': compute.details - }) + compute_data.append({ + 'id': compute.id, + 'name': compute.name, + 'hostname': compute.hostname, + 'status': connection_manager.host_is_up(compute.type, compute.hostname), + 'type': compute.type, + 'login': compute.login, + 'password': compute.password, + 'details': compute.details + }) return compute_data error_messages = [] computes = Compute.objects.filter().order_by('name') computes_info = get_hosts_status(computes) - + if request.method == 'POST': if 'host_del' in request.POST: compute_id = request.POST.get('host_id', '') @@ -133,6 +132,7 @@ def computes(request): return render(request, 'computes.html', locals()) +@superuser_only def overview(request, compute_id): """ :param request: @@ -140,17 +140,16 @@ def overview(request, compute_id): :return: """ - if not request.user.is_superuser: - return HttpResponseRedirect(reverse('index')) - error_messages = [] compute = get_object_or_404(Compute, pk=compute_id) try: - conn = wvmHostDetails(compute.hostname, - compute.login, - compute.password, - compute.type) + conn = wvmHostDetails( + compute.hostname, + compute.login, + compute.password, + compute.type, + ) hostname, host_arch, host_memory, logical_cpu, model_cpu, uri_conn = conn.get_node_info() hypervisor = conn.get_hypervisors_domain_types() mem_usage = conn.get_memory_usage() @@ -172,10 +171,12 @@ def compute_graph(request, compute_id): """ compute = get_object_or_404(Compute, pk=compute_id) try: - conn = wvmHostDetails(compute.hostname, - compute.login, - compute.password, - compute.type) + conn = wvmHostDetails( + compute.hostname, + compute.login, + compute.password, + compute.type, + ) current_time = timezone.now().strftime("%H:%M:%S") cpu_usage = conn.get_cpu_usage() mem_usage = conn.get_memory_usage() @@ -184,9 +185,11 @@ def compute_graph(request, compute_id): cpu_usage = {'usage': 0} mem_usage = {'usage': 0} - data = json.dumps({'cpudata': cpu_usage['usage'], - 'memdata': mem_usage, - 'timeline': current_time}) + data = json.dumps({ + 'cpudata': cpu_usage['usage'], + 'memdata': mem_usage, + 'timeline': current_time, + }) response = HttpResponse() response['Content-Type'] = "text/javascript" response.write(data) @@ -197,10 +200,12 @@ def get_compute_disk_buses(request, compute_id, arch, machine, disk): data = dict() compute = get_object_or_404(Compute, pk=compute_id) try: - conn = wvmConnect(compute.hostname, - compute.login, - compute.password, - compute.type) + conn = wvmConnect( + compute.hostname, + compute.login, + compute.password, + compute.type, + ) disk_device_types = conn.get_disk_device_types(arch, machine) @@ -223,10 +228,12 @@ def get_compute_machine_types(request, compute_id, arch): data = dict() try: compute = get_object_or_404(Compute, pk=compute_id) - conn = wvmConnect(compute.hostname, - compute.login, - compute.password, - compute.type) + conn = wvmConnect( + compute.hostname, + compute.login, + compute.password, + compute.type, + ) data['machines'] = conn.get_machine_types(arch) except libvirtError: pass @@ -238,10 +245,12 @@ def get_compute_video_models(request, compute_id, arch, machine): data = dict() try: compute = get_object_or_404(Compute, pk=compute_id) - conn = wvmConnect(compute.hostname, - compute.login, - compute.password, - compute.type) + conn = wvmConnect( + compute.hostname, + compute.login, + compute.password, + compute.type, + ) data['videos'] = conn.get_video_models(arch, machine) except libvirtError: pass @@ -253,10 +262,12 @@ def get_dom_capabilities(request, compute_id, arch, machine): data = dict() try: compute = get_object_or_404(Compute, pk=compute_id) - conn = wvmConnect(compute.hostname, - compute.login, - compute.password, - compute.type) + conn = wvmConnect( + compute.hostname, + compute.login, + compute.password, + compute.type, + ) data['videos'] = conn.get_disk_device_types(arch, machine) data['bus'] = conn.get_disk_device_types(arch, machine) except libvirtError: diff --git a/conf/requirements.txt b/conf/requirements.txt index b831e22..af0e93a 100644 --- a/conf/requirements.txt +++ b/conf/requirements.txt @@ -1,4 +1,6 @@ Django==2.2.12 +django-bootstrap3==12.1.0 +django-fa==1.0.0 django-login-required-middleware==0.5.0 gunicorn==20.0.4 libvirt-python==6.1.0 diff --git a/create/views.py b/create/views.py index 384d6eb..0c84bea 100644 --- a/create/views.py +++ b/create/views.py @@ -23,13 +23,12 @@ from webvirtcloud.settings import INSTANCE_FIRMWARE_DEFAULT_TYPE from django.contrib import messages from logs.views import addlogmsg +from admin.decorators import superuser_only +@superuser_only def create_instance_select_type(request, compute_id): - if not request.user.is_superuser: - return HttpResponseRedirect(reverse('index')) - conn = None error_messages = list() storages = list() @@ -39,10 +38,7 @@ def create_instance_select_type(request, compute_id): compute = get_object_or_404(Compute, pk=compute_id) try: - conn = wvmCreate(compute.hostname, - compute.login, - compute.password, - compute.type) + conn = wvmCreate(compute.hostname, compute.login, compute.password, compute.type) instances = conn.get_instances() all_hypervisors = conn.get_hypervisors_machines() # Supported hypervisors by webvirtcloud: i686, x86_64(for now) @@ -74,6 +70,7 @@ def create_instance_select_type(request, compute_id): return render(request, 'create_instance_w1.html', locals()) +@superuser_only def create_instance(request, compute_id, arch, machine): """ :param request: @@ -82,8 +79,6 @@ def create_instance(request, compute_id, arch, machine): :param machine: :return: """ - if not request.user.is_superuser: - return HttpResponseRedirect(reverse('index')) conn = None error_messages = list() @@ -96,10 +91,7 @@ def create_instance(request, compute_id, arch, machine): flavors = Flavor.objects.filter().order_by('id') try: - conn = wvmCreate(compute.hostname, - compute.login, - compute.password, - compute.type) + conn = wvmCreate(compute.hostname, compute.login, compute.password, compute.type) default_firmware = INSTANCE_FIRMWARE_DEFAULT_TYPE default_cpu_mode = INSTANCE_CPU_DEFAULT_MODE @@ -150,10 +142,7 @@ def create_instance(request, compute_id, arch, machine): form = FlavorAddForm(request.POST) if form.is_valid(): data = form.cleaned_data - create_flavor = Flavor(label=data['label'], - vcpu=data['vcpu'], - memory=data['memory'], - disk=data['disk']) + create_flavor = Flavor(label=data['label'], vcpu=data['vcpu'], memory=data['memory'], disk=data['disk']) create_flavor.save() return HttpResponseRedirect(request.get_full_path()) if 'delete_flavor' in request.POST: @@ -184,7 +173,9 @@ def create_instance(request, compute_id, arch, machine): error_messages.append(error_msg) else: try: - path = conn.create_volume(data['storage'], data['name'], data['hdd_size'], + path = conn.create_volume(data['storage'], + data['name'], + data['hdd_size'], metadata=meta_prealloc) volume = dict() volume['path'] = path @@ -203,7 +194,10 @@ def create_instance(request, compute_id, arch, machine): error_msg = _("Image has already exist. Please check volumes or change instance name") error_messages.append(error_msg) else: - clone_path = conn.clone_from_template(data['name'], templ_path, data['storage'], metadata=meta_prealloc) + clone_path = conn.clone_from_template(data['name'], + templ_path, + data['storage'], + metadata=meta_prealloc) volume = dict() volume['path'] = clone_path volume['type'] = conn.get_volume_type(clone_path) @@ -238,23 +232,36 @@ def create_instance(request, compute_id, arch, machine): firmware["readonly"] = 'yes' firmware["type"] = 'pflash' if 'secboot' in firmware["loader"] and machine != 'q35': - messages.warning(request, "Changing machine type from '%s' to 'q35' " - "which is required for UEFI secure boot." % machine) + messages.warning( + request, "Changing machine type from '%s' to 'q35' " + "which is required for UEFI secure boot." % machine) machine = 'q35' firmware["secure"] = 'yes' if not error_messages: uuid = util.randomUUID() try: - conn.create_instance(name=data['name'], memory=data['memory'], vcpu=data['vcpu'], - vcpu_mode=data['vcpu_mode'], uuid=uuid, arch=arch, machine=machine, + conn.create_instance(name=data['name'], + memory=data['memory'], + vcpu=data['vcpu'], + vcpu_mode=data['vcpu_mode'], + uuid=uuid, + arch=arch, + machine=machine, firmware=firmware, - images=volume_list, cache_mode=data['cache_mode'], - io_mode=default_io, discard_mode=default_discard, detect_zeroes_mode=default_zeroes, - networks=data['networks'], virtio=data['virtio'], - listen_addr=data["listener_addr"], nwfilter=data["nwfilter"], - graphics=data["graphics"], video=data["video"], - console_pass=data["console_pass"], mac=data['mac'], + images=volume_list, + cache_mode=data['cache_mode'], + io_mode=default_io, + discard_mode=default_discard, + detect_zeroes_mode=default_zeroes, + networks=data['networks'], + virtio=data['virtio'], + listen_addr=data["listener_addr"], + nwfilter=data["nwfilter"], + graphics=data["graphics"], + video=data["video"], + console_pass=data["console_pass"], + mac=data['mac'], qemu_ga=data['qemu_ga']) create_instance = Instance(compute_id=compute_id, name=data['name'], uuid=uuid) create_instance.save() diff --git a/instances/templatetags/tags_active.py b/instances/templatetags/tags_active.py deleted file mode 100644 index 8d8ce62..0000000 --- a/instances/templatetags/tags_active.py +++ /dev/null @@ -1,11 +0,0 @@ -from django import template -import re - -register = template.Library() - - -@register.simple_tag -def class_active(request, pattern): - if re.search(pattern, request.path): - return 'class="active"' - return '' diff --git a/interfaces/views.py b/interfaces/views.py index cd9f43f..0e8a52c 100644 --- a/interfaces/views.py +++ b/interfaces/views.py @@ -5,8 +5,10 @@ from computes.models import Compute from interfaces.forms import AddInterface from vrtManager.interface import wvmInterface, wvmInterfaces from libvirt import libvirtError +from admin.decorators import superuser_only +@superuser_only def interfaces(request, compute_id): """ :param request: @@ -14,18 +16,12 @@ def interfaces(request, compute_id): :return: """ - if not request.user.is_superuser: - return HttpResponseRedirect(reverse('index')) - ifaces_all = [] error_messages = [] compute = get_object_or_404(Compute, pk=compute_id) try: - conn = wvmInterfaces(compute.hostname, - compute.login, - compute.password, - compute.type) + conn = wvmInterfaces(compute.hostname, compute.login, compute.password, compute.type) ifaces = conn.get_ifaces() try: netdevs = conn.get_net_devices() @@ -40,10 +36,9 @@ def interfaces(request, compute_id): form = AddInterface(request.POST) if form.is_valid(): data = form.cleaned_data - conn.create_iface(data['name'], data['itype'], data['start_mode'], data['netdev'], - data['ipv4_type'], data['ipv4_addr'], data['ipv4_gw'], - data['ipv6_type'], data['ipv6_addr'], data['ipv6_gw'], - data['stp'], data['delay']) + conn.create_iface(data['name'], data['itype'], data['start_mode'], data['netdev'], data['ipv4_type'], + data['ipv4_addr'], data['ipv4_gw'], data['ipv6_type'], data['ipv6_addr'], + data['ipv6_gw'], data['stp'], data['delay']) return HttpResponseRedirect(request.get_full_path()) else: for msg_err in form.errors.values(): @@ -55,6 +50,7 @@ def interfaces(request, compute_id): return render(request, 'interfaces.html', locals()) +@superuser_only def interface(request, compute_id, iface): """ :param request: @@ -63,19 +59,12 @@ def interface(request, compute_id, iface): :return: """ - if not request.user.is_superuser: - return HttpResponseRedirect(reverse('index')) - ifaces_all = [] error_messages = [] compute = get_object_or_404(Compute, pk=compute_id) try: - conn = wvmInterface(compute.hostname, - compute.login, - compute.password, - compute.type, - iface) + conn = wvmInterface(compute.hostname, compute.login, compute.password, compute.type, iface) start_mode = conn.get_start_mode() state = conn.is_active() mac = conn.get_mac() diff --git a/logs/urls.py b/logs/urls.py index fb8fb0b..a8a5ae8 100644 --- a/logs/urls.py +++ b/logs/urls.py @@ -2,7 +2,5 @@ from django.urls import path, re_path from . import views urlpatterns = [ - path('', views.showlogs, name='showlogs'), - re_path(r'^(?P[0-9]+)/$', views.showlogs, name='showlogspage'), re_path(r'^vm_logs/(?P[\w\-\.]+)/$', views.vm_logs, name='vm_logs'), ] diff --git a/logs/views.py b/logs/views.py index 45569b0..c04e284 100644 --- a/logs/views.py +++ b/logs/views.py @@ -1,10 +1,13 @@ -from django.shortcuts import render +import json + +from django.conf import settings from django.http import HttpResponse, HttpResponseRedirect +from django.shortcuts import render from django.urls import reverse + +from admin.decorators import superuser_only from instances.models import Instance from logs.models import Logs -from django.conf import settings -import json def addlogmsg(user, instance, message): @@ -18,35 +21,13 @@ def addlogmsg(user, instance, message): add_log_msg.save() -def showlogs(request, page=1): - """ - :param request: - :param page: - :return: - """ - - if not request.user.is_superuser: - return HttpResponseRedirect(reverse('index')) - - page = int(page) - limit_from = (page-1)*settings.LOGS_PER_PAGE - limit_to = page*settings.LOGS_PER_PAGE - logs = Logs.objects.all().order_by('-date')[limit_from:limit_to+1] - has_next_page = logs.count() > settings.LOGS_PER_PAGE - # TODO: remove last element from queryset, but do not affect database - - return render(request, 'showlogs.html', locals()) - - +@superuser_only def vm_logs(request, vname): """ :param request: :param vname: :return: """ - - if not request.user.is_superuser: - return HttpResponseRedirect(reverse('index')) vm = Instance.objects.get(name=vname) logs_ = Logs.objects.filter(instance=vm.name, date__gte=vm.created).order_by('-date') @@ -58,5 +39,5 @@ def vm_logs(request, vname): log['message'] = l.message log['date'] = l.date.strftime('%x %X') logs.append(log) - + return HttpResponse(json.dumps(logs)) diff --git a/networks/views.py b/networks/views.py index 39b648d..8905d9c 100644 --- a/networks/views.py +++ b/networks/views.py @@ -1,15 +1,17 @@ -from django.shortcuts import render, get_object_or_404 +from django.contrib import messages from django.http import HttpResponseRedirect -from django.utils.translation import ugettext_lazy as _ +from django.shortcuts import get_object_or_404, render from django.urls import reverse +from django.utils.translation import ugettext_lazy as _ +from libvirt import libvirtError + +from admin.decorators import superuser_only from computes.models import Compute from networks.forms import AddNetPool -from vrtManager.network import wvmNetwork, wvmNetworks -from vrtManager.network import network_size -from libvirt import libvirtError -from django.contrib import messages +from vrtManager.network import network_size, wvmNetwork, wvmNetworks +@superuser_only def networks(request, compute_id): """ :param request: @@ -17,17 +19,16 @@ def networks(request, compute_id): :return: """ - if not request.user.is_superuser: - return HttpResponseRedirect(reverse('index')) - error_messages = [] compute = get_object_or_404(Compute, pk=compute_id) try: - conn = wvmNetworks(compute.hostname, - compute.login, - compute.password, - compute.type) + conn = wvmNetworks( + compute.hostname, + compute.login, + compute.password, + compute.type, + ) networks = conn.get_networks_info() dhcp4 = netmask4 = gateway4 = '' dhcp6 = prefix6 = gateway6 = '' @@ -52,11 +53,21 @@ def networks(request, compute_id): if prefix6 != '64': error_messages.append(_('For libvirt, the IPv6 network prefix must be /64')) if not error_messages: - conn.create_network(data['name'], - data['forward'], - ipv4, gateway4, netmask4, dhcp4, - ipv6, gateway6, prefix6, dhcp6, - data['bridge_name'], data['openvswitch'], data['fixed']) + conn.create_network( + data['name'], + data['forward'], + ipv4, + gateway4, + netmask4, + dhcp4, + ipv6, + gateway6, + prefix6, + dhcp6, + data['bridge_name'], + data['openvswitch'], + data['fixed'], + ) return HttpResponseRedirect(reverse('network', args=[compute_id, data['name']])) else: for msg_err in form.errors.values(): @@ -68,6 +79,7 @@ def networks(request, compute_id): return render(request, 'networks.html', locals()) +@superuser_only def network(request, compute_id, pool): """ :param request: @@ -76,18 +88,17 @@ def network(request, compute_id, pool): :return: """ - if not request.user.is_superuser: - return HttpResponseRedirect(reverse('index')) - error_messages = [] compute = get_object_or_404(Compute, pk=compute_id) try: - conn = wvmNetwork(compute.hostname, - compute.login, - compute.password, - compute.type, - pool) + conn = wvmNetwork( + compute.hostname, + compute.login, + compute.password, + compute.type, + pool, + ) networks = conn.get_networks() state = conn.is_active() device = conn.get_bridge_device() @@ -187,8 +198,7 @@ def network(request, compute_id, pool): if edit_xml: conn.edit_network(edit_xml) if conn.is_active(): - messages.success(request, _("Network XML is changed. \\" - "Stop and start network to activate new config.")) + messages.success(request, _("Network XML is changed. \\" "Stop and start network to activate new config.")) else: messages.success(request, _("Network XML is changed.")) return HttpResponseRedirect(request.get_full_path()) @@ -201,8 +211,10 @@ def network(request, compute_id, pool): try: conn.set_qos(qos_dir, average, peak, burst) if conn.is_active(): - messages.success(request, _("{} Qos is set. Network XML is changed.").format(qos_dir.capitalize()) + - _("Stop and start network to activate new config")) + messages.success( + request, + _("{} Qos is set. Network XML is changed.").format(qos_dir.capitalize()) + + _("Stop and start network to activate new config")) else: messages.success(request, _("{} Qos is set").format(qos_dir.capitalize())) except libvirtError as lib_err: @@ -213,8 +225,10 @@ def network(request, compute_id, pool): conn.unset_qos(qos_dir) if conn.is_active(): - messages.success(request, _("{} Qos is deleted. Network XML is changed. ").format(qos_dir.capitalize()) + - _("Stop and start network to activate new config.")) + messages.success( + request, + _("{} Qos is deleted. Network XML is changed. ").format(qos_dir.capitalize()) + + _("Stop and start network to activate new config.")) else: messages.success(request, _("{} Qos is deleted").format(qos_dir.capitalize())) return HttpResponseRedirect(request.get_full_path()) diff --git a/nwfilters/views.py b/nwfilters/views.py index 897db7e..00b7e4f 100644 --- a/nwfilters/views.py +++ b/nwfilters/views.py @@ -2,17 +2,20 @@ from __future__ import unicode_literals from django.http import HttpResponseRedirect -from django.shortcuts import render, get_object_or_404 -from django.utils.translation import ugettext_lazy as _ +from django.shortcuts import get_object_or_404, render from django.urls import reverse -from computes.models import Compute -from vrtManager import util -from vrtManager.nwfilters import wvmNWFilters, wvmNWFilter -from vrtManager.instance import wvmInstances, wvmInstance +from django.utils.translation import ugettext_lazy as _ from libvirt import libvirtError + +from admin.decorators import superuser_only +from computes.models import Compute from logs.views import addlogmsg +from vrtManager import util +from vrtManager.instance import wvmInstance, wvmInstances +from vrtManager.nwfilters import wvmNWFilter, wvmNWFilters +@superuser_only def nwfilters(request, compute_id): """ :param request: @@ -20,18 +23,12 @@ def nwfilters(request, compute_id): :return: """ - if not request.user.is_superuser: - return HttpResponseRedirect(reverse('index')) - error_messages = [] nwfilters_all = [] compute = get_object_or_404(Compute, pk=compute_id) try: - conn = wvmNWFilters(compute.hostname, - compute.login, - compute.password, - compute.type) + conn = wvmNWFilters(compute.hostname, compute.login, compute.password, compute.type) if request.method == 'POST': if 'create_nwfilter' in request.POST: @@ -105,9 +102,11 @@ def nwfilters(request, compute_id): error_messages.append(err) addlogmsg(request.user.username, compute.hostname, err) - return render(request, 'nwfilters.html', {'error_messages': error_messages, - 'nwfilters': nwfilters_all, - 'compute': compute}) + return render(request, 'nwfilters.html', { + 'error_messages': error_messages, + 'nwfilters': nwfilters_all, + 'compute': compute + }) def nwfilter(request, compute_id, nwfltr): @@ -117,15 +116,8 @@ def nwfilter(request, compute_id, nwfltr): compute = get_object_or_404(Compute, pk=compute_id) try: - nwfilter = wvmNWFilter(compute.hostname, - compute.login, - compute.password, - compute.type, - nwfltr) - conn = wvmNWFilters(compute.hostname, - compute.login, - compute.password, - compute.type) + nwfilter = wvmNWFilter(compute.hostname, compute.login, compute.password, compute.type, nwfltr) + conn = wvmNWFilters(compute.hostname, compute.login, compute.password, compute.type) for nwf in conn.get_nwfilters(): nwfilters_all.append(conn.get_nwfilter_info(nwf)) diff --git a/secrets/views.py b/secrets/views.py index d40eda0..97c6ef6 100644 --- a/secrets/views.py +++ b/secrets/views.py @@ -1,12 +1,16 @@ -from django.shortcuts import render, get_object_or_404 -from django.http import HttpResponseRedirect -from django.urls import reverse -from computes.models import Compute from secrets.forms import AddSecret -from vrtManager.secrets import wvmSecrets + +from django.http import HttpResponseRedirect +from django.shortcuts import get_object_or_404, render +from django.urls import reverse from libvirt import libvirtError +from admin.decorators import superuser_only +from computes.models import Compute +from vrtManager.secrets import wvmSecrets + +@superuser_only def secrets(request, compute_id): """ :param request: @@ -14,17 +18,11 @@ def secrets(request, compute_id): :return: """ - if not request.user.is_superuser: - return HttpResponseRedirect(reverse('index')) - secrets_all = [] error_messages = [] compute = get_object_or_404(Compute, pk=compute_id) try: - conn = wvmSecrets(compute.hostname, - compute.login, - compute.password, - compute.type) + conn = wvmSecrets(compute.hostname, compute.login, compute.password, compute.type) secrets = conn.get_secrets() for uuid in secrets: @@ -33,11 +31,12 @@ def secrets(request, compute_id): secret_value = conn.get_secret_value(uuid) except libvirtError as lib_err: secret_value = None - secrets_all.append({'usage': secrt.usageID(), - 'uuid': secrt.UUIDString(), - 'usageType': secrt.usageType(), - 'value': secret_value - }) + secrets_all.append({ + 'usage': secrt.usageID(), + 'uuid': secrt.UUIDString(), + 'usageType': secrt.usageType(), + 'value': secret_value + }) if request.method == 'POST': if 'create' in request.POST: form = AddSecret(request.POST) diff --git a/static/css/webvirtcloud.css b/static/css/webvirtcloud.css index 0bdbba5..c20c15f 100644 --- a/static/css/webvirtcloud.css +++ b/static/css/webvirtcloud.css @@ -154,4 +154,15 @@ p { .bottom-bar-margin { margin-top: 65px; +} + +.thumbnail { + padding: 10px; +} + +/* make dropdowns show on hover */ +@media only screen and (min-width: 768px) { + .dropdown:hover .dropdown-menu { + display: block; + } } \ No newline at end of file diff --git a/static/js/filter-table.js b/static/js/filter-table.js new file mode 100644 index 0000000..bbf4843 --- /dev/null +++ b/static/js/filter-table.js @@ -0,0 +1,12 @@ +function filter_table() { + var rex = new RegExp($(this).val(), 'i'); + $('.searchable tr').hide(); + $('.searchable tr').filter(function () { + return rex.test($(this).text()); + }).show(); +} +$(document).ready(function () { + (function ($) { + $('#filter').keyup(filter_table) + }(jQuery)); +}); diff --git a/storages/views.py b/storages/views.py index 76749a4..9f2215c 100644 --- a/storages/views.py +++ b/storages/views.py @@ -8,8 +8,10 @@ from vrtManager.storage import wvmStorage, wvmStorages from libvirt import libvirtError from django.contrib import messages import json +from admin.decorators import superuser_only +@superuser_only def storages(request, compute_id): """ :param request: @@ -17,17 +19,11 @@ def storages(request, compute_id): :return: """ - if not request.user.is_superuser: - return HttpResponseRedirect(reverse('index')) - error_messages = [] compute = get_object_or_404(Compute, pk=compute_id) try: - conn = wvmStorages(compute.hostname, - compute.login, - compute.password, - compute.type) + conn = wvmStorages(compute.hostname, compute.login, compute.password, compute.type) storages = conn.get_storages_info() secrets = conn.get_secrets() @@ -48,12 +44,10 @@ def storages(request, compute_id): error_messages.append(msg) if not error_messages: if data['stg_type'] == 'rbd': - conn.create_storage_ceph(data['stg_type'], data['name'], - data['ceph_pool'], data['ceph_host'], + conn.create_storage_ceph(data['stg_type'], data['name'], data['ceph_pool'], data['ceph_host'], data['ceph_user'], data['secret']) elif data['stg_type'] == 'netfs': - conn.create_storage_netfs(data['stg_type'], data['name'], - data['netfs_host'], data['source'], + conn.create_storage_netfs(data['stg_type'], data['name'], data['netfs_host'], data['source'], data['source_format'], data['target']) else: conn.create_storage(data['stg_type'], data['name'], data['source'], data['target']) @@ -68,6 +62,7 @@ def storages(request, compute_id): return render(request, 'storages.html', locals()) +@superuser_only def storage(request, compute_id, pool): """ :param request: @@ -75,10 +70,6 @@ def storage(request, compute_id, pool): :param pool: :return: """ - - if not request.user.is_superuser: - return HttpResponseRedirect(reverse('index')) - def handle_uploaded_file(path, f_name): target = path + '/' + str(f_name) destination = open(target, 'wb+') @@ -91,11 +82,7 @@ def storage(request, compute_id, pool): meta_prealloc = False try: - conn = wvmStorage(compute.hostname, - compute.login, - compute.password, - compute.type, - pool) + conn = wvmStorage(compute.hostname, compute.login, compute.password, compute.type, pool) storages = conn.get_storages() state = conn.is_active() @@ -216,11 +203,7 @@ def get_volumes(request, compute_id, pool): data = {} compute = get_object_or_404(Compute, pk=compute_id) try: - conn = wvmStorage(compute.hostname, - compute.login, - compute.password, - compute.type, - pool) + conn = wvmStorage(compute.hostname, compute.login, compute.password, compute.type, pool) conn.refresh() data['vols'] = sorted(conn.get_volumes()) except libvirtError: diff --git a/templates/403.html b/templates/403.html new file mode 100644 index 0000000..bb0a0ab --- /dev/null +++ b/templates/403.html @@ -0,0 +1,15 @@ +{% extends "base_auth.html" %} +{% load i18n %} +{% block title %}{% trans "404" %}{% endblock %} +{% block content %} +
+
+

{% trans 'Oops!'%}

+ +

{% trans "403 Forbidden" %}

+ +

{% trans "You do not have permission to access this page." %}

+ ← {% trans 'Back'%} +
+
+{% endblock %} diff --git a/templates/navbar.html b/templates/navbar.html index 1a1263b..f9896cf 100644 --- a/templates/navbar.html +++ b/templates/navbar.html @@ -1,48 +1,59 @@ {% load i18n %} -{% load tags_active %} - - \ No newline at end of file +{% load font_awesome %} +{% load common_tags %} + + \ No newline at end of file diff --git a/webvirtcloud/common_tags.py b/webvirtcloud/common_tags.py new file mode 100644 index 0000000..223a722 --- /dev/null +++ b/webvirtcloud/common_tags.py @@ -0,0 +1,26 @@ +from django import template +import re + +register = template.Library() + + +@register.simple_tag +def app_active(request, app_name): + if request.resolver_match.app_name == app_name: + return 'active' + return '' + + +@register.simple_tag +def view_active(request, view_name): + if request.resolver_match.view_name == view_name: + return 'active' + return '' + + +@register.simple_tag +def class_active(request, pattern): + if re.search(pattern, request.path): + # Not sure why 'class="active"' returns class=""active"" + return 'class=active' + return '' diff --git a/webvirtcloud/settings.py.template b/webvirtcloud/settings.py.template index ad2b200..339fcba 100644 --- a/webvirtcloud/settings.py.template +++ b/webvirtcloud/settings.py.template @@ -14,17 +14,18 @@ DEBUG = True ALLOWED_HOSTS = ['*'] - # Application definition INSTALLED_APPS = [ - 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'bootstrap3', + 'fa', 'accounts', + 'admin', 'computes', 'console', 'create', @@ -56,7 +57,9 @@ ROOT_URLCONF = 'webvirtcloud.urls' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [ os.path.join(BASE_DIR, 'templates'), ], + 'DIRS': [ + os.path.join(BASE_DIR, 'templates'), + ], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ @@ -65,13 +68,15 @@ TEMPLATES = [ 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], + 'libraries': { + 'common_tags': 'webvirtcloud.common_tags', + }, }, }, ] WSGI_APPLICATION = 'webvirtcloud.wsgi.application' - # Database # https://docs.djangoproject.com/en/3.0/ref/settings/#databases @@ -84,12 +89,12 @@ DATABASES = { AUTHENTICATION_BACKENDS = [ 'django.contrib.auth.backends.ModelBackend', - #'django.contrib.auth.backends.RemoteUserBackend', - #'accounts.backends.MyRemoteUserBackend', ] LOGIN_URL = '/accounts/login' +LOGOUT_REDIRECT_URL = '/accounts/login' + LANGUAGE_CODE = 'en-us' TIME_ZONE = 'UTC' @@ -168,9 +173,6 @@ SHOW_ACCESS_ROOT_PASSWORD = False SHOW_ACCESS_SSH_KEYS = False SHOW_PROFILE_EDIT_PASSWORD = False -# available: default (grid), list -VIEW_ACCOUNTS_STYLE = 'grid' - # available list style: default (grouped), nongrouped VIEW_INSTANCES_LIST_STYLE = 'grouped' From 2a0d2400384de1cb6c50d5a59848664481bd6bfe Mon Sep 17 00:00:00 2001 From: Real-Gecko Date: Wed, 27 May 2020 18:41:34 +0600 Subject: [PATCH 5/8] Fix admin urls not included into main urls.py --- webvirtcloud/urls.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/webvirtcloud/urls.py b/webvirtcloud/urls.py index 6c21817..05d2608 100644 --- a/webvirtcloud/urls.py +++ b/webvirtcloud/urls.py @@ -2,16 +2,14 @@ from django.urls import include, path from instances.views import index from console.views import console -# from django.contrib import admin urlpatterns = [ path('', index, name='index'), - + path('admin/', include(('admin.urls', 'admin'), namespace='admin')), path('instances/', include('instances.urls')), path('accounts/', include('accounts.urls')), path('computes/', include('computes.urls')), path('logs/', include('logs.urls')), path('datasource/', include('datasource.urls')), path('console/', console, name='console'), - # url(r'^admin/', include(admin.site.urls)), ] From a62daad87bf19d10718676e23bb9268101a2d008 Mon Sep 17 00:00:00 2001 From: Real-Gecko Date: Wed, 27 May 2020 18:46:37 +0600 Subject: [PATCH 6/8] Added change password permission Replaces SHOW_PROFILE_EDIT_PASSWORD option Can be set on per user or per group basis --- accounts/migrations/0003_permissionset.py | 24 +++++++++ .../migrations/0004_apply_change_password.py | 25 ++++++++++ accounts/models.py | 50 +++++++++++++++---- accounts/templates/profile.html | 2 +- accounts/views.py | 1 - webvirtcloud/settings.py.template | 1 - 6 files changed, 90 insertions(+), 13 deletions(-) create mode 100644 accounts/migrations/0003_permissionset.py create mode 100644 accounts/migrations/0004_apply_change_password.py diff --git a/accounts/migrations/0003_permissionset.py b/accounts/migrations/0003_permissionset.py new file mode 100644 index 0000000..d7cf465 --- /dev/null +++ b/accounts/migrations/0003_permissionset.py @@ -0,0 +1,24 @@ +# Generated by Django 2.2.12 on 2020-05-27 12:29 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0002_addAdmin'), + ] + + operations = [ + migrations.CreateModel( + name='PermissionSet', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ], + options={ + 'permissions': (('change_password', 'Can change password'),), + 'managed': False, + 'default_permissions': (), + }, + ), + ] diff --git a/accounts/migrations/0004_apply_change_password.py b/accounts/migrations/0004_apply_change_password.py new file mode 100644 index 0000000..f313b7e --- /dev/null +++ b/accounts/migrations/0004_apply_change_password.py @@ -0,0 +1,25 @@ +from django.db import migrations + + +def apply_change_password(apps, schema_editor): + from django.conf import settings + from django.contrib.auth.models import User, Permission + + if hasattr(settings, 'SHOW_PROFILE_EDIT_PASSWORD'): + if settings.SHOW_PROFILE_EDIT_PASSWORD: + permission = Permission.objects.get(codename='change_password') + users = User.objects.all() + user: User + for user in users: + user.user_permissions.add(permission) + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0003_permissionset'), + ] + + operations = [ + migrations.RunPython(apply_change_password), + ] diff --git a/accounts/models.py b/accounts/models.py index 77cf81a..97a6740 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -1,10 +1,11 @@ -from django.db.models import Model, BooleanField, IntegerField, CharField -from django.db.models import ForeignKey, OneToOneField -from django.db.models import CASCADE, DO_NOTHING -from django.contrib.auth.models import User from django.conf import settings -from instances.models import Instance +from django.contrib.auth.models import User from django.core.validators import MinValueValidator +from django.db.models import (CASCADE, DO_NOTHING, BooleanField, CharField, + ForeignKey, IntegerField, Model, OneToOneField) +from django.utils.translation import ugettext_lazy as _ + +from instances.models import Instance class UserInstance(Model): @@ -30,10 +31,26 @@ class UserSSHKey(Model): class UserAttributes(Model): user = OneToOneField(User, on_delete=CASCADE) can_clone_instances = BooleanField(default=True) - max_instances = IntegerField(default=1, help_text="-1 for unlimited. Any integer value", validators=[MinValueValidator(-1), ]) - max_cpus = IntegerField(default=1, help_text="-1 for unlimited. Any integer value", validators=[MinValueValidator(-1)]) - max_memory = IntegerField(default=2048, help_text="-1 for unlimited. Any integer value", validators=[MinValueValidator(-1)]) - max_disk_size = IntegerField(default=20, help_text="-1 for unlimited. Any integer value", validators=[MinValueValidator(-1)]) + max_instances = IntegerField(default=1, + help_text="-1 for unlimited. Any integer value", + validators=[ + MinValueValidator(-1), + ]) + max_cpus = IntegerField( + default=1, + help_text="-1 for unlimited. Any integer value", + validators=[MinValueValidator(-1)], + ) + max_memory = IntegerField( + default=2048, + help_text="-1 for unlimited. Any integer value", + validators=[MinValueValidator(-1)], + ) + max_disk_size = IntegerField( + default=20, + help_text="-1 for unlimited. Any integer value", + validators=[MinValueValidator(-1)], + ) @staticmethod def create_missing_userattributes(user): @@ -51,7 +68,7 @@ class UserAttributes(Model): instance = Instance.objects.get(name=instance_name) user_instance = UserInstance(user=user, instance=instance) user_instance.save() - + @staticmethod def configure_user(user): UserAttributes.create_missing_userattributes(user) @@ -59,3 +76,16 @@ class UserAttributes(Model): def __unicode__(self): return self.user.username + + +class PermissionSet(Model): + """ + Dummy model for holding set of permissions we need to be automatically added by Django + """ + class Meta: + default_permissions = () + permissions = ( + ('change_password', _('Can change password')), + ) + + managed = False diff --git a/accounts/templates/profile.html b/accounts/templates/profile.html index d2213b4..e5888ff 100644 --- a/accounts/templates/profile.html +++ b/accounts/templates/profile.html @@ -41,7 +41,7 @@ - {% if show_profile_edit_password %} + {% if perms.accounts.change_password %}
{% csrf_token %}
diff --git a/accounts/views.py b/accounts/views.py index 0664741..9a58e03 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -20,7 +20,6 @@ def profile(request): error_messages = [] # user = User.objects.get(id=request.user.id) publickeys = UserSSHKey.objects.filter(user_id=request.user.id) - show_profile_edit_password = settings.SHOW_PROFILE_EDIT_PASSWORD if request.method == 'POST': if 'username' in request.POST: diff --git a/webvirtcloud/settings.py.template b/webvirtcloud/settings.py.template index 339fcba..e6d57e7 100644 --- a/webvirtcloud/settings.py.template +++ b/webvirtcloud/settings.py.template @@ -171,7 +171,6 @@ QUOTA_DEBUG = True ALLOW_EMPTY_PASSWORD = True SHOW_ACCESS_ROOT_PASSWORD = False SHOW_ACCESS_SSH_KEYS = False -SHOW_PROFILE_EDIT_PASSWORD = False # available list style: default (grouped), nongrouped VIEW_INSTANCES_LIST_STYLE = 'grouped' From 3d0493537fb81bdd59fca11feea9574103841a2c Mon Sep 17 00:00:00 2001 From: Real-Gecko Date: Thu, 28 May 2020 11:20:23 +0600 Subject: [PATCH 7/8] Added 'instances:clone_instances' permission Replaces 'can_clone_instances' user attribute --- ...move_userattributes_can_clone_instances.py | 18 +++++++ accounts/models.py | 50 +++++++++---------- admin/templates/admin/user_list.html | 6 ++- instances/migrations/0002_permissionset.py | 24 +++++++++ .../0003_migrate_can_clone_instances.py | 35 +++++++++++++ instances/models.py | 17 ++++++- webvirtcloud/common_tags.py | 7 +++ 7 files changed, 127 insertions(+), 30 deletions(-) create mode 100644 accounts/migrations/0005_remove_userattributes_can_clone_instances.py create mode 100644 instances/migrations/0002_permissionset.py create mode 100644 instances/migrations/0003_migrate_can_clone_instances.py diff --git a/accounts/migrations/0005_remove_userattributes_can_clone_instances.py b/accounts/migrations/0005_remove_userattributes_can_clone_instances.py new file mode 100644 index 0000000..76e0458 --- /dev/null +++ b/accounts/migrations/0005_remove_userattributes_can_clone_instances.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.12 on 2020-05-28 04:24 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0004_apply_change_password'), + ('instances', '0003_migrate_can_clone_instances'), + ] + + operations = [ + migrations.RemoveField( + model_name='userattributes', + name='can_clone_instances', + ), + ] diff --git a/accounts/models.py b/accounts/models.py index 97a6740..8484a7e 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -1,52 +1,50 @@ from django.conf import settings from django.contrib.auth.models import User from django.core.validators import MinValueValidator -from django.db.models import (CASCADE, DO_NOTHING, BooleanField, CharField, - ForeignKey, IntegerField, Model, OneToOneField) +from django.db import models from django.utils.translation import ugettext_lazy as _ from instances.models import Instance -class UserInstance(Model): - user = ForeignKey(User, on_delete=CASCADE) - instance = ForeignKey(Instance, on_delete=CASCADE) - is_change = BooleanField(default=False) - is_delete = BooleanField(default=False) - is_vnc = BooleanField(default=False) +class UserInstance(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE) + instance = models.ForeignKey(Instance, on_delete=models.CASCADE) + is_change = models.BooleanField(default=False) + is_delete = models.BooleanField(default=False) + is_vnc = models.BooleanField(default=False) def __unicode__(self): return self.instance.name -class UserSSHKey(Model): - user = ForeignKey(User, on_delete=DO_NOTHING) - keyname = CharField(max_length=25) - keypublic = CharField(max_length=500) +class UserSSHKey(models.Model): + user = models.ForeignKey(User, on_delete=models.DO_NOTHING) + keyname = models.CharField(max_length=25) + keypublic = models.CharField(max_length=500) def __unicode__(self): return self.keyname -class UserAttributes(Model): - user = OneToOneField(User, on_delete=CASCADE) - can_clone_instances = BooleanField(default=True) - max_instances = IntegerField(default=1, - help_text="-1 for unlimited. Any integer value", - validators=[ - MinValueValidator(-1), - ]) - max_cpus = IntegerField( +class UserAttributes(models.Model): + user = models.OneToOneField(User, on_delete=models.CASCADE) + max_instances = models.IntegerField(default=1, + help_text="-1 for unlimited. Any integer value", + validators=[ + MinValueValidator(-1), + ]) + max_cpus = models.IntegerField( default=1, help_text="-1 for unlimited. Any integer value", validators=[MinValueValidator(-1)], ) - max_memory = IntegerField( + max_memory = models.IntegerField( default=2048, help_text="-1 for unlimited. Any integer value", validators=[MinValueValidator(-1)], ) - max_disk_size = IntegerField( + max_disk_size = models.IntegerField( default=20, help_text="-1 for unlimited. Any integer value", validators=[MinValueValidator(-1)], @@ -78,14 +76,12 @@ class UserAttributes(Model): return self.user.username -class PermissionSet(Model): +class PermissionSet(models.Model): """ Dummy model for holding set of permissions we need to be automatically added by Django """ class Meta: default_permissions = () - permissions = ( - ('change_password', _('Can change password')), - ) + permissions = (('change_password', _('Can change password')), ) managed = False diff --git a/admin/templates/admin/user_list.html b/admin/templates/admin/user_list.html index bba9a58..892d456 100644 --- a/admin/templates/admin/user_list.html +++ b/admin/templates/admin/user_list.html @@ -1,6 +1,7 @@ {% extends "base.html" %} {% load i18n %} {% load static %} +{% load common_tags %} {% load font_awesome %} {% block title %}{% trans "Users" %}{% endblock %} {% block content %} @@ -33,12 +34,13 @@ {% trans "Status" %} {% trans "Staff" %} {% trans "Superuser" %} - {% trans "Clone" %} + {% trans "Can Clone" %} {% trans "" %} {% for user in users %} + {% has_perm user 'instances.clone_instances' as can_clone %} {{ user.username }} @@ -52,7 +54,7 @@ {% if user.is_staff %}{% icon 'check' %}{% endif %} {% if user.is_superuser %}{% icon 'check' %}{% endif %} - {% if user.userattributes.can_clone_instances %}{% icon 'check' %}{% endif %} + {% if can_clone %}{% icon 'check' %}{% endif %}
{% icon 'eye' %} diff --git a/instances/migrations/0002_permissionset.py b/instances/migrations/0002_permissionset.py new file mode 100644 index 0000000..251a56a --- /dev/null +++ b/instances/migrations/0002_permissionset.py @@ -0,0 +1,24 @@ +# Generated by Django 2.2.12 on 2020-05-27 07:01 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('instances', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='PermissionSet', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ], + options={ + 'permissions': (('clone_instances', 'Can clone instances'),), + 'managed': False, + 'default_permissions': (), + }, + ), + ] diff --git a/instances/migrations/0003_migrate_can_clone_instances.py b/instances/migrations/0003_migrate_can_clone_instances.py new file mode 100644 index 0000000..6b7cc16 --- /dev/null +++ b/instances/migrations/0003_migrate_can_clone_instances.py @@ -0,0 +1,35 @@ +from django.db import migrations + + +def migrate_can_clone_instances(apps, schema_editor): + from django.contrib.auth.models import User, Permission + user: User + users = User.objects.all() + + permission = Permission.objects.get(codename='clone_instances') + + for user in users: + if user.userattributes.can_clone_instances: + user.user_permissions.add(permission) + + +def reverse_can_clone_instances(apps, schema_editor): + from django.contrib.auth.models import User, Permission + user: User + users = User.objects.all() + + permission = Permission.objects.get(codename='clone_instances') + + for user in users: + user.user_permissions.remove(permission) + + +class Migration(migrations.Migration): + + dependencies = [ + ('instances', '0002_permissionset'), + ] + + operations = [ + migrations.RunPython(migrate_can_clone_instances, reverse_can_clone_instances), + ] diff --git a/instances/models.py b/instances/models.py index 436ab41..dd44c54 100644 --- a/instances/models.py +++ b/instances/models.py @@ -1,4 +1,7 @@ -from django.db.models import Model, ForeignKey, CharField, BooleanField, DateField, CASCADE +from django.db.models import (CASCADE, BooleanField, CharField, DateField, + ForeignKey, Model) +from django.utils.translation import ugettext_lazy as _ + from computes.models import Compute @@ -11,3 +14,15 @@ class Instance(Model): def __unicode__(self): return self.name + +class PermissionSet(Model): + """ + Dummy model for holding set of permissions we need to be automatically added by Django + """ + class Meta: + default_permissions = () + permissions = ( + ('clone_instances', _('Can clone instances')), + ) + + managed = False diff --git a/webvirtcloud/common_tags.py b/webvirtcloud/common_tags.py index 223a722..468bb3b 100644 --- a/webvirtcloud/common_tags.py +++ b/webvirtcloud/common_tags.py @@ -24,3 +24,10 @@ def class_active(request, pattern): # Not sure why 'class="active"' returns class=""active"" return 'class=active' return '' + + +@register.simple_tag +def has_perm(user, permission_codename): + if user.has_perm(permission_codename): + return True + return False From e4e79d3d4bbd1cb3c1defcea2a5eabb702490684 Mon Sep 17 00:00:00 2001 From: Real-Gecko Date: Thu, 28 May 2020 11:36:49 +0600 Subject: [PATCH 8/8] Added info from #18 to README --- README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/README.md b/README.md index 503616c..6f40feb 100644 --- a/README.md +++ b/README.md @@ -273,6 +273,25 @@ You need to put cloud public key into authorized keys on the compute node. Simpl sudo -u www-data ssh-copy-id root@compute1 ``` +### Host SMBIOS information is not available + +If you see warning +``` +Unsupported configuration: Host SMBIOS information is not available +``` +Then you need to install `dmidecode` package on your host using your package manager and restart libvirt daemon. + +Debian/Ubuntu like: +``` +$ sudo apt-get install dmidecode +$ sudo service libvirt-bin restart +``` +Arch Linux +``` +$ sudo pacman -S dmidecode +$ systemctl restart libvirtd +``` + ### Cloud-init Currently supports only root ssh authorized keys and hostname. Example configuration of the cloud-init client follows. ```