CMDB_Agent+ssh版本+server端

CMDB_Agent版本

CMDB概念

  1. CMDB: Configure Manage DataBase
  2. 中文:配置管理数据库。
  3. 主要的作用是:收集服务器的基础信息(包括:服务器的主机名,ip,操作系统版本,磁盘,CPU等信息),将来提供给子系统(代码发布,工单系统等)数据

CMDB_Agent介绍

  1. 其本质上就是在各个服务器上执行subprocess.getoutput()命令,然后将每台机器上执行的结果,返回给主机API,然后主机API收到这些数据之后,放入到数据库中,最终通过web界面展现给用户
  2. 优点:速度快
  3. 缺点:需要为每台服务器步数一个Agent的程序

agent方案

  1. 将待采集的服务器看成一个agent,然后再服务器上使用pythonsubprocess模块执行linux相关的命令,然后分析得到的结果,将分析得到的结果通过requests模块发送给APIAPI获取到数据之后,进行二次比对数据,最后将比对的结果存入到数据库中,最后django起一个webserver从数据库中将数据获取出来,供用户查看

ssh类方案

  1. 在中控机服务器上安装一个模块叫paramiko模块,通过这个模块登录到带采集的服务器上,然后执行相关的linux命令,最后返回执行的结果,将分析得到的结果通过requests模块发送给APIAPI获取到数据之后,进行二次比对数据,最后将比对的结果存入到数据库中,最后django起一个webserver从数据库中将数据获取出来,供用户查看

相比较

  1. agent方案
  2. 优点:不需要额外的增加中控机。
  3. 缺点:每新增一台服务器,就需要额外部署agent脚本。使用场景是:服务器多的情况 (1000台以上)
  4. ssh方案
  5. 优点:不需要额外的部署脚本。
  6. 缺点:速度比较慢。使用场景是:服务器少 (1000台往下)

client端

架构目录

bin-start.py 启动文件

  1. from src.srcipt import run
  2. if __name__ == '__main__':
  3. run()

conf-config.py 自定义配置文件

模仿Django的setting,常用的配置写在这里面。不常用的写在global_settings.py中。

加载顺寻:先加载全局的。再加载局部的

  1. USER = 'root'
  2. MODE = 'agent'
  3. DEBUG = True # True:代表是开发测试阶段 False:代表是上现阶段
  4. import os
  5. BASEDIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
  6. PLUGINS_DICT = {
  7. 'basic': 'src.plugins.basic.Basic',
  8. 'cpu': 'src.plugins.cpu.Cpu',
  9. 'disk': 'src.plugins.disk.Disk',
  10. # 'memory': 'src.plugins.memory.Memory',
  11. }
  12. APIURL = 'http://127.0.0.1:8000/api/'

files 开发测试的文件

DEBUT=True时为测试阶段,用files的测试数据

lib-config-global_settings.py 全局配置的文件

  1. pass

lib-config-conf.py 读取配置的文件

全局配置放在前面先加载,自定义配置的放在后面后加载。自定义配置了就用自定义的(覆盖),没有配置久用全局的

  1. from conf import config
  2. from . import global_settings
  3. class mySettings():
  4. def __init__(self):
  5. # print('aa:', dir(global_settings))
  6. # print('bb:', dir(config))
  7. # 全局配置
  8. for k in dir(global_settings):
  9. if k.isupper():
  10. v = getattr(global_settings, k)
  11. setattr(self, k, v)
  12. # 自定义配置
  13. for k in dir(config):
  14. if k.isupper():
  15. v = getattr(config, k)
  16. setattr(self, k, v)
  17. settings = mySettings()

