diff --git a/apps/assets/api.py b/apps/assets/api.py index 3cc9e0773..d2ba7ca61 100644 --- a/apps/assets/api.py +++ b/apps/assets/api.py @@ -39,21 +39,20 @@ class AssetViewSet(IDInFilterMixin, BulkModelViewSet): def get_queryset(self): if self.request.user.is_superuser: - queryset = super(AssetViewSet, self).get_queryset() + queryset = super().get_queryset() else: queryset = get_user_granted_assets(self.request.user) - cluster_id = self.request.query_params.get('cluster_id', '') - system_users_id = self.request.query_params.get('system_user_id', '') - asset_group_id = self.request.query_params.get('asset_group_id', '') - admin_user_id = self.request.query_params.get('admin_user_id', '') + cluster_id = self.request.query_params.get('cluster_id') + asset_group_id = self.request.query_params.get('asset_group_id') + admin_user_id = self.request.query_params.get('admin_user_id') if cluster_id: queryset = queryset.filter(cluster__id=cluster_id) - if system_users_id: - queryset = queryset.filter(system_users__id=system_users_id) - if admin_user_id: - queryset = queryset.filter(admin_user__id=admin_user_id) if asset_group_id: queryset = queryset.filter(groups__id=asset_group_id) + if admin_user_id: + admin_user = get_object_or_404(AdminUser, id=admin_user_id) + clusters = [cluster.id for cluster in admin_user.cluster_set.all()] + queryset = queryset.filter(cluster__id__in=clusters) return queryset diff --git a/apps/assets/forms.py b/apps/assets/forms.py index d7d014a15..5b7d30d5a 100644 --- a/apps/assets/forms.py +++ b/apps/assets/forms.py @@ -10,40 +10,60 @@ logger = get_logger(__file__) class AssetCreateForm(forms.ModelForm): + # Form field name can not start with `_`, so redefine it, + password = forms.CharField( + widget=forms.PasswordInput, max_length=100, + strip=True, required=False, + help_text=_('If also set private key, use that first'), + ) + # Need use upload private key file except paste private key content + private_key_file = forms.FileField(required=False) + + def save(self, commit=True): + # Because we define custom field, so we need rewrite :method: `save` + obj = super().save(commit=commit) + password = self.cleaned_data['password'] + private_key = self.cleaned_data['private_key_file'] + + if password: + obj.password = password + if private_key: + obj.private_key = private_key + obj.save() + return obj + + def clean_private_key_file(self): + private_key_file = self.cleaned_data['private_key_file'] + if private_key_file: + private_key = private_key_file.read() + if not validate_ssh_private_key(private_key): + raise forms.ValidationError(_('Invalid private key')) + return private_key + return private_key_file + class Meta: model = Asset fields = [ 'hostname', 'ip', 'public_ip', 'port', 'type', 'comment', - 'admin_user', "cluster", 'groups', 'status', 'env', 'is_active' + 'cluster', 'groups', 'status', 'env', 'is_active', 'username', + ] widgets = { 'groups': forms.SelectMultiple( attrs={'class': 'select2', 'data-placeholder': _('Select asset groups')}), - 'admin_user': forms.Select( - attrs={'class': 'select2', - 'data-placeholder': _('Select asset admin user')}), } help_texts = { 'hostname': '* required', 'ip': '* required', - 'system_users': _('System user will be granted for user to login ' - 'assets (using ansible create automatic)'), - 'admin_user': _('Admin user should be exist on asset already, ' - 'And have sudo ALL permission'), } - def clean_admin_user(self): - if not self.cleaned_data['admin_user']: - raise forms.ValidationError(_('Select admin user')) - return self.cleaned_data['admin_user'] - class AssetUpdateForm(forms.ModelForm): class Meta: model = Asset fields = [ - 'hostname', 'ip', 'port', 'groups', 'admin_user', "cluster", 'is_active', + 'hostname', 'ip', 'port', 'groups', "cluster", 'is_active', 'type', 'env', 'status', 'public_ip', 'remote_card_ip', 'cabinet_no', 'cabinet_pos', 'number', 'comment' ] @@ -51,17 +71,10 @@ class AssetUpdateForm(forms.ModelForm): 'groups': forms.SelectMultiple( attrs={'class': 'select2', 'data-placeholder': _('Select asset groups')}), - 'admin_user': forms.Select( - attrs={'class': 'select2', - 'data-placeholder': _('Select asset admin user')}), } help_texts = { 'hostname': '* required', 'ip': '* required', - 'system_users': _('System user will be granted for user ' - 'to login assets (using ansible create automatic)'), - 'admin_user': _('Admin user should be exist on asset ' - 'already, And have sudo ALL permission'), } @@ -77,22 +90,16 @@ class AssetBulkUpdateForm(forms.ModelForm): } ) ) - port = forms.IntegerField(min_value=1, max_value=65535, - required=False, label=_('Port')) + port = forms.IntegerField(min_value=1, max_value=65535, required=False, label=_('Port')) class Meta: model = Asset fields = [ - 'assets', 'port', 'groups', 'admin_user', "cluster", + 'assets', 'port', 'groups', "cluster", 'type', 'env', 'status', ] widgets = { - 'groups': forms.SelectMultiple( - attrs={'class': 'select2', - 'data-placeholder': _('Select asset groups')}), - 'admin_user': forms.Select( - attrs={'class': 'select2', - 'data-placeholder': _('Select asset admin user')}), + 'groups': forms.SelectMultiple(attrs={'class': 'select2', 'data-placeholder': _('Select asset groups')}), } def save(self, commit=True): @@ -140,40 +147,19 @@ class AssetGroupForm(forms.ModelForm): class ClusterForm(forms.ModelForm): - # See AdminUserForm comment same it - assets = forms.ModelMultipleChoiceField( - queryset=Asset.objects.all(), - label=_('Asset'), - required=False, - widget=forms.SelectMultiple( - attrs={'class': 'select2', 'data-placeholder': _('Select assets')}) - ) - - def __init__(self, *args, **kwargs): - if kwargs.get('instance'): - initial = kwargs.get('initial', {}) - initial['assets'] = kwargs['instance'].assets.all() - super(ClusterForm, self).__init__(*args, **kwargs) - - def _save_m2m(self): - super(ClusterForm, self)._save_m2m() - assets = self.cleaned_data['assets'] - self.instance.assets.clear() - self.instance.assets.add(*tuple(assets)) class Meta: model = Cluster - fields = ['name', "bandwidth", "operator", 'contact', + fields = ['name', "bandwidth", "operator", 'contact', 'admin_user', 'phone', 'address', 'intranet', 'extranet', 'comment'] widgets = { 'name': forms.TextInput(attrs={'placeholder': _('Name')}), - 'intranet': forms.Textarea( - attrs={'placeholder': 'IP段之间用逗号隔开,如:192.168.1.0/24,192.168.1.0/24'}), - 'extranet': forms.Textarea( - attrs={'placeholder': 'IP段之间用逗号隔开,如:201.1.32.1/24,202.2.32.1/24'}) + 'intranet': forms.Textarea(attrs={'placeholder': 'IP段之间用逗号隔开,如:192.168.1.0/24,192.168.1.0/24'}), + 'extranet': forms.Textarea(attrs={'placeholder': 'IP段之间用逗号隔开,如:201.1.32.1/24,202.2.32.1/24'}) } help_texts = { - 'name': '* required' + 'name': '* required', + 'admin_user': 'The assets of this cluster will use this admin user as his admin user', } @@ -237,8 +223,7 @@ class SystemUserForm(forms.ModelForm): # Admin user assets define, let user select, save it in form not in view auto_generate_key = forms.BooleanField(initial=True, required=False) # Form field name can not start with `_`, so redefine it, - password = forms.CharField(widget=forms.PasswordInput, required=False, - max_length=100, strip=True) + password = forms.CharField(widget=forms.PasswordInput, required=False, max_length=100, strip=True) # Need use upload private key file except paste private key content private_key_file = forms.FileField(required=False) @@ -289,15 +274,19 @@ class SystemUserForm(forms.ModelForm): fields = [ 'name', 'username', 'protocol', 'auto_generate_key', 'password', 'private_key_file', 'auth_method', 'auto_push', 'sudo', - 'comment', 'shell' + 'comment', 'shell', 'cluster' ] widgets = { 'name': forms.TextInput(attrs={'placeholder': _('Name')}), 'username': forms.TextInput(attrs={'placeholder': _('Username')}), + 'cluster': forms.SelectMultiple( + attrs={'class': 'select2', + 'data-placeholder': _(' Select clusters')}), } help_texts = { 'name': '* required', 'username': '* required', + 'cluster': 'If auto push checked, then push system user to that cluster assets', 'auto_push': 'Auto push system user to asset', } @@ -306,8 +295,7 @@ class SystemUserUpdateForm(forms.ModelForm): # Admin user assets define, let user select, save it in form not in view auto_generate_key = forms.BooleanField(initial=False, required=False) # Form field name can not start with `_`, so redefine it, - password = forms.CharField(widget=forms.PasswordInput, required=False, - max_length=100, strip=True) + password = forms.CharField(widget=forms.PasswordInput, required=False, max_length=100, strip=True) # Need use upload private key file except paste private key content private_key_file = forms.FileField(required=False) @@ -341,17 +329,21 @@ class SystemUserUpdateForm(forms.ModelForm): class Meta: model = SystemUser fields = [ - 'name', 'username', 'protocol', 'auto_generate_key', 'password', - 'private_key_file', 'auth_method', 'auto_push', 'sudo', - 'comment', 'shell' + 'name', 'username', 'protocol', + 'auth_method', 'auto_push', 'sudo', + 'comment', 'shell', 'cluster' ] widgets = { 'name': forms.TextInput(attrs={'placeholder': _('Name')}), 'username': forms.TextInput(attrs={'placeholder': _('Username')}), + 'cluster': forms.SelectMultiple( + attrs={'class': 'select2', + 'data-placeholder': _(' Select clusters')}), } help_texts = { 'name': '* required', 'username': '* required', + 'cluster': 'If auto push checked, then push system user to that cluster assets', 'auto_push': 'Auto push system user to asset', } diff --git a/apps/assets/models/asset.py b/apps/assets/models/asset.py index e5656ca97..430183d16 100644 --- a/apps/assets/models/asset.py +++ b/apps/assets/models/asset.py @@ -3,12 +3,17 @@ # import uuid +import os +import logging +from hashlib import md5 from django.db import models -import logging +from django.conf import settings from django.utils.translation import ugettext_lazy as _ from django.core.cache import cache +from common.utils import signer, ssh_key_string_to_obj +from .utils import private_key_validator from .cluster import Cluster from .group import AssetGroup from .user import AdminUser, SystemUser @@ -47,14 +52,17 @@ class Asset(models.Model): hostname = models.CharField(max_length=128, unique=True, verbose_name=_('Hostname')) port = models.IntegerField(default=22, verbose_name=_('Port')) groups = models.ManyToManyField(AssetGroup, blank=True, related_name='assets', verbose_name=_('Asset groups')) - admin_user = models.ForeignKey(AdminUser, null=True, blank=True, related_name='assets', on_delete=models.SET_NULL, verbose_name=_("Admin user")) - system_users = models.ManyToManyField(SystemUser, blank=True, related_name='assets', verbose_name=_("System User")) cluster = models.ForeignKey(Cluster, blank=True, null=True, related_name='assets', on_delete=models.SET_NULL, verbose_name=_('Cluster'),) is_active = models.BooleanField(default=True, verbose_name=_('Is active')) type = models.CharField(choices=TYPE_CHOICES, max_length=16, blank=True, null=True, default='Server', verbose_name=_('Asset type'),) env = models.CharField(choices=ENV_CHOICES, max_length=8, blank=True, null=True, default='Prod', verbose_name=_('Asset environment'),) status = models.CharField(choices=STATUS_CHOICES, max_length=12, null=True, blank=True, default='In use', verbose_name=_('Asset status')) + # Auth + username = models.CharField(max_length=16, blank=True, null=True, 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, ]) + # Some information public_ip = models.GenericIPAddressField(max_length=32, blank=True, null=True, verbose_name=_('Public IP')) remote_card_ip = models.CharField(max_length=16, null=True, blank=True, verbose_name=_('Remote control card IP')) @@ -96,6 +104,41 @@ class Asset(models.Model): return True, '' return False, warning + @property + def password(self): + if self._password: + return signer.unsign(self._password) + else: + return '' + + @password.setter + def password(self, password_raw): + self._password = signer.sign(password_raw) + + @property + def private_key(self): + if self._private_key: + key_str = signer.unsign(self._private_key) + return ssh_key_string_to_obj(key_str) + else: + return None + + @private_key.setter + def private_key(self, private_key_raw): + self._private_key = signer.sign(private_key_raw) + + @property + def private_key_file(self): + if not self.private_key: + return None + project_dir = settings.PROJECT_DIR + tmp_dir = os.path.join(project_dir, 'tmp') + key_name = md5(self._private_key.encode()).hexdigest() + key_path = os.path.join(tmp_dir, key_name) + if not os.path.exists(key_path): + self.private_key.write_private_key_file(key_path) + return key_path + def to_json(self): return { 'id': self.id, @@ -115,15 +158,15 @@ class Asset(models.Model): Todo: May be move to ops implements it """ data = self.to_json() - if self.admin_user: + if self.cluster and self.cluster.admin_user: data.update({ - 'username': self.admin_user.username, - 'password': self.admin_user.password, - 'private_key': self.admin_user.private_key_file, + 'username': self.cluster.admin_user.username, + 'password': self.cluster.admin_user.password, + 'private_key': self.cluster.admin_user.private_key_file, 'become': { - 'method': self.admin_user.become_method, - 'user': self.admin_user.become_user, - 'pass': self.admin_user.become_pass, + 'method': self.cluster.admin_user.become_method, + 'user': self.cluster.admin_user.become_user, + 'pass': self.cluster.admin_user.become_pass, } }) return data diff --git a/apps/assets/models/cluster.py b/apps/assets/models/cluster.py index 54ce1be4e..dd30516a4 100644 --- a/apps/assets/models/cluster.py +++ b/apps/assets/models/cluster.py @@ -18,6 +18,7 @@ logger = logging.getLogger(__name__) class Cluster(models.Model): id = models.UUIDField(default=uuid.uuid4, primary_key=True) name = models.CharField(max_length=32, verbose_name=_('Name')) + admin_user = models.ForeignKey('assets.AdminUser', on_delete=models.CASCADE, verbose_name=_("Admin user")) bandwidth = models.CharField(max_length=32, blank=True, verbose_name=_('Bandwidth')) contact = models.CharField(max_length=128, blank=True, verbose_name=_('Contact')) phone = models.CharField(max_length=32, blank=True, verbose_name=_('Phone')) diff --git a/apps/assets/models/group.py b/apps/assets/models/group.py index 31a1cd2d5..965ceb1f3 100644 --- a/apps/assets/models/group.py +++ b/apps/assets/models/group.py @@ -23,9 +23,8 @@ class AssetGroup(models.Model): date_created = models.DateTimeField(auto_now_add=True, null=True, verbose_name=_('Date created')) comment = models.TextField(blank=True, verbose_name=_('Comment')) - def __unicode__(self): + def __str__(self): return self.name - __str__ = __unicode__ class Meta: ordering = ['name'] diff --git a/apps/assets/models/user.py b/apps/assets/models/user.py index 47bc41ce5..fb8a98ede 100644 --- a/apps/assets/models/user.py +++ b/apps/assets/models/user.py @@ -8,25 +8,18 @@ import logging import uuid from hashlib import md5 -from django.core.exceptions import ValidationError from django.db import models from django.utils.translation import ugettext_lazy as _ from django.conf import settings -from common.utils import signer, validate_ssh_private_key, ssh_key_string_to_obj +from common.utils import signer, ssh_key_string_to_obj +from .utils import private_key_validator -__all__ = ['AdminUser', 'SystemUser', 'private_key_validator'] + +__all__ = ['AdminUser', 'SystemUser',] logger = logging.getLogger(__name__) -def private_key_validator(value): - if not validate_ssh_private_key(value): - raise ValidationError( - _('%(value)s is not an even number'), - params={'value': value}, - ) - - class AdminUser(models.Model): """ A privileged user that ansible can use it to push system user and so on @@ -103,10 +96,12 @@ class AdminUser(models.Model): def become_pass(self, password): self._become_pass = signer.sign(password) - @property def assets_amount(self): - return self.assets.count() + amount = 0 + for cluster in self.cluster_set.all(): + amount += cluster.assets.all().count() + return amount class Meta: ordering = ['name'] @@ -143,6 +138,7 @@ class SystemUser(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=16, verbose_name=_('Username')) + cluster = models.ManyToManyField('assets.Cluster', verbose_name=_("Cluster")) _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.TextField(max_length=8192, blank=True, verbose_name=_('SSH private key')) diff --git a/apps/assets/models/utils.py b/apps/assets/models/utils.py index 8b1c94502..a819715ab 100644 --- a/apps/assets/models/utils.py +++ b/apps/assets/models/utils.py @@ -2,22 +2,34 @@ # -*- coding: utf-8 -*- # -from . import Cluster, SystemUser, AdminUser, AssetGroup, Asset +from django.core.exceptions import ValidationError +from common.utils import validate_ssh_private_key + __all__ = ['init_model', 'generate_fake'] def init_model(): + from . import Cluster, SystemUser, AdminUser, AssetGroup, Asset for cls in [Cluster, SystemUser, AdminUser, AssetGroup, 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]: if hasattr(cls, 'generate_fake'): cls.generate_fake() +def private_key_validator(value): + if not validate_ssh_private_key(value): + raise ValidationError( + _('%(value)s is not an even number'), + params={'value': value}, + ) + + if __name__ == '__main__': pass diff --git a/apps/assets/serializers.py b/apps/assets/serializers.py index 0a51adfb7..cfd6d1700 100644 --- a/apps/assets/serializers.py +++ b/apps/assets/serializers.py @@ -63,7 +63,7 @@ class ClusterUpdateAssetsSerializer(serializers.ModelSerializer): class AdminUserSerializer(serializers.ModelSerializer): - assets = serializers.PrimaryKeyRelatedField(many=True, queryset=Asset.objects.all()) + assets_amount = serializers.SerializerMethodField() unreachable_amount = serializers.SerializerMethodField() class Meta: @@ -78,14 +78,18 @@ class AdminUserSerializer(serializers.ModelSerializer): else: return 'Unknown' - def get_field_names(self, declared_fields, info): - fields = super(AdminUserSerializer, self).get_field_names(declared_fields, info) - fields.append('assets_amount') - return fields + @staticmethod + def get_assets_amount(obj): + amount = 0 + clusters = obj.cluster_set.all() + for cluster in clusters: + amount += len(cluster.assets.all()) + return amount class SystemUserSerializer(serializers.ModelSerializer): unreachable_amount = serializers.SerializerMethodField() + assets_amount = serializers.SerializerMethodField() class Meta: model = SystemUser @@ -99,10 +103,12 @@ class SystemUserSerializer(serializers.ModelSerializer): else: return "Unknown" - def get_field_names(self, declared_fields, info): - fields = super(SystemUserSerializer, self).get_field_names(declared_fields, info) - fields.extend(['assets_amount']) - return fields + @staticmethod + def get_assets_amount(obj): + amount = 0 + for cluster in obj.cluster.all(): + amount += cluster.assets.all().count() + return amount class AssetSystemUserSerializer(serializers.ModelSerializer): @@ -200,6 +206,7 @@ class MyAssetGrantedSerializer(AssetGrantedSerializer): class ClusterSerializer(BulkSerializerMixin, serializers.ModelSerializer): assets_amount = serializers.SerializerMethodField() + admin_user_name = serializers.SerializerMethodField() assets = serializers.PrimaryKeyRelatedField(many=True, queryset=Asset.objects.all()) class Meta: @@ -210,10 +217,9 @@ class ClusterSerializer(BulkSerializerMixin, serializers.ModelSerializer): def get_assets_amount(obj): return obj.assets.count() - def get_field_names(self, declared_fields, info): - fields = super(ClusterSerializer, self).get_field_names(declared_fields, info) - fields.append('assets_amount') - return fields + @staticmethod + def get_admin_user_name(obj): + return obj.admin_user.name class AssetGroupGrantedSerializer(BulkSerializerMixin, serializers.ModelSerializer): diff --git a/apps/assets/templates/assets/_system_user.html b/apps/assets/templates/assets/_system_user.html index cb3e8dd91..41c6dda51 100644 --- a/apps/assets/templates/assets/_system_user.html +++ b/apps/assets/templates/assets/_system_user.html @@ -38,6 +38,7 @@ {% bootstrap_field form.name layout="horizontal" %} {% bootstrap_field form.username layout="horizontal" %} {% bootstrap_field form.protocol layout="horizontal" %} + {% bootstrap_field form.cluster layout="horizontal" %}
+ + | {% trans 'Hostname' %} | {% trans 'IP' %} | {% trans 'Port' %} | -{% trans 'Alive' %} | +{% trans 'Type' %} | +{% trans 'Valid' %} |
---|---|---|---|---|---|---|
{{ asset.hostname }} | -{{ asset.ip }} | -{{ asset.port }} | - {% if asset.is_connective == '1' %} -- - | - {% else %} -- - | - {% endif %} -