一:介绍

作用:

       帮助我们定期从服务器上收集上节我们设计的表结构的对象的属性字段。并且同步到CMDB服务端,进行相应的数据更新(客户端存在asset_id)和写入(第一次收集数据,客户端不存在asset_id)。

设计: 

     客户端

需要判断本地是否存储asset_id,来判断想哪个url进行数据的推送。

  • 有asset_id 

  url:asset_report

  • 没有asset_Id

  url:asset_report_with_no_id

   服务端:

  • 需要对不同的url推送的数据,做不同处理。有asset_id的直接跟正式库存储的数据的进行比对更新。没有asset_id,需要把数据推送到临时表(NewAssetApprovalZone)。
  • 服务器端需要给客户端返回相应的asset_id,保证下次请求的时候客户端数据中携带asset_id。

注意:

       服务端是否需要做判断?如果客户端数据中携带asset_id的时候,服务器端不需要给客户端返回asset_id。还是一直给客户端返回asset_id?前者比较合理。但是本文介绍的是一直回传asset_id

   也许你有疑问,如果我们在客户端篡改客户端的asset_id的话,是否会造成正式的数据库的数据错误?

       答案:否定的,因为服务器端会根据客户端的asset_id和sn来唯一确定资产的归属,所以即使篡改一个,并不能导致数据的混乱。

CMDB服务端:可以根据url POST 过来的数据,url的不同,可以对数据做不同的处理(1、没有asset_id:数据需要审核才能录入正式数据库。2、有asset_id:数据需要和正式库中的数据进行比对更新。)

window 硬件收集模块: wmi。

1 pip install wmi

 在导入模块的时候需要window的扩展库,所以还需要安装win32,直接从官网下载:https://sourceforge.net/projects/pywin32/files/pywin32/Build%20220/pywin32-220.win-amd64-py3.6.exe/download

注意:我的环境:python3.6 64位 所以需要选择py3.6和64位,需要根据你的环境来确定下载的版本。

在安装win32的时候,如果我们安装的python没有在window操作系统中注册的话,会报如下错误:

cmdb  soa架构 cmdb设计思路_cmdb  soa架构

 

需要运行如下脚本将python注册掉操作系统中:

1 import sys
 2   
 3 from winreg import *
 4 
 5 # tweak as necessary
 6 version = sys.version[:3]
 7 installpath = sys.prefix
 8 
 9 regpath = "SOFTWARE\\Python\\Pythoncore\\%s\\" % (version)
10 installkey = "InstallPath"
11 pythonkey = "PythonPath"
12 pythonpath = "%s;%s\\Lib\\;%s\\DLLs\\" % (
13     installpath, installpath, installpath
14 )
15   
16 def RegisterPy():
17     try:
18         reg = OpenKey(HKEY_CURRENT_USER, regpath)
19     except EnvironmentError as e:
20         try:
21             reg = CreateKey(HKEY_CURRENT_USER, regpath)
22             SetValue(reg, installkey, REG_SZ, installpath)
23             SetValue(reg, pythonkey, REG_SZ, pythonpath)
24             CloseKey(reg)
25         except:
26             print("*** Unable to register!")
27             return
28         print("--- Python", version, "is now registered!")
29         return
30     if (QueryValue(reg, installkey) == installpath and
31         QueryValue(reg, pythonkey) == pythonpath):
32         CloseKey(reg)
33         print("=== Python", version, "is already registered!")
34         return
35     CloseKey(reg)
36     print("*** Unable to register!")
37     print("*** You probably have another Python installation!")
38   
39 if __name__ == "__main__":
40     RegisterPy()

 代码解析----执行步骤:

cmdb  soa架构 cmdb设计思路_数据_02

 

bin是客户端启动入口,调用core.HouseStark模块中的ArgvHangdler类,并传入参数,进行初始化。我们接下来看下ArgvHangdler类的构造方法都做了么?

模块导入:

1 from core import info_collection
2 from conf import settings
3 import urllib,sys,os,json,datetime
4 from core import api_token
1     def __init__(self,argv_list):
2         self.argvs = argv_list
3         self.parse_argv()

获取参数,并执行当前类中方法:parse_argv()

方法:parse_argv