src-plugins-init.py 核心文件

  1. from lib.config.conf import settings
  2. import importlib
  3. class PluginsManager():
  4. def __init__(self, hostname=None):
  5. self.plugins_dict = settings.PLUGINS_DICT
  6. self.debug = settings.DEBUG
  7. self.hostname = hostname
  8. # 1.采集数据
  9. def execute(self):
  10. response = {}
  11. for k, v in self.plugins_dict.items():
  12. '''
  13. k: basic
  14. v: src.plugins.basic.Basic
  15. '''
  16. res = {'status':None, 'data':None}
  17. try:
  18. # 2.循环导入(字符串路径)
  19. moudle_path, class_name = v.rsplit('.', 1) # ['src.plugins.basic','Basic']
  20. # 用importlib.import_module()导入字符串路径
  21. m = importlib.import_module(moudle_path)
  22. # 3.导入类
  23. cls = getattr(m, class_name)
  24. # 循环执行鸭子类型的process方法,command_func函数的内存地址传过去,把debug传过去
  25. ret = cls().process(self.command_func, self.debug)
  26. res['status'] = 10000
  27. res['data'] = ret
  28. response[k] = res
  29. except Exception as e:
  30. import traceback
  31. res['status'] = 10001
  32. res['data'] = '错误信息:%s'%(traceback.format_exc())
  33. response[k] = res
  34. return response
  35. # 真正的连接,执行命令,返回结果的函数。命令变成参数
  36. def command_func(self, cmd):
  37. if settings.MODE == 'agent':
  38. import subprocess
  39. res = subprocess.getoutput(cmd)
  40. return res
  41. else:
  42. import paramiko
  43. # 创建SSH对象
  44. ssh = paramiko.SSHClient()
  45. # 允许连接不再know_hosts文件中的主机
  46. ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
  47. # 连接服务器
  48. ssh.connect(hostname=self.hostname, port=22, username='root', password='123456')
  49. # 执行命令
  50. stdin, stdout, stderr = ssh.exec_command(cmd)
  51. # 获取命令结果
  52. result = stdout.read()
  53. # 关闭连接
  54. ssh.close()
  55. return result

src-plugins-basic.py 查看硬件信息

  1. from conf import config
  2. class Basic(object):
  3. def process(self, command_func, debug):
  4. if debug:
  5. output = {
  6. 'os_platform': "linux",
  7. 'os_version': "CentOS release 6.6 (Final)\nKernel \r on an \m",
  8. 'hostname': 'c1.com'
  9. }
  10. else:
  11. output = {
  12. 'os_platform': command_func("uname").strip(),
  13. 'os_version': command_func("cat /etc/issue").strip().split('\n')[0],
  14. 'hostname': command_func("hostname").strip(),
  15. }
  16. return output

src-plugins-cpu.py 查看cpu属性

  1. import os
  2. from lib.config.conf import settings
  3. class Cpu():
  4. def __init__(self):
  5. pass
  6. def process(self, command_func, debug):
  7. if debug:
  8. output = open(os.path.join(settings.BASEDIR, 'files/cpuinfo.out'), 'r', encoding='utf-8').read()
  9. else:
  10. output = command_func("cat /proc/cpuinfo")
  11. return self.parse(output)
  12. def parse(self, content):
  13. """
  14. 解析shell命令返回结果
  15. :param content: shell 命令结果
  16. :return:解析后的结果
  17. """
  18. response = {'cpu_count': 0, 'cpu_physical_count': 0, 'cpu_model': ''}
  19. cpu_physical_set = set()
  20. content = content.strip()
  21. for item in content.split('\n\n'):
  22. for row_line in item.split('\n'):
  23. key, value = row_line.split(':')
  24. key = key.strip()
  25. if key == 'processor':
  26. response['cpu_count'] += 1
  27. elif key == 'physical id':
  28. cpu_physical_set.add(value)
  29. elif key == 'model name':
  30. if not response['cpu_model']:
  31. response['cpu_model'] = value
  32. response['cpu_physical_count'] = len(cpu_physical_set)
  33. return response

