diff --git a/apps/assets/api/__init__.py b/apps/assets/api/__init__.py index 4a41c0706..a1a3beee7 100644 --- a/apps/assets/api/__init__.py +++ b/apps/assets/api/__init__.py @@ -3,3 +3,4 @@ from .asset import * from .label import * from .system_user import * from .node import * +from .domain import * diff --git a/apps/assets/api/admin_user.py b/apps/assets/api/admin_user.py index a69d771b3..e32bbe02a 100644 --- a/apps/assets/api/admin_user.py +++ b/apps/assets/api/admin_user.py @@ -28,7 +28,8 @@ from ..tasks import test_admin_user_connectability_manual logger = get_logger(__file__) __all__ = [ - 'AdminUserViewSet', 'ReplaceNodesAdminUserApi', 'AdminUserTestConnectiveApi' + 'AdminUserViewSet', 'ReplaceNodesAdminUserApi', + 'AdminUserTestConnectiveApi', 'AdminUserAuthApi', ] @@ -41,6 +42,12 @@ class AdminUserViewSet(IDInFilterMixin, BulkModelViewSet): permission_classes = (IsSuperUser,) +class AdminUserAuthApi(generics.UpdateAPIView): + queryset = AdminUser.objects.all() + serializer_class = serializers.AdminUserAuthSerializer + permission_classes = (IsSuperUser,) + + class ReplaceNodesAdminUserApi(generics.UpdateAPIView): queryset = AdminUser.objects.all() serializer_class = serializers.ReplaceNodeAdminUserSerializer diff --git a/apps/assets/api/domain.py b/apps/assets/api/domain.py new file mode 100644 index 000000000..5114b5561 --- /dev/null +++ b/apps/assets/api/domain.py @@ -0,0 +1,55 @@ +# ~*~ coding: utf-8 ~*~ + +from rest_framework_bulk import BulkModelViewSet +from rest_framework.views import APIView, Response +from rest_framework.generics import RetrieveAPIView + +from django.views.generic.detail import SingleObjectMixin + +from common.utils import get_logger +from ..hands import IsSuperUser, IsSuperUserOrAppUser +from ..models import Domain, Gateway +from ..utils import test_gateway_connectability +from .. import serializers + + +logger = get_logger(__file__) +__all__ = ['DomainViewSet', 'GatewayViewSet', "GatewayTestConnectionApi"] + + +class DomainViewSet(BulkModelViewSet): + queryset = Domain.objects.all() + permission_classes = (IsSuperUser,) + serializer_class = serializers.DomainSerializer + + def get_serializer_class(self): + if self.request.query_params.get('gateway'): + return serializers.DomainWithGatewaySerializer + return super().get_serializer_class() + + def get_permissions(self): + if self.request.query_params.get('gateway'): + self.permission_classes = (IsSuperUserOrAppUser,) + return super().get_permissions() + + +class GatewayViewSet(BulkModelViewSet): + filter_fields = ("domain",) + search_fields = filter_fields + queryset = Gateway.objects.all() + permission_classes = (IsSuperUser,) + serializer_class = serializers.GatewaySerializer + + +class GatewayTestConnectionApi(SingleObjectMixin, APIView): + permission_classes = (IsSuperUser,) + model = Gateway + object = None + + def get(self, request, *args, **kwargs): + self.object = self.get_object(Gateway.objects.all()) + ok, e = test_gateway_connectability(self.object) + if ok: + return Response("ok") + else: + return Response({"failed": e}, status=404) diff --git a/apps/assets/api/system_user.py b/apps/assets/api/system_user.py index dd92afad4..ca2a6b7f0 100644 --- a/apps/assets/api/system_user.py +++ b/apps/assets/api/system_user.py @@ -48,15 +48,6 @@ class SystemUserAuthInfoApi(generics.RetrieveUpdateAPIView): permission_classes = (IsSuperUserOrAppUser,) serializer_class = serializers.SystemUserAuthSerializer - def update(self, request, *args, **kwargs): - password = request.data.pop("password", None) - private_key = request.data.pop("private_key", None) - instance = self.get_object() - - if password or private_key: - instance.set_auth(password=password, private_key=private_key) - return super().update(request, *args, **kwargs) - class SystemUserPushApi(generics.RetrieveAPIView): """ diff --git a/apps/assets/forms/__init__.py b/apps/assets/forms/__init__.py index 9175d7c43..4eaa40948 100644 --- a/apps/assets/forms/__init__.py +++ b/apps/assets/forms/__init__.py @@ -3,3 +3,4 @@ from .asset import * from .label import * from .user import * +from .domain import * diff --git a/apps/assets/forms/asset.py b/apps/assets/forms/asset.py index 65716f533..a6f488761 100644 --- a/apps/assets/forms/asset.py +++ b/apps/assets/forms/asset.py @@ -16,6 +16,7 @@ class AssetCreateForm(forms.ModelForm): fields = [ 'hostname', 'ip', 'public_ip', 'port', 'comment', 'nodes', 'is_active', 'admin_user', 'labels', 'platform', + 'domain', ] widgets = { @@ -29,6 +30,9 @@ class AssetCreateForm(forms.ModelForm): 'class': 'select2', 'data-placeholder': _('Labels') }), 'port': forms.TextInput(), + 'domain': forms.Select(attrs={ + 'class': 'select2', 'data-placeholder': _('Domain') + }), } help_texts = { 'hostname': '* required', @@ -38,7 +42,8 @@ class AssetCreateForm(forms.ModelForm): 'root or other NOPASSWD sudo privilege user existed in asset,' 'If asset is windows or other set any one, more see admin user left menu' ), - 'platform': _("* required Must set exact system platform, Windows, Linux ...") + 'platform': _("* required Must set exact system platform, Windows, Linux ..."), + 'domain': _("If your have some network not connect with each other, you can set domain") } @@ -48,6 +53,7 @@ class AssetUpdateForm(forms.ModelForm): fields = [ 'hostname', 'ip', 'port', 'nodes', 'is_active', 'platform', 'public_ip', 'number', 'comment', 'admin_user', 'labels', + 'domain', ] widgets = { 'nodes': forms.SelectMultiple(attrs={ @@ -60,6 +66,9 @@ class AssetUpdateForm(forms.ModelForm): 'class': 'select2', 'data-placeholder': _('Labels') }), 'port': forms.TextInput(), + 'domain': forms.Select(attrs={ + 'class': 'select2', 'data-placeholder': _('Domain') + }), } help_texts = { 'hostname': '* required', @@ -70,7 +79,8 @@ class AssetUpdateForm(forms.ModelForm): 'root or other NOPASSWD sudo privilege user existed in asset,' 'If asset is windows or other set any one, more see admin user left menu' ), - 'platform': _("* required Must set exact system platform, Windows, Linux ...") + 'platform': _("* required Must set exact system platform, Windows, Linux ..."), + 'domain': _("If your have some network not connect with each other, you can set domain") } diff --git a/apps/assets/forms/domain.py b/apps/assets/forms/domain.py new file mode 100644 index 000000000..ec3af8f2e --- /dev/null +++ b/apps/assets/forms/domain.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +# +from django import forms +from django.utils.translation import gettext_lazy as _ + +from ..models import Domain, Asset, Gateway +from .user import PasswordAndKeyAuthForm + +__all__ = ['DomainForm', 'GatewayForm'] + + +class DomainForm(forms.ModelForm): + assets = forms.ModelMultipleChoiceField( + queryset=Asset.objects.all(), label=_('Asset'), required=False, + widget=forms.SelectMultiple( + attrs={'class': 'select2', 'data-placeholder': _('Select assets')} + ) + ) + + class Meta: + model = Domain + fields = ['name', 'comment', 'assets'] + + def __init__(self, *args, **kwargs): + if kwargs.get('instance', None): + initial = kwargs.get('initial', {}) + initial['assets'] = kwargs['instance'].assets.all() + super().__init__(*args, **kwargs) + + def save(self, commit=True): + instance = super().save(commit=commit) + assets = self.cleaned_data['assets'] + instance.assets.set(assets) + return instance + + +class GatewayForm(PasswordAndKeyAuthForm): + + def save(self, commit=True): + # Because we define custom field, so we need rewrite :method: `save` + instance = super().save() + password = self.cleaned_data.get('password') + private_key, public_key = super().gen_keys() + instance.set_auth(password=password, private_key=private_key) + return instance + + class Meta: + model = Gateway + fields = [ + 'name', 'ip', 'port', 'username', 'protocol', 'domain', 'password', + 'private_key_file', 'is_active', 'comment', + ] + widgets = { + 'name': forms.TextInput(attrs={'placeholder': _('Name')}), + 'username': forms.TextInput(attrs={'placeholder': _('Username')}), + } + help_texts = { + 'name': '* required', + 'username': '* required', + } diff --git a/apps/assets/forms/user.py b/apps/assets/forms/user.py index 487d2714b..2295dc005 100644 --- a/apps/assets/forms/user.py +++ b/apps/assets/forms/user.py @@ -8,7 +8,7 @@ from common.utils import validate_ssh_private_key, ssh_pubkey_gen, get_logger logger = get_logger(__file__) __all__ = [ - 'FileForm', 'SystemUserForm', 'AdminUserForm', + 'FileForm', 'SystemUserForm', 'AdminUserForm', 'PasswordAndKeyAuthForm', ] diff --git a/apps/assets/models/__init__.py b/apps/assets/models/__init__.py index dbc703706..35c275b99 100644 --- a/apps/assets/models/__init__.py +++ b/apps/assets/models/__init__.py @@ -5,6 +5,7 @@ from .user import AdminUser, SystemUser from .label import Label from .cluster import * from .group import * +from .domain import * from .node import * from .asset import * from .utils import * diff --git a/apps/assets/models/asset.py b/apps/assets/models/asset.py index 3f7dec4ec..b8c1f768e 100644 --- a/apps/assets/models/asset.py +++ b/apps/assets/models/asset.py @@ -4,14 +4,13 @@ import uuid import logging +import random from django.db import models from django.utils.translation import ugettext_lazy as _ from django.core.cache import cache from ..const import ASSET_ADMIN_CONN_CACHE_KEY -from .cluster import Cluster -from .group import AssetGroup from .user import AdminUser, SystemUser __all__ = ['Asset'] @@ -50,6 +49,7 @@ class Asset(models.Model): ip = models.GenericIPAddressField(max_length=32, verbose_name=_('IP'), db_index=True) hostname = models.CharField(max_length=128, unique=True, verbose_name=_('Hostname')) port = models.IntegerField(default=22, verbose_name=_('Port')) + domain = models.ForeignKey("assets.Domain", null=True, blank=True, related_name='assets', verbose_name=_("Domain")) nodes = models.ManyToManyField('assets.Node', default=default_node, related_name='assets', verbose_name=_("Nodes")) is_active = models.BooleanField(default=True, verbose_name=_('Is active')) @@ -122,12 +122,15 @@ class Asset(models.Model): return False def to_json(self): - return { + info = { 'id': self.id, 'hostname': self.hostname, 'ip': self.ip, 'port': self.port, } + if self.domain and self.domain.gateway_set.all(): + info["gateways"] = [d.id for d in self.domain.gateway_set.all()] + return info def _to_secret_json(self): """ @@ -168,7 +171,6 @@ class Asset(models.Model): try: asset.save() asset.system_users = [choice(SystemUser.objects.all()) for i in range(3)] - asset.groups = [choice(AssetGroup.objects.all()) for i in range(3)] logger.debug('Generate fake asset : %s' % asset.ip) except IntegrityError: print('Error continue') diff --git a/apps/assets/models/base.py b/apps/assets/models/base.py new file mode 100644 index 000000000..ad2a3b904 --- /dev/null +++ b/apps/assets/models/base.py @@ -0,0 +1,126 @@ +# -*- coding: utf-8 -*- +# +import os +import uuid +from hashlib import md5 + +import sshpubkeys +from django.db import models +from django.utils.translation import ugettext_lazy as _ +from django.conf import settings + +from common.utils import get_signer, ssh_key_string_to_obj, ssh_key_gen +from .utils import private_key_validator + +signer = get_signer() + + +class AssetUser(models.Model): + id = models.UUIDField(default=uuid.uuid4, primary_key=True) + name = models.CharField(max_length=128, unique=True, verbose_name=_('Name')) + username = models.CharField(max_length=128, verbose_name=_('Username')) + _password = models.CharField(max_length=256, blank=True, null=True, verbose_name=_('Password')) + _private_key = models.TextField(max_length=4096, blank=True, null=True, verbose_name=_('SSH private key'), validators=[private_key_validator, ]) + _public_key = models.TextField(max_length=4096, blank=True, verbose_name=_('SSH public key')) + comment = models.TextField(blank=True, verbose_name=_('Comment')) + date_created = models.DateTimeField(auto_now_add=True) + date_updated = models.DateTimeField(auto_now=True) + created_by = models.CharField(max_length=128, null=True, verbose_name=_('Created by')) + + @property + def password(self): + if self._password: + return signer.unsign(self._password) + else: + return None + + @password.setter + def password(self, password_raw): + raise AttributeError("Using set_auth do that") + # self._password = signer.sign(password_raw) + + @property + def private_key(self): + if self._private_key: + return signer.unsign(self._private_key) + + @private_key.setter + def private_key(self, private_key_raw): + raise AttributeError("Using set_auth do that") + # self._private_key = signer.sign(private_key_raw) + + @property + def private_key_obj(self): + if self._private_key: + key_str = signer.unsign(self._private_key) + return ssh_key_string_to_obj(key_str, password=self.password) + else: + return None + + @property + def private_key_file(self): + if not self.private_key_obj: + return None + project_dir = settings.PROJECT_DIR + tmp_dir = os.path.join(project_dir, 'tmp') + key_str = signer.unsign(self._private_key) + key_name = '.' + md5(key_str.encode('utf-8')).hexdigest() + key_path = os.path.join(tmp_dir, key_name) + if not os.path.exists(key_path): + self.private_key_obj.write_private_key_file(key_path) + os.chmod(key_path, 0o400) + return key_path + + @property + def public_key(self): + key = signer.unsign(self._public_key) + if key: + return key + else: + return None + + @property + def public_key_obj(self): + if self.public_key: + try: + return sshpubkeys.SSHKey(self.public_key) + except TabError: + pass + return None + + def set_auth(self, password=None, private_key=None, public_key=None): + update_fields = [] + if password: + self._password = signer.sign(password) + update_fields.append('_password') + if private_key: + self._private_key = signer.sign(private_key) + update_fields.append('_private_key') + if public_key: + self._public_key = signer.sign(public_key) + update_fields.append('_public_key') + + if update_fields: + self.save(update_fields=update_fields) + + def auto_gen_auth(self): + password = str(uuid.uuid4()) + private_key, public_key = ssh_key_gen( + username=self.name, password=password + ) + self.set_auth(password=password, + private_key=private_key, + public_key=public_key) + + def _to_secret_json(self): + """Push system user use it""" + return { + 'name': self.name, + 'username': self.username, + 'password': self.password, + 'public_key': self.public_key, + 'private_key': self.private_key_file, + } + + class Meta: + abstract = True diff --git a/apps/assets/models/domain.py b/apps/assets/models/domain.py new file mode 100644 index 000000000..6f29a0381 --- /dev/null +++ b/apps/assets/models/domain.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +# + +import uuid +import random + +from django.db import models +from django.utils.translation import ugettext_lazy as _ + +from .base import AssetUser + +__all__ = ['Domain', 'Gateway'] + + +class Domain(models.Model): + id = models.UUIDField(default=uuid.uuid4, primary_key=True) + name = models.CharField(max_length=128, unique=True, verbose_name=_('Name')) + comment = models.TextField(blank=True, verbose_name=_('Comment')) + date_created = models.DateTimeField(auto_now_add=True, null=True, + verbose_name=_('Date created')) + + def __str__(self): + return self.name + + def has_gateway(self): + return self.gateway_set.filter(is_active=True).exists() + + @property + def gateways(self): + return self.gateway_set.filter(is_active=True) + + def random_gateway(self): + return random.choice(self.gateways) + + +class Gateway(AssetUser): + SSH_PROTOCOL = 'ssh' + RDP_PROTOCOL = 'rdp' + PROTOCOL_CHOICES = ( + (SSH_PROTOCOL, 'ssh'), + (RDP_PROTOCOL, 'rdp'), + ) + ip = models.GenericIPAddressField(max_length=32, verbose_name=_('IP'), db_index=True) + port = models.IntegerField(default=22, verbose_name=_('Port')) + protocol = models.CharField(choices=PROTOCOL_CHOICES, max_length=16, default=SSH_PROTOCOL, verbose_name=_("Protocol")) + domain = models.ForeignKey(Domain, verbose_name=_("Domain")) + comment = models.CharField(max_length=128, blank=True, null=True, verbose_name=_("Comment")) + is_active = models.BooleanField(default=True, verbose_name=_("Is active")) + + def __str__(self): + return self.name + diff --git a/apps/assets/models/user.py b/apps/assets/models/user.py index 7eaff63e6..541ef8b6a 100644 --- a/apps/assets/models/user.py +++ b/apps/assets/models/user.py @@ -2,20 +2,15 @@ # -*- coding: utf-8 -*- # -import os import logging -import uuid -from hashlib import md5 -import sshpubkeys from django.core.cache import cache from django.db import models from django.utils.translation import ugettext_lazy as _ -from django.conf import settings -from common.utils import get_signer, ssh_key_string_to_obj, ssh_key_gen -from .utils import private_key_validator +from common.utils import get_signer from ..const import SYSTEM_USER_CONN_CACHE_KEY +from .base import AssetUser __all__ = ['AdminUser', 'SystemUser',] @@ -23,117 +18,6 @@ logger = logging.getLogger(__name__) signer = get_signer() -class AssetUser(models.Model): - id = models.UUIDField(default=uuid.uuid4, primary_key=True) - name = models.CharField(max_length=128, unique=True, verbose_name=_('Name')) - username = models.CharField(max_length=128, verbose_name=_('Username')) - _password = models.CharField(max_length=256, blank=True, null=True, verbose_name=_('Password')) - _private_key = models.TextField(max_length=4096, blank=True, null=True, verbose_name=_('SSH private key'), validators=[private_key_validator, ]) - _public_key = models.TextField(max_length=4096, blank=True, verbose_name=_('SSH public key')) - comment = models.TextField(blank=True, verbose_name=_('Comment')) - date_created = models.DateTimeField(auto_now_add=True) - date_updated = models.DateTimeField(auto_now=True) - created_by = models.CharField(max_length=128, null=True, verbose_name=_('Created by')) - - @property - def password(self): - if self._password: - return signer.unsign(self._password) - else: - return None - - @password.setter - def password(self, password_raw): - raise AttributeError("Using set_auth do that") - # self._password = signer.sign(password_raw) - - @property - def private_key(self): - if self._private_key: - return signer.unsign(self._private_key) - - @private_key.setter - def private_key(self, private_key_raw): - raise AttributeError("Using set_auth do that") - # self._private_key = signer.sign(private_key_raw) - - @property - def private_key_obj(self): - if self._private_key: - key_str = signer.unsign(self._private_key) - return ssh_key_string_to_obj(key_str, password=self.password) - else: - return None - - @property - def private_key_file(self): - if not self.private_key_obj: - return None - project_dir = settings.PROJECT_DIR - tmp_dir = os.path.join(project_dir, 'tmp') - key_str = signer.unsign(self._private_key) - key_name = '.' + md5(key_str.encode('utf-8')).hexdigest() - key_path = os.path.join(tmp_dir, key_name) - if not os.path.exists(key_path): - self.private_key_obj.write_private_key_file(key_path) - os.chmod(key_path, 0o400) - return key_path - - @property - def public_key(self): - key = signer.unsign(self._public_key) - if key: - return key - else: - return None - - @property - def public_key_obj(self): - if self.public_key: - try: - return sshpubkeys.SSHKey(self.public_key) - except TabError: - pass - return None - - def set_auth(self, password=None, private_key=None, public_key=None): - update_fields = [] - if password: - self._password = signer.sign(password) - update_fields.append('_password') - if private_key: - self._private_key = signer.sign(private_key) - update_fields.append('_private_key') - if public_key: - self._public_key = signer.sign(public_key) - update_fields.append('_public_key') - - if update_fields: - self.save(update_fields=update_fields) - - def auto_gen_auth(self): - password = str(uuid.uuid4()) - private_key, public_key = ssh_key_gen( - username=self.name, password=password - ) - self.set_auth(password=password, - private_key=private_key, - public_key=public_key) - - def _to_secret_json(self): - """Push system user use it""" - return { - 'name': self.name, - 'username': self.username, - 'password': self.password, - 'public_key': self.public_key, - 'private_key': self.private_key_file, - } - - class Meta: - abstract = True - - class AdminUser(AssetUser): """ A privileged user that ansible can use it to push system user and so on diff --git a/apps/assets/models/utils.py b/apps/assets/models/utils.py index a819715ab..11d347685 100644 --- a/apps/assets/models/utils.py +++ b/apps/assets/models/utils.py @@ -10,15 +10,15 @@ __all__ = ['init_model', 'generate_fake'] def init_model(): - from . import Cluster, SystemUser, AdminUser, AssetGroup, Asset - for cls in [Cluster, SystemUser, AdminUser, AssetGroup, Asset]: + from . import SystemUser, AdminUser, Asset + for cls in [SystemUser, AdminUser, Asset]: if hasattr(cls, 'initial'): cls.initial() def generate_fake(): - from . import Cluster, SystemUser, AdminUser, AssetGroup, Asset - for cls in [Cluster, SystemUser, AdminUser, AssetGroup, Asset]: + from . import SystemUser, AdminUser, Asset + for cls in [SystemUser, AdminUser, Asset]: if hasattr(cls, 'generate_fake'): cls.generate_fake() diff --git a/apps/assets/serializers/__init__.py b/apps/assets/serializers/__init__.py index c39d34767..111f070a7 100644 --- a/apps/assets/serializers/__init__.py +++ b/apps/assets/serializers/__init__.py @@ -6,3 +6,4 @@ from .admin_user import * from .label import * from .system_user import * from .node import * +from .domain import * diff --git a/apps/assets/serializers/admin_user.py b/apps/assets/serializers/admin_user.py index d59ca64ae..dbd0d1b39 100644 --- a/apps/assets/serializers/admin_user.py +++ b/apps/assets/serializers/admin_user.py @@ -2,9 +2,12 @@ # from django.core.cache import cache from rest_framework import serializers + from ..models import Node, AdminUser from ..const import ADMIN_USER_CONN_CACHE_KEY +from .base import AuthSerializer + class AdminUserSerializer(serializers.ModelSerializer): """ @@ -18,6 +21,10 @@ class AdminUserSerializer(serializers.ModelSerializer): model = AdminUser fields = '__all__' + def get_field_names(self, declared_fields, info): + fields = super().get_field_names(declared_fields, info) + return [f for f in fields if not f.startswith('_')] + @staticmethod def get_unreachable_amount(obj): data = cache.get(ADMIN_USER_CONN_CACHE_KEY.format(obj.name)) @@ -39,6 +46,13 @@ class AdminUserSerializer(serializers.ModelSerializer): return obj.assets_amount +class AdminUserAuthSerializer(AuthSerializer): + + class Meta: + model = AdminUser + fields = ['password', 'private_key'] + + class ReplaceNodeAdminUserSerializer(serializers.ModelSerializer): """ 管理用户更新关联到的集群 @@ -50,3 +64,6 @@ class ReplaceNodeAdminUserSerializer(serializers.ModelSerializer): class Meta: model = AdminUser fields = ['id', 'nodes'] + + + diff --git a/apps/assets/serializers/asset.py b/apps/assets/serializers/asset.py index e479a7149..bcb3b81b1 100644 --- a/apps/assets/serializers/asset.py +++ b/apps/assets/serializers/asset.py @@ -38,7 +38,7 @@ class AssetGrantedSerializer(serializers.ModelSerializer): model = Asset fields = ( "id", "hostname", "ip", "port", "system_users_granted", - "is_active", "system_users_join", "os", + "is_active", "system_users_join", "os", 'domain', "platform", "comment" ) diff --git a/apps/assets/serializers/base.py b/apps/assets/serializers/base.py new file mode 100644 index 000000000..bb34813b6 --- /dev/null +++ b/apps/assets/serializers/base.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# + +from rest_framework import serializers +from common.utils import ssh_pubkey_gen + + +class AuthSerializer(serializers.ModelSerializer): + password = serializers.CharField(required=False, allow_blank=True, allow_null=True, max_length=1024) + private_key = serializers.CharField(required=False, allow_blank=True, allow_null=True, max_length=4096) + + def gen_keys(self, private_key=None, password=None): + if private_key is None: + return None, None + public_key = ssh_pubkey_gen(private_key=private_key, password=password) + return private_key, public_key + + def save(self, **kwargs): + password = self.validated_data.pop('password', None) or None + private_key = self.validated_data.pop('private_key', None) or None + self.instance = super().save(**kwargs) + if password or private_key: + private_key, public_key = self.gen_keys(private_key, password) + self.instance.set_auth(password=password, private_key=private_key, + public_key=public_key) + return self.instance diff --git a/apps/assets/serializers/cluster.py b/apps/assets/serializers/cluster.py deleted file mode 100644 index 43724a4a2..000000000 --- a/apps/assets/serializers/cluster.py +++ /dev/null @@ -1,46 +0,0 @@ -# -*- coding: utf-8 -*- -# - -from rest_framework import serializers -from common.mixins import BulkSerializerMixin -from ..models import Asset, Cluster - - -class ClusterUpdateAssetsSerializer(serializers.ModelSerializer): - """ - 集群更新资产数据结构 - """ - assets = serializers.PrimaryKeyRelatedField(many=True, queryset=Asset.objects.all()) - - class Meta: - model = Cluster - fields = ['id', 'assets'] - - -class ClusterSerializer(BulkSerializerMixin, serializers.ModelSerializer): - """ - cluster - """ - assets_amount = serializers.SerializerMethodField() - admin_user_name = serializers.SerializerMethodField() - assets = serializers.PrimaryKeyRelatedField(many=True, queryset=Asset.objects.all()) - system_users = serializers.SerializerMethodField() - - class Meta: - model = Cluster - fields = '__all__' - - @staticmethod - def get_assets_amount(obj): - return obj.assets.count() - - @staticmethod - def get_admin_user_name(obj): - try: - return obj.admin_user.name - except AttributeError: - return '' - - @staticmethod - def get_system_users(obj): - return ', '.join(obj.name for obj in obj.systemuser_set.all()) diff --git a/apps/assets/serializers/domain.py b/apps/assets/serializers/domain.py new file mode 100644 index 000000000..034c29387 --- /dev/null +++ b/apps/assets/serializers/domain.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +# +from rest_framework import serializers + +from ..models import Domain, Gateway + + +class DomainSerializer(serializers.ModelSerializer): + asset_count = serializers.SerializerMethodField() + gateway_count = serializers.SerializerMethodField() + + class Meta: + model = Domain + fields = '__all__' + + @staticmethod + def get_asset_count(obj): + return obj.assets.count() + + @staticmethod + def get_gateway_count(obj): + return obj.gateway_set.all().count() + + +class GatewaySerializer(serializers.ModelSerializer): + + class Meta: + model = Gateway + fields = [ + 'id', 'name', 'ip', 'port', 'protocol', 'username', + 'domain', 'is_active', 'date_created', 'date_updated', + 'created_by', 'comment', + ] + + +class GatewayWithAuthSerializer(GatewaySerializer): + def get_field_names(self, declared_fields, info): + fields = super().get_field_names(declared_fields, info) + fields.extend( + ['password', 'private_key'] + ) + return fields + + +class DomainWithGatewaySerializer(serializers.ModelSerializer): + gateways = GatewayWithAuthSerializer(many=True, read_only=True) + + class Meta: + model = Domain + fields = '__all__' diff --git a/apps/assets/serializers/system_user.py b/apps/assets/serializers/system_user.py index 38aad66ce..1dff79422 100644 --- a/apps/assets/serializers/system_user.py +++ b/apps/assets/serializers/system_user.py @@ -1,6 +1,7 @@ from rest_framework import serializers from ..models import SystemUser +from .base import AuthSerializer class SystemUserSerializer(serializers.ModelSerializer): @@ -36,12 +37,10 @@ class SystemUserSerializer(serializers.ModelSerializer): return len(obj.assets) -class SystemUserAuthSerializer(serializers.ModelSerializer): +class SystemUserAuthSerializer(AuthSerializer): """ 系统用户认证信息 """ - password = serializers.CharField(max_length=1024) - private_key = serializers.CharField(max_length=4096) class Meta: model = SystemUser diff --git a/apps/assets/tasks.py b/apps/assets/tasks.py index 0cf5cbfd8..e47d15d41 100644 --- a/apps/assets/tasks.py +++ b/apps/assets/tasks.py @@ -3,6 +3,7 @@ import json import re import os +import paramiko from celery import shared_task from django.core.cache import cache from django.utils.translation import ugettext as _ @@ -12,7 +13,7 @@ from common.utils import get_object_or_none, capacity_convert, \ from common.celery import register_as_period_task, after_app_shutdown_clean, \ after_app_ready_start, app as celery_app -from .models import SystemUser, AdminUser, Asset, Cluster +from .models import SystemUser, AdminUser, Asset from . import const @@ -395,6 +396,7 @@ def get_node_push_system_user_task_name(system_user, node): def push_system_user_to_node(system_user, node): + logger.info("Start push system user node: {} => {}".format(system_user.name, node.value)) assets = node.get_all_assets() task_name = get_node_push_system_user_task_name(system_user, node) push_system_user_util.delay([system_user], assets, task_name) @@ -438,3 +440,7 @@ def push_node_system_users_to_asset(node, assets): # def push_system_user_period(): # for system_user in SystemUser.objects.all(): # push_system_user_related_nodes(system_user) + + + + diff --git a/apps/assets/templates/assets/asset_create.html b/apps/assets/templates/assets/asset_create.html index 1eae7ebd4..7f01e0530 100644 --- a/apps/assets/templates/assets/asset_create.html +++ b/apps/assets/templates/assets/asset_create.html @@ -19,6 +19,7 @@ {% bootstrap_field form.port layout="horizontal" %} {% bootstrap_field form.platform layout="horizontal" %} {% bootstrap_field form.public_ip layout="horizontal" %} + {% bootstrap_field form.domain layout="horizontal" %}