1     def parse_argv(self):
2         if len(self.argvs) >1:#判断参数列表长度大于1表示合法。
3             if hasattr(self,self.argvs[1]):#获取参数列表中第2个参数,并进行反射查找是否为当前类的中方法。返回布尔值。
4                 func = getattr(self,self.argvs[1])#获取对应方法。
5                 func()#执行方法。
6             else:#参数不在我们定义的帮助信息或者不在我们的当前类中的方法。
7                 self.help_msg()#执行帮助函数。
8         else:#,长度为一表示参数非法,执行帮助函数。
9             self.help_msg()

帮助函数:

1     def help_msg(self):
2         msg = '''
3         collect_data  # 收集资产数据
4         run_forever   # 未实现
5         get_asset_id  # 获取资产id
6         report_asset  # 汇报资产数据到服务器
7         '''
8         print(msg)

 

选择功能:collect_data

根据上面的反射,我们进行查找对应的函数的:collect_data

函数如下:

1         def collect_data(self):
2             '''
3             功能:调用函数执行相应的操作系统的函数并进行相应的数据的收集。
4             :param self: 
5             :return: 
6             '''
7             obj = info_collection.InfoCollection()#
8             asset_data = obj.collect() #收集
9             print(asset_data)

 

根据上面模块的导入我们可以看到:from core import info_collection

初始化core下infor_collection模块里的类InfoCollection对象。

cmdb  soa架构 cmdb设计思路_python_03

我们来看初始化对象做了什么?

1 class InfoCollection(object):
2     '''收集硬件信息'''
3     def __init__(self):
4         pass

没做任何操作。然后通过这个对象执行collect方法。来看下collect方法:

1     def collect(self):
 2         '''
 3         功能:该函数主要功能是当前客户端运行的操作系统查看,以及执行对象的操作系统的函数,进行收集信息。
 4         :return: 返回收集的数据信息。
 5         '''
 6         os_platform = self.get_platform()#查看当前操作系统
 7         try:
 8             func = getattr(self,os_platform)#当前操作系统是否在当前类中的方法中。
 9             info_data = func()#如果在的话,获取对应的函数。
10             formatted_data = self.build_report_data(info_data)#对数据进行加工。
11             return formatted_data#返回收集的数据。
12         except AttributeError as e:#如果运行的客户端的操作系统不存在话,我们将进行抛错误,提示不支持该类os.
13             sys.exit("Error:MadKing doens't support os [%s]! " % os_platform)#退出程序。

 

因为当前我们在自己的window的机器上执行,所以执行window()函数。

1     def Windows(self):
 2         '''
 3         功能:该函数主要是执行插件中的WindowsSysInfo函数。
 4         :return: 返回收集信息,
 5         '''
 6         sys_info = plugin_api.WindowsSysInfo()
 7         #print(sys_info)
 8         #f = file('data_tmp.txt','wb')
 9         #f.write(json.dumps(sys_info))
10         #f.close()
11         return sys_info

 

然后我们看下插件api中的函数:

cmdb  soa架构 cmdb设计思路_python_04

看下函数:WindowsSysInfo()

cmdb  soa架构 cmdb设计思路_cmdb  soa架构_05

 

函数WindowsSysInfo()调用plugins 下的windows下的sysinfo.py模块的中的collect 并返回该值。

1 def collect():
 2     '''
 3     收集window客户端数据,并返回特定格式的数据。
 4     :return: 
 5     '''
 6     data = {
 7         'os_type': platform.system(),
 8         'os_release':"%s %s  %s "%( platform.release() ,platform.architecture()[0],platform.version()),
 9         'os_distribution': 'Microsoft',
10         'asset_type':'server',
11         #'ram':[]
12     }
13     #data.update(cpuinfo())
14     win32obj = Win32Info()#初始化类Win32Info,并调用其方法:获取cpu、ram、server、disk、nic等。
15     data.update(win32obj.get_cpu_info())#调用cpu方法。
16     data.update(win32obj.get_ram_info())#调用内存方法。
17     data.update(win32obj.get_server_info())#调用服务器信息。
18     data.update(win32obj.get_disk_info())#调用硬盘信息。
19     data.update(win32obj.get_nic_info())#调用网卡信息。
20 
21     #for k,v in data.items():
22     #    print k,v
23     return data##并返回我们构建的信息。
Win32Info类的方法:
cpu收集函数:
1     def get_cpu_info(self):
 2         '''
 3         收集cpu信息。
 4         :return: 返回cpu信息。
 5         '''
 6         data = {}
 7         cpu_lists = self.wmi_obj.Win32_Processor()#获取cpu列表
 8         cpu_core_count = 0#核数变量
 9 
