perf: 优化 account 结构

pull/8931/head
ibuler 2022-09-20 13:54:25 +08:00
parent 9e84989bbe
commit 8c72bab82d
17 changed files with 116 additions and 80 deletions

View File

@ -23,10 +23,8 @@ class Migration(migrations.Migration):
('id', models.UUIDField(db_index=True, default=uuid.uuid4)),
('name', models.CharField(max_length=128, verbose_name='Name')),
('username', models.CharField(blank=True, db_index=True, max_length=128, verbose_name='Username')),
('password', common.db.fields.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')),
('private_key', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH private key')),
('public_key', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH public key')),
('token', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Token')),
('secret_type', models.CharField(choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'), ('token', 'Token')], default='password', max_length=16, verbose_name='Secret type')),
('secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')),
('comment', models.TextField(blank=True, verbose_name='Comment')),
('date_created', models.DateTimeField(blank=True, editable=False, verbose_name='Date created')),
('date_updated', models.DateTimeField(blank=True, editable=False, verbose_name='Date updated')),
@ -55,10 +53,8 @@ class Migration(migrations.Migration):
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('name', models.CharField(max_length=128, verbose_name='Name')),
('username', models.CharField(blank=True, db_index=True, max_length=128, verbose_name='Username')),
('password', common.db.fields.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')),
('private_key', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH private key')),
('public_key', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH public key')),
('token', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Token')),
('secret_type', models.CharField(choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'), ('token', 'Token')], default='password', max_length=16, verbose_name='Secret type')),
('secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')),
('comment', models.TextField(blank=True, verbose_name='Comment')),
('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')),
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
@ -70,7 +66,7 @@ class Migration(migrations.Migration):
options={
'verbose_name': 'Account',
'permissions': [('view_accountsecret', 'Can view asset account secret'), ('change_accountsecret', 'Can change asset account secret'), ('view_historyaccount', 'Can view asset history account'), ('view_historyaccountsecret', 'Can view asset history account secret')],
'unique_together': {('username', 'asset'), ('name', 'asset')},
'unique_together': {('name', 'asset'), ('username', 'asset', 'secret_type')},
},
),
migrations.AddField(

View File

@ -29,29 +29,51 @@ def migrate_accounts(apps, schema_editor):
accounts = []
# auth book 和 account 相同的属性
same_attrs = [
'id', 'comment', 'date_created', 'date_updated',
'id', 'username', 'comment', 'date_created', 'date_updated',
'created_by', 'asset_id', 'org_id',
]
# 认证的属性,可能是 authbook 的,可能是 systemuser 的
auth_attrs = ['username', 'password', 'private_key', 'public_key']
auth_attrs = ['password', 'private_key', 'token']
all_attrs = same_attrs + auth_attrs
for auth_book in auth_books:
values = {attr: getattr(auth_book, attr) for attr in same_attrs}
values['version'] = 1
values = {'version': 1}
system_user = auth_book.systemuser
if system_user:
values.update({attr: getattr(system_user, attr) for attr in auth_attrs})
# 更新一次系统用户的认证属性
values.update({attr: getattr(system_user, attr, '') for attr in all_attrs})
values['created_by'] = str(system_user.id)
values['privileged'] = system_user.type == 'admin'
auth_book_auth = {attr: getattr(auth_book, attr) for attr in auth_attrs}
auth_book_auth = {attr: value for attr, value in auth_book_auth.items() if value}
auth_book_auth = {attr: getattr(auth_book, attr, '') for attr in all_attrs if getattr(auth_book, attr, '')}
# 最终使用 authbook 的认证属性
values.update(auth_book_auth)
values['name'] = values['username']
account = account_model(**values)
accounts.append(account)
auth_infos = []
username = values['username']
for attr in auth_attrs:
secret = values.pop(attr, None)
if not secret:
continue
if attr == 'private_key':
secret_type = 'ssh_key'
name = f'{username}(ssh key)'
elif attr == 'token':
secret_type = 'token'
name = f'{username}(token)'
else:
secret_type = attr
name = username
auth_infos.append((name, secret_type, secret))
if not auth_infos:
auth_infos.append((username, 'password', ''))
for name, secret_type, secret in auth_infos:
account = account_model(**values, name=name, secret=secret, secret_type=secret_type)
accounts.append(account)
account_model.objects.bulk_create(accounts, ignore_conflicts=True)
print("Create accounts: {}-{} using: {:.2f}s".format(

View File

@ -10,24 +10,11 @@ class Migration(migrations.Migration):
]
operations = [
migrations.RemoveField(
model_name='asset',
name='port',
),
migrations.RemoveField(
model_name='asset',
name='protocol',
),
migrations.RenameField(
model_name='asset',
old_name='protocols',
new_name='_protocols',
),
migrations.AlterField(
model_name='systemuser',
name='protocol',
field=models.CharField(default='ssh', max_length=16, verbose_name='Protocol'),
),
migrations.CreateModel(
name='Protocol',
fields=[

View File

@ -6,12 +6,10 @@ from django.db import migrations
def migrate_asset_protocols(apps, schema_editor):
asset_model = apps.get_model('assets', 'Asset')
protocol_model = apps.get_model('assets', 'Protocol')
asset_protocol_through = asset_model.protocols.through
count = 0
bulk_size = 1000
print("\nStart migrate asset protocols")
protocol_map = {}
while True:
start = time.time()
assets = asset_model.objects.all()[count:count+bulk_size]
@ -19,23 +17,25 @@ def migrate_asset_protocols(apps, schema_editor):
break
count += len(assets)
assets_protocols = []
for asset in assets:
old_protocols = asset._protocols
for name_port in old_protocols.split(','):
for asset in assets:
old_protocols = asset._protocols or '{}/{}'.format(asset.protocol, asset.port) or 'ssh/22'
if ',' in old_protocols:
_protocols = old_protocols.split(',')
else:
_protocols = old_protocols.split()
for name_port in _protocols:
name_port_list = name_port.split('/')
if len(name_port_list) != 2:
continue
name, port = name_port_list
protocol = protocol_map.get(name_port)
if not protocol:
protocol = protocol_model.objects.get_or_create(
defaults={'name': name, 'port': port, 'asset': asset},
name=name, port=port
)[0]
assets_protocols.append(asset_protocol_through(asset_id=asset.id, protocol_id=protocol.id))
asset_model.protocols.through.objects.bulk_create(assets_protocols, ignore_conflicts=True)
protocol = protocol_model(**{'name': name, 'port': port, 'asset': asset})
assets_protocols.append(protocol)
protocol_model.objects.bulk_create(assets_protocols, ignore_conflicts=True)
print("Create asset protocols: {}-{} using: {:.2f}s".format(
count - len(assets), count, time.time()-start
))

View File

@ -18,11 +18,24 @@ class Migration(migrations.Migration):
),
migrations.RemoveField(
model_name='asset',
name='_protocols',
name='admin_user',
),
migrations.RemoveField(
model_name='asset',
name='admin_user',
name='port',
),
migrations.RemoveField(
model_name='asset',
name='protocol',
),
migrations.RemoveField(
model_name='asset',
name='_protocols',
),
migrations.AlterField(
model_name='systemuser',
name='protocol',
field=models.CharField(default='ssh', max_length=16, verbose_name='Protocol'),
),
migrations.RemoveField(
model_name='asset',

View File

@ -20,14 +20,12 @@ class Migration(migrations.Migration):
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('name', models.CharField(max_length=128, verbose_name='Name')),
('username', models.CharField(blank=True, db_index=True, max_length=128, verbose_name='Username')),
('password', common.db.fields.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')),
('private_key', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH private key')),
('public_key', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH public key')),
('secret_type', models.CharField(choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'), ('token', 'Token')], default='password', max_length=16, verbose_name='Secret type'),),
('secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')),
('comment', models.TextField(blank=True, verbose_name='Comment')),
('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')),
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('created_by', models.CharField(max_length=128, null=True, verbose_name='Created by')),
('token', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Token')),
('privileged', models.BooleanField(default=False, verbose_name='Privileged')),
],
options={

View File

@ -11,11 +11,6 @@ class Migration(migrations.Migration):
]
operations = [
migrations.AddField(
model_name='platform',
name='brand',
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Brand'),
),
migrations.CreateModel(
name='PlatformAutomation',
fields=[

View File

@ -23,7 +23,7 @@ class Account(BaseAccount):
class Meta:
verbose_name = _('Account')
unique_together = [
('username', 'asset'),
('username', 'asset', 'secret_type'),
('name', 'asset'),
]
permissions = [

View File

@ -77,6 +77,9 @@ class Protocol(models.Model):
port = models.IntegerField(verbose_name=_("Port"))
asset = models.ForeignKey('Asset', on_delete=models.CASCADE, related_name='protocols', verbose_name=_("Asset"))
def __str__(self):
return '{}/{}'.format(self.name, self.port)
class Asset(AbsConnectivity, NodesRelationMixin, JMSOrgBaseModel):
id = models.UUIDField(default=uuid.uuid4, primary_key=True)

View File

@ -55,15 +55,17 @@ class AbsConnectivity(models.Model):
class BaseAccount(OrgModelMixin):
class SecretType(models.TextChoices):
password = 'password', _('Password')
ssh_key = 'ssh_key', _('SSH key')
access_key = 'access_key', _('Access key')
token = 'token', _('Token')
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
name = models.CharField(max_length=128, verbose_name=_("Name"))
username = models.CharField(max_length=128, blank=True, verbose_name=_('Username'), db_index=True)
secret_type = models.CharField(max_length=16, default='password', verbose_name=_('Secret type'))
secret_type = models.CharField(max_length=16, choices=SecretType.choices, default='password', verbose_name=_('Secret type'))
secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('Secret'))
password = fields.EncryptCharField(max_length=256, blank=True, null=True, verbose_name=_('Password'))
private_key = fields.EncryptTextField(blank=True, null=True, verbose_name=_('SSH private key'))
public_key = fields.EncryptTextField(blank=True, null=True, verbose_name=_('SSH public key'))
token = fields.EncryptTextField(blank=True, null=True, verbose_name=_('Token'))
privileged = models.BooleanField(verbose_name=_("Privileged"), default=False)
comment = models.TextField(blank=True, verbose_name=_('Comment'))
date_created = models.DateTimeField(auto_now_add=True, verbose_name=_("Date created"))
@ -76,6 +78,28 @@ class BaseAccount(OrgModelMixin):
APPS_AMOUNT_CACHE_KEY = "APP_USER_{}_APPS_AMOUNT"
APP_USER_CACHE_TIME = 600
@property
def public_key(self):
return ''
@property
def private_key(self):
return ''
@private_key.setter
def private_key(self, value):
self.secret = value
self.secret_type = 'private_key'
@property
def password(self):
return self.secret
@password.setter
def password(self, value):
self.secret = value
self.secret_type = 'password'
def expire_assets_amount(self):
cache_key = self.ASSETS_AMOUNT_CACHE_KEY.format(self.id)
cache.delete(cache_key)

View File

@ -10,6 +10,7 @@ from django.db import models
from django.utils.translation import ugettext_lazy as _
from common.utils import get_logger, lazyproperty
from common.db import fields
from orgs.mixins.models import OrgModelMixin
from .base import BaseAccount
@ -64,7 +65,12 @@ class Gateway(BaseAccount):
domain = models.ForeignKey(Domain, on_delete=models.CASCADE, 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"))
token = None
password = fields.EncryptCharField(max_length=256, blank=True, null=True, verbose_name=_('Password'))
private_key = fields.EncryptTextField(blank=True, null=True, verbose_name=_('SSH private key'))
public_key = fields.EncryptTextField(blank=True, null=True, verbose_name=_('SSH public key'))
secret = None
secret_type = None
privileged = None
def __str__(self):

View File

@ -52,7 +52,6 @@ class Platform(models.Model):
comment = models.TextField(blank=True, null=True, verbose_name=_("Comment"))
# 资产有关的
charset = models.CharField(default='utf8', choices=CHARSET_CHOICES, max_length=8, verbose_name=_("Charset"))
brand = models.CharField(max_length=128, blank=True, null=True, verbose_name=_("Brand")) # 厂商主要是给网络设备
domain_enabled = models.BooleanField(default=True, verbose_name=_("Domain enabled"))
protocols_enabled = models.BooleanField(default=True, verbose_name=_("Protocols enabled"))
protocols = models.ManyToManyField(PlatformProtocol, blank=True, verbose_name=_("Protocols"))

View File

@ -9,16 +9,16 @@ class AccountFieldsSerializerMixin(serializers.ModelSerializer):
class Meta:
fields_mini = [
'id', 'name', 'username', 'privileged',
'platform', 'version'
'platform', 'version', 'secret_type',
]
fields_write_only = ['password', 'private_key', 'public_key', 'passphrase']
fields_write_only = ['secret', 'passphrase']
fields_other = ['date_created', 'date_updated', 'comment']
fields_small = fields_mini + fields_write_only + fields_other
fields_fk = ['asset']
fields = fields_small + fields_fk
extra_kwargs = {
'private_key': {'write_only': True},
'public_key': {'write_only': True},
'secret': {'write_only': True},
'passphrase': {'write_only': True},
'token': {'write_only': True},
'password': {'write_only': True},
}

View File

@ -22,13 +22,6 @@ class AssetProtocolsSerializer(serializers.ModelSerializer):
model = Protocol
fields = ['id', 'name', 'port']
def create(self, validated_data):
instance = Protocol.objects.filter(**validated_data).first()
if instance:
return instance
instance = Protocol.objects.create(**validated_data)
return instance
class AssetLabelSerializer(serializers.ModelSerializer):
class Meta:
@ -55,10 +48,10 @@ class AssetAccountSerializer(AccountSerializer):
class Meta(AccountSerializer.Meta):
fields_mini = [
'id', 'name', 'username', 'privileged', 'version',
'secret_type',
]
fields_write_only = [
'password', 'private_key', 'public_key',
'passphrase', 'token', 'push_now'
'secret', 'passphrase', 'push_now'
]
fields = fields_mini + fields_write_only

View File

@ -70,7 +70,6 @@ class PlatformSerializer(JMSWritableNestedModelSerializer):
choices=[('sudo', 'sudo su -'), ('su', 'su - ')],
label='切换方式', required=False, default='sudo'
)
brand = LabeledChoiceField(choices=[], label='厂商', required=False, allow_null=True)
class Meta:
model = Platform
@ -80,7 +79,7 @@ class PlatformSerializer(JMSWritableNestedModelSerializer):
]
fields = fields_small + [
'protocols_enabled', 'protocols', 'domain_enabled',
'su_enabled', 'su_method', 'brand', 'automation', 'comment',
'su_enabled', 'su_method', 'automation', 'comment',
]
extra_kwargs = {
'su_enabled': {'label': '启用切换账号'},

View File

@ -13,6 +13,7 @@ def migrate_system_role_binding(apps, schema_editor):
count = 0
bulk_size = 1000
print('')
while True:
users = user_model.objects.using(db_alias) \
.only('role', 'id') \

View File

@ -16,7 +16,7 @@ def migrate_system_to_account(apps, schema_editor):
)
for model, old_field, new_field, m2m in model_system_user_account:
print("Start migrate '{}' system user to account".format(model))
print("Start migrate '{}' system user to account".format(model.__name__))
count = 0
bulk_size = 1000