src-plugins-disk.py 查看磁盘信息

  1. # 采集磁盘信息
  2. from lib.config.conf import settings
  3. import os
  4. import re
  5. class Disk(object):
  6. def __init__(self):
  7. pass
  8. def process(self, command_func, debug):
  9. if debug:
  10. output = open(os.path.join(settings.BASEDIR, 'files/disk.out'), 'r', encoding='utf-8').read()
  11. else:
  12. output = command_func('MegaCli -PDList -aALL') # radi 卡 磁盘阵列
  13. return self.parse(output) # 调用过滤的函数
  14. # 过滤函数,对字符串的处理过滤
  15. def parse(self, content):
  16. """
  17. 解析shell命令返回结果
  18. :param content: shell 命令结果
  19. :return:解析后的结果
  20. """
  21. response = {}
  22. result = []
  23. for row_line in content.split("\n\n\n\n"):
  24. result.append(row_line)
  25. for item in result:
  26. temp_dict = {}
  27. for row in item.split('\n'):
  28. if not row.strip():
  29. continue
  30. if len(row.split(':')) != 2:
  31. continue
  32. key, value = row.split(':')
  33. name = self.mega_patter_match(key)
  34. if name:
  35. if key == 'Raw Size':
  36. raw_size = re.search('(\d+\.\d+)', value.strip())
  37. if raw_size:
  38. temp_dict[name] = raw_size.group()
  39. else:
  40. raw_size = '0'
  41. else:
  42. temp_dict[name] = value.strip()
  43. if temp_dict:
  44. response[temp_dict['slot']] = temp_dict
  45. return response
  46. @staticmethod
  47. def mega_patter_match(needle):
  48. grep_pattern = {'Slot': 'slot', 'Raw Size': 'capacity', 'Inquiry': 'model', 'PD Type': 'pd_type'}
  49. for key, value in grep_pattern.items():
  50. if needle.startswith(key):
  51. return value
  52. return False

server端

架构目录

  1. 服务端目录结构的设计 djangoapp
  2. - api 负责接收数据, 并且对比入库的
  3. - backend: 前端数据的展示
  4. - repository: 负责数据表的设计

配置

  1. 数据库,App注册等省略