10         for cpu in cpu_lists:
11             cpu_core_count += cpu.NumberOfCores#计算核数。
12             cpu_model = cpu.Name#cpu的型号。
13         data["cpu_count"] = len(cpu_lists)#cpu数量
14         data["cpu_model"] = cpu_model#cpu型号
15         data["cpu_core_count"] =cpu_core_count#cpu核心数量。
16         return data#返回cpu数据。
内存收集函数:
1     def get_ram_info(self):
 2         '''
 3         功能:该函数实现收集服务器的内存信息。
 4         :return: 返回内存信息。
 5         '''
 6         data = []#定义返回数据格式。
 7         ram_collections = self.wmi_service_connector.ExecQuery("Select * from Win32_PhysicalMemory")#内存信息列表。
 8         for item in ram_collections:#循环列表。
 9             item_data = {}
10             #print item
11             mb = int(1024 * 1024)
12             ram_size = int(item.Capacity) / mb#以G为单位。
13             item_data = {#构建特定格式的数据。
14                 "slot":item.DeviceLocator.strip(),
15                 "capacity":ram_size,
16                 "model":item.Caption,
17                 "manufactory":item.Manufacturer,
18                 "sn":item.SerialNumber,
19             }
20             data.append(item_data)
21         #for i in data:
22         #    print i
23         return {"ram":data}#返回内存信息。

收集server系统的信息。

1     def get_server_info(self):
 2         '''
 3          功能:服务器信息收集,
 4         :return: 返回服务器信息。
 5         '''
 6         computer_info =  self.wmi_obj.Win32_ComputerSystem()[0]#服务器操作系统。
 7         system_info =  self.wmi_obj.Win32_OperatingSystem()[0]#操作系统信息。
 8         data = {}
 9         data['manufactory'] = computer_info.Manufacturer#厂商。
10         data['model'] = computer_info.Model#型号。
11         data['wake_up_type'] = computer_info.WakeUpType#
12         data['sn'] = system_info.SerialNumber#收集sn信息。
13         #print data
14         return data#返回特定格式信息数据。

 

收集硬盘信息

1     def get_disk_info(self):
 2         '''
 3         收集硬盘信息。
 4         :return: 返回相应的硬盘信息。
 5         '''
 6         data = []#特定格式的数据。
 7         for disk in self.wmi_obj.Win32_DiskDrive():
 8             #print  disk.Model,disk.Size,disk.DeviceID,disk.Name,disk.Index,disk.SerialNumber,disk.SystemName,disk.Description
 9             item_data = {}
10             iface_choices = ["SAS","SCSI","SATA","SSD"]#接口类型
11             for iface in iface_choices:#
12                 if iface in disk.Model:#判断收集的硬盘接口信息是否在我们的列表中。
13                     item_data['iface_type']  = iface#如果存在的话,就指定硬盘接口,
14                     break#并跳出循环。
15             else:#如果不在我们 接口中,则表示无法识别硬盘的接口类型,
16                 item_data['iface_type']  = 'unknown'#标识为unknown。
17             item_data['slot']  = disk.Index#插槽。
18             item_data['sn']  = disk.SerialNumber#sn号
19             item_data['model']  = disk.Model#硬盘型号。
20             item_data['manufactory']  = disk.Manufacturer#硬盘厂商。
21             item_data['capacity']  = int(disk.Size ) / (1024*1024*1024)#计算硬盘的大小。
22             data.append(item_data)#一个服务器有可能有多个硬盘,所以把单个硬盘放在列表中。
23         return {'physical_disk_driver':data}#返回硬盘信息。

收集网卡信息:

1     def get_nic_info(self):
 2         '''
 3         收集网卡信息的函数。
 4         :return: 返回网卡信息。
 5         '''
 6         data = []#初始化列表。
 7         for nic in self.wmi_obj.Win32_NetworkAdapterConfiguration():#循环网卡信息列表。
 8             if nic.MACAddress is not None:#判断网卡的mac地址。
 9                 item_data = {}
