一、项目架构:目录规范
# 遵循软件开发架构目录规范 bin 启动文件 src 源文件(核心代码) config 配置文件 lib 公共方法 tests 测试文件
二、采集规范
# bin目录下新建start.py # Autor:cxiong from lib.conf.config import settings if __name__ == '__main__': print(settings.USER) # config下新建custom_settings.py USER = '自定义用户配置' # lib下新建conf目录,再新建config.py和global_settings.py # config.py from config import custom_settings from . import global_settings class Settings: def __init__(self): # 先设置默认配置,再设置自定义配置 for name in dir(global_settings): if name.isupper(): k = name v = getattr(global_settings, k) setattr(self, k, v) # 自定义配置 for name in dir(custom_settings): if name.isupper(): k = name v = getattr(custom_settings, k) setattr(self, k, v) settings = Settings() # global_settings.py USER = "默认用户配置" # 运行start.py后可以查看获得测试结果
#直接在start.py中书写逻辑代码 # mode在settings设置 # Autor:cxiong from lib.conf.config import settings if __name__ == '__main__': # 先读取配置文件方案配置 mode = settings.MODE # 根据方案的不同,书写不同代码 if mode == 'agent': import subprocess res = subprocess.getoutput('ipconfig') # 针对获取到的数据进行筛选处理 print(res) elif mode == 'ssh': import paramiko # 创建对象 ssh = paramiko.SSHClient() # 允许链接不在konows_hosts里的主机 ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) # 链接服务器 ssh.connect(hostname='127. 0.0.1',port=2222,username='root',password='123123') # 执行命令 stdin,stdout,stderr = ssh.exec_command('ifconfig') # 获取结果 res = stdout.read() # 断开链接 ssh.close() print(res) else: import salt.client local = salt.client.LocalClient() res = local.cmd('127.0.0.1','cmd.run',['ifconfig']) print(res) # 这种方案存在的问题 存在的问题: 1.面向过程编程,扩展性差,后期不好维护、扩展 2.不符合代码设计规范 # 高内聚低耦合 """在写函数或者类的时候,代码中尽量不要多一行与函数或者类无关的代码(按照功能的不同拆分细化成不同的代码块)""" def get_user(): # 先获取订单数据 get_order() # 才能获取用户数据 pass def get_order(): pass
# 遵循高内聚低耦合 ''' 将面向过程编程修改为面向对象编程 在src文件夹内创建plugins文件夹,在该文件夹内根据信息的不同创建不同的py文件 存在的问题: 根据业务逻辑的不同,可能需要增加或者减少功能 代码需要修改,比较麻烦 3.参考django中间件 中间件如果我们不想执行某个只需要在配置文件中注释掉一行即可 如果想只需要添加一行字符串即可,并且也可以自定义中间件 ''' """ django中间件方法例子 需求:开发一个通知系统 可以发邮件通知 短信通知 微信通知 # settings.py NOTIFY_LIST = [ 'notify.email.Email', # 类的字符串路径 'notify.message.Message', 'notify.weixin.Weixin', 'notify.qq.QQ' ] # notify/目录下 # email.py class Email: def __init__(self): pass def send(self, message): print('邮箱通知:%s' %message) # settings文件 # 模仿django配置文件功能 NOTIFY_LIST = [ 'notify.email.Email', # 类的字符串路径 'notify.message.Message', 'notify.weixin.Weixin', 'notify.qq.QQ' ] # __init__.py # 主要配置方法 import settings import importlib def send_all(message): # 获取到所有发送通知的类,并且实例化产生对象调用send的方法 for i in settings.NOTIFY_LIST: module_path, class_str = i.rsplit('.', maxsplit=1) # print(module_path, class_str) module = importlib.import_module(module_path) # from notify import email class_name = getattr(module,class_str) # 根据字符串获取模块里的变量名 # print(class_name) obj = class_name() # 实例化对象 obj.send(message) # 调用类里面绑定给对象的方法 """
上面迭代版本有插拔式模块,可以根据需求增加模块并在settings.py中添加就可以实现
""" 最终版本 多个采集py文件中出现了大量的重复代码 1.将多个类里面相同的属性或者代码抽取出来形成一个父类 """ 对象:具有一系列属性和功能的结合体 类:多个对象共同的属性和功能的结合体 父类:多个类共同的属性和功能的结合体 """ class Base: # 填写if代码 class Board(Base): pass 2.在PluginsManager中定义一个方法传递给所有的对象 完善代码 1.__cmd_shh需要用户名、密码、端口等信息 2.__cmd_salt需要服务器地址 也就意味着不同的方案需要有不同的额外参数 class PluginsManager: def __init__(self, hostname=None): self.plugins_dict = settings.PLUGINS_DICT self.hostname = hostname if settings.mode == 'ssh': self.port = settings.SSH_PORT self.name = settings.SSH_USERNAME self.pwd = settings.SSH_PASSWORD """ 这里有一个前提:所有的服务器上都必须有一个相同的用户 而这个前提在实际工作中也是可以的实现的,是安全且被允许的 """
代码:IP以及账号密码暂未处理;服务器信息采集命令就是固定的
# 服务器账号密码IP端口和命令暂未分离 # 仅分离了功能 """ bin/start.py """ from src.plugins import PluginsManager if __name__ == '__main__': res = PluginsManager().execute() print(res) """ config/custom_settings.py """ # 采集方案 MODE = 'agent' # 基于django中间件思想完成功能的插拔式设计 PLUGINS_DICT = { "board": "src.plugins.board.Board", "disk": "src.plugins.disk.Disk", "memory": "src.plugins.memory.Memory", } """lib/conf/config.py""" from config import custom_settings from . import global_settings class Settings: def __init__(self): # 先设置默认配置,再设置自定义配置 for name in dir(global_settings): if name.isupper(): k = name v = getattr(global_settings, k) setattr(self, k, v) # 自定义配置 for name in dir(custom_settings): if name.isupper(): k = name v = getattr(custom_settings, k) setattr(self, k, v) settings = Settings() """lib/conf/global_settings.py""" USER = "默认用户配置" """src/plugins/board.py""" # 采集主板信息 from lib.conf.config import settings class Board: def process(self,command_func): command_func('ipconfig') return "board info" """src/plugins/__init__.py""" from lib.conf.config import settings class PluginsManager: def __init__(self): self.plugins_dict = settings.PLUGINS_DICT def execute(self): # {'board': 'src.plugins.board.Board', 'disk': 'src.plugins.disk.Disk', 'memory': 'src.plugins.memory.Memory'} response = {} for k,v in self.plugins_dict.items(): # k标识,v类路径 module_path,class_str = v.rsplit('.',maxsplit=1) # 利用字符串导入模块 import importlib module_name = importlib.import_module(module_path) # 获取类变量名 class_name=getattr(module_name,class_str) #类名加括号实例化对象 class_obj=class_name() # 执行绑定方法process res = class_obj.process(self.__cmd_run) response[k] = res return response # 定义一个私有的方法 def __cmd_run(self,cmd): # 根据方案的不同,书写不同代码 mode = settings.MODE if mode == 'agent': self.__cmd_agent(cmd) elif mode == 'ssh': self.__cmd_ssh(cmd) # import paramiko # # 创建对象 # ssh = paramiko.SSHClient() # # 允许链接不在konows_hosts里的主机 # ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) # # 链接服务器 # ssh.connect(hostname='127.0.0.1', port=2222, username='root', password='123123') # # 执行命令 # stdin, stdout, stderr = ssh.exec_command(cmd) # # 获取结果 # res = stdout.read() # # 断开链接 # ssh.close() # print(res) elif mode == 'salt': self.__cmd_salt(cmd) # import salt.client # local = salt.client.LocalClient() # res = local.cmd('127.0.0.1', 'cmd.run', [cmd]) # print(res) else: print('目前只支持agent/SSH/salt-stack方案') # 根据模式的不同拆分不同的方法 def __cmd_agent(self,cmd): import subprocess res = subprocess.getoutput(cmd) # 针对获取到的数据进行筛选处理 return res def __cmd_ssh(self,cmd): import paramiko # 创建对象 ssh = paramiko.SSHClient() # 允许链接不在konows_hosts里的主机 ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) # 链接服务器 ssh.connect(hostname='127.0.0.1', port=2222, username='root', password='123123') # 执行命令 stdin, stdout, stderr = ssh.exec_command(cmd) # 获取结果 res = stdout.read() # 断开链接 ssh.close() return res def __cmd_salt(self,cmd): """python不支持salt模块,python2才能使用""" # import salt.client # local = salt.client.LocalClient() # res = local.cmd('127.0.0.1', 'cmd.run', [cmd]) # return res """python3使用subprocess模块代替""" import subprocess command = 'salt "xxxxx" cmd.run %s' %cmd res = subprocess.getoutput(command) return res
完善代码:
代码再次改善:分离IP账号密码等
# 需要修改的代码 """src/plugins/__init__.py""" from lib.conf.config import settings class PluginsManager: def __init__(self,hostname=None): self.plugins_dict = settings.PLUGINS_DICT self.hostname = # 前提是所有服务器都必须有一个相同的用户,实际工作中也可以实现,是安全也被允许的 if settings.mode == 'ssh': self.port = settings.SSH_PORT self.name = settings.SSH_USERNAME self.pwd = settings.SSH_PASSWORD def execute(self): # {'board': 'src.plugins.board.Board', 'disk': 'src.plugins.disk.Disk', 'memory': 'src.plugins.memory.Memory'} response = {} for k,v in self.plugins_dict.items(): # k标识,v类路径 module_path,class_str = v.rsplit('.',maxsplit=1) # 利用字符串导入模块 import importlib module_name = importlib.import_module(module_path) # 获取类变量名 class_name=getattr(module_name,class_str) #类名加括号实例化对象 class_obj=class_name() # 执行绑定方法process res = class_obj.process(self.__cmd_run) response[k] = res return response # 定义一个私有的方法 def __cmd_run(self,cmd): # 根据方案的不同,书写不同代码 mode = settings.MODE if mode == 'agent': self.__cmd_agent(cmd) elif mode == 'ssh': self.__cmd_ssh(cmd) elif mode == 'salt': self.__cmd_salt(cmd) else: print('目前只支持agent/SSH/salt-stack方案') # 根据模式的不同拆分不同的方法 def __cmd_agent(self,cmd): import subprocess res = subprocess.getoutput(cmd) # 针对获取到的数据进行筛选处理 return res def __cmd_ssh(self,cmd): import paramiko # 创建对象 ssh = paramiko.SSHClient() # 允许链接不在konows_hosts里的主机 ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) # 链接服务器 ssh.connect(hostname=self.hostname, port=self.port, username=self.name ,password=self.pwd) # 执行命令 stdin, stdout, stderr = ssh.exec_command(cmd) # 获取结果 res = stdout.read() # 断开链接 ssh.close() return res def __cmd_salt(self,cmd): """python不支持salt模块,python2才能使用""" # import salt.client # local = salt.client.LocalClient() # res = local.cmd('127.0.0.1', 'cmd.run', [cmd]) # return res """python3使用subprocess模块代替""" import subprocess command = 'salt %s cmd.run %s' %(self.hostname,cmd) res = subprocess.getoutput(command) return res """config/custom_settings""" # 采集方案 MODE = 'agent' # 基于django中间件思想完成功能的插拔式设计 PLUGINS_DICT = { "board": "src.plugins.board.Board", "disk": "src.plugins.disk.Disk", "memory": "src.plugins.memory.Memory", } SSH_PORT = 22 SSH_USERNAME = 'root' SSH_PASSWORD = '123'
三、信息采集
# 采集主板信息 class Board: def process(self,command_func,debug): if debug: # 读取本地数据 output = open(os.path.join(settings.BASEDIR,'files/board.out'),'r',encoding='utf-8').read() else: # 读取线上服务器数据 output = command_func('sudo dmidecode |grep -A8 "System Information"') return output # 针对服务器获取到的数据进行处理大致都是一样的逻辑,对字符串进行切割处理
key_map = {
"Manufacturer": '',
"Product Name": '',
"Serial Number": '',
}
res = {} for i in data.split('\n'): row = i.strip().split(':') if len(row) == 2: # 判断列表第一个元素在不在需要的字典键中 if row[0] in key_map: res[row[0]] = row[1].strip() print(res)
四、采集异常处理
各类接口:https://www.juhe.cn/?bd_vid=11652700683301661916
1.status_code 响应状态码 由于在采集数据的时候可能会出现各式各样的错误,我们应该在交互数据的环境添加上响应状态码 2.异常处理 import traceback def func(): name try: func() except Exception as e: print('打印的结果:',traceback.format_exc()) 打印的结果: Traceback (most recent call last): File "/Users/jiboyuan/PycharmProjects/autoclient/tests/s2.py", line 7, in <module> func() File "/Users/jiboyuan/PycharmProjects/autoclient/tests/s2.py", line 5, in func name NameError: name 'name' is not defined
五、服务端数据采集
1.需要将采集到的数据发送给服务端 但是直接在start.py中书写又不符合代码编写规范 2.针对django后端的requests对象 当提交post请求获取数据都是用的requests.POST 但是只有在contentType参数是urlencoded的时候requests.POST才会有数据 如果是application/json,提交post请求数据并不会放到requests.POST中而是原封不动的放在requests.body中 requests.body中数据都是原封不动的二进制格式(bytes类型) 3.针对agent模式我们需要将数据基于网络发送给服务端 4.针对ssh和saltstack模式我们并不是需要将数据发送给服务端而是需要从服务端这里获取到我们想要采集的服务器地址 """api接口中的视图函数需要做get请求和post请求处理""" get请求用来给ssh和saltstack返回服务器地址列表 post请求用来给agent模式发送数据 def getInfo(requests): if requests.method == 'POST': server_info = json.loads(requests.body) return HttpResponse('OK') # 连接后台数据库获取主机名列表并返回 return ['c1.com','c2.com'] # 我们为了偷懒直接合并到一个视图函数 其实也可以拆开 都行
django代码:
# 创建项目 startapp API # 项目注册:settings.py,注销csrf # INSTALLED_APPS中新增项目 'API', # urls.py新增,接收agent发送的数据 url(r'^getInfo/',views.getInfo) # API/views.py from django.shortcuts import render, HttpResponse # Create your views here. import json def getInfo(requests): # 数据获取 if requests.method == 'POST': server_info = json.loads(requests.body) for k,v in server_info.items(): print(k,v) return HttpResponse('OK') # 链接后台数据库获取主机名列表并返回 return ['c1.com','c2.com']
5.1进程池与线程池
""" python2 有进程池但是没有线程池 python3 既有进程池又有线程池 """ # 客户端代码修改
# src/client.py # 由于ssh和saltstack都是获取主机名,所以两者直接整合到一起 class SSHSalt(Base): def get_hostnames(self): hostnames = requests.get(settings.API_URL) return hostnames # 暂时用固定代码代替 # return ['c1.com','c2.com'] def run(self,hostname): server_info = PluginsManager(hostname).execute() self.post_data(server_info) def collectAndPost(self): hostnames = self.get_hostnames() # 循环每一个主机名,依次采集,单线程 # for hostname in hostnames: # server_info = PluginsManager(hostname).execute() # self.post_data(server_info) """当主机名列表过于庞大,上述单线程处理方式非常慢,需要换成多线程""" # 采取线程池 多线程 from concurrent.futures import ThreadPoolExecutor p = ThreadPoolExecutor(20) for hostname in hostnames: p.submit(self.run,hostname)
# bin/start.py修改
from lib.conf.config import settings
from src import client
if __name__ == '__main__':
if settings.MODE == 'agent':
client.Agent().collectAndPost()
else:
client.SSHSalt().collectAndPost()
5.2 优化start.py启动文件,避免出现逻辑判断
# 针对start.py最最后的优化处理 # 新建文件src/srcipt.py from src.client import Agent,SSHSalt from lib.conf.config import settings def run(): if settings.MODE == 'agent': obj= Agent() else: obj = SSHSalt() obj.collectAndPost() # 启动文件bin/start.py修改 from src.srcipt import run if __name__ == '__main__': run()
总结:
上述采集功能代码
如果是agent模式,只需要将代码部署到服务器上并执行定时任务即可
如果是ssh和saltstack方案只需要找一台服务器(中控机),通过api获取需要采集的服务器地址即可
六、唯一标识
# 在资产统计过程中要想实现数据的更新和新增依据什么字段??? 原则是在新的post数据中选取一个唯一字段然后到数据库中作为wehre条件获取对应的数据 # 唯一字段 选取sn序列号(mac地址)作为唯一的字段 可能存在的问题 虚拟机和实体机是共用一个sn的,会导致数据不准确 解决的措施 1.纯业务层面上,如果公司不需要采集虚拟机信息那么使用sn没有问题(很少见) 2.采用hostname作为唯一标识 上述方案需要加认为的限制 在服务器给开发使用之前需要提前完成下列的操作 1.给这些服务器分配唯一的主机名 2.将分配好的主机名录入到后台管理的DB server表中 3.将采集的client代码运行一次,然后将得到的主机名地址保存到各自服务器某个文件中 4.之后就以该文件内主机名地址为准 """ 1.针对agent模式 上述方案可以考虑使用 2.但是针对ssh和saltstack模式一旦主机名修改没有还原直接导致该服务器资产无法采集到,责任落实到修改者 """
# src/client.py文件修改:增加主机名 class Agent(Base): def collectAndPost(self): server_info = PluginsManager().execute() hostname = server_info['basic']['data']['hostname'] res = open(os.path.join(settings.BASEDIR, 'config/cert'), 'r', encoding='utf-8').read() if not res.strip(): # 第一次采集,将采集到的hostname写入文件中 with open(os.path.join(settings.BASEDIR, 'config/cert'), 'w', encoding='utf-8') as f: f.write(hostname) else: # 第二次采集的时候,永远以第一次文件中保存的主机名为准 server_info['basic']['data']['hostname'] = res # for k, v in server_info.items(): # print(k, v) self.post_data(server_info)