repository-models.py 表设计

  1. from django.db import models
  2. # Create your models here.
  3. class UserProfile(models.Model):
  4. """
  5. 用户信息
  6. """
  7. name = models.CharField(u'姓名', max_length=32)
  8. email = models.EmailField(u'邮箱')
  9. phone = models.CharField(u'座机', max_length=32)
  10. mobile = models.CharField(u'手机', max_length=32)
  11. class Meta:
  12. verbose_name_plural = "用户表"
  13. def __str__(self):
  14. return self.name
  15. class UserGroup(models.Model):
  16. """
  17. 用户组
  18. """
  19. name = models.CharField(max_length=32, unique=True)
  20. users = models.ManyToManyField('UserProfile')
  21. class Meta:
  22. verbose_name_plural = "用户组表"
  23. def __str__(self):
  24. return self.name
  25. class BusinessUnit(models.Model):
  26. """
  27. 业务线
  28. """
  29. name = models.CharField('业务线', max_length=64, unique=True)
  30. contact = models.ForeignKey('UserGroup', verbose_name='业务联系人', related_name='c', on_delete=models.CASCADE)
  31. class Meta:
  32. verbose_name_plural = "业务线表"
  33. def __str__(self):
  34. return self.name
  35. class IDC(models.Model):
  36. """
  37. 机房信息
  38. """
  39. name = models.CharField('机房', max_length=32)
  40. floor = models.IntegerField('楼层', default=1)
  41. class Meta:
  42. verbose_name_plural = "机房表"
  43. def __str__(self):
  44. return self.name
  45. class Server(models.Model):
  46. """
  47. 服务器信息
  48. """
  49. device_type_choices = (
  50. (1, '服务器'),
  51. (2, '交换机'),
  52. (3, '防火墙'),
  53. )
  54. device_status_choices = (
  55. (1, '上架'),
  56. (2, '在线'),
  57. (3, '离线'),
  58. (4, '下架'),
  59. )
  60. device_type_id = models.IntegerField('服务器类型', choices=device_type_choices, default=1)
  61. device_status_id = models.IntegerField('服务器状态', choices=device_status_choices, default=1)
  62. cabinet_num = models.CharField('机柜号', max_length=30, null=True, blank=True)
  63. cabinet_order = models.CharField('机柜中序号', max_length=30, null=True, blank=True)
  64. idc = models.ForeignKey('IDC', verbose_name='IDC机房', null=True, blank=True, on_delete=models.CASCADE)
  65. business_unit = models.ForeignKey('BusinessUnit', verbose_name='属于的业务线', null=True, blank=True,
  66. on_delete=models.CASCADE)
  67. hostname = models.CharField('主机名', max_length=128, unique=True)
  68. sn = models.CharField('SN号', max_length=64, db_index=True, blank=True)
  69. manufacturer = models.CharField(verbose_name='制造商', max_length=64, null=True, blank=True)
  70. model = models.CharField('型号', max_length=64, null=True, blank=True)
  71. manage_ip = models.GenericIPAddressField('管理IP', null=True, blank=True)
  72. os_platform = models.CharField('系统', max_length=16, null=True, blank=True)
  73. os_version = models.CharField('系统版本', max_length=16, null=True, blank=True)
  74. cpu_count = models.IntegerField('CPU个数', null=True, blank=True)
  75. cpu_physical_count = models.IntegerField('CPU物理个数', null=True, blank=True)
  76. cpu_model = models.CharField('CPU型号', max_length=128, null=True, blank=True)
  77. create_at = models.DateTimeField(auto_now_add=True, blank=True)
  78. class Meta:
  79. verbose_name_plural = "服务器表"
  80. def __str__(self):
  81. return self.hostname
  82. class Disk(models.Model):
  83. """
  84. 硬盘信息
  85. """
  86. slot = models.CharField('插槽位', max_length=8)
  87. model = models.CharField('磁盘型号', max_length=32)
  88. capacity = models.CharField('磁盘容量GB', max_length=32)
  89. pd_type = models.CharField('磁盘类型', max_length=32)
  90. server_obj = models.ForeignKey('Server', related_name='disk', on_delete=models.CASCADE)
  91. class Meta:
  92. verbose_name_plural = "硬盘表"
  93. def __str__(self):
  94. return self.slot
  95. class NIC(models.Model):
  96. """
  97. 网卡信息
  98. """
  99. name = models.CharField('网卡名称', max_length=128)
  100. hwaddr = models.CharField('网卡mac地址', max_length=64)
  101. netmask = models.CharField(max_length=64)
  102. ipaddrs = models.CharField('ip地址', max_length=256)
  103. up = models.BooleanField(default=False)
  104. server_obj = models.ForeignKey('Server', related_name='nic', on_delete=models.CASCADE)
  105. class Meta:
  106. verbose_name_plural = "网卡表"
  107. def __str__(self):
  108. return self.name
  109. class Memory(models.Model):
  110. """
  111. 内存信息
  112. """
  113. slot = models.CharField('插槽位', max_length=32)
  114. manufacturer = models.CharField('制造商', max_length=32, null=True, blank=True)
  115. model = models.CharField('型号', max_length=64)
  116. capacity = models.FloatField('容量', null=True, blank=True)
  117. sn = models.CharField('内存SN号', max_length=64, null=True, blank=True)
  118. speed = models.CharField('速度', max_length=16, null=True, blank=True)
  119. server_obj = models.ForeignKey('Server', related_name='memory', on_delete=models.CASCADE)
  120. class Meta:
  121. verbose_name_plural = "内存表"
  122. def __str__(self):
  123. return self.slot
  124. class ErrorLog(models.Model):
  125. """
  126. 错误日志,如:agent采集数据错误 或 运行错误
  127. """
  128. server_obj = models.ForeignKey('Server', null=True, blank=True, on_delete=models.CASCADE)
  129. title = models.CharField(max_length=16)
  130. content = models.TextField()
  131. create_at = models.DateTimeField(auto_now_add=True)
  132. class Meta:
  133. verbose_name_plural = "错误日志表"
  134. def __str__(self):
  135. return self.title

Api-views.py 数据处理

