mirror of https://github.com/jumpserver/jumpserver
Add asset system user
parent
5259dd8054
commit
4fc9274e00
|
@ -90,6 +90,7 @@ class AdminUserForm(forms.ModelForm):
|
|||
widget=forms.SelectMultiple(
|
||||
attrs={'class': 'select2', 'data-placeholder': _('Select assets')})
|
||||
)
|
||||
auto_generate_key = forms.BooleanField(required=True, initial=True)
|
||||
# Form field name can not start with `_`, so redefine it,
|
||||
password = forms.CharField(widget=forms.PasswordInput, max_length=100, min_length=8, strip=True,
|
||||
help_text=_('If also set private key, use that first'), required=False)
|
||||
|
@ -120,15 +121,15 @@ class AdminUserForm(forms.ModelForm):
|
|||
admin_user.password = password
|
||||
print(password)
|
||||
# Todo: Validate private key file, and generate public key
|
||||
# Todo: Auto generate private key and public key
|
||||
if private_key_file:
|
||||
print(private_key_file)
|
||||
admin_user.private_key = private_key_file.read()
|
||||
admin_user.save()
|
||||
return self.instance
|
||||
|
||||
class Meta:
|
||||
model = AdminUser
|
||||
fields = ['name', 'username', 'password', 'private_key_file', 'as_default', 'comment']
|
||||
fields = ['name', 'username', 'auto_generate_key', 'password', 'private_key_file', 'as_default', 'comment']
|
||||
widgets = {
|
||||
'name': forms.TextInput(attrs={'placeholder': _('Name')}),
|
||||
'username': forms.TextInput(attrs={'placeholder': _('Username')}),
|
||||
|
@ -138,3 +139,76 @@ class AdminUserForm(forms.ModelForm):
|
|||
'username': '* required',
|
||||
}
|
||||
|
||||
|
||||
class SystemUserForm(forms.ModelForm):
|
||||
# Admin user assets define, let user select, save it in form not in view
|
||||
assets = forms.ModelMultipleChoiceField(queryset=Asset.objects.all(),
|
||||
label=_('Asset'),
|
||||
required=False,
|
||||
widget=forms.SelectMultiple(
|
||||
attrs={'class': 'select2', 'data-placeholder': _('Select assets')})
|
||||
)
|
||||
asset_groups = forms.ModelMultipleChoiceField(queryset=AssetGroup.objects.all(),
|
||||
label=_('Asset group'),
|
||||
required=False,
|
||||
widget=forms.SelectMultiple(
|
||||
attrs={'class': 'select2',
|
||||
'data-placeholder': _('Select asset groups')})
|
||||
)
|
||||
auto_generate_key = forms.BooleanField(required=True, initial=True)
|
||||
# Form field name can not start with `_`, so redefine it,
|
||||
password = forms.CharField(widget=forms.PasswordInput, max_length=100, min_length=8, strip=True,
|
||||
help_text=_('If also set private key, use that first'), required=False)
|
||||
# Need use upload private key file except paste private key content
|
||||
private_key_file = forms.FileField(required=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# When update a admin user instance, initial it
|
||||
if kwargs.get('instance'):
|
||||
initial = kwargs.get('initial', {})
|
||||
initial['assets'] = kwargs['instance'].assets.all()
|
||||
initial['asset_groups'] = kwargs['instance'].asset_groups.all()
|
||||
super(SystemUserForm, self).__init__(*args, **kwargs)
|
||||
|
||||
def _save_m2m(self):
|
||||
# Save assets relation with admin user
|
||||
super(SystemUserForm, self)._save_m2m()
|
||||
assets = self.cleaned_data['assets']
|
||||
asset_groups = self.cleaned_data['asset_groups']
|
||||
self.instance.assets.clear()
|
||||
self.instance.assets.add(*tuple(assets))
|
||||
self.instance.asset_groups.clear()
|
||||
self.instance.asset_groups.add(*tuple(asset_groups))
|
||||
|
||||
def save(self, commit=True):
|
||||
# Because we define custom field, so we need rewrite :method: `save`
|
||||
system_user = super(SystemUserForm, self).save(commit=commit)
|
||||
password = self.cleaned_data['password']
|
||||
private_key_file = self.cleaned_data['private_key_file']
|
||||
|
||||
if password:
|
||||
system_user.password = password
|
||||
print(password)
|
||||
# Todo: Validate private key file, and generate public key
|
||||
# Todo: Auto generate private key and public key
|
||||
if private_key_file:
|
||||
system_user.private_key = private_key_file.read()
|
||||
system_user.save()
|
||||
return self.instance
|
||||
|
||||
class Meta:
|
||||
model = SystemUser
|
||||
fields = [
|
||||
'name', 'username', 'protocol', 'auto_generate_key', 'password', 'private_key_file', 'as_default',
|
||||
'auto_push', 'auto_update', 'sudo', 'comment', 'shell', 'home', 'uid',
|
||||
]
|
||||
widgets = {
|
||||
'name': forms.TextInput(attrs={'placeholder': _('Name')}),
|
||||
'username': forms.TextInput(attrs={'placeholder': _('Username')}),
|
||||
}
|
||||
help_texts = {
|
||||
'name': '* required',
|
||||
'username': '* required',
|
||||
'auth_push': 'Auto push system user to asset',
|
||||
'auth_update': 'Auto update system user ssh key',
|
||||
}
|
|
@ -135,28 +135,72 @@ class SystemUser(models.Model):
|
|||
('telnet', 'telnet'),
|
||||
)
|
||||
name = models.CharField(max_length=128, unique=True, verbose_name=_('Name'))
|
||||
username = models.CharField(max_length=16, blank=True, verbose_name=_('Username'))
|
||||
password = models.CharField(max_length=256, blank=True, verbose_name=_('Password'))
|
||||
protocol = models.CharField(max_length=16, default='ssh', verbose_name=_('Protocol'))
|
||||
private_key = models.CharField(max_length=4096, blank=True, verbose_name=_('SSH private key'))
|
||||
public_key = models.CharField(max_length=4096, blank=True, verbose_name=_('SSH public key'))
|
||||
is_default = models.BooleanField(default=True, verbose_name=_('As default'))
|
||||
username = models.CharField(max_length=16, verbose_name=_('Username'))
|
||||
_password = models.CharField(max_length=256, blank=True, verbose_name=_('Password'))
|
||||
protocol = models.CharField(max_length=16, choices=PROTOCOL_CHOICES, default='ssh', verbose_name=_('Protocol'))
|
||||
_private_key = models.CharField(max_length=4096, blank=True, verbose_name=_('SSH private key'))
|
||||
_public_key = models.CharField(max_length=4096, blank=True, verbose_name=_('SSH public key'))
|
||||
as_default = models.BooleanField(default=False, verbose_name=_('As default'))
|
||||
auto_push = models.BooleanField(default=True, verbose_name=_('Auto push'))
|
||||
auto_update = models.BooleanField(default=True, verbose_name=_('Auto update pass/key'))
|
||||
sudo = models.TextField(max_length=4096, blank=True, verbose_name=_('Sudo'))
|
||||
shell = models.CharField(max_length=64, blank=True, verbose_name=_('Shell'))
|
||||
sudo = models.TextField(max_length=4096, default='/user/bin/whoami', verbose_name=_('Sudo'))
|
||||
shell = models.CharField(max_length=64, default='/bin/bash', verbose_name=_('Shell'))
|
||||
home = models.CharField(max_length=64, blank=True, verbose_name=_('Home'))
|
||||
uid = models.IntegerField(blank=True, verbose_name=_('Uid'))
|
||||
date_created = models.DateTimeField(auto_now=True, null=True)
|
||||
uid = models.IntegerField(null=True, blank=True, verbose_name=_('Uid'))
|
||||
date_created = models.DateTimeField(auto_now=True)
|
||||
created_by = models.CharField(max_length=32, blank=True, verbose_name=_('Created by'))
|
||||
comment = models.CharField(max_length=128, blank=True, verbose_name=_('Comment'))
|
||||
comment = models.TextField(max_length=128, blank=True, verbose_name=_('Comment'))
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def password(self):
|
||||
return decrypt(self._password)
|
||||
|
||||
@password.setter
|
||||
def password(self, password_raw):
|
||||
self._password = encrypt(password_raw)
|
||||
|
||||
@property
|
||||
def private_key(self):
|
||||
return decrypt(self._private_key)
|
||||
|
||||
@private_key.setter
|
||||
def private_key(self, private_key_raw):
|
||||
self._private_key = encrypt(private_key_raw)
|
||||
|
||||
@property
|
||||
def public_key(self):
|
||||
return decrypt(self._public_key)
|
||||
|
||||
@public_key.setter
|
||||
def public_key(self, public_key_raw):
|
||||
self._public_key = encrypt(public_key_raw)
|
||||
|
||||
class Meta:
|
||||
db_table = 'system_user'
|
||||
|
||||
@classmethod
|
||||
def generate_fake(cls, count=100):
|
||||
from random import seed
|
||||
import forgery_py
|
||||
from django.db import IntegrityError
|
||||
|
||||
seed()
|
||||
for i in range(count):
|
||||
obj = cls(name=forgery_py.name.full_name(),
|
||||
username=forgery_py.internet.user_name(),
|
||||
password=forgery_py.lorem_ipsum.word(),
|
||||
comment=forgery_py.lorem_ipsum.sentence(),
|
||||
created_by='Fake')
|
||||
try:
|
||||
obj.save()
|
||||
logger.debug('Generate fake asset group: %s' % obj.name)
|
||||
except IntegrityError:
|
||||
print('Error continue')
|
||||
continue
|
||||
|
||||
|
||||
class AssetGroup(models.Model):
|
||||
name = models.CharField(max_length=64, unique=True, verbose_name=_('Name'))
|
||||
|
@ -206,7 +250,7 @@ class Asset(models.Model):
|
|||
password = models.CharField(max_length=256, null=True, blank=True, verbose_name=_("Admin password"))
|
||||
admin_user = models.ForeignKey(AdminUser, null=True, related_name='assets',
|
||||
on_delete=models.SET_NULL, verbose_name=_("Admin user"))
|
||||
system_user = models.ManyToManyField(SystemUser, blank=True, verbose_name=_("System User"))
|
||||
system_user = models.ManyToManyField(SystemUser, blank=True, related_name='assets', verbose_name=_("System User"))
|
||||
idc = models.ForeignKey(IDC, null=True, related_name='assets', on_delete=models.SET_NULL, verbose_name=_('IDC'))
|
||||
mac_address = models.CharField(max_length=20, null=True, blank=True, verbose_name=_("Mac address"))
|
||||
brand = models.CharField(max_length=64, null=True, blank=True, verbose_name=_('Brand'))
|
||||
|
@ -227,7 +271,7 @@ class Asset(models.Model):
|
|||
comment = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Comment'))
|
||||
|
||||
def __unicode__(self):
|
||||
return '%(ip)s:%(port)d' % {'ip': self.ip, 'port': self.port}
|
||||
return '%(ip)s:%(port)s' % {'ip': self.ip, 'port': self.port}
|
||||
|
||||
def initial(self):
|
||||
pass
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
<div class="col-sm-12">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="ibox-title">
|
||||
<h5>{% trans 'Create asset group' %}</h5>
|
||||
<h5>{% trans 'Create admin user' %}</h5>
|
||||
<div class="ibox-tools">
|
||||
<a class="collapse-link">
|
||||
<i class="fa fa-chevron-up"></i>
|
||||
|
@ -31,6 +31,12 @@
|
|||
{% csrf_token %}
|
||||
{{ form.name|bootstrap_horizontal }}
|
||||
{{ form.username|bootstrap_horizontal }}
|
||||
<div class="form-group">
|
||||
<label for="{{ form.auto_generate_key.id_for_label }}" class="col-sm-2 control-label">{% trans 'Auto generate key' %}</label>
|
||||
<div class="col-sm-8">
|
||||
{{ form.auto_generate_key}}
|
||||
</div>
|
||||
</div>
|
||||
{{ form.password|bootstrap_horizontal }}
|
||||
{{ form.private_key_file|bootstrap_horizontal }}
|
||||
<div class="form-group">
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load bootstrap %}
|
||||
{% block custom_head_css_js %}
|
||||
<link href="{% static "css/plugins/select2/select2.min.css" %}" rel="stylesheet">
|
||||
<script src="{% static "js/plugins/select2/select2.full.min.js" %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="wrapper wrapper-content animated fadeInRight">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="ibox-title">
|
||||
<h5>{% trans 'Create system user' %}</h5>
|
||||
<div class="ibox-tools">
|
||||
<a class="collapse-link">
|
||||
<i class="fa fa-chevron-up"></i>
|
||||
</a>
|
||||
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
|
||||
<i class="fa fa-wrench"></i>
|
||||
</a>
|
||||
<a class="close-link">
|
||||
<i class="fa fa-times"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ibox-content">
|
||||
<form enctype="multipart/form-data" method="post" class="form-horizontal" action="" >
|
||||
{% csrf_token %}
|
||||
{{ form.name|bootstrap_horizontal }}
|
||||
{{ form.username|bootstrap_horizontal }}
|
||||
{{ form.protocol|bootstrap_horizontal }}
|
||||
<div class="form-group">
|
||||
<label for="{{ form.auto_generate_key.id_for_label }}" class="col-sm-2 control-label">{% trans 'Auto generate key' %}</label>
|
||||
<div class="col-sm-8">
|
||||
{{ form.auto_generate_key}}
|
||||
</div>
|
||||
</div>
|
||||
{{ form.password|bootstrap_horizontal }}
|
||||
{{ form.private_key_file|bootstrap_horizontal }}
|
||||
<div class="form-group">
|
||||
<label for="{{ form.as_default.id_for_label }}" class="col-sm-2 control-label">{% trans 'As default' %}</label>
|
||||
<div class="col-sm-8">
|
||||
{{ form.as_default}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.as_push.id_for_label }}" class="col-sm-2 control-label">{% trans 'Auto push' %}</label>
|
||||
<div class="col-sm-8">
|
||||
{{ form.auto_push}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.as_update.id_for_label }}" class="col-sm-2 control-label">{% trans 'Auto update' %}</label>
|
||||
<div class="col-sm-8">
|
||||
{{ form.auto_update}}
|
||||
</div>
|
||||
</div>
|
||||
{{ form.assets|bootstrap_horizontal }}
|
||||
{{ form.asset_groups|bootstrap_horizontal }}
|
||||
{{ form.sudo|bootstrap_horizontal }}
|
||||
{{ form.comment|bootstrap_horizontal }}
|
||||
{{ form.home|bootstrap_horizontal }}
|
||||
{{ form.shell|bootstrap_horizontal }}
|
||||
{{ form.uid|bootstrap_horizontal }}
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-4 col-sm-offset-2">
|
||||
<button class="btn btn-white" type="reset">{% trans 'Reset' %}</button>
|
||||
<button id="submit_button" class="btn btn-primary" type="submit">{% trans 'Submit' %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block custom_foot_js %}
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
$('.select2').select2();
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -0,0 +1,43 @@
|
|||
{% extends '_list_base.html' %}
|
||||
{% load i18n %}
|
||||
{% load common_tags %}
|
||||
{% block content_left_head %}
|
||||
<a href="{% url 'assets:system-user-create' %}" class="btn btn-sm btn-primary "> {% trans "Create system user" %} </a>
|
||||
{% endblock %}
|
||||
|
||||
{% block table_head %}
|
||||
<th class="text-center">{% trans 'ID' %}</th>
|
||||
<th class="text-center"><a href="{% url 'assets:system-user-list' %}?sort=name">{% trans 'Name' %}</a></th>
|
||||
<th class="text-center"><a href="{% url 'assets:system-user-list' %}?sort=username">{% trans 'Username' %}</a></th>
|
||||
<th class="text-center">{% trans 'Asset num' %}</th>
|
||||
<th class="text-center">{% trans 'Asset group num' %}</th>
|
||||
<th class="text-center">{% trans 'Lost connection' %}</th>
|
||||
<th class="text-center">{% trans 'Comment' %}</th>
|
||||
<th class="text-center"></th>
|
||||
{% endblock %}
|
||||
|
||||
{% block table_body %}
|
||||
{% for system_user in system_user_list %}
|
||||
<tr class="gradeX">
|
||||
<td class="text-center">{{ system_user.id }}</td>
|
||||
<td>
|
||||
<a href="{% url 'assets:system-user-detail' pk=system_user.id %}">
|
||||
{{ system_user.name }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="text-center">{{ system_user.username }}</td>
|
||||
<td class="text-center">{{ system_user.assets.count }}</td>
|
||||
<td class="text-center">{{ system_user.asset_groups.count }}</td>
|
||||
<td class="text-center">{{ system_user.assets.count }}</td>
|
||||
<td class="text-center">{{ system_user.comment|truncatewords:8 }}</td>
|
||||
<td class="text-center">
|
||||
<!-- Todo: Click script button will paste a url to clipboard like: curl http://url/system_user_create.sh | bash -->
|
||||
<a href="{% url 'assets:system-user-update' pk=system_user.id %}" class="btn btn-xs btn-primary">{% trans 'Script' %}</a>
|
||||
<!-- Todo: Click refresh button will run a task to test admin user could connect asset or not immediately -->
|
||||
<a href="{% url 'assets:system-user-update' pk=system_user.id %}" class="btn btn-xs btn-warning">{% trans 'Refresh' %}</a>
|
||||
<a href="{% url 'assets:system-user-update' pk=system_user.id %}" class="btn btn-xs btn-info">{% trans 'Update' %}</a>
|
||||
<a href="{% url 'assets:system-user-delete' pk=system_user.id %}" class="btn btn-xs btn-danger del">{% trans 'Delete' %}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
|
@ -33,5 +33,10 @@ urlpatterns = [
|
|||
url(r'^admin-user/(?P<pk>[0-9]+)$', views.AdminUserDetailView.as_view(), name='admin-user-detail'),
|
||||
url(r'^admin-user/(?P<pk>[0-9]+)/update', views.AdminUserUpdateView.as_view(), name='admin-user-update'),
|
||||
url(r'^admin-user/(?P<pk>[0-9]+)/delete$', views.AdminUserDeleteView.as_view(), name='admin-user-delete'),
|
||||
url(r'^system-user$', views.SystemUserListView.as_view(), name='system-user-list'),
|
||||
url(r'^system-user/create$', views.SystemUserCreateView.as_view(), name='system-user-create'),
|
||||
url(r'^system-user/(?P<pk>[0-9]+)$', views.SystemUserDetailView.as_view(), name='system-user-detail'),
|
||||
url(r'^system-user/(?P<pk>[0-9]+)/update', views.SystemUserUpdateView.as_view(), name='system-user-update'),
|
||||
url(r'^system-user/(?P<pk>[0-9]+)/delete$', views.SystemUserDeleteView.as_view(), name='system-user-delete'),
|
||||
# url(r'^api/v1.0/', include(router.urls)),
|
||||
]
|
||||
|
|
|
@ -11,7 +11,7 @@ from django.contrib.messages.views import SuccessMessageMixin
|
|||
from django.views.generic.detail import DetailView, SingleObjectMixin
|
||||
|
||||
from .models import Asset, AssetGroup, IDC, AssetExtend, AdminUser, SystemUser
|
||||
from .forms import AssetForm, AssetGroupForm, IDCForm, AdminUserForm
|
||||
from .forms import AssetForm, AssetGroupForm, IDCForm, AdminUserForm, SystemUserForm
|
||||
from .hands import AdminUserRequiredMixin
|
||||
|
||||
|
||||
|
@ -294,3 +294,68 @@ class AdminUserDeleteView(AdminUserRequiredMixin, DeleteView):
|
|||
model = AdminUser
|
||||
template_name = 'assets/delete_confirm.html'
|
||||
success_url = 'assets:admin-user-list'
|
||||
|
||||
|
||||
class SystemUserListView(AdminUserRequiredMixin, ListView):
|
||||
model = SystemUser
|
||||
paginate_by = settings.CONFIG.DISPLAY_PER_PAGE
|
||||
context_object_name = 'system_user_list'
|
||||
template_name = 'assets/system_user_list.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'app': _('Assets'),
|
||||
'action': _('Admin user list'),
|
||||
'keyword': self.request.GET.get('keyword', '')
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super(SystemUserListView, self).get_context_data(**kwargs)
|
||||
|
||||
def get_queryset(self):
|
||||
# Todo: Default order by lose asset connection num
|
||||
self.queryset = super(SystemUserListView, self).get_queryset()
|
||||
self.keyword = keyword = self.request.GET.get('keyword', '')
|
||||
self.sort = sort = self.request.GET.get('sort', '-date_created')
|
||||
|
||||
if keyword:
|
||||
self.queryset = self.queryset.filter(Q(name__icontains=keyword) |
|
||||
Q(comment__icontains=keyword))
|
||||
|
||||
if sort:
|
||||
self.queryset = self.queryset.order_by(sort)
|
||||
return self.queryset
|
||||
|
||||
|
||||
class SystemUserCreateView(AdminUserRequiredMixin, SuccessMessageMixin, CreateView):
|
||||
model = SystemUser
|
||||
form_class = SystemUserForm
|
||||
template_name = 'assets/system_user_create_update.html'
|
||||
success_url = reverse_lazy('assets:system-user-list')
|
||||
success_message = _('Create system user <a href="%s">%s</a> successfully.')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'app': 'assets',
|
||||
'action': 'Create system user'
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super(SystemUserCreateView, self).get_context_data(**kwargs)
|
||||
|
||||
def get_success_message(self, cleaned_data):
|
||||
return self.success_message % (
|
||||
reverse_lazy('assets:system-user-detail', kwargs={'pk': self.object.pk}),
|
||||
self.object.name,
|
||||
)
|
||||
|
||||
|
||||
class SystemUserUpdateView(UpdateView):
|
||||
pass
|
||||
|
||||
|
||||
class SystemUserDetailView(DetailView):
|
||||
pass
|
||||
|
||||
|
||||
class SystemUserDeleteView(DeleteView):
|
||||
pass
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
<li id="asset-group"><a href="{% url 'assets:asset-group-list' %}">{% trans 'Asset group' %}</a></li>
|
||||
<li id="idc"><a href="{% url 'assets:idc-list' %}">{% trans 'IDC' %}</a></li>
|
||||
<li id="admin-user"><a href="{% url 'assets:admin-user-list' %}">{% trans 'Admin user' %}</a></li>
|
||||
<li id="system-user"><a href="">{% trans 'System user' %}</a></li>
|
||||
<li id="system-user"><a href="{% url 'assets:system-user-list' %}">{% trans 'System user' %}</a></li>
|
||||
<li id=""><a href="">{% trans 'Label' %}</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
|
Loading…
Reference in New Issue