diff --git a/apps/assets/models/asset.py b/apps/assets/models/asset.py index 1a66e2d38..499ad2cf0 100644 --- a/apps/assets/models/asset.py +++ b/apps/assets/models/asset.py @@ -86,7 +86,6 @@ class Asset(models.Model): def __unicode__(self): return '%s <%s: %s>' % (self.hostname, self.ip, self.port) - __str__ = __unicode__ @property diff --git a/apps/assets/models/group.py b/apps/assets/models/group.py index c7e72acfe..b31b80871 100644 --- a/apps/assets/models/group.py +++ b/apps/assets/models/group.py @@ -23,6 +23,7 @@ class AssetGroup(models.Model): def __unicode__(self): return self.name + __str__ = __unicode__ class Meta: ordering = ['name'] diff --git a/apps/assets/models/idc.py b/apps/assets/models/idc.py index fbbde2c98..d79d897fc 100644 --- a/apps/assets/models/idc.py +++ b/apps/assets/models/idc.py @@ -36,6 +36,7 @@ class IDC(models.Model): def __unicode__(self): return self.name + __str__ = __unicode__ @classmethod def initial(cls): diff --git a/apps/assets/models/user.py b/apps/assets/models/user.py index 04a4282d8..1504f9c2f 100644 --- a/apps/assets/models/user.py +++ b/apps/assets/models/user.py @@ -50,7 +50,6 @@ class AdminUser(models.Model): def __unicode__(self): return self.name - __str__ = __unicode__ @property diff --git a/apps/assets/templates/assets/_asset_import_modal.html b/apps/assets/templates/assets/_asset_import_modal.html index d226ace94..8ef73e531 100644 --- a/apps/assets/templates/assets/_asset_import_modal.html +++ b/apps/assets/templates/assets/_asset_import_modal.html @@ -7,11 +7,14 @@ {% csrf_token %}
diff --git a/apps/assets/views/asset.py b/apps/assets/views/asset.py
index 3844e2fd8..893de66a6 100644
--- a/apps/assets/views/asset.py
+++ b/apps/assets/views/asset.py
@@ -1,14 +1,14 @@
# coding:utf-8
from __future__ import absolute_import, unicode_literals
+
+import csv
import json
import uuid
+import codecs
+from io import StringIO
-from openpyxl import Workbook
-from openpyxl.writer.excel import save_virtual_workbook
-from openpyxl import load_workbook
from django.conf import settings
-from django.db import IntegrityError
-from django.urls import reverse
+from django.utils.translation import ugettext_lazy as _
from django.views.generic import TemplateView, ListView, View
from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateView
from django.urls import reverse_lazy
@@ -206,45 +206,42 @@ class AssetModalListView(AdminUserRequiredMixin, ListView):
@method_decorator(csrf_exempt, name='dispatch')
class AssetExportView(View):
- @staticmethod
- def get_asset_attr(asset, attr):
- if attr in ['admin_user', 'idc']:
- return getattr(asset, attr)
- elif attr in ['status', 'type', 'env']:
- return getattr(asset, 'get_{}_display'.format(attr))()
- else:
- return getattr(asset, attr)
-
def get(self, request, *args, **kwargs):
spm = request.GET.get('spm', '')
- assets_id = cache.get(spm)
- if not assets_id and not isinstance(assets_id, list):
- return HttpResponse('May be expired', status=404)
-
+ assets_id = cache.get(spm, [Asset.objects.first().id])
+ print(assets_id)
+ fields = [
+ field for field in Asset._meta.fields
+ if field.name not in [
+ 'date_created'
+ ]
+ ]
+ filename = 'assets-{}.csv'.format(
+ timezone.localtime(timezone.now()).strftime('%Y-%m-%d_%H-%M-%S'))
+ response = HttpResponse(content_type='text/csv')
+ response['Content-Disposition'] = 'attachment; filename="%s"' % filename
+ response.write(codecs.BOM_UTF8)
assets = Asset.objects.filter(id__in=assets_id)
- wb = Workbook()
- ws = wb.active
- ws.title = 'Asset'
- header = ['hostname', 'ip', 'port', 'admin_user', 'idc', 'memory', 'disk',
- 'mac_address', 'other_ip', 'remote_card_ip', 'os', 'cabinet_no',
- 'cabinet_pos', 'number', 'status', 'type', 'env', 'sn', 'comment']
- ws.append(header)
+ writer = csv.writer(response, dialect='excel',
+ quoting=csv.QUOTE_MINIMAL)
+
+ header = [field.verbose_name for field in fields]
+ header.append(_('Asset groups'))
+ writer.writerow(header)
for asset in assets:
- ws.append([self.get_asset_attr(asset, attr) for attr in header])
-
- filename = 'assets-{}.xlsx'.format(timezone.localtime(timezone.now()).strftime('%Y-%m-%d_%H-%M-%S'))
- response = HttpResponse(save_virtual_workbook(wb), content_type='applications/vnd.ms-excel')
- response['Content-Disposition'] = 'attachment; filename="%s"' % filename
+ groups = ','.join([group.name for group in asset.groups.all()])
+ data = [getattr(asset, field.name) for field in fields]
+ data.append(groups)
+ writer.writerow(data)
return response
def post(self, request, *args, **kwargs):
try:
assets_id = json.loads(request.body).get('assets_id', [])
- print(assets_id)
except ValueError:
return HttpResponse('Json object not valid', status=400)
- spm = uuid.uuid4().get_hex()
+ spm = uuid.uuid4().hex
cache.set(spm, assets_id, 300)
url = reverse_lazy('assets:asset-export') + '?spm=%s' % spm
return JsonResponse({'redirect': url})
@@ -254,67 +251,74 @@ class BulkImportAssetView(AdminUserRequiredMixin, JSONResponseMixin, FormView):
form_class = forms.FileForm
def form_valid(self, form):
- try:
- wb = load_workbook(form.cleaned_data['file'])
- ws = wb.get_active_sheet()
- except Exception as e:
- print(e)
- data = {'valid': False, 'msg': 'Not a valid Excel file'}
+ file = form.cleaned_data['file']
+ data = file.read().decode('utf-8').strip(
+ codecs.BOM_UTF8.decode('utf-8'))
+ csv_file = StringIO(data)
+ reader = csv.reader(csv_file)
+ csv_data = [row for row in reader]
+ fields = [
+ field for field in Asset._meta.fields
+ if field.name not in [
+ 'date_created'
+ ]
+ ]
+ header_ = csv_data[0]
+ mapping_reverse = {field.verbose_name: field.name for field in fields}
+ mapping_reverse[_('Asset groups')] = 'groups'
+ attr = [mapping_reverse.get(n, None) for n in header_]
+ if None in attr:
+ data = {'valid': False,
+ 'msg': 'Must be same format as '
+ 'template or export file'}
return self.render_json_response(data)
- rows = ws.rows
- header_all = ['hostname', 'ip', 'port', 'admin_user', 'idc', 'cpu', 'memory', 'disk',
- 'mac_address', 'other_ip', 'remote_card_ip', 'os', 'cabinet_no',
- 'cabinet_pos', 'number', 'status', 'type', 'env', 'sn', 'comment']
- header_min = ['hostname', 'ip', 'port', 'admin_user', 'comment']
- header = [col.value for col in next(rows)]
- if not set(header).issubset(set(header_all)) and not set(header).issuperset(set(header_min)):
- data = {'valid': False, 'msg': 'Must be same format as template or export file'}
- return self.render_json_response(data)
-
- created = []
- updated = []
- failed = []
- for row in rows:
- asset_dict = dict(zip(header, [col.value for col in row]))
- if asset_dict.get('admin_user', None):
- admin_user = get_object_or_none(AdminUser, name=asset_dict['admin_user'])
- asset_dict['admin_user'] = admin_user
-
- if asset_dict.get('idc'):
- idc = get_object_or_none(IDC, name=asset_dict['idc'])
- asset_dict['idc'] = idc
-
- if asset_dict.get('type'):
- asset_display_type_map = dict(zip(dict(Asset.TYPE_CHOICES).values(), dict(Asset.TYPE_CHOICES).keys()))
- asset_type = asset_display_type_map.get(asset_dict['type'], 'Server')
- asset_dict['type'] = asset_type
-
- if asset_dict.get('status'):
- asset_display_status_map = dict(zip(dict(Asset.STATUS_CHOICES).values(),
- dict(Asset.STATUS_CHOICES).keys()))
- asset_status = asset_display_status_map.get(asset_dict['status'], 'In use')
- asset_dict['status'] = asset_status
-
- if asset_dict.get('env'):
- asset_display_env_map = dict(zip(dict(Asset.ENV_CHOICES).values(),
- dict(Asset.ENV_CHOICES).keys()))
- asset_env = asset_display_env_map.get(asset_dict['env'], 'Prod')
- asset_dict['env'] = asset_env
-
- try:
- Asset.objects.create(**asset_dict)
- created.append(asset_dict['ip'])
- except IntegrityError as e:
- asset = Asset.objects.filter(ip=asset_dict['ip'], port=asset_dict['port'])
- if not asset:
- failed.append(asset_dict['ip'])
+ created, updated, failed = [], [], []
+ for row in csv_data[1:]:
+ if set(row) == {''}:
+ continue
+ asset_dict = dict(zip(attr, row))
+ id_ = asset_dict.pop('id', 0)
+ asset = get_object_or_none(Asset, id=id_)
+ for k, v in asset_dict.items():
+ if k == 'idc':
+ v = get_object_or_none(IDC, name=v)
+ elif k == 'is_active':
+ v = bool(v)
+ elif k == 'admin_user':
+ v = get_object_or_none(AdminUser, name=v)
+ elif k in ['port', 'cabinet_pos', 'cpu_count', 'cpu_cores']:
+ try:
+ v = int(v)
+ except ValueError:
+ v = 0
+ elif k == 'groups':
+ groups_name = v.split(',')
+ v = AssetGroup.objects.filter(name__in=groups_name)
+ else:
continue
- asset.update(**asset_dict)
- updated.append(asset_dict['ip'])
- except TypeError as e:
- print(e)
- failed.append(asset_dict['ip'])
+ asset_dict[k] = v
+
+ if not asset:
+ try:
+ groups = asset_dict.pop('groups')
+ asset = Asset.objects.create(**asset_dict)
+ asset.groups.set(groups)
+ created.append(asset_dict['hostname'])
+ except IndexError as e:
+ failed.append('%s: %s' % (asset_dict['hostname'], str(e)))
+ else:
+ for k, v in asset_dict.items():
+ if k == 'groups':
+ asset.groups.set(v)
+ continue
+ if v:
+ setattr(asset, k, v)
+ try:
+ asset.save()
+ updated.append(asset_dict['hostname'])
+ except Exception as e:
+ failed.append('%s: %s' % (asset_dict['hostname'], str(e)))
data = {
'created': created,
@@ -324,7 +328,8 @@ class BulkImportAssetView(AdminUserRequiredMixin, JSONResponseMixin, FormView):
'failed': failed,
'failed_info': 'Failed {}'.format(len(failed)),
'valid': True,
- 'msg': 'Created: {}. Updated: {}, Error: {}'.format(len(created), len(updated), len(failed))
+ 'msg': 'Created: {}. Updated: {}, Error: {}'.format(
+ len(created), len(updated), len(failed))
}
return self.render_json_response(data)
diff --git a/apps/jumpserver/settings.py b/apps/jumpserver/settings.py
index 83ead6169..a81ab5ab0 100644
--- a/apps/jumpserver/settings.py
+++ b/apps/jumpserver/settings.py
@@ -260,7 +260,7 @@ MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media').replace('\\', '/') + '/'
# Use django-bootstrap-form to format template, input max width arg
-BOOTSTRAP_COLUMN_COUNT = 11
+# BOOTSTRAP_COLUMN_COUNT = 11
# Init data or generate fake data source for development
FIXTURE_DIRS = [os.path.join(BASE_DIR, 'fixtures'), ]
diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo
index 1c72f8960..4772033de 100644
Binary files a/apps/locale/zh/LC_MESSAGES/django.mo and b/apps/locale/zh/LC_MESSAGES/django.mo differ
diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po
index 23ab70107..dc9099344 100644
--- a/apps/locale/zh/LC_MESSAGES/django.po
+++ b/apps/locale/zh/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Jumpserver 0.3.3\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-04-07 17:03+0800\n"
+"POT-Creation-Date: 2017-04-08 21:58+0800\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: ibuler {% trans 'Role' %}:
{{ user_object.get_role_display }}
+
+
{% trans 'Enable OTP' %}:
+ {{ user_object.enable_otp|yesno:"Yes,No,Unknown"}}
+
{% trans 'Date expired' %}:
{{ user_object.date_expired|date:"Y-m-j H:i:s" }}
diff --git a/apps/users/templates/users/user_pubkey_update.html b/apps/users/templates/users/user_pubkey_update.html
index de10272c7..ee621eb8d 100644
--- a/apps/users/templates/users/user_pubkey_update.html
+++ b/apps/users/templates/users/user_pubkey_update.html
@@ -61,6 +61,7 @@
+
{% trans 'Update public key' %}
{% bootstrap_field form.public_key layout="horizontal" %}
diff --git a/apps/users/views/group.py b/apps/users/views/group.py
index dda420606..12697fece 100644
--- a/apps/users/views/group.py
+++ b/apps/users/views/group.py
@@ -103,7 +103,7 @@ class UserGroupDetailView(AdminUserRequiredMixin, DetailView):
users = User.objects.exclude(id__in=self.object.users.all())
context = {
'app': _('Users'),
- 'action': _('User Group Detail'),
+ 'action': _('User group detail'),
'users': users,
}
kwargs.update(context)
diff --git a/apps/users/views/user.py b/apps/users/views/user.py
index 8e0d881e9..e5c2a25c9 100644
--- a/apps/users/views/user.py
+++ b/apps/users/views/user.py
@@ -4,10 +4,9 @@ from __future__ import unicode_literals
import json
import uuid
-
-from openpyxl import load_workbook
-from openpyxl import Workbook
-from openpyxl.writer.excel import save_virtual_workbook
+import csv
+import codecs
+from io import StringIO
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.messages.views import SuccessMessageMixin
@@ -31,7 +30,7 @@ from .. import forms
from ..models import User, UserGroup
from ..utils import AdminUserRequiredMixin, user_add_success_next
from common.mixins import JSONResponseMixin
-from common.utils import get_logger
+from common.utils import get_logger, get_object_or_none
from perms.models import AssetPermission
__all__ = ['UserListView', 'UserCreateView', 'UserDetailView',
@@ -123,34 +122,44 @@ class UserDetailView(AdminUserRequiredMixin, DetailView):
return super(UserDetailView, self).get_context_data(**kwargs)
+USER_ATTR_MAPPING = (
+ ('name', 'Name'),
+ ('username', 'Username'),
+ ('email', 'Email'),
+ ('groups', 'User groups'),
+ ('role', 'Role'),
+ ('phone', 'Phone'),
+ ('wechat', 'Wechat'),
+ ('comment', 'Comment'),
+)
+
+
@method_decorator(csrf_exempt, name='dispatch')
class UserExportView(View):
def get(self, request):
+ mapping = [
+ (k, _(v)) for k, v in USER_ATTR_MAPPING
+ ]
spm = request.GET.get('spm', '')
- users_id = cache.get(spm)
- if not users_id and not isinstance(users_id, list):
- return HttpResponse('May be expired', status=404)
-
+ users_id = cache.get(spm, ['1'])
+ filename = 'users-{}.csv'.format(
+ timezone.localtime(timezone.now()).strftime('%Y-%m-%d_%H-%M-%S'))
+ response = HttpResponse(content_type='text/csv')
+ response['Content-Disposition'] = 'attachment; filename="%s"' % filename
+ response.write(codecs.BOM_UTF8)
users = User.objects.filter(id__in=users_id)
- print(users)
- wb = Workbook()
- ws = wb.active
- ws.title = 'User'
- header = ["name", 'username', 'email', 'groups',
- "role", "phone", "wechat", "comment"]
- ws.append(header)
+ writer = csv.writer(response, dialect='excel', quoting=csv.QUOTE_MINIMAL)
+
+ header = [v for k, v in mapping]
+ writer.writerow(header)
for user in users:
- ws.append([user.name, user.username, user.email,
- ','.join([group.name for group in user.groups.all()]),
- user.role, user.phone, user.wechat, user.comment])
+ groups = ','.join([group.name for group in user.groups.all()])
+ writer.writerow([
+ user.name, user.username, user.email, groups,
+ user.role, user.phone, user.wechat, user.comment
+ ])
- filename = 'users-{}.xlsx'.format(
- timezone.localtime(timezone.now()).strftime('%Y-%m-%d_%H-%M-%S'))
- response = HttpResponse(save_virtual_workbook(wb),
- content_type='applications/vnd.ms-excel')
- response[
- 'Content-Disposition'] = 'attachment; filename="%s"' % filename
return response
def post(self, request):
@@ -158,7 +167,7 @@ class UserExportView(View):
users_id = json.loads(request.body).get('users_id', [])
except ValueError:
return HttpResponse('Json object not valid', status=400)
- spm = uuid.uuid4().get_hex()
+ spm = uuid.uuid4().hex
cache.set(spm, users_id, 300)
url = reverse('users:user-export') + '?spm=%s' % spm
return JsonResponse({'redirect': url})
@@ -179,54 +188,51 @@ class UserBulkImportView(AdminUserRequiredMixin, JSONResponseMixin, FormView):
return self.render_json_response(data)
def form_valid(self, form):
- try:
- wb = load_workbook(form.cleaned_data['file'])
- ws = wb.get_active_sheet()
- except Exception as e:
- print(e)
- data = {'valid': False, 'msg': 'Not a valid Excel file'}
+ file = form.cleaned_data['file']
+ data = file.read().decode('utf-8').strip(codecs.BOM_UTF8.decode('utf-8'))
+ csv_file = StringIO(data)
+ reader = csv.reader(csv_file)
+ csv_data = [row for row in reader]
+ header_ = csv_data[0]
+ mapping_reverse = {_(v): k for k, v in USER_ATTR_MAPPING}
+ user_attr = [mapping_reverse.get(n, None) for n in header_]
+ if None in user_attr:
+ data = {'valid': False,
+ 'msg': 'Must be same format as '
+ 'template or export file'}
return self.render_json_response(data)
- rows = ws.rows
- header_need = ["name", 'username', 'email', 'groups',
- "role", "phone", "wechat", "comment"]
- header = [col.value for col in next(rows)]
- print(header)
- if header != header_need:
- data = {'valid': False, 'msg': 'Must be same format as '
- 'template or export file'}
- return self.render_json_response(data)
-
- created = []
- updated = []
- failed = []
- for row in rows:
- user_dict = dict(zip(header, [col.value for col in row]))
+ created, updated, failed = [], [], []
+ for row in csv_data[1:]:
+ if set(row) == {''}:
+ continue
+ user_dict = dict(zip(user_attr, row))
groups_name = user_dict.pop('groups')
if groups_name:
groups_name = groups_name.split(',')
groups = UserGroup.objects.filter(name__in=groups_name)
else:
groups = None
- try:
- user = User.objects.create(**user_dict)
- user_add_success_next(user)
- created.append(user_dict['username'])
- except User.IntegrityError as e:
- user = User.objects.filter(username=user_dict['username'])
- if not user:
- failed.append(user_dict['username'])
- continue
- user.update(**user_dict)
- user = user[0]
- updated.append(user_dict['username'])
- except TypeError as e:
- print(e)
- failed.append(user_dict['username'])
- user = None
-
+ username = user_dict['username']
+ user = get_object_or_none(User, username=username)
+ if not user:
+ try:
+ user = User.objects.create(**user_dict)
+ created.append(user_dict['username'])
+ user_add_success_next(user)
+ except Exception as e:
+ failed.append('%s: %s' % (user_dict['username'], str(e)))
+ else:
+ for k, v in user_dict.items():
+ if v:
+ setattr(user, k, v)
+ try:
+ user.save()
+ updated.append(user_dict['username'])
+ except Exception as e:
+ failed.append('%s: %s' % (user_dict['username'], str(e)))
if user and groups:
- user.groups.add(*tuple(groups))
+ user.groups = groups
user.save()
data = {