mirror of
https://github.com/retspen/webvirtcloud
synced 2024-12-24 23:25:24 +00:00
Accounts app improvements and tests
This commit is contained in:
parent
8afef36656
commit
5172a9f619
20 changed files with 622 additions and 227 deletions
|
@ -1,9 +1,10 @@
|
||||||
|
from appsettings.settings import app_settings
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
from django.forms import ModelForm, ValidationError
|
from django.forms import ModelForm, ValidationError
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from appsettings.models import AppSettings
|
from .models import UserInstance, UserSSHKey
|
||||||
|
from .utils import validate_ssh_key
|
||||||
from .models import UserInstance
|
|
||||||
|
|
||||||
|
|
||||||
class UserInstanceForm(ModelForm):
|
class UserInstanceForm(ModelForm):
|
||||||
|
@ -18,7 +19,7 @@ class UserInstanceForm(ModelForm):
|
||||||
|
|
||||||
def clean_instance(self):
|
def clean_instance(self):
|
||||||
instance = self.cleaned_data['instance']
|
instance = self.cleaned_data['instance']
|
||||||
if AppSettings.objects.get(key="ALLOW_INSTANCE_MULTIPLE_OWNER").value == 'False':
|
if app_settings.ALLOW_INSTANCE_MULTIPLE_OWNER == 'False':
|
||||||
exists = UserInstance.objects.filter(instance=instance)
|
exists = UserInstance.objects.filter(instance=instance)
|
||||||
if exists:
|
if exists:
|
||||||
raise ValidationError(_('Instance owned by another user'))
|
raise ValidationError(_('Instance owned by another user'))
|
||||||
|
@ -28,3 +29,43 @@ class UserInstanceForm(ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = UserInstance
|
model = UserInstance
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
|
|
||||||
|
|
||||||
|
class ProfileForm(ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = get_user_model()
|
||||||
|
fields = ('first_name', 'last_name', 'email')
|
||||||
|
|
||||||
|
|
||||||
|
class UserSSHKeyForm(ModelForm):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.user = kwargs.pop('user', None)
|
||||||
|
self.publickeys = UserSSHKey.objects.filter(user=self.user)
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def clean_keyname(self):
|
||||||
|
for key in self.publickeys:
|
||||||
|
if self.cleaned_data['keyname'] == key.keyname:
|
||||||
|
raise ValidationError(_("Key name already exist"))
|
||||||
|
|
||||||
|
return self.cleaned_data['keyname']
|
||||||
|
|
||||||
|
def clean_keypublic(self):
|
||||||
|
for key in self.publickeys:
|
||||||
|
if self.cleaned_data['keypublic'] == key.keypublic:
|
||||||
|
raise ValidationError(_("Public key already exist"))
|
||||||
|
|
||||||
|
if not validate_ssh_key(self.cleaned_data['keypublic']):
|
||||||
|
raise ValidationError(_('Invalid key'))
|
||||||
|
return self.cleaned_data['keypublic']
|
||||||
|
|
||||||
|
def save(self, commit=True):
|
||||||
|
ssh_key = super().save(commit=False)
|
||||||
|
ssh_key.user = self.user
|
||||||
|
if commit:
|
||||||
|
ssh_key.save()
|
||||||
|
return ssh_key
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = UserSSHKey
|
||||||
|
fields = ('keyname', 'keypublic')
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
from django.conf import settings
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.core.validators import MinValueValidator
|
from django.core.validators import MinValueValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from instances.models import Instance
|
from instances.models import Instance
|
||||||
|
|
||||||
|
|
||||||
|
@ -11,6 +9,7 @@ class UserInstanceManager(models.Manager):
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return super().get_queryset().select_related('instance', 'user')
|
return super().get_queryset().select_related('instance', 'user')
|
||||||
|
|
||||||
|
|
||||||
class UserInstance(models.Model):
|
class UserInstance(models.Model):
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
instance = models.ForeignKey(Instance, on_delete=models.CASCADE)
|
instance = models.ForeignKey(Instance, on_delete=models.CASCADE)
|
||||||
|
|
|
@ -5,20 +5,15 @@
|
||||||
{% load qr_code %}
|
{% load qr_code %}
|
||||||
|
|
||||||
{% block title %}{% trans "User Profile" %} - {{ user }}{% endblock %}
|
{% block title %}{% trans "User Profile" %} - {{ user }}{% endblock %}
|
||||||
|
{% block page_header %}{% trans "User Profile" %}: {{ user }}{% endblock page_header %}
|
||||||
|
|
||||||
|
{% block page_header_extra %}
|
||||||
|
<a href="{% url 'accounts:user_instance_create' user.id %}" class="btn btn-success">
|
||||||
|
{% icon 'plus' %}
|
||||||
|
</a>
|
||||||
|
{% endblock page_header_extra %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<!-- Page Heading -->
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-12">
|
|
||||||
<a href="{% url 'user_instance_create' user.id %}" class="btn btn-success btn-header float-right">
|
|
||||||
{% icon 'plus' %}
|
|
||||||
</a>
|
|
||||||
<h2 class="page-header">{% trans "User Profile" %}: {{ user }}</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- /.row -->
|
|
||||||
|
|
||||||
{% include 'errors_block.html' %}
|
|
||||||
|
|
||||||
<ul class="nav nav-tabs">
|
<ul class="nav nav-tabs">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link active" data-toggle="tab" href="#instances">{% trans "Instances" %}</a>
|
<a class="nav-link active" data-toggle="tab" href="#instances">{% trans "Instances" %}</a>
|
||||||
|
@ -55,12 +50,12 @@
|
||||||
<td>{{ inst.is_change }}</td>
|
<td>{{ inst.is_change }}</td>
|
||||||
<td>{{ inst.is_delete }}</td>
|
<td>{{ inst.is_delete }}</td>
|
||||||
<td style="width:5px;">
|
<td style="width:5px;">
|
||||||
<a href="{% url 'user_instance_update' inst.id %}" class="btn btn-sm btn-secondary" title="{% trans "edit" %}">
|
<a href="{% url 'accounts:user_instance_update' inst.id %}" class="btn btn-sm btn-secondary" title="{% trans "edit" %}">
|
||||||
{% icon 'pencil' %}
|
{% icon 'pencil' %}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td style="width:5px;">
|
<td style="width:5px;">
|
||||||
<a class="btn btn-sm btn-secondary" href="{% url 'user_instance_delete' inst.id %}" title="{% trans "Delete" %}">
|
<a class="btn btn-sm btn-secondary" href="{% url 'accounts:user_instance_delete' inst.id %}" title="{% trans "Delete" %}">
|
||||||
{% icon 'trash' %}
|
{% icon 'trash' %}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -41,7 +41,7 @@
|
||||||
{% for user in users %}
|
{% for user in users %}
|
||||||
<tr class="{% if not user.is_active %}danger{% endif %}">
|
<tr class="{% if not user.is_active %}danger{% endif %}">
|
||||||
<td>
|
<td>
|
||||||
<a href="{% url 'account' user.id %}"><strong>{{ user.username }}</strong></a>
|
<a href="{% url 'accounts:account' user.id %}"><strong>{{ user.username }}</strong></a>
|
||||||
<a data-toggle="modal" href="#editUser{{ user.id }}" class="float-right" title="{% trans "Edit" %}">
|
<a data-toggle="modal" href="#editUser{{ user.id }}" class="float-right" title="{% trans "Edit" %}">
|
||||||
<span class="fa fa-cog"></span>
|
<span class="fa fa-cog"></span>
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -32,7 +32,7 @@
|
||||||
<div class="card-header bg-secondary">
|
<div class="card-header bg-secondary">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<h5 class="my-0 card-title">
|
<h5 class="my-0 card-title">
|
||||||
<a class="card-link text-light" href="{% url 'account' user.id %}"><strong>{{ user.username }}</strong></a>
|
<a class="card-link text-light" href="{% url 'accounts:account' user.id %}"><strong>{{ user.username }}</strong></a>
|
||||||
<a class="card-link text-light float-right" data-toggle="modal" href="#editUser{{ user.id }}" title="{% trans "Edit" %}">
|
<a class="card-link text-light float-right" data-toggle="modal" href="#editUser{{ user.id }}" title="{% trans "Edit" %}">
|
||||||
<span class="fa fa-cog"></span>
|
<span class="fa fa-cog"></span>
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -7,23 +7,28 @@
|
||||||
{% block title %}{%trans "Change Password" %}{% endblock title %}
|
{% block title %}{%trans "Change Password" %}{% endblock title %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="card">
|
<div class="row">
|
||||||
<div class="card-header">
|
<div class="offset-2 col-lg-8">
|
||||||
<h4 class="card-title">{%trans "Change Password" %}: {{ user }}</h4>
|
<div class="card">
|
||||||
</div>
|
<div class="card-header">
|
||||||
<div class="card-body">
|
<h4 class="card-title">{%trans "Change Password" %}: {{ user }}</h4>
|
||||||
<form method="post" id="password-change">
|
</div>
|
||||||
{% csrf_token %}
|
<div class="card-body">
|
||||||
{% bootstrap_form form layout='horizontal' %}
|
<form method="post" id="password-change">
|
||||||
</form>
|
{% csrf_token %}
|
||||||
</div>
|
{% bootstrap_form form layout='horizontal' %}
|
||||||
<div class="card-footer">
|
</form>
|
||||||
<div class="float-right">
|
</div>
|
||||||
<a class="btn btn-primary" href="javascript:history.back()">{% icon 'times' %} {% trans "Cancel" %}</a>
|
<div class="card-footer">
|
||||||
<button type="submit" form="password-change" class="btn btn-success">
|
<div class="float-right">
|
||||||
{% icon 'check' %} {% trans "Change" %}
|
<a class="btn btn-primary" href="javascript:history.back()">{% icon 'times' %}
|
||||||
</button>
|
{% trans "Cancel" %}</a>
|
||||||
|
<button type="submit" form="password-change" class="btn btn-success">
|
||||||
|
{% icon 'check' %} {% trans "Change" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
|
@ -1,93 +1,80 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
{% load bootstrap4 %}
|
||||||
{% load icons %}
|
{% load icons %}
|
||||||
{% load tags_fingerprint %}
|
{% load tags_fingerprint %}
|
||||||
{% block title %}{% trans "Profile" %}{% endblock %}
|
|
||||||
|
{% block title %}{% trans "Profile" %}: {{ request.user.first_name }} {{ request.user.last_name}}{% endblock %}
|
||||||
|
|
||||||
|
{% block page_header %}{% trans "Profile" %}: {{ request.user.first_name }} {{ request.user.last_name}}{% endblock page_header %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<!-- Page Heading -->
|
<ul class="nav nav-tabs">
|
||||||
<div class="row">
|
<li class="nav-item">
|
||||||
<div class="col-lg-12">
|
<a class="nav-link active" data-toggle="tab" href="#edit-profile">{% trans "Edit Profile" %}</a>
|
||||||
<h2 class="page-header">{% trans "Profile" %}</h2>
|
</li>
|
||||||
</div>
|
<li class="nav-item">
|
||||||
</div>
|
<a class="nav-link" data-toggle="tab" href="#ssh-keys">{% trans "SSH Keys" %}</a>
|
||||||
<!-- /.row -->
|
</li>
|
||||||
|
</ul>
|
||||||
{% include 'errors_block.html' %}
|
<div class="tab-content">
|
||||||
|
<div class="tab-pane tab-pane-bordered active" id="edit-profile">
|
||||||
<div class="row">
|
<div class="card">
|
||||||
<div class="col-lg-12">
|
<div class="card-body">
|
||||||
<h3 class="page-header">{% trans "Edit Profile" %}</h3>
|
<form method="post" action="" role="form" aria-label="Edit user info form">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% bootstrap_form profile_form layout='horizontal' %}
|
||||||
{% if perms.accounts.change_password %}
|
{% if perms.accounts.change_password %}
|
||||||
<a href="{% url 'change_password' %}" class="ml-3 btn btn-primary">{% icon 'lock' %} {% trans "Change Password" %}</a>
|
<a href="{% url 'accounts:change_password' %}" class="btn btn-primary">
|
||||||
|
{% icon 'lock' %} {% trans "Change Password" %}
|
||||||
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<form method="post" action="" role="form" aria-label="Edit user info form">{% csrf_token %}
|
<div class="form-group mb-0 float-right">
|
||||||
<div class="form-group">
|
<button type="submit" class="btn btn-primary">
|
||||||
<label class="col-sm-2 col-form-label">{% trans "Login" %}</label>
|
{% icon 'pencil' %} {% trans "Update" %}
|
||||||
<div class="col-sm-4">
|
</button>
|
||||||
<input type="text" class="form-control" value="{{ request.user.username }}" disabled>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
</div>
|
|
||||||
<div class="form-group bridge_name_form_group_dhcp">
|
|
||||||
<label class="col-sm-2 col-form-label">{% trans "Username" %}</label>
|
|
||||||
<div class="col-sm-4">
|
|
||||||
<input type="text" class="form-control" name="username" value="{{ request.user.first_name }}" pattern="[0-9a-zA-Z]+">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group bridge_name_form_group_dhcp">
|
|
||||||
<label class="col-sm-2 col-form-label">{% trans "Email" %}</label>
|
|
||||||
<div class="col-sm-4">
|
|
||||||
<input type="email" class="form-control" name="email" value="{{ request.user.email }}">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="col-sm-offset-2 col-sm-10">
|
|
||||||
<button type="submit" class="btn btn-primary">{% trans "Change" %}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<h3 class="page-header">{% trans "SSH Keys" %}</h3>
|
|
||||||
{% if publickeys %}
|
|
||||||
<div class="col-lg-12">
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-hover">
|
|
||||||
<tbody class="text-center">
|
|
||||||
{% for key in publickeys %}
|
|
||||||
<tr>
|
|
||||||
<td>{{ key.keyname }} ({% ssh_to_fingerprint key.keypublic %})</td>
|
|
||||||
<td>
|
|
||||||
<form action="" method="post" role="form" aria-label="Delete user public key form">{% csrf_token %}
|
|
||||||
<input type="hidden" name="keyid" value="{{ key.id }}"/>
|
|
||||||
<button type="submit" class="btn btn-sm btn-secondary" name="keydelete" title="{% trans "Delete" %}" onclick="return confirm('{% trans "Are you sure?" %}')">
|
|
||||||
<span class="fa fa-trash"></span>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
<form method="post" action="" role="form" aria-label="Add key to user form">{% csrf_token %}
|
|
||||||
<div class="form-group bridge_name_form_group_dhcp">
|
|
||||||
<label class="col-sm-2 col-form-label">{% trans "Key name" %}</label>
|
|
||||||
<div class="col-sm-4">
|
|
||||||
<input type="text" class="form-control" name="keyname" placeholder="{% trans "Enter Name" %}">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group bridge_name_form_group_dhcp">
|
|
||||||
<label class="col-sm-2 col-form-label">{% trans "Public key" %}</label>
|
|
||||||
<div class="col-sm-8">
|
|
||||||
<textarea name="keypublic" class="form-control" rows="6" placeholder="{% trans "Enter Public Key" %}"></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="col-sm-10">
|
|
||||||
<button type="submit" class="btn btn-primary">{% trans "Add" %}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tab-pane tab-pane-bordered fade" id="ssh-keys">
|
||||||
|
{% if publickeys %}
|
||||||
|
<div class="col-lg-12">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<tbody class="text-center">
|
||||||
|
{% for key in publickeys %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ key.keyname }} ({% ssh_to_fingerprint key.keypublic %})</td>
|
||||||
|
<td>
|
||||||
|
<a href="{% url 'accounts:ssh_key_delete' key.id %}" title="{% trans "Delete" %}" class="btn btn-sm btn-secondary">
|
||||||
|
{% icon 'trash' %}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
{%trans "Add SSH Key" %}
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="post" action="{% url 'accounts:ssh_key_create' %}" role="form" aria-label="Add key to user form">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% bootstrap_form ssh_key_form layout='horizontal' %}
|
||||||
|
<div class="form-group mb-0 float-right">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
{% icon 'plus' %} {% trans "Add" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
|
@ -1,45 +1,111 @@
|
||||||
from django.contrib.auth.models import Permission, User
|
from appsettings.settings import app_settings
|
||||||
|
from computes.models import Compute
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.contrib.auth.models import Permission
|
||||||
from django.shortcuts import reverse
|
from django.shortcuts import reverse
|
||||||
from django.test import Client, TestCase
|
from django.test import Client, TestCase
|
||||||
|
from instances.models import Instance
|
||||||
|
from instances.utils import refr
|
||||||
|
from libvirt import VIR_DOMAIN_UNDEFINE_NVRAM
|
||||||
|
from vrtManager.create import wvmCreate
|
||||||
|
|
||||||
|
from accounts.forms import UserInstanceForm, UserSSHKeyForm
|
||||||
|
from accounts.models import UserInstance, UserSSHKey
|
||||||
|
from accounts.utils import validate_ssh_key
|
||||||
|
|
||||||
|
|
||||||
class AccountsTestCase(TestCase):
|
class AccountsTestCase(TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super().setUpClass()
|
||||||
|
|
||||||
|
# Add users for testing purposes
|
||||||
|
User = get_user_model()
|
||||||
|
cls.admin_user = User.objects.get(pk=1)
|
||||||
|
cls.test_user = User.objects.create_user(username='test', password='test')
|
||||||
|
|
||||||
|
# Add localhost compute
|
||||||
|
cls.compute = Compute(
|
||||||
|
name='test-compute',
|
||||||
|
hostname='localhost',
|
||||||
|
login='',
|
||||||
|
password='',
|
||||||
|
details='local',
|
||||||
|
type=4,
|
||||||
|
)
|
||||||
|
cls.compute.save()
|
||||||
|
|
||||||
|
cls.connection = wvmCreate(
|
||||||
|
cls.compute.hostname,
|
||||||
|
cls.compute.login,
|
||||||
|
cls.compute.password,
|
||||||
|
cls.compute.type,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add disks for testing
|
||||||
|
cls.connection.create_volume(
|
||||||
|
'default',
|
||||||
|
'test-volume',
|
||||||
|
1,
|
||||||
|
'qcow2',
|
||||||
|
False,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
|
||||||
|
# XML for testing vm
|
||||||
|
with open('conf/test-vm.xml', 'r') as f:
|
||||||
|
cls.xml = f.read()
|
||||||
|
|
||||||
|
# Create testing vm from XML
|
||||||
|
cls.connection._defineXML(cls.xml)
|
||||||
|
refr(cls.compute)
|
||||||
|
cls.instance = Instance.objects.get(pk=1)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
# Destroy testing vm
|
||||||
|
cls.instance.proxy.delete_all_disks()
|
||||||
|
cls.instance.proxy.delete(VIR_DOMAIN_UNDEFINE_NVRAM)
|
||||||
|
super().tearDownClass()
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.client.login(username='admin', password='admin')
|
self.client.login(username='admin', password='admin')
|
||||||
user = User.objects.create_user(username='test', password='test')
|
|
||||||
permission = Permission.objects.get(codename='change_password')
|
permission = Permission.objects.get(codename='change_password')
|
||||||
user.user_permissions.add(permission)
|
self.test_user.user_permissions.add(permission)
|
||||||
|
self.rsa_key = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC6OOdbfv27QVnSC6sKxGaHb6YFc+3gxCkyVR3cTSXE/n5BEGf8aOgBpepULWa1RZfxYHY14PlKULDygdXSdrrR2kNSwoKz/Oo4d+3EE92L7ocl1+djZbptzgWgtw1OseLwbFik+iKlIdqPsH+IUQvX7yV545ZQtAP8Qj1R+uCqkw== test@test'
|
||||||
|
self.ecdsa_key = 'ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJc5xpT3R0iFJYNZbmWgAiDlHquX/BcV1kVTsnBfiMsZgU3lGaqz2eb7IBcir/dxGnsVENTTmPQ6sNcxLxT9kkQ= realgecko@archlinux'
|
||||||
|
|
||||||
def test_profile(self):
|
def test_profile(self):
|
||||||
response = self.client.get(reverse('profile'))
|
response = self.client.get(reverse('accounts:profile'))
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
response = self.client.get(reverse('account', args=[2]))
|
response = self.client.get(reverse('accounts:account', args=[self.test_user.id]))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_account_with_otp(self):
|
||||||
|
settings.OTP_ENABLED = True
|
||||||
|
response = self.client.get(reverse('accounts:account', args=[self.test_user.id]))
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
def test_login_logout(self):
|
def test_login_logout(self):
|
||||||
user = User.objects.get(username='test')
|
|
||||||
self.assertEqual(user.id, 2)
|
|
||||||
|
|
||||||
client = Client()
|
client = Client()
|
||||||
|
|
||||||
response = client.post(reverse("login"), {"username": "test", "password": "test"})
|
response = client.post(reverse("accounts:login"), {"username": "test", "password": "test"})
|
||||||
self.assertRedirects(response, reverse('profile'))
|
self.assertRedirects(response, reverse('accounts:profile'))
|
||||||
|
|
||||||
response = client.get(reverse('logout'))
|
response = client.get(reverse('accounts:logout'))
|
||||||
self.assertRedirects(response, reverse('login'))
|
self.assertRedirects(response, reverse('accounts:login'))
|
||||||
|
|
||||||
def test_password_change(self):
|
def test_change_password(self):
|
||||||
client = Client()
|
self.client.force_login(self.test_user)
|
||||||
|
|
||||||
logged_in = client.login(username='test', password='test')
|
response = self.client.get(reverse('accounts:change_password'))
|
||||||
self.assertTrue(logged_in)
|
|
||||||
|
|
||||||
response = client.get(reverse('change_password'))
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
response = client.post(
|
response = self.client.post(
|
||||||
reverse('change_password'),
|
reverse('accounts:change_password'),
|
||||||
{
|
{
|
||||||
'old_password': 'wrongpass',
|
'old_password': 'wrongpass',
|
||||||
'new_password1': 'newpw',
|
'new_password1': 'newpw',
|
||||||
|
@ -48,17 +114,154 @@ class AccountsTestCase(TestCase):
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
response = client.post(
|
response = self.client.post(
|
||||||
reverse('change_password'),
|
reverse('accounts:change_password'),
|
||||||
{
|
{
|
||||||
'old_password': 'test',
|
'old_password': 'test',
|
||||||
'new_password1': 'newpw',
|
'new_password1': 'newpw',
|
||||||
'new_password2': 'newpw',
|
'new_password2': 'newpw',
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
self.assertRedirects(response, reverse('profile'))
|
self.assertRedirects(response, reverse('accounts:profile'))
|
||||||
|
|
||||||
client.logout()
|
self.client.logout()
|
||||||
|
|
||||||
logged_in = client.login(username='test', password='newpw')
|
logged_in = self.client.login(username='test', password='newpw')
|
||||||
self.assertTrue(logged_in)
|
self.assertTrue(logged_in)
|
||||||
|
|
||||||
|
def test_user_instance_create_update_delete(self):
|
||||||
|
# create
|
||||||
|
response = self.client.get(reverse('accounts:user_instance_create', args=[self.test_user.id]))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse('accounts:user_instance_create', args=[self.test_user.id]),
|
||||||
|
{
|
||||||
|
'user': self.test_user.id,
|
||||||
|
'instance': self.instance.id,
|
||||||
|
'is_change': False,
|
||||||
|
'is_delete': False,
|
||||||
|
'is_vnc': False,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertRedirects(response, reverse('accounts:account', args=[self.test_user.id]))
|
||||||
|
|
||||||
|
user_instance: UserInstance = UserInstance.objects.get(pk=1)
|
||||||
|
self.assertEqual(user_instance.user, self.test_user)
|
||||||
|
self.assertEqual(user_instance.instance, self.instance)
|
||||||
|
self.assertEqual(user_instance.is_change, False)
|
||||||
|
self.assertEqual(user_instance.is_delete, False)
|
||||||
|
self.assertEqual(user_instance.is_vnc, False)
|
||||||
|
|
||||||
|
# update
|
||||||
|
response = self.client.get(reverse('accounts:user_instance_update', args=[user_instance.id]))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse('accounts:user_instance_update', args=[user_instance.id]),
|
||||||
|
{
|
||||||
|
'user': self.test_user.id,
|
||||||
|
'instance': self.instance.id,
|
||||||
|
'is_change': True,
|
||||||
|
'is_delete': True,
|
||||||
|
'is_vnc': True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertRedirects(response, reverse('accounts:account', args=[self.test_user.id]))
|
||||||
|
|
||||||
|
user_instance: UserInstance = UserInstance.objects.get(pk=1)
|
||||||
|
self.assertEqual(user_instance.user, self.test_user)
|
||||||
|
self.assertEqual(user_instance.instance, self.instance)
|
||||||
|
self.assertEqual(user_instance.is_change, True)
|
||||||
|
self.assertEqual(user_instance.is_delete, True)
|
||||||
|
self.assertEqual(user_instance.is_vnc, True)
|
||||||
|
|
||||||
|
# delete
|
||||||
|
response = self.client.get(reverse('accounts:user_instance_delete', args=[user_instance.id]))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
response = self.client.post(reverse('accounts:user_instance_delete', args=[user_instance.id]))
|
||||||
|
self.assertRedirects(response, reverse('accounts:account', args=[self.test_user.id]))
|
||||||
|
|
||||||
|
# test 'next' redirect during deletion
|
||||||
|
user_instance = UserInstance.objects.create(user=self.test_user, instance=self.instance)
|
||||||
|
response = self.client.post(
|
||||||
|
reverse('accounts:user_instance_delete', args=[user_instance.id]) + '?next=' + reverse('index'))
|
||||||
|
self.assertRedirects(response, reverse('index'))
|
||||||
|
|
||||||
|
def test_update_user_profile(self):
|
||||||
|
self.client.force_login(self.test_user)
|
||||||
|
|
||||||
|
user = get_user_model().objects.get(username='test')
|
||||||
|
self.assertEqual(user.first_name, '')
|
||||||
|
self.assertEqual(user.last_name, '')
|
||||||
|
self.assertEqual(user.email, '')
|
||||||
|
|
||||||
|
response = self.client.post(reverse('accounts:profile'), {
|
||||||
|
'first_name': 'first name',
|
||||||
|
'last_name': 'last name',
|
||||||
|
'email': 'email@mail.mail',
|
||||||
|
})
|
||||||
|
self.assertRedirects(response, reverse('accounts:profile'))
|
||||||
|
|
||||||
|
user = get_user_model().objects.get(username='test')
|
||||||
|
self.assertEqual(user.first_name, 'first name')
|
||||||
|
self.assertEqual(user.last_name, 'last name')
|
||||||
|
self.assertEqual(user.email, 'email@mail.mail')
|
||||||
|
|
||||||
|
def test_create_delete_ssh_key(self):
|
||||||
|
response = self.client.get(reverse('accounts:ssh_key_create'))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
response = self.client.post(reverse('accounts:ssh_key_create'), {
|
||||||
|
'keyname': 'keyname',
|
||||||
|
'keypublic': self.rsa_key,
|
||||||
|
})
|
||||||
|
self.assertRedirects(response, reverse('accounts:profile'))
|
||||||
|
|
||||||
|
key = UserSSHKey.objects.get(pk=1)
|
||||||
|
self.assertEqual(key.keyname, 'keyname')
|
||||||
|
self.assertEqual(key.keypublic, self.rsa_key)
|
||||||
|
|
||||||
|
response = self.client.get(reverse('accounts:ssh_key_delete', args=[1]))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
response = self.client.post(reverse('accounts:ssh_key_delete', args=[1]))
|
||||||
|
self.assertRedirects(response, reverse('accounts:profile'))
|
||||||
|
|
||||||
|
def test_validate_ssh_key(self):
|
||||||
|
self.assertFalse(validate_ssh_key(''))
|
||||||
|
self.assertFalse(validate_ssh_key('ssh-rsa ABBA test@test'))
|
||||||
|
self.assertFalse(validate_ssh_key('ssh-rsa AAAABwdzZGY= test@test'))
|
||||||
|
self.assertFalse(validate_ssh_key('ssh-rsa AAA test@test'))
|
||||||
|
# validate ecdsa key
|
||||||
|
self.assertTrue(validate_ssh_key(self.ecdsa_key))
|
||||||
|
|
||||||
|
def test_forms(self):
|
||||||
|
# raise available validation errors for maximum coverage
|
||||||
|
form = UserSSHKeyForm({'keyname': 'keyname', 'keypublic': self.rsa_key}, user=self.test_user)
|
||||||
|
form.save()
|
||||||
|
|
||||||
|
form = UserSSHKeyForm({'keyname': 'keyname', 'keypublic': self.rsa_key}, user=self.test_user)
|
||||||
|
self.assertFalse(form.is_valid())
|
||||||
|
|
||||||
|
form = UserSSHKeyForm({'keyname': 'keyname', 'keypublic': 'invalid key'}, user=self.test_user)
|
||||||
|
self.assertFalse(form.is_valid())
|
||||||
|
|
||||||
|
app_settings.ALLOW_INSTANCE_MULTIPLE_OWNER = 'False'
|
||||||
|
form = UserInstanceForm({
|
||||||
|
'user': self.admin_user.id,
|
||||||
|
'instance': self.instance.id,
|
||||||
|
'is_change': False,
|
||||||
|
'is_delete': False,
|
||||||
|
'is_vnc': False,
|
||||||
|
})
|
||||||
|
form.save()
|
||||||
|
form = UserInstanceForm({
|
||||||
|
'user': self.test_user.id,
|
||||||
|
'instance': self.instance.id,
|
||||||
|
'is_change': False,
|
||||||
|
'is_delete': False,
|
||||||
|
'is_vnc': False,
|
||||||
|
})
|
||||||
|
self.assertFalse(form.is_valid())
|
||||||
|
|
|
@ -5,6 +5,8 @@ from django_otp.forms import OTPAuthenticationForm
|
||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
|
app_name = 'accounts'
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('logout/', LogoutView.as_view(template_name='logout.html'), name='logout'),
|
path('logout/', LogoutView.as_view(template_name='logout.html'), name='logout'),
|
||||||
path('profile/', views.profile, name='profile'),
|
path('profile/', views.profile, name='profile'),
|
||||||
|
@ -13,6 +15,8 @@ urlpatterns = [
|
||||||
path('user_instance/create/<int:user_id>/', views.user_instance_create, name='user_instance_create'),
|
path('user_instance/create/<int:user_id>/', views.user_instance_create, name='user_instance_create'),
|
||||||
path('user_instance/<int:pk>/update/', views.user_instance_update, name='user_instance_update'),
|
path('user_instance/<int:pk>/update/', views.user_instance_update, name='user_instance_update'),
|
||||||
path('user_instance/<int:pk>/delete/', views.user_instance_delete, name='user_instance_delete'),
|
path('user_instance/<int:pk>/delete/', views.user_instance_delete, name='user_instance_delete'),
|
||||||
|
path('ssh_key/create/', views.ssh_key_create, name='ssh_key_create'),
|
||||||
|
path('ssh_key/<int:pk>/delete/', views.ssh_key_delete, name='ssh_key_delete'),
|
||||||
]
|
]
|
||||||
|
|
||||||
if settings.OTP_ENABLED:
|
if settings.OTP_ENABLED:
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
import base64
|
||||||
|
import binascii
|
||||||
|
import struct
|
||||||
|
|
||||||
from django_otp import devices_for_user
|
from django_otp import devices_for_user
|
||||||
from django_otp.plugins.otp_totp.models import TOTPDevice
|
from django_otp.plugins.otp_totp.models import TOTPDevice
|
||||||
|
|
||||||
|
@ -7,3 +11,29 @@ def get_user_totp_device(user):
|
||||||
for device in devices:
|
for device in devices:
|
||||||
if isinstance(device, TOTPDevice):
|
if isinstance(device, TOTPDevice):
|
||||||
return device
|
return device
|
||||||
|
|
||||||
|
|
||||||
|
def validate_ssh_key(key):
|
||||||
|
array = key.encode().split()
|
||||||
|
# Each rsa-ssh key has 3 different strings in it, first one being
|
||||||
|
# typeofkey second one being keystring third one being username .
|
||||||
|
if len(array) != 3:
|
||||||
|
return False
|
||||||
|
typeofkey = array[0]
|
||||||
|
string = array[1]
|
||||||
|
username = array[2]
|
||||||
|
# must have only valid rsa-ssh key characters ie binascii characters
|
||||||
|
try:
|
||||||
|
data = base64.decodestring(string)
|
||||||
|
except binascii.Error:
|
||||||
|
return False
|
||||||
|
# unpack the contents of data, from data[:4] , property of ssh key .
|
||||||
|
try:
|
||||||
|
str_len = struct.unpack('>I', data[:4])[0]
|
||||||
|
except struct.error:
|
||||||
|
return False
|
||||||
|
# data[4:str_len] must have string which matches with the typeofkey, another ssh key property.
|
||||||
|
if data[4:4 + str_len] == typeofkey:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
from admin.decorators import superuser_only
|
from admin.decorators import superuser_only
|
||||||
|
from django.conf import settings
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth import update_session_auth_hash
|
from django.contrib.auth import update_session_auth_hash
|
||||||
from django.contrib.auth.decorators import permission_required
|
from django.contrib.auth.decorators import permission_required
|
||||||
from django.contrib.auth.forms import PasswordChangeForm
|
from django.contrib.auth.forms import PasswordChangeForm
|
||||||
from django.http import HttpResponseRedirect
|
|
||||||
from django.shortcuts import get_object_or_404, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from instances.models import Instance
|
from instances.models import Instance
|
||||||
|
|
||||||
|
from accounts.forms import ProfileForm, UserSSHKeyForm
|
||||||
from accounts.models import *
|
from accounts.models import *
|
||||||
|
|
||||||
from . import forms
|
from . import forms
|
||||||
|
@ -16,49 +17,50 @@ from .utils import get_user_totp_device
|
||||||
|
|
||||||
|
|
||||||
def profile(request):
|
def profile(request):
|
||||||
error_messages = []
|
|
||||||
publickeys = UserSSHKey.objects.filter(user_id=request.user.id)
|
publickeys = UserSSHKey.objects.filter(user_id=request.user.id)
|
||||||
|
profile_form = ProfileForm(request.POST or None, instance=request.user)
|
||||||
|
ssh_key_form = UserSSHKeyForm()
|
||||||
|
|
||||||
if request.method == "POST":
|
if profile_form.is_valid():
|
||||||
if "username" in request.POST:
|
profile_form.save()
|
||||||
username = request.POST.get("username", "")
|
messages.success(request, _('Profile updated'))
|
||||||
email = request.POST.get("email", "")
|
return redirect('accounts:profile')
|
||||||
request.user.first_name = username
|
|
||||||
request.user.email = email
|
return render(request, "profile.html", {
|
||||||
request.user.save()
|
'publickeys': publickeys,
|
||||||
return HttpResponseRedirect(request.get_full_path())
|
'profile_form': profile_form,
|
||||||
if "keyname" in request.POST:
|
'ssh_key_form': ssh_key_form,
|
||||||
keyname = request.POST.get("keyname", "")
|
})
|
||||||
keypublic = request.POST.get("keypublic", "")
|
|
||||||
for key in publickeys:
|
|
||||||
if keyname == key.keyname:
|
def ssh_key_create(request):
|
||||||
msg = _("Key name already exist")
|
key_form = UserSSHKeyForm(request.POST or None, user=request.user)
|
||||||
error_messages.append(msg)
|
if key_form.is_valid():
|
||||||
if keypublic == key.keypublic:
|
key_form.save()
|
||||||
msg = _("Public key already exist")
|
messages.success(request, _('SSH key added'))
|
||||||
error_messages.append(msg)
|
return redirect('accounts:profile')
|
||||||
if "\n" in keypublic or "\r" in keypublic:
|
|
||||||
msg = _("Invalid characters in public key")
|
return render(request, 'common/form.html', {
|
||||||
error_messages.append(msg)
|
'form': key_form,
|
||||||
if not error_messages:
|
'title': _('Add SSH key'),
|
||||||
addkeypublic = UserSSHKey(
|
})
|
||||||
user_id=request.user.id,
|
|
||||||
keyname=keyname,
|
|
||||||
keypublic=keypublic,
|
def ssh_key_delete(request, pk):
|
||||||
)
|
ssh_key = get_object_or_404(UserSSHKey, pk=pk, user=request.user)
|
||||||
addkeypublic.save()
|
if request.method == 'POST':
|
||||||
return HttpResponseRedirect(request.get_full_path())
|
ssh_key.delete()
|
||||||
if "keydelete" in request.POST:
|
messages.success(request, _('SSH key deleted'))
|
||||||
keyid = request.POST.get("keyid", "")
|
return redirect('accounts:profile')
|
||||||
delkeypublic = UserSSHKey.objects.get(id=keyid)
|
|
||||||
delkeypublic.delete()
|
return render(request, 'common/confirm_delete.html', {
|
||||||
return HttpResponseRedirect(request.get_full_path())
|
'object': ssh_key,
|
||||||
return render(request, "profile.html", locals())
|
'title': _('Delete SSH key'),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
@superuser_only
|
@superuser_only
|
||||||
def account(request, user_id):
|
def account(request, user_id):
|
||||||
error_messages = []
|
|
||||||
user = User.objects.get(id=user_id)
|
user = User.objects.get(id=user_id)
|
||||||
user_insts = UserInstance.objects.filter(user_id=user_id)
|
user_insts = UserInstance.objects.filter(user_id=user_id)
|
||||||
instances = Instance.objects.all().order_by("name")
|
instances = Instance.objects.all().order_by("name")
|
||||||
|
@ -74,17 +76,14 @@ def account(request, user_id):
|
||||||
|
|
||||||
@permission_required("accounts.change_password", raise_exception=True)
|
@permission_required("accounts.change_password", raise_exception=True)
|
||||||
def change_password(request):
|
def change_password(request):
|
||||||
if request.method == "POST":
|
form = PasswordChangeForm(request.user, request.POST or None)
|
||||||
form = PasswordChangeForm(request.user, request.POST)
|
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
user = form.save()
|
user = form.save()
|
||||||
update_session_auth_hash(request, user) # Important!
|
update_session_auth_hash(request, user) # Important!
|
||||||
messages.success(request, _("Password Changed"))
|
messages.success(request, _("Password Changed"))
|
||||||
return redirect("profile")
|
return redirect("accounts:profile")
|
||||||
else:
|
|
||||||
messages.error(request, _("Wrong Data Provided"))
|
|
||||||
else:
|
|
||||||
form = PasswordChangeForm(request.user)
|
|
||||||
return render(request, "accounts/change_password_form.html", {"form": form})
|
return render(request, "accounts/change_password_form.html", {"form": form})
|
||||||
|
|
||||||
|
|
||||||
|
@ -95,7 +94,7 @@ def user_instance_create(request, user_id):
|
||||||
form = forms.UserInstanceForm(request.POST or None, initial={"user": user})
|
form = forms.UserInstanceForm(request.POST or None, initial={"user": user})
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
form.save()
|
form.save()
|
||||||
return redirect(reverse("account", args=[user.id]))
|
return redirect(reverse("accounts:account", args=[user.id]))
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
|
@ -113,7 +112,7 @@ def user_instance_update(request, pk):
|
||||||
form = forms.UserInstanceForm(request.POST or None, instance=user_instance)
|
form = forms.UserInstanceForm(request.POST or None, instance=user_instance)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
form.save()
|
form.save()
|
||||||
return redirect(reverse("account", args=[user_instance.user.id]))
|
return redirect(reverse("accounts:account", args=[user_instance.user.id]))
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
|
@ -135,7 +134,7 @@ def user_instance_delete(request, pk):
|
||||||
if next:
|
if next:
|
||||||
return redirect(next)
|
return redirect(next)
|
||||||
else:
|
else:
|
||||||
return redirect(reverse("account", args=[user.id]))
|
return redirect(reverse("accounts:account", args=[user.id]))
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
|
|
|
@ -57,7 +57,7 @@
|
||||||
<td>{% if can_clone %}{% icon 'check' %}{% endif %}</td>
|
<td>{% if can_clone %}{% icon 'check' %}{% endif %}</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="float-right btn-group">
|
<div class="float-right btn-group">
|
||||||
<a class="btn btn-success" title="{%trans "View Profile" %}" href="{% url 'account' user.id %}">{% icon 'eye' %}</a>
|
<a class="btn btn-success" title="{%trans "View Profile" %}" href="{% url 'accounts:account' user.id %}">{% icon 'eye' %}</a>
|
||||||
<a class="btn btn-primary" title="{%trans "Edit" %}" href="{% url 'admin:user_update' user.id %}">{% icon 'pencil' %}</a>
|
<a class="btn btn-primary" title="{%trans "Edit" %}" href="{% url 'admin:user_update' user.id %}">{% icon 'pencil' %}</a>
|
||||||
{% if user.is_active %}
|
{% if user.is_active %}
|
||||||
<a class="btn btn-warning" title="{%trans "Block" %}" href="{% url 'admin:user_block' user.id %}">{% icon 'stop' %}</a>
|
<a class="btn btn-warning" title="{%trans "Block" %}" href="{% url 'admin:user_block' user.id %}">{% icon 'stop' %}</a>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from .models import AppSettings
|
from .models import AppSettings
|
||||||
|
|
||||||
|
|
||||||
class Settings(object):
|
class Settings:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
120
conf/test-vm.xml
Normal file
120
conf/test-vm.xml
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
<domain type="kvm">
|
||||||
|
<name>test-vm</name>
|
||||||
|
<uuid>1bd3c1f2-dd12-4b8d-a298-dff387cb9f93</uuid>
|
||||||
|
<description>None</description>
|
||||||
|
<memory unit="KiB">131072</memory>
|
||||||
|
<currentMemory unit="KiB">131072</currentMemory>
|
||||||
|
<vcpu placement="static">1</vcpu>
|
||||||
|
<os>
|
||||||
|
<type arch="x86_64" machine="pc-q35-5.1">hvm</type>
|
||||||
|
<boot dev="hd" />
|
||||||
|
<boot dev="cdrom" />
|
||||||
|
<bootmenu enable="yes" />
|
||||||
|
</os>
|
||||||
|
<features>
|
||||||
|
<acpi />
|
||||||
|
<apic />
|
||||||
|
</features>
|
||||||
|
<cpu mode="host-model" check="partial" />
|
||||||
|
<clock offset="utc" />
|
||||||
|
<on_poweroff>destroy</on_poweroff>
|
||||||
|
<on_reboot>restart</on_reboot>
|
||||||
|
<on_crash>restart</on_crash>
|
||||||
|
<devices>
|
||||||
|
<emulator>/usr/bin/qemu-system-x86_64</emulator>
|
||||||
|
<disk type="file" device="disk">
|
||||||
|
<driver name="qemu" type="qcow2" cache="directsync" />
|
||||||
|
<source file="/var/lib/libvirt/images/test-volume.qcow2" />
|
||||||
|
<target dev="vda" bus="virtio" />
|
||||||
|
<address type="pci" domain="0x0000" bus="0x03" slot="0x00" function="0x0" />
|
||||||
|
</disk>
|
||||||
|
<disk type="file" device="cdrom">
|
||||||
|
<driver name="qemu" type="raw" />
|
||||||
|
<target dev="sda" bus="sata" />
|
||||||
|
<readonly />
|
||||||
|
<address type="drive" controller="0" bus="0" target="0" unit="0" />
|
||||||
|
</disk>
|
||||||
|
<controller type="usb" index="0" model="qemu-xhci">
|
||||||
|
<address type="pci" domain="0x0000" bus="0x02" slot="0x00" function="0x0" />
|
||||||
|
</controller>
|
||||||
|
<controller type="sata" index="0">
|
||||||
|
<address type="pci" domain="0x0000" bus="0x00" slot="0x1f" function="0x2" />
|
||||||
|
</controller>
|
||||||
|
<controller type="pci" index="0" model="pcie-root" />
|
||||||
|
<controller type="pci" index="1" model="pcie-root-port">
|
||||||
|
<model name="pcie-root-port" />
|
||||||
|
<target chassis="1" port="0x10" />
|
||||||
|
<address type="pci" domain="0x0000" bus="0x00" slot="0x02" function="0x0" multifunction="on" />
|
||||||
|
</controller>
|
||||||
|
<controller type="pci" index="2" model="pcie-root-port">
|
||||||
|
<model name="pcie-root-port" />
|
||||||
|
<target chassis="2" port="0x11" />
|
||||||
|
<address type="pci" domain="0x0000" bus="0x00" slot="0x02" function="0x1" />
|
||||||
|
</controller>
|
||||||
|
<controller type="pci" index="3" model="pcie-root-port">
|
||||||
|
<model name="pcie-root-port" />
|
||||||
|
<target chassis="3" port="0x12" />
|
||||||
|
<address type="pci" domain="0x0000" bus="0x00" slot="0x02" function="0x2" />
|
||||||
|
</controller>
|
||||||
|
<controller type="pci" index="4" model="pcie-root-port">
|
||||||
|
<model name="pcie-root-port" />
|
||||||
|
<target chassis="4" port="0x13" />
|
||||||
|
<address type="pci" domain="0x0000" bus="0x00" slot="0x02" function="0x3" />
|
||||||
|
</controller>
|
||||||
|
<controller type="pci" index="5" model="pcie-root-port">
|
||||||
|
<model name="pcie-root-port" />
|
||||||
|
<target chassis="5" port="0x14" />
|
||||||
|
<address type="pci" domain="0x0000" bus="0x00" slot="0x02" function="0x4" />
|
||||||
|
</controller>
|
||||||
|
<controller type="pci" index="6" model="pcie-root-port">
|
||||||
|
<model name="pcie-root-port" />
|
||||||
|
<target chassis="6" port="0x15" />
|
||||||
|
<address type="pci" domain="0x0000" bus="0x00" slot="0x02" function="0x5" />
|
||||||
|
</controller>
|
||||||
|
<controller type="pci" index="7" model="pcie-root-port">
|
||||||
|
<model name="pcie-root-port" />
|
||||||
|
<target chassis="7" port="0x16" />
|
||||||
|
<address type="pci" domain="0x0000" bus="0x00" slot="0x02" function="0x6" />
|
||||||
|
</controller>
|
||||||
|
<controller type="pci" index="8" model="pcie-root-port">
|
||||||
|
<model name="pcie-root-port" />
|
||||||
|
<target chassis="8" port="0x17" />
|
||||||
|
<address type="pci" domain="0x0000" bus="0x00" slot="0x02" function="0x7" />
|
||||||
|
</controller>
|
||||||
|
<interface type="network">
|
||||||
|
<mac address="52:54:00:a2:3c:e7" />
|
||||||
|
<source network="default" />
|
||||||
|
<model type="virtio" />
|
||||||
|
<address type="pci" domain="0x0000" bus="0x01" slot="0x00" function="0x0" />
|
||||||
|
</interface>
|
||||||
|
<serial type="pty">
|
||||||
|
<target type="isa-serial" port="0">
|
||||||
|
<model name="isa-serial" />
|
||||||
|
</target>
|
||||||
|
</serial>
|
||||||
|
<console type="pty">
|
||||||
|
<target type="serial" port="0" />
|
||||||
|
</console>
|
||||||
|
<input type="mouse" bus="virtio">
|
||||||
|
<address type="pci" domain="0x0000" bus="0x05" slot="0x00" function="0x0" />
|
||||||
|
</input>
|
||||||
|
<input type="keyboard" bus="virtio">
|
||||||
|
<address type="pci" domain="0x0000" bus="0x06" slot="0x00" function="0x0" />
|
||||||
|
</input>
|
||||||
|
<input type="tablet" bus="virtio">
|
||||||
|
<address type="pci" domain="0x0000" bus="0x07" slot="0x00" function="0x0" />
|
||||||
|
</input>
|
||||||
|
<input type="mouse" bus="ps2" />
|
||||||
|
<input type="keyboard" bus="ps2" />
|
||||||
|
<graphics type="spice" autoport="yes" listen="0.0.0.0">
|
||||||
|
<listen type="address" address="0.0.0.0" />
|
||||||
|
</graphics>
|
||||||
|
<video>
|
||||||
|
<model type="vga" vram="16384" heads="1" primary="yes" />
|
||||||
|
<address type="pci" domain="0x0000" bus="0x00" slot="0x01" function="0x0" />
|
||||||
|
</video>
|
||||||
|
<memballoon model="virtio">
|
||||||
|
<address type="pci" domain="0x0000" bus="0x04" slot="0x00" function="0x0" />
|
||||||
|
</memballoon>
|
||||||
|
</devices>
|
||||||
|
</domain>
|
|
@ -204,6 +204,10 @@ class Instance(models.Model):
|
||||||
def formats(self):
|
def formats(self):
|
||||||
return self.proxy.get_image_formats()
|
return self.proxy.get_image_formats()
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def interfaces(self):
|
||||||
|
return self.proxy.get_ifaces()
|
||||||
|
|
||||||
|
|
||||||
class PermissionSet(models.Model):
|
class PermissionSet(models.Model):
|
||||||
"""
|
"""
|
||||||
|
@ -211,8 +215,9 @@ class PermissionSet(models.Model):
|
||||||
"""
|
"""
|
||||||
class Meta:
|
class Meta:
|
||||||
default_permissions = ()
|
default_permissions = ()
|
||||||
permissions = [('clone_instances', 'Can clone instances'),
|
permissions = [
|
||||||
('passwordless_console', _('Can access console without password')),
|
('clone_instances', 'Can clone instances'),
|
||||||
]
|
('passwordless_console', _('Can access console without password')),
|
||||||
|
]
|
||||||
|
|
||||||
managed = False
|
managed = False
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
{% for c_net in networks_host %}
|
{% for c_net in networks_host %}
|
||||||
<option value="net:{{ c_net }}">Network {{ c_net }}</option>
|
<option value="net:{{ c_net }}">Network {{ c_net }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% for c_iface in interfaces_host %}
|
{% for c_iface in instance.interfaces %}
|
||||||
<option value="iface:{{ c_iface }}">Interface {{ c_iface }}</option>
|
<option value="iface:{{ c_iface }}">Interface {{ c_iface }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
|
|
|
@ -393,7 +393,7 @@
|
||||||
{% for c_net in networks_host %}
|
{% for c_net in networks_host %}
|
||||||
<option value="net:{{ c_net }}" {% if c_net == network.nic %} selected {% endif %}>{% trans 'Network' %} {{ c_net }}</option>
|
<option value="net:{{ c_net }}" {% if c_net == network.nic %} selected {% endif %}>{% trans 'Network' %} {{ c_net }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% for c_iface in interfaces_host %}
|
{% for c_iface in instance.interfaces %}
|
||||||
<option value="iface:{{ c_iface }}" {% if c_iface == network.nic %} selected {% endif %}>{% trans 'Interface' %} {{ c_iface }}</option>
|
<option value="iface:{{ c_iface }}" {% if c_iface == network.nic %} selected {% endif %}>{% trans 'Interface' %} {{ c_iface }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
|
@ -657,9 +657,9 @@
|
||||||
<tbody class="searchable">
|
<tbody class="searchable">
|
||||||
{% for userinstance in userinstances %}
|
{% for userinstance in userinstances %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="{% url 'account' userinstance.user.id %}">{{ userinstance.user }}</a></td>
|
<td><a href="{% url 'accounts:account' userinstance.user.id %}">{{ userinstance.user }}</a></td>
|
||||||
<td style="width:30px;">
|
<td style="width:30px;">
|
||||||
<a href="{% url 'user_instance_delete' userinstance.id %}?next={% url 'instances:instance' instance.id %}#users">
|
<a href="{% url 'accounts:user_instance_delete' userinstance.id %}?next={% url 'instances:instance' instance.id %}#users">
|
||||||
{% icon 'trash' %}
|
{% icon 'trash' %}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -116,7 +116,7 @@ class InstancesTestCase(TestCase):
|
||||||
self.assertEqual(instance.disks[0]['size'], 1024**3)
|
self.assertEqual(instance.disks[0]['size'], 1024**3)
|
||||||
|
|
||||||
response = self.client.post(reverse('instances:resize_disk', args=[instance.id]), {
|
response = self.client.post(reverse('instances:resize_disk', args=[instance.id]), {
|
||||||
'disk_size_vda': '2.0 GB',
|
'disk_size_vda': '2',
|
||||||
})
|
})
|
||||||
self.assertRedirects(response, reverse('instances:instance', args=[instance.id]) + '#resize')
|
self.assertRedirects(response, reverse('instances:instance', args=[instance.id]) + '#resize')
|
||||||
|
|
||||||
|
@ -449,9 +449,8 @@ class InstancesTestCase(TestCase):
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse('instances:destroy', args=[instance.id]),
|
reverse('instances:destroy', args=[instance.id]),
|
||||||
{'delete_disk': True},
|
{'delete_disk': True},
|
||||||
HTTP_REFERER=reverse('index'),
|
|
||||||
)
|
)
|
||||||
self.assertRedirects(response, reverse('instances', args=[compute.id]))
|
self.assertRedirects(response, reverse('instances:index'))
|
||||||
|
|
||||||
# # create volume
|
# # create volume
|
||||||
# response = self.client.post(
|
# response = self.client.post(
|
||||||
|
|
|
@ -3,17 +3,25 @@
|
||||||
{% load icons %}
|
{% load icons %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block title %}{%trans "Delete" %}{% endblock %}
|
{% block title %}{{ title }}{% endblock %}
|
||||||
|
|
||||||
|
{% block page_header %}{{ title }}{% endblock page_header %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<form method="post">
|
<div class="row">
|
||||||
{% csrf_token %}
|
<div class="offset-3 col-6">
|
||||||
<div class="alert alert-warning">
|
<form method="post">
|
||||||
{%trans "Are you sure you want to delete" %} "{{ object }}"?
|
{% csrf_token %}
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
{%trans "Are you sure you want to delete" %} "{{ object }}"?
|
||||||
|
</div>
|
||||||
|
<div class="form-group mb-0 float-right">
|
||||||
|
<a class="btn btn-primary" href="javascript:history.back()">{% icon 'times' %} {% trans "Cancel" %}</a>
|
||||||
|
<button type="submit" class="btn btn-danger">
|
||||||
|
{% icon 'trash' %} {% trans "Delete" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<a class="btn btn-primary" href="javascript:history.back()">{% icon 'times' %} {% trans "Cancel" %}</a>
|
</div>
|
||||||
<button type="submit" class="btn btn-danger">
|
|
||||||
{% icon 'check' %} {% trans "Delete" %}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
|
@ -42,9 +42,9 @@
|
||||||
<a class="dropdown-item disabled" href="#">
|
<a class="dropdown-item disabled" href="#">
|
||||||
{% trans "Language" %}: <span class="badge badge-secondary">{{ LANGUAGE_CODE }}</span>
|
{% trans "Language" %}: <span class="badge badge-secondary">{{ LANGUAGE_CODE }}</span>
|
||||||
</a>
|
</a>
|
||||||
<a class="dropdown-item {% view_active request 'profile' %}" href="{% url 'profile' %}">{% icon 'vcard' %} {% trans "Profile" %}</a>
|
<a class="dropdown-item {% view_active request 'accounts:profile' %}" href="{% url 'accounts:profile' %}">{% icon 'vcard' %} {% trans "Profile" %}</a>
|
||||||
<div class="dropdown-divider"></div>
|
<div class="dropdown-divider"></div>
|
||||||
<a class="dropdown-item" href="{% url 'logout' %}"><i class="fa fa-fw fa-power-off"></i> {% trans "Log Out" %}</a>
|
<a class="dropdown-item" href="{% url 'accounts:logout' %}"><i class="fa fa-fw fa-power-off"></i> {% trans "Log Out" %}</a>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
Loading…
Reference in a new issue