20200310 CMDB基础设计
目录
昨日回顾
1.补充了一些 linux 相关的命令,全部都是要求大家记得,希望大家多敲多练
2.传统运维和自动化运维的比较 传统运维在上线的时候,会做一些重复事情。自动化运维就是设计各种系统,然后所有的后 续操作,全部都是通过这些系统完成,不需要运维人员再一次的介入 传统运维更强调人工的干预操作,而自动化运维不需要再一次人工介入操作 docker + k8s 用GO写的
3.自动化运维中非常重要的一个系统,基石项目:CMDB cmdb 管理服务器的所有的基本信息, 采集的信息,包括:服务器的主机名,IP,操作系统版本,磁盘信息,CPU信息,网卡信息 等,人工输入的信息,包括:服务器的机房,几层,机架,机架的第几层,服务器的管理人员
4.CMDB的两套设计方案
agent方案
将每一台服务器上部署相同的采集脚本,每天晚上定点执行这个采集的项目脚本,采集 完成之后,会将采集到的结果通过requests模块下面的post方法发送给服务端的API 接口,API拿到数据之后,会对数据进行二次分析,然后将得到的数据入库,最后, django启动一个webserver,从数据库中将数据展示出来
上述方案的缺点:当我们需要在增加服务器的时候,将脚本再一次的部署到服务器上,比较麻烦
ssh类方案
搞一台中控机,中控机上安装paramiko模块,登录到待采集的服务器上,执行相关的 linux命令。采集完成之后,会将采集到的结果通过requests模块下面的post方法 发送给服务端的API接口,API拿到数据之后,会对数据进行二次分析,然后将得到的 数据入库,最后,django启动一个webserver,从数据库中将数据展示出来
Paramiko介绍
安装
pip3 install pycrypto
pip3 install paramiko
paramiko包含两个核心组件:SSHClient和SFTPClient。
- SSHClient的作用类似于Linux的ssh命令,是对SSH会话的封装,该类封装了传输(Transport),通道(Channel)及SFTPClient建立的方法(open_sftp),通常用于执行远程命令。
- SFTPClient的作用类似与Linux的sftp命令,是对SFTP客户端的封装,用以实现远程文件操作,如文件上传、下载、修改文件权限等操作。
# Paramiko中的几个基础名词:
1、Channel:是一种类Socket,一种安全的SSH传输通道;
2、Transport:是一种加密的会话,使用时会同步创建了一个加密的Tunnels(通道),这个Tunnels叫做Channel;
3、Session:是client与Server保持连接的对象,用connect()/start_client()/start_server()开始会话。
获取信息
import paramiko
# 创建SSH对象
ssh = paramiko.SSHClient()
# 允许连接不在know_hosts文件中的主机
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
# 连接服务器
ssh.connect(hostname='192.168.79.131', port=22, username='root', password='root')
# 执行命令
stdin, stdout, stderr = ssh.exec_command(cmd)
# 获取命令结果
result = stdout.read()
# 关闭连接
ssh.close()
CMDB
目标:
同时实现这两套方案,好处是:想用哪一套方案,就可以根据配置选项来灵活的进行切换
采集端client的目录设计
- bin : 整个项目的启动入口文件 start.py
- src/core : 整个项目的源代码目录
- lib : 项目中第三方的库文件
- conf : 配置文件
- log : 写代码的时候,一定要打日志。但是日志文件的位置一定不再这个项目中的
- test: 测试使用的文件
高级配置文件
参考Django的配置,目标:
from django.conf import global_settings, settings print(settings.LANGUAGE_CODE)
通过settings这个对象,既能点出来用户自定义的配置,又能点出来高级的默认配置
代码
# 导入配置
from conf import settings
from . import global_settings
# 进行整合
class MySettings():
# 在init方法中整合自定制的配置和默认的配置
def __init__(self):
# 整合高级的配置文件
for k in dir(global_settings):
if k.isupper():
v = getattr(global_settings,k)
setattr(self,k,v)
# 整合自定义的配置文件
for k in dir(settings):
if k.isupper():
v = getattr(settings,k)
setattr(self,k,v)
settings = MySettings()
采集插件具体的思路
敏捷开发
快速上线开发一个项目,迅速上线.然后根据代码进行
- 第一版的核心代码
#### 1.两套方案采集ip信息
if settings.MODE == 'agent':
res = subprocess.getoutput('ifconfig')
print(res)
elif settings.MODE == 'ssh':
import paramiko # 创建SSH对象
ssh = paramiko.SSHClient()
# 允许连接不在know_hosts文件中的主机
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
# 连接服务器
ssh.connect(hostname='192.168.79.131', port=22, username='root', password='root')
# 执行命令
stdin, stdout, stderr = ssh.exec_command('ifconfig')
# 获取命令结果
result = stdout.read()
print(result)
# 关闭连接
ssh.close()
第一版更多的使用if else
来进行判断编码的方式使用面向过程的方式进行编程
存在的缺点:
- low
- 代码的扩展性差
- 不符合高内聚低耦合的原则
- 写函数类的时候,代码与该函数的功能一致
编码规范
1.变量名与值中间的等号,要有空格
2.变量名,函数名,类名命名风格必须一致
3.变量名,函数名,类名命名要有意义
4.函数体代码不超过50行
- 第二版改进方案
可插拔式的采集方式
参考Django的中间件
start启动文件
from src.plugins import PluginsManager
if __name__ == '__main__':
PluginsManager().execute()
plugins文件中的__init__
文件 (核心管理代码)
#-*- coding: utf-8 -*-
#!/usr/bin/env python3
' 包初始化 '
__author__ = 'Fwzzz'
from lib.conf.config import settings
import importlib
# 主要进行管理插件的类
class PluginsManager():
def __init__(self):
self.plugins_dict = settings.PLUGINS_DICT
# 从配置文件中读取采集的插件配置,执行每一个插件类对应的方法
def execute(self):
ret = {}
# 1.循环读取配置中的values
for k,v in self.plugins_dict.items():
'''
k : base, nic, cpu
v : src.plugins.base.Base
'''
# 2.分析采集类的路径 [src.plugins.base, Base]
module_path, class_name = v.rsplit('.',1)
# 3.导入模块的路径 import_module导入字符串的模块路径
module = importlib.import_module(module_path)
# print(module)
# 4.从模块中导入类
cls = getattr(module, class_name)
# print(cls)
# 5.实例化类,执行类对应的具体采集方法 (每个类都是process方法)
res = cls().process()
ret[k] = res
print(ret)
def command_func(self):
pass
settings文件中定义
# 参考Django中间件进行设置
PLUGINS_DICT = {
'base':'src.plugins.base.Base',
'cpu':'src.plugins.cpu.Cpu',
'disk':'src.plugins.disk.Disk',
'nic':'src.plugins.nic.Nic',
}
base 获取硬件信息
class Base():
def process(self):
return 'base...'
优化process
process代码冗余度过高
解决方法:
- 继承一个基类,其他的类执行这个基类中的方法 (上述解决方案存在的问题是:以后新增一个子类时候,都需要继承基类的 )
- 将函数名当成参数传入到另一个函数中 ( 此时传递的函数名实际上是一个内存地址,函数名加括号,代表执行一个函数)
**process 具体的分析代码 **
files文件夹:文件里面的内容,是执行linux命令得到的结果
debug模式
- debug 为True, 代表此时是测试开发阶段,数据源从files文件夹的对应的文件中读取
- debug 为FALSe, 代表此时是上线模式, 数据源就是执行具体的linux命令即可
代码
启动文件start
from src.plugins import PluginsManager
if __name__ == '__main__':
# 启动执行execute()方法获取服务器的数据 (定义返回的是字典)
ret = PluginsManager().execute()
for k, v in ret.items():
print(k, v)
自定义设置文件 settings
import os
# 获取基础文件路径
BASEDIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
USER = 'root'
# 切换获取服务器信息的方式
MODE = 'ssh' # anget
# True 代表开发测试阶段, False 代表上线 (自定义文件便于测试)
DEBUG = True
# 指定获取的服务器信息的内容 (参考django的中间件方式)
PLUGINS_DICT = {
'basic': 'src.plugins.basic.Basic',
'cpu': 'src.plugins.cpu.Cpu',
'disk': 'src.plugins.disk.Disk',
'nic': 'src.plugins.nic.Nic',
}
全局默认配置 global_settings
' 全局配置 '
__author__ = 'Fwzzz'
# 设置默认的配置,用户自定义的配置会覆盖全局的
USER = 'qqq'
整合配置 config
' 整合全局与自定义配置文件 '
__author__ = 'Fwzzz'
# 导入配置文件
from conf import settings
from . import global_settings
class MySettings():
# 在init方法中整合自定制的配置和默认的配置
def __init__(self):
# 先整合高级的配置文件
for k in dir(global_settings): # 循环出global设置中的所有属性
if k.isupper(): # 判读大写
v = getattr(global_settings, k) # 反射获取名称对应的值
setattr(self, k, v) # setattr设置属性
# 整合自定制的配置文件
for k in dir(settings):
if k.isupper():
v = getattr(settings, k)
setattr(self, k, v)
# 实例化
setting = MySettings()
获取服务器的信息
__init__
初始化获取信息
# 导入整合配置
from lib.conf.config import setting
import importlib
# 用于管理插件的类
class PluginsManager():
def __init__(self):
# 初始化,获取setting中的配置
self.plugins_dict = setting.PLUGINS_DICT # 规定获取服务器信息
self.settings = setting.MODE # 获取服务器信息的方式
self.debug = setting.DEBUG # 开发上线模式
# 从配置文件中读取采集的插件配置,执行插件类中对应的方法
def execute(self):
# 1.循环获取配置中的value值
ret = {}
for k, v in self.plugins_dict.items():
'''
自定义的配置,k是想要获取的信息名,v是具体执行文件路径
'''
# 2.分析采集类的路径
'''
获得方法的路径信息与具体类名
'''
module_path, class_name = v.rsplit('.', 1)
# 3.获取导入模块的路径 import_module: 导入字符串的模块路径
module = importlib.import_module(module_path)
# 4.从模块中获取导入类
cls = getattr(module,class_name)
# 实例化类,执行类对应的具体采集方法 (将类comand_func传入获取方法中,减少代码冗余)
res = cls().process(self.comand_func, self.debug)
# 执行注册文件中的每一个方法,并保存信息到字典中
ret[k] = res
return ret
# 定义获取信息的方法
def command_func(self,cmd):
# 判断当前获取的方式
if self.settings == 'agent':
import subprocess
res = subprocess.getoutput(cmd)
return res
elif self.settings == 'ssh':
import paramiko
# 创建ssh对象
ssh = paramiko.SSHClient()
# 允许连接不在know_hosts文件中的主机
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
# 连接服务器
ssh.connect(hostname='192.168.79.131', port=22, username='root', password='root')
# 执行命令 (传入的cmd命令)
stdin, stdout, stderr = ssh.exec_command(cmd)
# 获取命令结果
result = stdout.read()
# 关闭连接
ssh.close()
return result
basic.py
class Basic():
def __init__(self):
pass
def process(self, command_func, debug):
# 开发测试阶段
if debug:
output = {
'os_platform': "linux",
'os_version': "CentOS release 6.6 (Final)\nKernel \r on an \m",
'hostname': 'c1.com'
}
# 上线阶段
else:
output = {
'os_platform': command_func("uname").strip(),
'os_version': command_func("cat /etc/issue").strip().split('\n')[0],
'hostname': command_func("hostname").strip(),
}
return output
disk.py
#-*- coding: utf-8 -*-
#!/usr/bin/env python3
' 硬盘相关 '
__author__ = 'Fwzzz'
import re
import os
from lib.conf.config import setting
class Disk():
def __init__(self):
pass
@classmethod
def initial(cls):
return cls()
def process(self, command_func, debug):
if debug:
output = open(os.path.join(setting.BASEDIR, 'files/disk.out'), 'r', encoding='utf-8').read()
else:
output = command_func("sudo MegaCli -PDList -aALL")
return self.parse(output)
def parse(self, content):
"""
解析shell命令返回结果
:param content: shell 命令结果
:return:解析后的结果
"""
response = {}
result = []
for row_line in content.split("\n\n\n\n"):
result.append(row_line)
for item in result:
temp_dict = {}
for row in item.split('\n'):
if not row.strip():
continue
if len(row.split(':')) != 2:
continue
key, value = row.split(':')
name = self.mega_patter_match(key)
if name:
if key == 'Raw Size':
raw_size = re.search('(\d+\.\d+)', value.strip())
if raw_size:
temp_dict[name] = raw_size.group()
else:
raw_size = '0'
else:
temp_dict[name] = value.strip()
if temp_dict:
response[temp_dict['slot']] = temp_dict
return response
@staticmethod
def mega_patter_match(needle):
grep_pattern = {'Slot': 'slot', 'Raw Size': 'capacity', 'Inquiry': 'model', 'PD Type': 'pd_type'}
for key, value in grep_pattern.items():
if needle.startswith(key):
return value
return False
board.py
#-*- coding: utf-8 -*-
#!/usr/bin/env python3
' '
__author__ = 'Fwzzz'
import os
from lib.conf.config import settings
class Board(object):
def __init__(self):
pass
@classmethod
def initial(cls):
return cls()
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 -t1")
return self.parse(output)
def parse(self, content):
result = {}
key_map = {
'Manufacturer': 'manufacturer',
'Product Name': 'model',
'Serial Number': 'sn',
}
for item in content.split('\n'):
row_data = item.strip().split(':')
if len(row_data) == 2:
if row_data[0] in key_map:
result[key_map[row_data[0]]] = row_data[1].strip() if row_data[1] else row_data[1]
return result
memory.py
#-*- coding: utf-8 -*-
#!/usr/bin/env python3
' '
__author__ = 'Fwzzz'
import os
from lib import convert
from lib.conf.config import settings
class Memory(object):
def __init__(self):
pass
@classmethod
def initial(cls):
return cls()
def process(self, command_func, debug):
if debug:
output = open(os.path.join(settings.BASEDIR, 'files/memory.out'), 'r', encoding='utf-8').read()
else:
output = command_func("sudo dmidecode -q -t 17 2>/dev/null")
return self.parse(output)
def parse(self, content):
"""
解析shell命令返回结果
:param content: shell 命令结果
:return:解析后的结果
"""
ram_dict = {}
key_map = {
'Size': 'capacity',
'Locator': 'slot',
'Type': 'model',
'Speed': 'speed',
'Manufacturer': 'manufacturer',
'Serial Number': 'sn',
}
devices = content.split('Memory Device')
for item in devices:
item = item.strip()
if not item:
continue
if item.startswith('#'):
continue
segment = {}
lines = item.split('\n\t')
for line in lines:
if not line.strip():
continue
if len(line.split(':')):
key, value = line.split(':')
else:
key = line.split(':')[0]
value = ""
if key in key_map:
if key == 'Size':
segment[key_map['Size']] = convert.convert_mb_to_gb(value, 0)
else:
segment[key_map[key.strip()]] = value.strip()
ram_dict[segment['slot']] = segment
return ram_dict
nic.py
#-*- coding: utf-8 -*-
#!/usr/bin/env python3
' '
__author__ = 'Fwzzz'
import os
import re
from lib.conf.config import settings
class Nic(object):
def __init__(self):
pass
@classmethod
def initial(cls):
return cls()
def process(self, command_func, debug):
if debug:
output = open(os.path.join(settings.BASEDIR, 'files/nic.out'), 'r', encoding='utf-8').read()
interfaces_info = self._interfaces_ip(output)
else:
interfaces_info = self.linux_interfaces(command_func)
self.standard(interfaces_info)
return interfaces_info
def linux_interfaces(self, command_func):
'''
Obtain interface information for *NIX/BSD variants
'''
ifaces = dict()
ip_path = 'ip'
if ip_path:
cmd1 = command_func('sudo {0} link show'.format(ip_path))
cmd2 = command_func('sudo {0} addr show'.format(ip_path))
ifaces = self._interfaces_ip(cmd1 + '\n' + cmd2)
return ifaces
def which(self, exe):
def _is_executable_file_or_link(exe):
# check for os.X_OK doesn't suffice because directory may executable
return (os.access(exe, os.X_OK) and
(os.path.isfile(exe) or os.path.islink(exe)))
if exe:
if _is_executable_file_or_link(exe):
# executable in cwd or fullpath
return exe
# default path based on busybox's default
default_path = '/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin'
search_path = os.environ.get('PATH', default_path)
path_ext = os.environ.get('PATHEXT', '.EXE')
ext_list = path_ext.split(';')
search_path = search_path.split(os.pathsep)
if True:
# Add any dirs in the default_path which are not in search_path. If
# there was no PATH variable found in os.environ, then this will be
# a no-op. This ensures that all dirs in the default_path are
# searched, which lets salt.utils.which() work well when invoked by
# salt-call running from cron (which, depending on platform, may
# have a severely limited PATH).
search_path.extend(
[
x for x in default_path.split(os.pathsep)
if x not in search_path
]
)
for path in search_path:
full_path = os.path.join(path, exe)
if _is_executable_file_or_link(full_path):
return full_path
return None
def _number_of_set_bits_to_ipv4_netmask(self, set_bits): # pylint: disable=C0103
'''
Returns an IPv4 netmask from the integer representation of that mask.
Ex. 0xffffff00 -> '255.255.255.0'
'''
return self.cidr_to_ipv4_netmask(self._number_of_set_bits(set_bits))
def cidr_to_ipv4_netmask(self, cidr_bits):
'''
Returns an IPv4 netmask
'''
try:
cidr_bits = int(cidr_bits)
if not 1 <= cidr_bits <= 32:
return ''
except ValueError:
return ''
netmask = ''
for idx in range(4):
if idx:
netmask += '.'
if cidr_bits >= 8:
netmask += '255'
cidr_bits -= 8
else:
netmask += '{0:d}'.format(256 - (2 ** (8 - cidr_bits)))
cidr_bits = 0
return netmask
def _number_of_set_bits(self, x):
'''
Returns the number of bits that are set in a 32bit int
'''
# Taken from http://stackoverflow.com/a/4912729. Many thanks!
x -= (x >> 1) & 0x55555555
x = ((x >> 2) & 0x33333333) + (x & 0x33333333)
x = ((x >> 4) + x) & 0x0f0f0f0f
x += x >> 8
x += x >> 16
return x & 0x0000003f
def _interfaces_ip(self, out):
'''
Uses ip to return a dictionary of interfaces with various information about
each (up/down state, ip address, netmask, and hwaddr)
'''
ret = dict()
right_keys = ['name', 'hwaddr', 'up', 'netmask', 'ipaddrs']
def parse_network(value, cols):
'''
Return a tuple of ip, netmask, broadcast
based on the current set of cols
'''
brd = None
if '/' in value: # we have a CIDR in this address
ip, cidr = value.split('/') # pylint: disable=C0103
else:
ip = value # pylint: disable=C0103
cidr = 32
if type_ == 'inet':
mask = self.cidr_to_ipv4_netmask(int(cidr))
if 'brd' in cols:
brd = cols[cols.index('brd') + 1]
return (ip, mask, brd)
groups = re.compile('\r?\n\\d').split(out)
for group in groups:
iface = None
data = dict()
for line in group.splitlines():
if ' ' not in line:
continue
match = re.match(r'^\d*:\s+([\w.\-]+)(?:@)?([\w.\-]+)?:\s+<(.+)>', line)
if match:
iface, parent, attrs = match.groups()
if 'UP' in attrs.split(','):
data['up'] = True
else:
data['up'] = False
if parent and parent in right_keys:
data[parent] = parent
continue
cols = line.split()
if len(cols) >= 2:
type_, value = tuple(cols[0:2])
iflabel = cols[-1:][0]
if type_ in ('inet',):
if 'secondary' not in cols:
ipaddr, netmask, broadcast = parse_network(value, cols)
if type_ == 'inet':
if 'inet' not in data:
data['inet'] = list()
addr_obj = dict()
addr_obj['address'] = ipaddr
addr_obj['netmask'] = netmask
addr_obj['broadcast'] = broadcast
data['inet'].append(addr_obj)
else:
if 'secondary' not in data:
data['secondary'] = list()
ip_, mask, brd = parse_network(value, cols)
data['secondary'].append({
'type': type_,
'address': ip_,
'netmask': mask,
'broadcast': brd,
})
del ip_, mask, brd
elif type_.startswith('link'):
data['hwaddr'] = value
if iface:
if iface.startswith('pan') or iface.startswith('lo') or iface.startswith('v'):
del iface, data
else:
ret[iface] = data
del iface, data
return ret
def standard(self, interfaces_info):
for key, value in interfaces_info.items():
ipaddrs = set()
netmask = set()
if not 'inet' in value:
value['ipaddrs'] = ''
value['netmask'] = ''
else:
for item in value['inet']:
ipaddrs.add(item['address'])
netmask.add(item['netmask'])
value['ipaddrs'] = '/'.join(ipaddrs)
value['netmask'] = '/'.join(netmask)
del value['inet']
cpu.py
#-*- coding: utf-8 -*-
#!/usr/bin/env python3
' 获取cpu相关信息 '
__author__ = 'Fwzzz'
import os
from lib.conf.config import settings
class Cpu(object):
def process(self, command_func, debug):
if debug:
output = open(os.path.join(settings.BASEDIR, 'files/cpuinfo.out'), 'r', encoding='utf-8').read()
else:
output = command_func("cat /proc/cpuinfo")
return self.parse(output)
def parse(self, content):
"""
解析shell命令返回结果
:param content: shell 命令结果
:return:解析后的结果
"""
response = {'cpu_count': 0, 'cpu_physical_count': 0, 'cpu_model': ''}
cpu_physical_set = set()
content = content.strip()
for item in content.split('\n\n'):
for row_line in item.split('\n'):
key, value = row_line.split(':')
key = key.strip()
if key == 'processor':
response['cpu_count'] += 1
elif key == 'physical id':
cpu_physical_set.add(value)
elif key == 'model name':
if not response['cpu_model']:
response['cpu_model'] = value
response['cpu_physical_count'] = len(cpu_physical_set)
return response