这里只对磁盘信息做了处理

  1. 数据处理:
  2. '''
  3. 老的槽位信息: [1,2,6]
  4. 新的槽位信息:[1,2,3,4,5]
  5. 处理:增加3,4,5槽位
  6. 删除6槽位
  7. 检测1,2槽位有无更新磁盘信息
  8. 下面的所有事情:分析磁盘的信息与老信息
  9. 1.增加了那些槽位,
  10. 2.删除了那些槽位
  11. 3.更新了那些槽位,记录变更的日志
  12. '''
  1. from django.shortcuts import render,HttpResponse
  2. import json
  3. from repository import models
  4. def asset(request):
  5. if request.method == 'POST':
  6. info = json.loads(request.body)
  7. hostname = info['basic']['data']['hostname'] # c1.com
  8. # 每一个服务器的对象,这里固定为c1.com服务器
  9. server_obj = models.Server.objects.filter(hostname=hostname).first()
  10. if not server_obj:
  11. return HttpResponse('服务器为录入')
  12. # 磁盘数据状态码为例
  13. status = info['disk']['status'] # 状态码
  14. if status != 10000:
  15. # 添加错误信息
  16. models.ErrorLog.objects.create(title='错误信息', content=info['disk']['data'], server_obj=server_obj)
  17. return HttpResponse('采集出错!')
  18. '''
  19. 老的槽位信息: [1,2,6]
  20. 新的槽位信息:[1,2,3,4,5]
  21. 处理:增加3,4,5槽位
  22. 删除6槽位
  23. 检测1,2槽位有无更新磁盘信息
  24. 下面的所有事情:分析磁盘的信息与老信息
  25. 1.增加了那些槽位,
  26. 2.删除了那些槽位
  27. 3.更新了那些槽位,记录变更的日志
  28. '''
  29. new_disk_info = info['disk']['data'] # 新的磁盘信息
  30. print(new_disk_info)
  31. old_disk_info = models.Disk.objects.filter(server_obj=server_obj).all() # 老的磁盘信息
  32. # 集合去重
  33. new_slot = set(new_disk_info.keys())
  34. old_slot = []
  35. for obj in old_disk_info:
  36. old_slot.append(obj.slot)
  37. old_slot = set(old_slot)
  38. print(new_slot)
  39. print(old_slot)
  40. # 增加的槽位数据
  41. add_slot = new_slot.difference(old_slot)
  42. if add_slot:
  43. for slot in add_slot:
  44. ### {'slot': '3', 'pd_type': 'SATA', 'capacity': '476.939', 'model': 'S1AXNSAF912433K Samsung SSD 840 PRO Series DXM06B0Q'},
  45. add_disk_info = new_disk_info[slot]
  46. add_disk_info['server_obj'] = server_obj
  47. #### 可以将增加的变更 {2,3,4,5} 数据记录到变更日志表中
  48. models.Disk.objects.create(**add_disk_info)
  49. # 删除的槽位数据
  50. del_slot = old_slot.difference(new_slot)
  51. if del_slot:
  52. models.Disk.objects.filter(server_obj=server_obj, slot__in=del_slot).delete()
  53. ### 将删除的槽位数据记录到变更日志表中
  54. # 更新的槽位数据
  55. up_slot = new_slot.intersection(old_slot)
  56. if up_slot:
  57. for slot in up_slot:
  58. ## {'slot': '0', 'pd_type': 'SATA', 'capacity': '279.396', 'model': 'SEAGATE ST300MM0006 LS08S0K2B5NV'}
  59. new_disk_row = new_disk_info[slot]
  60. ## obj(slot:0, pd_type:sas, capacity:234,...)
  61. old_disk_row = models.Disk.objects.filter(slot=slot, server_obj=server_obj).first()
  62. for k, new_v in new_disk_row.items():
  63. '''
  64. k: slot, pd_type, capacity, model...
  65. new_v: 0, SATA, 279 , ...
  66. '''
  67. # 利用反射获取
  68. old_v = getattr(old_disk_row, k)
  69. if new_v != old_v:
  70. # 记录变更日志,利用反射添加
  71. setattr(old_disk_row, k, new_v)
  72. old_disk_row.save()
  73. print(info)
  74. return HttpResponse('ok')
  75. else:
  76. print('get')
  77. print(request.body)
  78. return ['c1.com','a2']