10                 item_data['macaddress'] = nic.MACAddress#MAC地址。
11                 item_data['model'] = nic.Caption#型号
12                 item_data['name'] = nic.Index#名字。eth0
13                 if nic.IPAddress  is not None:#如果网卡的IP不为空。
14                     item_data['ipaddress'] = nic.IPAddress[0]#网卡的ip地址。
15                     item_data['netmask'] = nic.IPSubnet#网卡的子网掩码。
16                 else:#因为一个服务器上有多块网卡,有的存在IP,有的不存在IP。
17                     item_data['ipaddress'] = ''#不存在的请情况为空。
18                     item_data['netmask'] = ''#不存在的请情况为空。
19                 bonding = 0
20                 #print nic.MACAddress ,nic.IPAddress,nic.ServiceName,nic.Caption,nic.IPSubnet
21                 #print item_data
22                 data.append(item_data)#多块网卡的情况。
23         return {'nic':data}#放回网卡信息。

 根据上面的收集之后,会构建我们自己的数据:data返回到我们之前第三步中的collect函数。collect函数:

1     def collect(self):
 2         '''
 3         功能:该函数主要功能是当前客户端运行的操作系统查看,以及执行对象的操作系统的函数,进行收集信息。
 4         :return:
 5         '''
 6         os_platform = self.get_platform()
 7         try:
 8             func = getattr(self,os_platform)
 9             info_data = func()#上面调用函数返回值。
10             formatted_data = self.build_report_data(info_data)
11             return formatted_data
12         except AttributeError as e:
13             sys.exit("Error:MadKing doens't support os [%s]! " % os_platform)

接下来我们看下函数:ormatted_data = self.build_report_data(info_data)中build_report_data函数执行了什么?加入了token 并进行数据接口的认证。

1     def build_report_data(self,data):
2 
3         #add token info in here before send
4 
5         return data

然后数据放回collect_data函数种,并打印输出:

1         def collect_data(self):
2             '''
3             功能:调用函数执行相应的操作系统的函数并进行相应的数据的收集。
4             :param self:
5             :return:
6             '''
7             obj = info_collection.InfoCollection()
8             asset_data = obj.collect() #收集
9             print(asset_data)

至此,客户端收集数据整个过程结束。

总结:

  • 通过window的模块wmi和win32com进行window服务器信息的收集。不同的操作系统的收集的模块是不一样。
  • 这里需要注意的一个服务器:disk、nic在大多数操作系统中是多个。

汇报收集服务器信息:

收集信息过程大致和上面是一样的。

不同的是:report_data函数做了一些其他操作

1 def report_asset(self):
 2         '''
 3         功能:该函数主要作用收集幸运的操作系统信息,并发送给服务端。
 4         :return:
 5         '''
 6         obj = info_collection.InfoCollection()#初始对象。
 7         asset_data = obj.collect()#收集信息。
 8         asset_id = self.load_asset_id(asset_data["sn"])#判断此次收集的信息是第一次收集还是之前已经收集并返回asset_id。
 9         if asset_id: #reported to server before  如果 有表示以前发送服务端
10             asset_data["asset_id"] = asset_id#给数据设置相应的assert_id
11             post_url = "asset_report"#指定该数据的post的url。
12         else:#first time report to server,如果第一次进行数据的推送。
13             '''report to another url,this will put the asset into approval waiting zone, when the asset is approved ,this request returns
14             asset's ID'''
15 
16             asset_data["asset_id"] = None#设置成None asset_id
17             post_url = "asset_report_with_no_id"#设置post的url。
18 
19 
20 
21         data = {"asset_data": json.dumps(asset_data)}#进行json话。
22         response = self.__submit_data(post_url,data,method="post")#将数据发送给cmdb的服务端。
23         if "asset_id" in response:#判断客户端返回信息。
24             self.__update_asset_id(response["asset_id"])#根据返回的asset_id,对本地相应的文件进行写入。
25 
26         self.log_record(response)#调用日志函数并记录日志。

 

调用函数:__submit_data,__update_asset_id,log_record

我先看下我们setting的设置:

1 import os
 2 BaseDir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
 3 
 4 Params = {
 5     "server": "192.168.1.38",#推送的服务器。
 6     "port":9000,#推送的服务器的端口,如果没写使用80端口。
 7     'request_timeout':30,#请求超时。
 8     "urls":{#url 根据第一次还是之前已经推送。
 9           "asset_report_with_no_id":"/asset/report/asset_with_no_asset_id/",#第一次请求url。
10           "asset_report":"/asset/report/",#更新到数据库的数据。
11         },
12     'asset_id': '%s/var/.asset_id' % BaseDir,asset_id储存到本地文件路径,
13     'log_file': '%s/logs/run_log' % BaseDir,本地记录日志。
14 
15     'auth':{
16         'user':'lijie3721@126.com',#api认证。
17         'token': 'abc'
18         },
19 }

 