{% trans 'Auth' %}

diff --git a/apps/assets/templates/assets/asset_list.html b/apps/assets/templates/assets/asset_list.html index 3841b34bb..ec905b119 100644 --- a/apps/assets/templates/assets/asset_list.html +++ b/apps/assets/templates/assets/asset_list.html @@ -451,6 +451,14 @@ $(document).ready(function(){ }) .on('click', '#btn_asset_import', function () { var $form = $('#fm_asset_import'); + var action = $form.attr("action"); + var nodes = zTree.getSelectedNodes(); + var current_node; + if (nodes && nodes.length ===1 ){ + current_node = nodes[0]; + action += "?node_id=" + current_node.id; + $form.attr("action", action) + } $form.find('.help-block').remove(); function success (data) { if (data.valid === false) { diff --git a/apps/assets/templates/assets/asset_update.html b/apps/assets/templates/assets/asset_update.html index 5d8006451..3e96438bc 100644 --- a/apps/assets/templates/assets/asset_update.html +++ b/apps/assets/templates/assets/asset_update.html @@ -24,6 +24,7 @@ {% bootstrap_field form.port layout="horizontal" %} {% bootstrap_field form.platform layout="horizontal" %} {% bootstrap_field form.public_ip layout="horizontal" %} + {% bootstrap_field form.domain layout="horizontal" %}

{% trans 'Auth' %}

diff --git a/apps/assets/templates/assets/domain_create_update.html b/apps/assets/templates/assets/domain_create_update.html new file mode 100644 index 000000000..36ca40fc1 --- /dev/null +++ b/apps/assets/templates/assets/domain_create_update.html @@ -0,0 +1,31 @@ +{% extends '_base_create_update.html' %} +{% load static %} +{% load bootstrap3 %} +{% load i18n %} + +{% block form %} +
+ {% csrf_token %} + {% bootstrap_field form.name layout="horizontal" %} + {% bootstrap_field form.assets layout="horizontal" %} + {% bootstrap_field form.comment layout="horizontal" %} + +
+
+
+ + +
+
+
+{% endblock %} + +{% block custom_foot_js %} + +{% endblock %} \ No newline at end of file diff --git a/apps/assets/templates/assets/domain_detail.html b/apps/assets/templates/assets/domain_detail.html new file mode 100644 index 000000000..e60daa334 --- /dev/null +++ b/apps/assets/templates/assets/domain_detail.html @@ -0,0 +1,132 @@ +{% extends 'base.html' %} +{% load static %} +{% load i18n %} + +{% block custom_head_css_js %} + + +{% endblock %} + +{% block content %} +
+
+
+
+ +
+
+
+
+ {{ object.name }} +
+ + + + + + + + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
{% trans 'Name' %}:{{ object.name }}
{% trans 'Asset' %}:{{ object.assets.count }}
{% trans 'Gateway' %}:{{ object.gateway_set.count }}
{% trans 'Date created' %}:{{ object.date_created }}
{% trans 'Created by' %}:{{ object.created_by }}
{% trans 'Comment' %}:{{ object.comment }}
+
+
+
+
+
+
+
+
+{% endblock %} +{% block content_bottom_left %}{% endblock %} +{% block custom_foot_js %} + +{% endblock %} \ No newline at end of file diff --git a/apps/assets/templates/assets/domain_gateway_list.html b/apps/assets/templates/assets/domain_gateway_list.html new file mode 100644 index 000000000..1f25e6ac1 --- /dev/null +++ b/apps/assets/templates/assets/domain_gateway_list.html @@ -0,0 +1,131 @@ +{% extends 'base.html' %} +{% load static %} +{% load i18n %} + +{% block custom_head_css_js %} + + +{% endblock %} + +{% block content %} +
+
+
+
+ +
+
+
+
+
+
+ {% trans 'Gateway list' %} +
+ + + + + + + + + + +
+
+
+ + + + + + + + + + + + + + + + +
+ + {% trans 'Name' %}{% trans 'IP' %}{% trans 'Port' %}{% trans 'Protocol' %}{% trans 'Username' %}{% trans 'Comment' %}{% trans 'Action' %}
+
+
+
+
+
+
+
+
+{% endblock %} +{% block content_bottom_left %}{% endblock %} +{% block custom_foot_js %} + +{% endblock %} diff --git a/apps/assets/templates/assets/domain_list.html b/apps/assets/templates/assets/domain_list.html new file mode 100644 index 000000000..03b671bf3 --- /dev/null +++ b/apps/assets/templates/assets/domain_list.html @@ -0,0 +1,70 @@ +{% extends '_base_list.html' %} +{% load i18n static %} +{% block table_search %}{% endblock %} +{% block table_container %} +
+ {% trans "Create domain" %} +
+ + + + + + + + + + + + + +
+ + {% trans 'Name' %}{% trans 'Asset' %}{% trans 'Gateway' %}{% trans 'Comment' %}{% trans 'Action' %}
+{% endblock %} +{% block content_bottom_left %}{% endblock %} +{% block custom_foot_js %} + +{% endblock %} + + + diff --git a/apps/assets/templates/assets/gateway_create_update.html b/apps/assets/templates/assets/gateway_create_update.html new file mode 100644 index 000000000..7d6800c41 --- /dev/null +++ b/apps/assets/templates/assets/gateway_create_update.html @@ -0,0 +1,68 @@ +{% extends 'base.html' %} +{% load i18n %} +{% load static %} +{% load bootstrap3 %} +{% block custom_head_css_js %} + + +{% endblock %} + +{% block content %} +
+
+
+
+
+
{{ action }}
+ +
+
+
+ {% csrf_token %} + {% if form.non_field_errors %} +
+ {{ form.non_field_errors }} +
+ {% endif %} +

{% trans 'Basic' %}

+ {% bootstrap_field form.name layout="horizontal" %} + {% bootstrap_field form.ip layout="horizontal" %} + {% bootstrap_field form.port layout="horizontal" %} + {% bootstrap_field form.protocol layout="horizontal" %} + {% bootstrap_field form.domain layout="horizontal" %} + + {% block auth %} +

{% trans 'Auth' %}

+
+ {% bootstrap_field form.username layout="horizontal" %} + {% bootstrap_field form.password layout="horizontal" %} + {% bootstrap_field form.private_key_file layout="horizontal" %} +
+ {% endblock %} + +

{% trans 'Other' %}

+ {% bootstrap_field form.is_active layout="horizontal" %} + {% bootstrap_field form.comment layout="horizontal" %} +
+
+ + +
+
+
+
+
+
+
+
+{% endblock %} diff --git a/apps/assets/templates/assets/system_user_detail.html b/apps/assets/templates/assets/system_user_detail.html index 28d792bd2..b74b402cf 100644 --- a/apps/assets/templates/assets/system_user_detail.html +++ b/apps/assets/templates/assets/system_user_detail.html @@ -206,7 +206,7 @@ {% endblock %} {% block custom_foot_js %} {% endblock %} diff --git a/apps/users/templates/users/user_pubkey_update.html b/apps/users/templates/users/user_pubkey_update.html index ee621eb8d..82c4a3590 100644 --- a/apps/users/templates/users/user_pubkey_update.html +++ b/apps/users/templates/users/user_pubkey_update.html @@ -64,6 +64,12 @@

{% trans 'Update public key' %}

{% bootstrap_field form.public_key layout="horizontal" %} +
+ +
+ {% trans 'Reset' %} +
+
diff --git a/apps/users/templates/users/user_update.html b/apps/users/templates/users/user_update.html index a83d89859..88ec01c5c 100644 --- a/apps/users/templates/users/user_update.html +++ b/apps/users/templates/users/user_update.html @@ -5,4 +5,5 @@ {% block password %}

{% trans 'Auth' %}

{% bootstrap_field form.password layout="horizontal" %} + {% bootstrap_field form.public_key layout="horizontal" %} {% endblock %} diff --git a/apps/users/urls/views_urls.py b/apps/users/urls/views_urls.py index 5d23976ae..b9d6788ee 100644 --- a/apps/users/urls/views_urls.py +++ b/apps/users/urls/views_urls.py @@ -20,6 +20,7 @@ urlpatterns = [ url(r'^profile/update/$', views.UserProfileUpdateView.as_view(), name='user-profile-update'), url(r'^profile/password/update/$', views.UserPasswordUpdateView.as_view(), name='user-password-update'), url(r'^profile/pubkey/update/$', views.UserPublicKeyUpdateView.as_view(), name='user-pubkey-update'), + url(r'^profile/pubkey/generate/$', views.UserPublicKeyGenerateView.as_view(), name='user-pubkey-generate'), # User view url(r'^user$', views.UserListView.as_view(), name='user-list'), diff --git a/apps/users/utils.py b/apps/users/utils.py index abc4b2018..c8a5b60a8 100644 --- a/apps/users/utils.py +++ b/apps/users/utils.py @@ -200,7 +200,7 @@ def get_ip_city(ip, timeout=10): url = 'http://int.dpool.sina.com.cn/iplookup/iplookup.php?ip=%s&format=json' % ip try: r = requests.get(url, timeout=timeout) - except requests.Timeout: + except: r = None city = 'Unknown' if r and r.status_code == 200: diff --git a/apps/users/views/user.py b/apps/users/views/user.py index bf2ecca57..84c670370 100644 --- a/apps/users/views/user.py +++ b/apps/users/views/user.py @@ -31,7 +31,7 @@ from django.contrib.auth import logout as auth_logout from common.const import create_success_msg, update_success_msg from common.mixins import JSONResponseMixin -from common.utils import get_logger, get_object_or_none, is_uuid +from common.utils import get_logger, get_object_or_none, is_uuid, ssh_key_gen from .. import forms from ..models import User, UserGroup from ..utils import AdminUserRequiredMixin @@ -45,6 +45,7 @@ __all__ = [ 'UserExportView', 'UserBulkImportView', 'UserProfileView', 'UserProfileUpdateView', 'UserPasswordUpdateView', 'UserPublicKeyUpdateView', 'UserBulkUpdateView', + 'UserPublicKeyGenerateView', ] logger = get_logger(__name__) @@ -375,3 +376,15 @@ class UserPublicKeyUpdateView(LoginRequiredMixin, UpdateView): } kwargs.update(context) return super().get_context_data(**kwargs) + + +class UserPublicKeyGenerateView(LoginRequiredMixin, View): + def get(self, request, *args, **kwargs): + private, public = ssh_key_gen(username=request.user.username, hostname='jumpserver') + request.user.public_key = public + request.user.save() + response = HttpResponse(private, content_type='text/plain') + filename = "{0}-jumpserver.pem".format(request.user.username) + response['Content-Disposition'] = 'attachment; filename={}'.format(filename) + return response + diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 7b316c304..58aac783c 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -3,7 +3,7 @@ Jumpserver 封装了一个 All in one Docker,可以快速启动。该镜像集成了所需要的组件(Windows组件未暂未集成),也支持使用外置 Database 和 Redis -Tips: 不建议在生产中使用, 生产中请使用 详细安装 `详细安装 `_ +Tips: 不建议在生产中使用, 生产中请使用 详细安装 `CentOS `_ `Ubuntu `_ Docker 安装见: `Docker官方安装文档 `_ diff --git a/jms b/jms index 08505cc17..ea48ab6a7 100755 --- a/jms +++ b/jms @@ -82,7 +82,10 @@ def get_pid(service): pid_file = get_pid_file_path(service) if os.path.isfile(pid_file): with open(pid_file) as f: - return int(f.read().strip()) + try: + return int(f.read().strip()) + except ValueError: + return 0 return 0 @@ -282,9 +285,9 @@ if __name__ == '__main__': parser = argparse.ArgumentParser( description=""" Jumpserver service control tools; - - Example: \r\n - + + Example: \r\n + %(prog)s start all -d; """ )