class Tunnel(models.Model): """ SSH隧道配置 """ tunnel_name = models.CharField('隧道名称', max_length=50, unique=True) host = models.CharField('隧道连接', max_length=200) port = models.IntegerField('端口', default=0) user = fields.EncryptedCharField(verbose_name='用户名', max_length=200, default='', blank=True, null=True) password = fields.EncryptedCharField(verbose_name='密码', max_length=300, default='', blank=True, null=True) pkey = fields.EncryptedTextField(verbose_name="密钥", blank=True, null=True) pkey_path = models.FileField(verbose_name="密钥地址", blank=True, null=True, upload_to='keys/') pkey_password = fields.EncryptedCharField(verbose_name='密钥密码', max_length=300, default='', blank=True, null=True) create_time = models.DateTimeField('创建时间', auto_now_add=True) update_time = models.DateTimeField('更新时间', auto_now=True) def __str__(self): return self.tunnel_name def short_pkey(self): if len(str(self.pkey)) > 20: return '{}...'.format(str(self.pkey)[0:19]) else: return str(self.pkey) class Meta: managed = True db_table = 'ssh_tunnel' verbose_name = u'隧道配置' verbose_name_plural = u'隧道配置'
class ProcLogin(models.Model): def __str__(self): return self.name + ' - ' + self.sz_id class Meta: # Add verbose name verbose_name_plural = '나무 로그인 정보' verbose_name = '나무 로그인 정보' name = models.CharField(max_length=15, verbose_name='별칭') # 별칭 sz_id = models.CharField(max_length=15, verbose_name='로그인 아이디') # 로그인 ID sz_pw = fields.EncryptedCharField( verbose_name='로그인 비밀번호', null=False ) # models.CharField(max_length=15, verbose_name='로그인 PW') # 로그인 PW sz_cert_pw = fields.EncryptedCharField( verbose_name='인증서 비밀번호', null=False ) # models.CharField(max_length=15, verbose_name='인증서 비밀번호') # 인증서 비밀번호 account_pw = models.CharField(max_length=44, verbose_name='계좌 비밀번호', null=False) # 계좌 비밀번호 trade_pw = models.CharField(max_length=44, verbose_name='거래 비밀번호', null=False) # 거래 비밀번호 is_hts = models.BooleanField(default=True, verbose_name='모의투자 여부', null=False) # 완료 유무 def save(self, force_insert=False, force_update=False, using=None, update_fields=None): # if self. # TODO 거래비밀번호, 게좌비밀번호 hash 암호화 처리 필요(문의한 내용 답변 받아야 처리 가능) super().save(force_insert, force_update, using, update_fields)
class Instance(models.Model): """ 各个线上实例配置 """ instance_name = models.CharField('实例名称', max_length=50, unique=True) type = models.CharField('实例类型', max_length=6, choices=(('master', '主库'), ('slave', '从库'))) db_type = models.CharField('数据库类型', max_length=20, choices=DB_TYPE_CHOICES) host = models.CharField('实例连接', max_length=200) port = models.IntegerField('端口', default=0) user = fields.EncryptedCharField(verbose_name='用户名', max_length=200, default='', blank=True) password = fields.EncryptedCharField(verbose_name='密码', max_length=300, default='', blank=True) db_name = models.CharField('数据库', max_length=64, default='', blank=True) charset = models.CharField('字符集', max_length=20, default='', blank=True) service_name = models.CharField('Oracle service name', max_length=50, null=True, blank=True) sid = models.CharField('Oracle sid', max_length=50, null=True, blank=True) resource_group = models.ManyToManyField(ResourceGroup, verbose_name='资源组', blank=True) instance_tag = models.ManyToManyField(InstanceTag, verbose_name='实例标签', blank=True) tunnel = models.ForeignKey(Tunnel, blank=True, null=True, on_delete=models.CASCADE, default=None) create_time = models.DateTimeField('创建时间', auto_now_add=True) update_time = models.DateTimeField('更新时间', auto_now=True) def __str__(self): return self.instance_name class Meta: managed = True db_table = 'sql_instance' verbose_name = u'实例配置' verbose_name_plural = u'实例配置'
class Host(models.Model): """"Host info""" host_id = models.AutoField('host id', primary_key=True) host_name = models.CharField('host name', max_length=32) host_addr = models.CharField('host address', max_length=64) ssh_port = models.IntegerField('ssh port', blank=True, default=22) ssh_user = models.CharField('ssh user', max_length=32, blank=True, default='') ssh_password = fields.EncryptedCharField(verbose_name='ssh password', max_length=128, default='', blank=True) create_time = models.DateTimeField('create time', auto_now_add=True) update_time = models.DateTimeField('update time', auto_now=True) class Meta: managed = True db_table = 'sql_host' verbose_name = u'主机列表' verbose_name_plural = u'主机列表' def __str__(self): return self.host_name
class TestModel(models.Model): char = fields.EncryptedCharField(blank=True, null=True) text = fields.EncryptedTextField(blank=True, null=True) textraw = models.TextField(blank=True, null=True) integer = fields.EncryptedIntegerField(blank=True, null=True) email = fields.EncryptedEmailField(blank=True, null=True) url = fields.EncryptedURLField(blank=True, null=True)
class sys_user(models.Model): """ username唯一索引 status,del_flag Btree索引 """ GENDER_CHOICES = ( (0, 'Male'), (1, 'Female'), ) LOCK_CHOICES = ( (0, 'normal'), (1, 'locked'), ) username = models.CharField(unique=True,null=True,max_length=100, verbose_name='用户名') nickname = models.CharField(null=True,max_length=100, verbose_name='显示名称') password = fields.EncryptedCharField(null=True,max_length=500, verbose_name='显示名称') type = models.IntegerField(default=5,verbose_name='0-超级管理员,1-管理员,2-项目管理员,3-开发人员,4-测试人员,5-访客') salt = models.CharField(null=True,max_length=100, verbose_name='MD5密码盐') avatar = models.CharField(null=True,max_length=100, verbose_name='头像') gender = models.IntegerField(choices=GENDER_CHOICES, verbose_name='性别') email = models.CharField(null=True,max_length=100, verbose_name='电子邮箱') phone = models.CharField(null=True,max_length=100, verbose_name='电话') status = models.IntegerField(db_index=True, choices=LOCK_CHOICES, verbose_name='用户状态,锁定 正常') deleted = models.CharField(max_length=200, default=0,db_index=True,verbose_name='删除状态,锁定 正常') createTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') updateTime = models.DateTimeField(auto_now=True, verbose_name='更新时间') class Meta: unique_together = ('username', 'deleted',)
class MyUser(AbstractBaseUser, PermissionsMixin): email = fields.EncryptedEmailField(blank=False, null=True, verbose_name='邮件') nickname = fields.EncryptedCharField(max_length=30, verbose_name='名字') username = models.CharField(unique=True, max_length=30, verbose_name='用户名') position = models.CharField(max_length=50, default="学生", verbose_name="职位") is_active = models.BooleanField(default=True) is_staff = models.BooleanField(default=False) is_superuser = models.BooleanField(default=False) join_time = models.DateTimeField(auto_now_add=True) desc = models.TextField(blank=True, null=True) USERNAME_FIELD = 'username' REQUIRED_FIELDS = ["email"] EMAIL_FIELD = "email" objects = UserManager() def get_full_username(self): return self.username def get_short_username(self): return self.username def __str__(self): return self.nickname
class InstanceAccount(models.Model): """ 实例账号列表 """ instance = models.ForeignKey(Instance, on_delete=models.CASCADE) user = fields.EncryptedCharField(verbose_name='账号', max_length=128) host = models.CharField(verbose_name='主机', max_length=64) password = fields.EncryptedCharField(verbose_name='密码', max_length=128, default='', blank=True) remark = models.CharField('备注', max_length=255) sys_time = models.DateTimeField('系统修改时间', auto_now=True) class Meta: managed = True db_table = 'instance_account' unique_together = ('instance', 'user', 'host') verbose_name = '实例账号列表' verbose_name_plural = '实例账号列表'
class ServerVPN(models.Model): name = models.CharField(max_length=50, unique=True, null=False, blank=False, verbose_name='Server name') address = models.GenericIPAddressField(null=False, blank=False, verbose_name='IP address') port = models.IntegerField(null=False, blank=False, default=22, verbose_name='Port number') user_name = models.CharField(max_length=150, null=False, blank=False, verbose_name='User name') password = fields.EncryptedCharField(max_length=100, null=False, blank=False, verbose_name='Password') is_active = models.BooleanField(null=False, blank=False, default=True, verbose_name='Active') def __str__(self): return self.name
class ossconf(models.Model): """ oss配置 """ name = models.CharField(max_length=500, verbose_name='名称') description = models.CharField(max_length=500, verbose_name='描述信息') accessKey = models.CharField(max_length=500, verbose_name='oss访问key') accessSecret = fields.EncryptedCharField(max_length=500, verbose_name='oss访问secret') endPoint = models.CharField(max_length=500, verbose_name='oss endpoint') bucketName = models.CharField(max_length=500, verbose_name='bucketname')
class sys_mailserver(models.Model): """ 邮件服务配置 """ name = models.CharField(max_length=500,verbose_name='邮件服务器') description = models.CharField(max_length=500, verbose_name='描述信息') server_type = models.CharField(max_length=32, verbose_name='邮件服务器类型') mail_server = models.CharField(max_length=100, verbose_name='邮件服务器地址') smtp_port = models.IntegerField(verbose_name='smtp端口') protocol = models.CharField(max_length=32, verbose_name='smtp协议') mailusername = models.CharField(max_length=300, verbose_name='邮件发送者') mailpasswd = fields.EncryptedCharField(max_length=300, verbose_name='邮件密码')
class Tunnel(models.Model): """ SSH隧道配置 """ tunnel_name = models.CharField('隧道名称', max_length=50, unique=True) host = models.CharField('隧道连接', max_length=200) port = models.IntegerField('端口', default=0) user = fields.EncryptedCharField(verbose_name='用户名', max_length=200, default='', blank=True, null=True) password = fields.EncryptedCharField(verbose_name='密码', max_length=300, default='', blank=True, null=True) pkey_path = fields.EncryptedCharField(verbose_name='密钥地址', max_length=300, default='', blank=True, null=True) pkey_password = fields.EncryptedCharField(verbose_name='密钥密码', max_length=300, default='', blank=True, null=True) create_time = models.DateTimeField('创建时间', auto_now_add=True) update_time = models.DateTimeField('更新时间', auto_now=True) def __str__(self): return self.tunnel_name class Meta: managed = True db_table = 'ssh_tunnel' verbose_name = u'隧道配置' verbose_name_plural = u'隧道配置'
class Config(models.Model): """ 配置信息表 """ item = models.CharField('配置项', max_length=200, primary_key=True) value = fields.EncryptedCharField(verbose_name='配置项值', max_length=500) description = models.CharField('描述', max_length=200, default='', blank=True) class Meta: managed = True db_table = 'sql_config' verbose_name = u'系统配置' verbose_name_plural = u'系统配置'
class CustomUser(AbstractBaseUser, PermissionsMixin): username_validator = UnicodeUsernameValidator() email = fields.EncryptedEmailField(blank=True, unique=True) username = fields.EncryptedCharField( max_length=150, unique=True, help_text= ('Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.' ), validators=[username_validator], error_messages={ 'unique': ("Пользователь с таким логином уже существует."), }, ) is_active = models.BooleanField(default=True) first_name = models.CharField(('first name'), max_length=150, blank=True) last_name = models.CharField(('last name'), max_length=150, blank=True) date_joined = models.DateTimeField(('date joined'), default=timezone.now) USERNAME_FIELD = "username" REQUIRED_FIELDS = [] is_staff = models.BooleanField(default=False, ) objects = UserManager() class Meta: verbose_name = ('user') verbose_name_plural = ('Пользователи') def clean(self): super().clean() self.email = self.__class__.objects.normalize_email(self.email) def get_full_name(self): """ Return the first_name plus the last_name, with a space in between. """ full_name = '%s %s' % (self.first_name, self.last_name) return full_name.strip() def get_short_name(self): """Return the short name for the user.""" return self.first_name def email_user(self, subject, message, from_email=None, **kwargs): """Send an email to this user.""" send_mail(subject, message, from_email, [self.email], **kwargs)
class User(models.Model): surname = models.CharField(max_length=100, verbose_name='Фамилия') name = models.CharField(max_length=100, verbose_name='Имя') lastName = models.CharField(max_length=100, verbose_name='Отчество') phone = models.CharField(max_length=15, null=True, blank=True, verbose_name='Телефон') address = models.CharField(max_length=200, verbose_name='Адрес') inn = fields.EncryptedCharField(max_length=50, verbose_name='ИНН') # Данные ИНН будут зашифрованы в базе def __str__(self): if self.lastName: return "{} {} {}".format(self.surname, self.name, self.lastName) else: return '{} {}'.format(self.name, self.surname) class Meta: verbose_name = 'Пользователь' verbose_name_plural = 'Пользователи'
class InstanceDatabase(models.Model): """ 实例数据库列表 """ instance = models.ForeignKey(Instance, on_delete=models.CASCADE) db_name = fields.EncryptedCharField(verbose_name='数据库名', max_length=128) owner = models.CharField('负责人', max_length=50, default='', blank=True) owner_display = models.CharField('负责人中文名', max_length=50, default='', blank=True) remark = models.CharField('备注', max_length=255, default='', blank=True) sys_time = models.DateTimeField('系统修改时间', auto_now=True) class Meta: managed = True db_table = 'instance_database' unique_together = ('instance', 'db_name') verbose_name = '实例数据库' verbose_name_plural = '实例数据库列表'
class Profile(models.Model): user = models.OneToOneField("auth_.CustomUser", null=False, on_delete=models.CASCADE, verbose_name='Пользователь') first_name = fields.EncryptedCharField(default='', verbose_name='Имя') surname = fields.EncryptedCharField(default='', verbose_name='Фамилия') patronymic = fields.EncryptedCharField(default='', verbose_name='Отчество') partner = models.OneToOneField("utils.Partner", null=True, on_delete=models.CASCADE, related_name='+') avatar = models.ImageField(upload_to='avatars', blank=True, default='avatars/default.jpg') activated = models.BooleanField(default=False) whatsapp = fields.EncryptedCharField(default=None, null=True) instagram = fields.EncryptedCharField(default=None, null=True) telegram = fields.EncryptedCharField(default=None, null=True) def __str__(self): return self.user.username class Meta: verbose_name_plural = ('Профили пользователей') def get_partner_list(self): return Profile.objects.filter(inviter=self).all() def upload_file(self, image_pillow): path = os.path.join(settings.MEDIA_ROOT, 'avatars', self.user.username + '.png') image_pillow.save(path) self.avatar = os.path.join('avatars', self.user.username + '.png') self.save() return get_avatar_profile_link(self)
class VehicleLicense(BasicModel, BIDModel): """ 车辆行驶证信息 """ class Meta: verbose_name = 'VehicleLicense' verbose_name_plural = verbose_name index_together = [ 'vehicle_type', 'use_character', 'model', 'is_verified' ] db_table = 'k_ls_vehicle_license' ordering = ('-pk', ) oss_key = models.URLField('图片路径', db_index=True, default='') usrid = models.BigIntegerField('用户', db_index=True, default=0) plate_num = models.CharField('车牌号', db_index=True, max_length=20, default='') vehicle_type = models.CharField('车辆类型', max_length=100, default='') use_character = models.CharField('车辆使用性质', max_length=100, default='') owner = mg_fields.EncryptedCharField(verbose_name='所有者名字', max_length=200, default='') address = mg_fields.EncryptedCharField(verbose_name='住址', max_length=255, default='') vin = mg_fields.EncryptedCharField(verbose_name='车辆识别代号', max_length=100, default='') engine_num = mg_fields.EncryptedCharField(verbose_name='发动机号码', max_length=100, default='') model = models.CharField('车辆品牌', max_length=200, default='') register_date = models.DateField('注册日期', null=True, default=None) issue_date = models.DateField('发证日期', null=True, default=None) reason = models.CharField('原因', max_length=200, default='') verified_at = models.DateTimeField('验证时间', null=True, default=None) is_verified = models.BooleanField('已验证', default=False) objects = VehicleLicenseManager() @cached_property def model_info(self): inst = VehicleModel.objects.get_vehicle_model( self.model, vehicle_type=self.vehicle_type) return inst @property def oss_url(self): """ OSS URL,无水印,60秒内有效 """ url = oss_sign_url(self.oss_key) return url @property def url_watermark(self): """ OSS URL,带水印,99秒内有效 """ url = oss_sign_url(self.oss_key, expires=99, watermark=True) return url @property def is_success(self): is_ok = self.vehicle_type and self.plate_num and self.owner return is_ok @property def is_support(self): """ 是否支持,仅支持 非营运的小型汽车或新能源车 """ is_type_ok = self.get_vehicle_type() in mc.VehicleTypeSupport is_character_ok = self.use_character == '非营运' is_ok = is_type_ok and is_character_ok return is_ok def get_vehicle_type(self): """ 车辆类型转车牌类型 """ if self.vehicle_type in vehicle_small_types: if len(self.plate_num) == 7: return mc.VehicleType.Small # 小型汽车 elif len(self.plate_num) == 8: return mc.VehicleType.Energy # 新能源车 return None def get_ocr_result(self): """ OCR查询 """ if self.is_success: logger.info( f'vehicle_license_get_ocr_result__is_success {self.pk}') return from server.applibs.outside.models import ImageOcr inst = ImageOcr.objects.image_ocr_create( self.oss_key, mc.OCRType.VehicleLicenseFront, usrid=self.usrid) if not inst.is_victor: logger.warning( f'vehicle_license_get_ocr_result__not_victor {self.pk}') return data = inst.result_dic try: self.vin = data['vin'] self.model = data['model'] self.owner = data['owner'] self.address = data['address'] self.plate_num = data['plateNum'] self.engine_num = data['engineNum'] self.vehicle_type = data['vehicleType'] self.use_character = data['useCharacter'] self.issue_date = pendulum.parse(data['issueDate'], exact=True) self.register_date = pendulum.parse(data['registerDate'], exact=True) up_fields = [ 'vin', 'model', 'owner', 'address', 'plate_num', 'engine_num', 'vehicle_type', 'use_character', 'issue_date', 'register_date', 'updated_at', ] self.save(update_fields=up_fields) except Exception as exc: self.reason = '行驶证信息识别不完整' self.save(update_fields=['reason', 'updated_at']) logger.exception( f'vehicle_license_ocr_result__save_error {str(exc)}') def check_verifid(self): """ 验证信息 """ if not (self.is_success and self.usrid > 0): self.is_verified = False self.reason = self.reason or '行驶证认证失败' elif not self.is_support: self.is_verified = False self.reason = '目前仅支持非营运的小型汽车或新能源车' else: self.is_verified = True self.save(update_fields=['is_verified', 'reason', 'updated_at']) return self.is_verified
class ExportDestination(TitleDescriptionModel): """ Some SSH reachable destination for file export. """ #: SSH destination host IP. ip = models.GenericIPAddressField( blank=False, null=False, verbose_name="IP", help_text=help_text.EXPORT_DESTINATION_IP, ) #: Authentication username. username = models.CharField( max_length=128, blank=False, null=False, help_text=help_text.EXPORT_DESTINATION_USERNAME, ) #: Authentication password. password = fields.EncryptedCharField( max_length=128, blank=False, null=False, help_text=help_text.EXPORT_DESTINATION_PASSWORD, ) #: Destination in the host filesystem. destination = models.CharField( max_length=512, blank=False, null=False, help_text=help_text.EXPORT_DESTINATION_PATH, ) #: Connection port. port = models.PositiveIntegerField(default=DEFAULT_PORT, null=False, help_text=help_text.SSH_PORT) #: SSH banner timeout in seconds. banner_timeout = models.PositiveIntegerField( default=DEFAULT_BANNER_TIMEOUT, null=False, help_text=help_text.SSH_BANNER_TIMEOUT, ) #: Socket connection timeout in seconds. socket_timeout = models.PositiveIntegerField( default=DEFAULT_SOCKET_TIMEOUT, null=False, help_text=help_text.SSH_SOCKET_TIMEOUT, ) #: Transport session negotiation timeout in seconds. negotiation_timeout = models.PositiveIntegerField( default=DEFAULT_NEGOTIATION_TIMEOUT, null=False, help_text=help_text.SSH_NEGOTIATION_TIMEOUT, ) #: Users that may export to this destination. users = models.ManyToManyField("accounts.User") # Host key cache. _key = None # Transport instance cache. _transport = None # SFTPClient cache. _sftp_client = None _logger = logging.getLogger("accounts.export_destination") #: String representation template. STRING_TEMPLATE: str = "{username}@{ip}" def __str__(self) -> str: """ Returns the string representation of this instance. Returns ------- str Export destination string representation """ return self.STRING_TEMPLATE.format(username=self.username, ip=self.ip) def get_key(self) -> paramiko.ecdsakey.ECDSAKey: """ Returns the public key of the host if it exists within the *known_hosts* file. See Also -------- * :meth:`key` Returns ------- paramiko.ecdsakey.ECDSAKey Host key """ # Log public key query start. start_log = logs.SSH_KEY_QUERY_START.format(ip=self.ip) self._logger.debug(start_log) try: key_dict = get_known_hosts()[self.ip] except KeyError: # IP address not found in the known hosts file. unknown_host_log = logs.SSH_KEY_QUERY_FAILURE.format(ip=self.ip) self._logger.info(unknown_host_log) pass else: # Log public key query success and return. key_name = key_dict.keys()[0] success_log = logs.SSH_KEY_QUERY_SUCCESS.format(key_name=key_name, ip=self.ip) self._logger.info(success_log) return key_dict[key_name] def create_transport(self, banner_timeout: int = None, socket_timeout: int = None) -> paramiko.Transport: """ Returns the transport instance which will be used to negotiate the connection. Parameters ---------- banner_timeout : int Timeout in seconds protocol banner read See Also -------- :meth:`transport` Returns ------- paramiko.Transport SSH transport thread """ # Log start start_log = logs.SSH_TRANSPORT_INIT_START.format( export_destination=self) self._logger.debug(start_log) # Create Transport instance. try: transport = paramiko.Transport((self.ip, self.port), socket_timeout=self.socket_timeout) except Exception as e: # Log exception and re-raise, exception_log = logs.SSH_TRANSPORT_INIT_FAILURE.format(exception=e) self._logger.warn(exception_log) raise else: # Log success. success_log = logs.SSH_TRANSPORT_INIT_SUCCESS.format( export_destination=self) self._logger.info(success_log) # Increase banner timeout to prevent exception raised due to lack of # resources. See: https://stackoverflow.com/a/59453832/4416932. transport.banner_timeout = self.banner_timeout # Set encryption algorithm type. if self.key is not None: expected_name = self.key.get_name() transport._preferred_keys = [expected_name] # Log transport encryption key name. transport_key_log = logs.SSH_TRANSPORT_KEY.format( ip=self.ip, key_name=expected_name) self._logger.debug(transport_key_log) return transport def query_public_key(self) -> Tuple[str, bytes]: """ Queries the remote host for a public key. Returns ------- Tuple[str, bytes] Key type name, Value as bytes """ # Log start. start_log = logs.SSH_HOST_KEY_QUERY_START.format(ip=self.ip) self._logger.debug(start_log) # Query host for public key. try: remote_key = self.transport.get_remote_server_key() except SSHException as e: # Log exception and re-raise. failure_log = logs.SSH_HOST_KEY_QUERY_FAILURE.format(ip=self.ip, exception=e) self._logger.info(failure_log) raise else: # Read key encryption name and value. name = remote_key.get_name() value = remote_key.asbytes() # Log success. success_log = logs.SSH_HOST_KEY_QUERY_SUCCESS.format(key_name=name, ip=self.ip) self._logger.info(success_log) return name, value def validate_public_key(self): """ Validates the host's public key against the known hosts file. """ if self.key: # Log validation start. start_log = logs.SSH_KEY_VALIDATION_START.format(ip=self.ip) self._logger.debug(start_log) # Read existing key information. expected_name = self.key.get_name() expected_value = self.key.asbytes() # Query key information from the host. name, value = self.query_public_key() # Check key validity. valid_key = name == expected_name and value == expected_value if not valid_key: # Log and raise exception for an invalid key. failure_log = logs.SSH_KEY_VALIDATION_FAILURE.format( ip=self.ip, expected_name=expected_name, name=name) self._logger.warn(failure_log) raise SSHException(failure_log) # Log public key validation success. sucess_log = logs.SSH_KEY_VALIDATION_SUCCESS.format(ip=self.ip) self._logger.info(sucess_log) else: skip_log = logs.SSH_KEY_VALIDATION_SKIP.format(ip=self.ip) self._logger.info(skip_log) def authenticate(self): """ Authenticate the transport session to the host using :attr:`username` and :attr:`password`. """ # Log password authentication start. start_log = logs.SSH_PASSWORD_AUTH_START.format( export_destination=self) self._logger.debug(start_log) try: self.transport.auth_password(self.username, self.password) except Exception as e: failure_log = logs.SSH_TRANSPORT_INIT_FAILURE.format( export_destination=self, exception=e) self._logger.warn(failure_log) else: success_log = logs.SSH_PASSWORD_AUTH_SUCCESS.format( export_destination=self) self._logger.info(success_log) def connect(self, timeout: int = None) -> None: """ Negotiates a connection with the host. Parameters ---------- timeout : int SSH session negotiation timeout value (seconds) """ if not self.transport.active: # Log SSH client session initialization start. start_log = logs.SSH_CONNECTION_START.format( export_destination=self) self._logger.debug(start_log) # Initialize SSH client session. try: self.transport.start_client(timeout=self.negotiation_timeout) except SSHException as e: # Log raised exception and re-raise. failure_log = logs.SSH_CONNECTION_FAILURE.format( export_destination=self, exception=e) self._logger.info(failure_log) raise else: # Log success. success_log = logs.SSH_CONNECTION_SUCCESS.format( export_destination=self) self._logger.info(success_log) self.validate_public_key() self.authenticate() else: # Log existing active connection found. skip_log = logs.SSH_TRANSPORT_ACTIVE.format( export_destination=self) self._logger.debug(skip_log) def start_sftp_client(self) -> paramiko.sftp_client.SFTPClient: """ Returns an SFTP client, enabling secure interaction with the host filesystem. See Also -------- * :meth:`sftp_client` Returns ------- paramiko.sftp_client.SFTPClient SFTP Client connected to the host filesystem """ # Log SFTP connection initialization. start_log = logs.SFTP_CLIENT_START.format(export_destination=self) self._logger.debug(start_log) # Establish SSH connection if it isn't already active. if not self.transport.active: self.connect() # Start SFTP client. try: sftp_client = paramiko.SFTPClient.from_transport(self.transport) except Exception as e: # Log exception and re-raise. failure_log = logs.SFTP_CLIENT_FAILURE.format( export_destination=self, exception=e) self._logger.warn(failure_log) raise else: # Log success and return SFTPClient instance. success_log = logs.SFTP_CLIENT_SUCCESS.format( export_destination=self) self._logger.info(success_log) # Disable relative path emulation, see: # https://docs.paramiko.org/en/stable/api/sftp.html#paramiko.sftp_client.SFTPClient.chdir sftp_client.chdir(path=None) return sftp_client def mkdir( self, path: Union[str, Path], parents: bool = True, exist_ok: bool = True, ): """ Create directory within the host. Parameters ---------- path : Union[str, Path] Directory path parents : bool Whether to create destination parents if they don't already exist exist_ok : bool Whether to raise an exception if the destination directory already exists """ # Log directory creation start. start_log = logs.SFTP_MKDIR_START.format(path=path, export_destination=self) self._logger.debug(start_log) # Iterate given directory destination parts and try to create. directory_names = [] for part in path.parts: if part == "/": continue # Create path instance for current iteration. directory_names.append(part) current_path = Path("/" + "/".join(directory_names)) # If not *parents* and the current path is not the destination # path, skip to last iteration. if not (parents or part == path.name): continue # If *parents* and the current path is not the destination path, # try to create the parent. elif parents and part != path.name: self.mkdir(current_path, parents=False, exist_ok=exist_ok) continue # Handle destination directory creation. else: try: # Check if the directory already exists. self.sftp_client.stat(str(current_path)) except FileNotFoundError: # Directory does not exist, create it. try: self.sftp_client.mkdir(str(current_path)) except OSError as e: # Log failure and re-raise. failure_log = logs.SFTP_MKDIR_FAILURE.format( export_destination=self, path=current_path, exception=e, ) self._logger.warn(failure_log) raise else: # Log success. success_log = logs.SFTP_MKDIR_SUCCESS.format( export_destination=self, path=current_path) self._logger.debug(success_log) else: # Log existing directory found and raise or return. log_exists = logs.SFTP_MKDIR_EXISTS.format( path=current_path, export_destination=self) self._logger.debug(log_exists) if not exist_ok: raise OSError(log_exists) def _put(self, source: Path, destination: Path): """ Utility method to reduce clutter due to logging and surrounding logic. Parameters ---------- source : Path Local file to copy destination : Path Absolute destination in the host file system """ # Create parent directory if needed. self.mkdir(destination.parent, parents=True, exist_ok=True) # Transfer file. try: self.sftp_client.put(str(source), str(destination), confirm=True) except OSError as e: # Log file transfer failure and re-raise. failure_log = logs.SFTP_PUT_FAILURE.format( source=source, export_destination=self, destination=destination, exception=e, ) self._logger.warning(failure_log) raise else: # Log success and return. success_log = logs.SFTP_PUT_SUCCESS.format( source=source, export_destination=self, destination=destination, ) self._logger.debug(success_log) def put( self, source: Union[Path, str, Iterable[Union[Path, str]]], destination: Union[Path, str] = None, exist_ok: bool = True, force: bool = False, progressbar: bool = False, ) -> None: """ Copies *source* (a filesystem accessible file path) to *destination* in the host using SFTP. Parameters ---------- source : Union[Path, str] Local file to copy destination : Union[Path, str] Destination in the host file system. if None, tries to use the *source* path relative to the application's MEDIA_ROOT exist_ok : bool, optional Whether to forgive trying to put an existing file (rather than raising an exception), default is True force : bool, optional Whether to override the file if it already exists in the host, default is False (if set to True, *exist_ok* is meaningless) progressbar : bool, optional Whether to display a progressbar or not, applicable only if *source* is an interable of paths, default is False """ # Handle iterable of paths. if not isinstance(source, (Path, str)): try: # Create progressbar if *progressbar* is True. iterable = (tqdm( source, unit="file", desc=f"Copying to {self}") if progressbar else source) # Iterate *source* and transfer files. for i, path in enumerate(iterable): dest = destination[i] if destination else None self.put(path, dest, exist_ok=exist_ok, force=force) except TypeError: # If iteration failed, log and re-raise. bad_input_log = logs.SFTP_PUT_BAD_INPUT.format( bad_type=type(source)) self._logger.warn(bad_input_log) raise else: return # Handle single file path. # Infer absolute destination path. destination = (Path(source).relative_to(settings.MEDIA_ROOT) if destination is None else destination) destination = (Path(destination) if Path(destination).is_absolute() else Path(self.destination) / destination) # Log file transfer start. start_log = logs.SFTP_PUT_START.format(source=source, export_destination=self, destination=destination) self._logger.debug(start_log) # Look for an existing file at the destination. try: self.sftp_client.stat(str(destination)) except FileNotFoundError: # No existing file found, continue to file transfer. pass else: # Log existing file found. exists_log = logs.SFTP_PUT_EXISTS.format(export_destination=self, destination=destination) self._logger.debug(exists_log) # Handle existing file found and *force* is False. if not force: # Log transfer termination and return. abort_log = logs.SFTP_PUT_ABORT.format( source=source, export_destination=self, destination=destination, ) self._logger.debug(abort_log) return # Handle existing file found and *exist_ok* is False. elif not exist_ok: raise OSError(exists_log) # Create parent directory if needed. self.mkdir(destination.parent, parents=True, exist_ok=True) # Transfer file. self._put(source, destination) @property def key(self): """ Returns the key of the host if it exists within the *known_hosts* file. See Also -------- * :meth:`get_key` Returns ------- paramiko.ecdsakey.ECDSAKey Host key """ if self._key is None: self._key = self.get_key() return self._key @property def transport(self): """ Returns the transport instance which will be used to negotiate the connection. See Also -------- :meth:`create_transport` Returns ------- paramiko.Transport SSH transport thread """ if self._transport is None: self._transport = self.create_transport() return self._transport @property def sftp_client(self) -> paramiko.sftp_client.SFTPClient: """ Returns an SFTP client, enabling secure interaction with the host filesystem. See Also -------- * :meth:`start_sftp_client` Returns ------- paramiko.sftp_client.SFTPClient SFTP Client connected to the host filesystem """ if self._sftp_client is None: self._sftp_client = self.start_sftp_client() return self._sftp_client
class CallRecord(BasicModel, BIDModel): """ 点击拨号 """ class Meta: verbose_name = 'CallRecord' verbose_name_plural = verbose_name index_together = ['provider', 'status', 'call_state'] db_table = 'k_os_call_record' ordering = ('-created_at',) free_limit = 2 # 每天免费通话次数 msgid = models.BigIntegerField('消息', unique=True) usrid = models.BigIntegerField('主叫用户', db_index=True, default=0) touchid = models.BigIntegerField('被叫用户', db_index=True, default=0) req_id = models.CharField('呼叫ID(供应商)', unique=True, max_length=50, null=True, default=None) status = models.SmallIntegerField('呼叫状态', choices=mc.CallStatus.choices, default=0) caller = mg_fields.EncryptedCharField(verbose_name='主叫号码', max_length=50, default='') called = mg_fields.EncryptedCharField(verbose_name='被叫号码', max_length=50, default='') callers_at = models.DateTimeField('主叫接听时间', db_index=True, null=True, default=None) callere_at = models.DateTimeField('主叫挂机时间', db_index=True, null=True, default=None) calleds_at = models.DateTimeField('被叫接听时间', db_index=True, null=True, default=None) callede_at = models.DateTimeField('被叫挂机时间', db_index=True, null=True, default=None) status_at = models.DateTimeField('状态同步时间戳', null=True, default=None) duration = models.PositiveSmallIntegerField('通话时长(秒)', default=0) # 32767 call_state = models.CharField('结果状态', max_length=20, default='') provider = models.CharField('服务提供商', max_length=20, default='') cost = models.PositiveSmallIntegerField('成本', default=0) fee = models.PositiveSmallIntegerField('计费', default=0) is_record = models.BooleanField('是否录音', default=False) record_file = models.URLField('录音文件', default='') objects = CallRecordManager() @property def callid(self): """ 外部ID """ return self.hid @property def is_end(self): """ 是否终态 """ is_yes = self.status in [ mc.CallStatus.ENDCaller, mc.CallStatus.ENDCalled, mc.CallStatus.ENDOKCall, ] return is_yes @cached_property def msg_info(self): """ 会话消息 """ from server.applibs.convert.models import Message inst = Message.objects.get(pk=self.msgid, sender=self.usrid) return inst @property def caller_seconds(self): """ 主叫接听时长 """ if not (self.callers_at and self.callere_at): return 0 assert self.callere_at > self.callers_at, self.pk seconds = (self.callere_at - self.callers_at).seconds return seconds @property def called_seconds(self): """ 被叫接听时长 """ if not (self.calleds_at and self.callede_at): return 0 assert self.callede_at > self.calleds_at, self.pk seconds = (self.callede_at - self.calleds_at).seconds return seconds @property def day_index(self): """ 当天第几次有效通话,以通话结束时间过滤 """ if not self.call_ts: return 0 end_at = deal_time.time_tzcn(self.callede_at) start_at = deal_time.time_floor_day(end_at) count = self.__class__.objects.filter( usrid=self.usrid, status=mc.CallStatus.ENDOKCall, callede_at__gte=start_at, callede_at__lt=end_at, ).count() return count + 1 @property def call_ts(self): """ 有效通话时长 """ if not (self.status == mc.CallStatus.ENDOKCall): return 0 return self.called_seconds @property def summary(self): """ 呼叫摘要 """ per = 60 # 分钟秒数 m, s = self.call_ts // per, self.call_ts % per status = self.get_status_display() state_desc = str(self.call_state).split('|').pop() state_desc = state_desc.replace('正常', '') state_desc = state_desc.replace('应答', '') state_desc = state_desc.replace('未知', '') state_desc = state_desc or '请稍后重试' if self.status == mc.CallStatus.ENDOKCall: desc = f'{status},时长:{m}′ {s}″' elif self.status == mc.CallStatus.ENDCalled: desc = f'{status}:{state_desc}' elif self.status == mc.CallStatus.ENDCaller: desc = f'{status}:{state_desc}' else: desc = f'{status}...' return desc def call_yxt(self): """ 云讯,双向呼叫 """ if self.req_id: warn_msg = f'call_yxt__done {self.pk} {self.req_id}' capture_message(warn_msg) logger.warning(warn_msg) return src = phonenumbers.parse(self.caller, None).national_number dst = phonenumbers.parse(self.called, None).national_number try: result = ytx_apis.YTXDailBackCallApi(src, dst, self.callid).fetch_result() self.req_id, self.provider = result['requestId'], mc.ThirdProvider.YTX self.save(update_fields=['req_id', 'provider', 'updated_at']) except Exception as exc: # {'statusCode': '-104', 'statusMsg': '请求频率过高'} self.call_state = str(exc) self.status = mc.CallStatus.ENDCaller self.save(update_fields=['status', 'call_state', 'updated_at']) self.msg_info.up_call_reach(self.status, self.summary) # 更新触达状态,发起失败 def query_call_ytx(self): """ 云讯,话单获取 """ if not (self.req_id and (self.provider == mc.ThirdProvider.YTX)): warn_msg = f'query_ytx__info_error {self.pk} {self.provider}' capture_message(warn_msg) logger.warning(warn_msg) return now = deal_time.get_now() created_at = deal_time.time_floor_ts(self.created_at) cut_seconds = (now - created_at).seconds if cut_seconds < 20: logger.warning(f'query_ytx__too_early {self.pk} {cut_seconds}') return # 查询太早没有结果 result = ytx_apis.YTXCallCdrByResIdOneApi( lastresid=self.req_id ).fetch_result() if not isinstance(result, dict): return # 无查询结果 self.call_result_ytx_up(result, action='query') def callback_call_ytx(self, result): """ 云讯,话单回调 """ if self.provider != mc.ThirdProvider.YTX: logger.warning(f'callback_ytx__provider_error {self.pk} {result}') return if self.req_id != result['requestid']: logger.warning(f'callback_ytx__req_id_error {self.pk} {result}') return self.call_result_ytx_up(result, action='callback') def call_result_ytx_up(self, result, action='query'): """ 云讯,话单更新 """ key = f'{self.pk} {action} {self.req_id}' assert self.provider == mc.ThirdProvider.YTX, f'{key}: {result}' assert self.req_id == result['requestid'], f'{key}: {result}' self.duration = result['duration'] self.call_state = result['stateDesc'] self.cost = int(100 * result['oriamount']) self.callers_at = deal_time.get_tzcn_parse(result['callerstime']) self.callere_at = deal_time.get_tzcn_parse(result['calleretime']) self.calleds_at = deal_time.get_tzcn_parse(result['calledstime']) self.callede_at = deal_time.get_tzcn_parse(result['calledetime']) self.save(update_fields=[ 'callers_at', 'callere_at', 'calleds_at', 'callede_at', 'call_state', 'duration', 'cost', 'updated_at', ]) self.extra_log(f'result-{action}', result=result) self.final_status_check() self.checkout() def final_status_check(self): """ 话单更新后,确认通话最终状态 """ old_status = self.status if self.caller_seconds and self.called_seconds: new_status = mc.CallStatus.ENDOKCall elif self.caller_seconds: new_status = mc.CallStatus.ENDCalled else: new_status = mc.CallStatus.ENDCaller self.status = new_status self.save(update_fields=['status', 'updated_at']) self.extra_log('status-check', status=self.status) if self.status == mc.CallStatus.ENDCalled: # 暂未接通 task = send_wxsm_for_msg_one.delay(self.msgid) logger.info(f'send_wxsm_for_msg_one__task {self.pk} {self.msgid} {task}') elif self.status == mc.CallStatus.ENDOKCall: # 通话结束 self.mark_msg_read() # 消息更新已读 self.msg_info.up_call_reach(self.status, self.summary) # 更新触达状态,终态 logger.warning(f'final_status_check__up {self.pk} {old_status} > {self.status}') def callback_status_ytx_up(self, result): """ 云讯回调,呼叫状态同步 """ if self.is_end: self.extra_log('status-callback-end', result=result) logger.warning(f'cb_status_ytx__end {self.pk} {self.status} {result}') return state_desc = result['stateDesc'] phone, state = result['dsc'], result['state'] status_at = deal_time.get_tzcn_parse(result['timestamp']) if self.status_at and status_at and (self.status_at > status_at): later_info = f'{self.pk} {self.status} {self.status_at} {result}' logger.warning(f'callback_status_ytx_up__time_later {later_info}') elif status_at: self.status_at = status_at if str(self.caller).endswith(phone): # 主叫 if state in [YTXCallState.Callout, YTXCallState.Alerting]: # 呼叫主叫... self.status = mc.CallStatus.OUTCaller elif state == YTXCallState.Answer: # 主叫接听 self.status = mc.CallStatus.OUTCalled elif state == YTXCallState.Disconnect: if self.status == mc.CallStatus.OUTCaller: # 主叫未接听挂断 self.status = mc.CallStatus.ENDCaller elif str(self.called).endswith(phone): # 被叫 if state in [YTXCallState.Callout, YTXCallState.Alerting]: # 呼叫被叫... if self.status != mc.CallStatus.OUTCalled: self.status = mc.CallStatus.OUTCalled elif state == YTXCallState.Answer: # 被叫接听 self.status = mc.CallStatus.ONCalling elif state == YTXCallState.Disconnect: if self.status == mc.CallStatus.OUTCalled: # 被叫未接听挂断 self.status = mc.CallStatus.ENDCalled else: logger.warning(f'callback_status_ytx_up__phone_error {self.pk} {result}') if self.is_end and not self.call_state: self.call_state = state_desc # 话单消息可能先到达 self.save(update_fields=['status', 'status_at', 'call_state', 'updated_at']) self.extra_log('status-callback', status=self.status, result=result) self.msg_info.up_call_reach(self.status, self.summary) # 更新触达状态 def checkout(self): """ 用户费用计算,每天两次免费通话,超次0.2元/分钟 """ from server.applibs.billing.models import BillDetail if not (self.call_ts > 0): return self.fee = int(20 * math.ceil(self.call_ts / 60)) self.save(update_fields=['fee', 'updated_at']) bill = BillDetail.objects.call_record_add(self) if not bill: return if bill.is_free or bill.is_paid: return # RTM推送通话账单 # 因小程序审核,计费功能2020-1105下线 # self.msg_info.rtm_event_bill_reach(bill.hid) logger.info(f'rtm_event_bill_reach__offline {bill.pk}') def mark_msg_read(self): if self.status != mc.CallStatus.ENDOKCall: return if not self.callede_at: return self.msg_info.mark_read(self.touchid, self.callede_at) self.msg_info.conv_info.check_unread()
class IDCard(BasicModel, BIDModel): """ 身份证 """ class Meta: verbose_name = 'IDCard' verbose_name_plural = verbose_name index_together = ['sex', 'birth', 'nationality', 'is_valid'] db_table = 'k_ac_idcard' ordering = ('-pk', ) oss_keys = models.JSONField('身份证照片', default=dict) # front、back shahash = models.CharField('SHA1签名', max_length=50, unique=True) usrid = models.BigIntegerField('用户', unique=True, null=True, default=None) number = mg_fields.EncryptedCharField(verbose_name='身份证号', max_length=100, unique=True) name = mg_fields.EncryptedCharField(verbose_name='姓名', max_length=200, default='') sex = models.CharField('性别', max_length=20, default='') nationality = models.CharField('民族', max_length=50, default='') birth = models.DateField('出生年月', null=True, default=None) address = mg_fields.EncryptedCharField(verbose_name='住址', max_length=255, default='') authority = mg_fields.EncryptedCharField(verbose_name='签发机构', max_length=255, default='') start_date = models.DateField('有效期开始日期', null=True, default=None) end_date = models.DateField('有效期结束日期', null=True, default=None) # 可能为None: 长期 is_valid = models.BooleanField('是否有效', default=False) # 身份证照片是否是复印件、身份证照片是否是翻拍 objects = IDCardManager() @property def img_front(self): """ 身份证正面图片,带水印,60秒内有效 """ oss_front = self.oss_keys['front'] url = oss.oss_sign_url(oss_front, expires=60, watermark=True) return url @property def img_back(self): """ 身份证背面图片,带水印,60秒内有效 """ oss_back = self.oss_keys['back'] url = oss.oss_sign_url(oss_back, expires=60, watermark=True) return url def img_hold_on(self): """ 认证通过后图片迁移目录 """ oss_keys = self.oss_keys oss_front, oss_back = oss_keys['front'], oss_keys['back'] is_ok_front, new_front_key = oss.oss_idcard_hold_on(oss_front) oss_keys['front'] = new_front_key if is_ok_front else oss_front is_ok_back, new_back_key = oss.oss_idcard_hold_on(oss_back) oss_keys['back'] = new_back_key if is_ok_back else oss_back if not (is_ok_front and is_ok_back): return False, '图片拉取失败' self.is_valid = True self.oss_keys = oss_keys self.save(update_fields=['is_valid', 'oss_keys', 'updated_at']) return True, self
class SmsRecord(BasicModel, BIDModel): """ 阿里短信发送记录,仅通知消息,需要关心发送结果 """ class Meta: verbose_name = 'SmsRecord' verbose_name_plural = verbose_name index_together = ['scene', 'status', 'err_code'] db_table = 'k_os_sms_record' ordering = ('-created_at', ) scene = models.CharField('场景', choices=mc.SMSNoticeScene.choices, max_length=25) number = mg_fields.EncryptedCharField(verbose_name='手机号', max_length=50, db_index=True) # E164,加密 usrid = models.BigIntegerField('触发用户', db_index=True, default=0) touchid = models.BigIntegerField('触达用户', db_index=True, default=0) bizid = models.CharField('回执', db_index=True, max_length=50, default='') # sign = models.CharField('签名', max_length=25, default='') template = models.CharField('模板', max_length=50, default='') params = models.JSONField('模板参数', default=dict) status = models.SmallIntegerField('发送状态', choices=mc.SMSStatus.choices, default=0) report_at = models.DateTimeField('收到运营商回执时间', null=True, default=None) send_at = models.DateTimeField('转发给运营商时间', null=True, default=None) instid = models.BigIntegerField('关联对象', db_index=True, default=0) # Message、PNVerify err_msg = models.CharField('错误信息', max_length=200, default='') err_code = models.CharField('错误码', max_length=50, default='') objects = SmsRecordManager() @property def sms_outid(self): """ 短信发送外部ID """ return f'notice-{self.hid}' @cached_property def parse_info(self): info = phonenumbers.parse(self.number, None) return info @property def is_status_final(self): """ 是否已终态 """ is_yes = self.status in [ mc.SMSStatus.Success, mc.SMSStatus.Failure, ] return is_yes @property def is_dev_fake(self): """ 开发测试,转钉钉通知 """ is_yes = self.bizid.startswith('dd-mock-') return is_yes @property def national(self): """ 国内号码,不带+86 """ number = str(self.parse_info.national_number) return number def send(self): """ 发送 """ if self.is_status_final: logger.warning(f'sms_send__status_final {self.pk}') return self.extra['resp_send'] resp_dic = sms_action.sms_send__notice(self) self.extra['resp_send'] = resp_dic self.bizid = resp_dic.get('BizId', '') self.status = mc.SMSStatus.Waiting if self.bizid else mc.SMSStatus.Init self.save(update_fields=['status', 'bizid', 'extra', 'updated_at']) logger.info(f'SmsRecord.send__done {self.pk} {resp_dic}') return resp_dic def query(self): """ 主动查询回执状态 """ if self.is_status_final and self.report_at: logger.info(f'sms_notice_query__final {self.pk}') return result = sms_action.sms_query__notice(self) assert result['OutId'] == self.sms_outid, f'{self.sms_outid} {result}' up_fields = [ 'extra', 'report_at', 'send_at', 'err_code', 'status', 'updated_at' ] self.report_at = get_tzcn_parse(result['ReceiveDate']) # 短信接收日期和时间 ?! self.send_at = get_tzcn_parse(result['SendDate']) # 短信发送日期和时间 ?! self.status = result['SendStatus'] self.err_code = result['ErrCode'] self.extra['resp_query'] = result self.save(update_fields=up_fields) def report_receipt(self, result): """ 短信发送回执MNS订阅 """ self.err_msg = result['err_msg'] self.err_code = result['err_code'] self.extra['size'] = result['sms_size'] self.send_at = get_tzcn_parse(result['send_time']) self.report_at = get_tzcn_parse(result['report_time']) if self.status in [mc.SMSStatus.Init, mc.SMSStatus.Waiting]: # 回调时序问题 self.status = mc.SMSStatus.Success if result[ 'success'] else mc.SMSStatus.Failure up_fields = [ 'err_msg', 'err_code', 'status', 'send_at', 'report_at', 'extra', 'updated_at' ] self.save(update_fields=up_fields) return True