然后来看提交数据的函数:

1     def __submit_data(self,action_type,data,method):
 2         '''
 3         该函数主要作用是:对数据进行推送数据功能。
 4         :param action_type: get和post的url
 5         :param data: 推送的数据 服务器信息,
 6         :param method: get还是post请求,
 7         :return: 服务端回传的数据,
 8         '''
 9         if action_type in settings.Params['urls']:#判断传入url是否为们定义好的url。
10             if type(settings.Params['port']) is int:#如果我们设置了端口,直接使用我们设置的端口,
11                 url = "http://%s:%s%s" %(settings.Params['server'],settings.Params['port'],settings.Params['urls'][action_type])
12             else:#如果我们没有设置端口话,默认采用80端口。
13                 url = "http://%s%s" %(settings.Params['server'],settings.Params['urls'][action_type])
14 
15             url =  self.__attach_token(url)#对url进行api认证,
16             print('Connecting [%s], it may take a minute' % url)
17             if method == "get":#如果是get请求推送数据,
18                 args = ""#?后面的参数,
19                 for k,v in data.items():#get   ?传参数是以key value形式,循环我们数据拼接推送参数,
20                     args += "&%s=%s" %(k,v)
21                 args = args[1:]#因为首字母是不&所以只取1到最后的字符串。
22                 url_with_args = "%s?%s" %(url,args)#拼接推送url
23                 try:
24                     req = urllib.Request(url_with_args)#建立推送get请求。
25                     req_data = urllib.urlopen(req,timeout=settings.Params['request_timeout'])
26                     callback = req_data.read()#读取cmdb回传的数据,
27                     print("-->server response:",callback)
28                     return callback#返回回传数据。
29                 except urllib.URLError as e:
30                     sys.exit("\033[31;1m%s\033[0m"%e)
31             elif method == "post":#post对数据请求的时候。
32                 try:
33                     data_encode = urllib.urlencode(data)
34                     req = urllib.Request(url=url,data=data_encode)
35                     res_data = urllib.urlopen(req,timeout=settings.Params['request_timeout'])
36                     callback = res_data.read()
37                     callback = json.loads(callback)
38                     print("\033[31;1m[%s]:[%s]\033[0m response:\n%s" %(method,url,callback) )
39                     return callback
40                 except Exception as e:
41                     sys.exit("\033[31;1m%s\033[0m"%e)
42         else:
43             raise KeyError#如果传入非法参数的时候,报错,

 

客户端推送数据之后,cmdb返回数据处理函数:__update_asset_id

1     def __update_asset_id(self,new_asset_id):
 2         '''
 3         对本地asset_id重新的写入。
 4         :param new_asset_id:
 5         :return:无。
 6         '''
 7         asset_id_file = settings.Params['asset_id']
 8         f = open(asset_id_file,"wb")
 9         f.write(str(new_asset_id))
10         f.close()

本地日志记录:

1     def log_record(self,log,action_type=None):
 2         '''
 3         功能:该函数主要功能是记录推送的日志。
 4         :param log: handler
 5         :param action_type:
 6         :return:
 7         '''
 8         f = open(settings.Params["log_file"],"ab")
 9         if log is str:
10             pass
11         if type(log) is dict:
12 
13             if "info" in log:
14                 for msg in log["info"]:
15                     log_format = "%s\tINFO\t%s\n" %(datetime.datetime.now().strftime("%Y-%m-%d-%H:%M:%S"),msg)
16                     #print msg
17                     f.write(log_format)
18             if "error" in log:
19                 for msg in log["error"]:
20                     log_format = "%s\tERROR\t%s\n" %(datetime.datetime.now().strftime("%Y-%m-%d-%H:%M:%S"),msg)
21                     f.write(log_format)
22             if "warning" in log:
23                 for msg in log["warning"]:
24                     log_format = "%s\tWARNING\t%s\n" %(datetime.datetime.now().strftime("%Y-%m-%d-%H:%M:%S"),msg)
25                     f.write(log_format)
26 
27         f.close()

 

学习是一种态度,坚持是质变的利器!