CMDB_Agent_ssh版本分析的更多相关文章

  1. Eclipse各版本分析比较

    Eclipse最初是由IBM公司开发的替代商业软件Visual Age for Java的下一代IDE开发环境,2001年11月贡献给开源社区,现在它由非营利软件供应商联盟Eclipse基金会. Ec ...

  2. OpenFlow协议1.0及1.3版本分析

    OpenFlow是SDN控制器和交换之间交流的协议,在SDN领域有着十分重要的地位. OpenFlow协议发展到现在已经经过了1.0.1.3.1.4等版本.其中1.0和1.3版本使用的是最为广泛的. ...

  3. [uboot] (第三章)uboot流程——uboot-spl代码流程 后续2018版本分析

    board_init_f在/u-boot-2018.07-fmxx/arch/arm/mach-fmxx/spl.c中定义 board_init_f之后,和转载的部分有出入: u-boot-2018. ...

  4. [阿里DIEN] 深度兴趣进化网络源码分析 之 Keras版本

    [阿里DIEN] 深度兴趣进化网络源码分析 之 Keras版本 目录 [阿里DIEN] 深度兴趣进化网络源码分析 之 Keras版本 0x00 摘要 0x01 背景 1.1 代码进化 1.2 Deep ...

  5. Android 各个版本WebView

    转载请注明出处   http://blog.csdn.net/typename/ powered by miechal zhao : miechalzhao@gmail.com 前言: 根据Googl ...

  6. MySQL 并行复制从库发生自动重启分析

    并行复制从库发生自动重启分析 背景 半同步复制从库在晚上凌晨2点半发生自动重启,另一个异步复制从库在第二天凌晨3点也发生了自动重启. 分析 版本mysql 5.7.16 mysql> show ...

  7. glusterfs4.0.1 mempool 分析笔记

    关于3.2.5版本分析,详见<GlusterFS之内存池(mem-pool)实现原理及代码详解> 此4.0.1版本内存池与版本3中的描述变化有点大,总的原理还是类似LINUX中的SLAB算 ...

  8. Restrramework源码(包含组件)分析

    1.总体流程分析 rest_framework/view.py 请求通过url分发,触发as_view方法,该方法在ViewSetMixin类下 点进去查看as_view源码说明,可以看到它在正常情况 ...

  9. 使用ucontext组件实现的coroutine代码分析

    coroutine一般翻译过来就是协程,类似于线程可以切换,而跟线程是由操作系统调度器来实现切换不一样,协程由用户程序自己调度进行切换.我以前也看过协程相关的内容,但没有自己去实现过.最近搞OpenS ...

随机推荐

  1. ConxtMenu高级用法

    ##背景我们经常在列表的页面中,点击列表中的行,一般进入详情页面,长按列表中一行,会弹出一个菜单,包含了对某一行的操作(编辑.删除等等),也知道通常的用法: 0x01. 在Activity中注册需要上 ...

  2. 使用itchat发送天气信息

    微信发送当日天气情况 念头萌生 之前在浏览网站的时候发现了篇文章「玩转树莓派」为女朋友打造一款智能语音闹钟,文章中介绍了使用树莓派打造一款语音播报天气的闹钟. 当时就想照着来,也自己做个闹钟.因为一直 ...

  3. 什么是AWVS

    什么是AWVS Acunetix Web Vulnerability Scanner(简称AWVS)是一款知名的网络漏洞扫描工具,它通过网络爬虫测试你的网站安全,检测流行安全漏洞,现已更新到10.(下 ...

  4. 告别ThinkPHP6的异常页面, 让我们来拥抱whoops吧

    春节期间熟悉了TP6, 也写了一个TP6的博客程序,但系统的异常页面实在另外头疼,很多时候无法查看到是哪行代码出的问题. 所以就特别的想把whoops引进来,经过一系列的研究,终于找到了解决的办法: ...

  5. cocoapods相关的知识点

    目录 1.安装和卸载cocoapods 安装 卸载 2.常规问题解决思路 50%报错问题可以通过 pod install或者pod update解决 指定swift编译版本 由于墙的原因,可能会ins ...

  6. 关于在layui中的table checkbox 默认选中设置

    一.layui版本 layui-v2.4.5 二.设置table的checkbox默认选中 总共有两种方法: 方法1:在返回的json中设置LAY_CHECKED为true,页面上的checkbox就 ...

  7. python初学者必看学习路线图!!!

    python应该是近几年比较火的语言之一,很多人刚学python不知道该如何学习,尤其是没有编程基础想要从事程序员工作的小白,想必应该都会有此疑惑,包括我刚学python的时候也是通过从网上查找相关资 ...

  8. html5 中高级选择器 querySelector

    简介 HTML5向Web API新引入了document.querySelector以及document.querySelectorAll两个方法用来更方便地从DOM选取元素,功能类似于jQuery的 ...

  9. Markdown试用

    目录 今天又是充满希望的一天 一.是什么 二.为什么 三.怎么做 代码 这世界上好人坏人都很多,我不是一个坏人. 我不是个英雄,我只是个拿

  10. 在Java中使用Collections.sort 依据多个字段排序

    一.如何使用Collections工具类进行排序 使用Collections工具类进行排序主要有两种方式: 1.对象实现Comparable接口,重写compareTo方法 /** * @author ...