Python的方向
1.web开发(Django,flask,tornado)
2.自动化运维(CMDB项目)
3.爬虫和数据分析
4.人工智能 机器学习 算法
5.自动化测试
CMDB项目
自动化运维
运维:管理服务器 分为:基础运维,应用运维
为什么使用cmdb?
-为了提高运维效率(实现自动化运维的第一步)
-Excel管理资产过于混乱,不方便年底进行资产的审计,因此需要开发一套cmdb项目
为什么需要自动化运维?
1.项目上线
流程:产品经理调研(画出原型图)--> 定需求 -->三方会谈(产品经理,研发,大佬们) --> 定日期 -->测试项目 --> 最终上线 -->应用运维
目前:把代码打包给运维,运维解压上线
问题:随着机器数量的线性增加,运维的工作量也是线性增加,重复而无意义的劳动
解决:
1.写一个shell脚本,进行部署
2.搞一个自动化代码上线系统
必要条件:
服务器的各种信息(主机名,cpu,硬盘大小等)
2.监控系统
检测服务器的各种信息(硬盘是否满,cpu的使用率,内存的使用率,网站服务运行是否正常)
问题:之前写简单的脚本,检测服务器的信息,比较麻烦
解决:想将服务器的各种信息以图表的信息展示在web界面上(可视化)
必要条件:服务器的各种信息(主机名,cpu,硬盘大小等)
3.自动化装机系统
问题:人工去装机,一台一台的装
解决:搞一个装机系统,cobbler软件
必要条件:服务器的各种信息(主机名,cpu等)
4.Excel表格审计管理资产
cmdb实现的核心
目标:收集服务器的信息(cpu,内存,网卡,硬盘等)
实现方式:
1.linux命令获取cpu,内存,网卡
2.python执行linux的命令
subprocess模块 getoutput函数
4种实现方案:
1.agent方式:
实现方式:用subprocess模块执行linux命令安装agent脚本,执行完成以后用requests模块发送get,post请求给api(主动推送),api拿到数据以后进行数据清洗分析,分析完成入库,然后可以在web管理界面进行展示.
缺点:每台服务器都要放置agent脚本
优点:速度快
使用场景:服务器较多的时候
2.ssh类的方式:
实现:中控机写paramiko模块脚本,登录上去,直接在服务器上执行命令,执行成功以后将命令结果返回给中控机.中控机将结果返回给api,api拿到数据以后进行数据清洗分析,分析完成入库,然后可以在web管理界面进行展示.
优点:无agent
缺点:有一个中控机,速度慢
使用场景:服务器比较少的时候
3.slat-stack方式(python写的):
实现:salt-master通过执行命令从ZeroMQ中拿到执行salt-minion放入其中的数据,将结果发送给api,pi拿到数据以后进行数据清洗分析,分析完成入库,然后可以在web管理界面进行展示.
使用场景:公司已经使用salt-stack软件
优点:开发成本低,速度快
缺点:依赖saltstack软件
linux安装salt-master:
[root@db01 ~]# yum install salt-master
配置文件
[root@db01 ~]# vim /etc/salt/master
interface: 本机IP
启动
[root@db01 ~]# service salt-master start
linux安装salt-minion:
[root@db02 ~]# yum install salt-minion
配置文件
[root@db02 ~]# vim /etc/salt/minion
master: salt-master IP
启动
[root@db02 ~]# service salt-minion start
授权:
[root@db01 ~]# salt-key -L : 列出所有的minion主机
[root@db01 ~]# salt-key -A : 接受所有的主机
发送命令:
[root@db01 ~]# salt "主机名" cmd.run "命令"
[root@db01 ~]# salt "db02" cmd.run "ifconfig"
4.puppet方式
rubby写的
CMDB项目代码分为三大部分
1.服务器数据采集
目标:
实现上述三种方案,然后通过配置,可以任意切换方案
规划采集项目目录:
bin:启动文件
conf:配置文件
lib:库文件或公共文件
src/core:源代码
test:测试文件
配置文件的管理
参考django的高级配置文件
django的全局配置文件:管理一些不常用的默认配置 比如:语言,email配置等
目的:引入settings不仅可以点出来用户自定义的配置也可以点出来全局的配置
config.py 用户自定义的配置
global_settings.py 全局配置
settings.py 整合自定义配置和全局配置
核心:setattr, getattr, dir 的用法
代码:
from conf import config
from lib.config import global_settings
class settings():
def __init__(self):
##整合自定义配置文件
for k in dir(config):
if k.isupper():
v = getattr(config,k)
setattr(self,k,v)
##整合全局配置文件
for k in dir(global_settings):
if k.isupper():
v = getattr(global_settings,k)
setattr(self,k,v)
settings = settings()
采集代码
实现高内聚低耦合
-一个文件就负责干你这个文件该干的事情
版本一:
在start.py中编写业务逻辑
缺点:扩产性差
优点:上线快
版本二:
将业务逻辑的代码以插件的形式,写在src目录下,相对于版本一更加的清爽,业务逻辑更加清晰
src下创建plugins(插件文件夹),该文件夹下包括:
basic.py(用来采集基础的信息:主机名,ip信息等)
board.py(收集主板信息:主板sn号)
disk.py(硬盘的信息)
cpu,py(cpu信息)
nic.py(网卡信息)
问题:无法随心所欲的备注
版本三:
可拔式插件采集信息
参考django的中间件的写法
config.py:
PLUGINS_DICT = {
'xxx' : 'src.plugins.xxx.Xxx',
......
}
src下plugins文件夹中
__init__.py:
def execute(self):
# 1. 获取配置
self.pluginSettings = settings.PLUGINS_DICT
response = {}
# 2. 循环执行
for k, v in self.pluginSettings.items():
# k: basic v: src.plugins.basic.Basic
module_name, class_name = v.rsplit('.', 1)
# 使用字符串路径导入模块
m = importlib.import_module(module_name)
# 通过类名获取模块下面的类
cls = getattr(m, class_name)
res = cls().process()
response[k] = res
return response
插件代码冗余
1.写一个基类,有一个通用的执行方法,然后所有插件继承基类
2.将函数名作为参数传入一个函数
salt的调用:
python2:
import salt.client
local = salt.client.LocalClient()
result = local.cmd('c2.salt.com', 'cmd.run', [cmd])
python3:
import subprocess
res_cmd = "salt '%s' cmd.run '%s'" % (self.hostname,cmd)
res = subprocess.getoutput(res_cmd)
return res
错误信息的管理:
容错(代码健壮性):
import traceback
traceback.format_exc()
post数据:
使用request.body获取数据
资产的采集过程中遇到的问题?
技术问题:
-Linux命令不熟 方案:查资料,问运维
-唯一标识问题:
我们之前用sn相当于电脑mac地址作为唯一标识,到数据库中去取数据
问题:虚拟机和实体机共用一个sn,导致最终取出来的数据丢失
解决:
业务逻辑解决:
-和产品商量,不采集虚拟机信息,但99%的公司,都是要采集的
技术方案解决:
-hostname(主机名)作为唯一标识
问题:主机名可以随时被修改
解决方案:
运维标准化流程:
a.手工录入服务器的机房,机架,第几层
b.提前对服务器分配主机名(主机名一定是唯一的)
c.第一次采集,需要运行一下采集资产的客户端(cmdbclient)
client端:
-将主机名保存到一个文件中
d.第n次采集,尽管主机名已经被开发更改,但我们永远以文件中的主机名为主
ps:生产过程中线上主机名一定要改回去
只要涉及到线上或生产环境中的东西千万不要动
解决唯一标识问题代码:
hostname = info['basic']['data']['hostname']
h = open(os.path.join(settings.BASEDIR, 'conf/cert'), 'r', encoding='utf-8').read()
if not h:
with open(os.path.join(settings.BASEDIR, 'conf/cert'), 'w', encoding='utf-8') as f:
f.write(hostname)
else:
info['basic']['data']['hostname'] = h
client.py中,当使用ssh方法或者slat-stack方法给server发送info时,因为是要一台一台服务器连接,发送,所以我们需要用线程池/进程池完成并发处理.
python2:
线程池:无
进程池:有
python3:
线程池:有
进程池:有
多线程节省资源所以使用多线程
def run(self,hostname):
info = PluginsManager(hostname=hostname).execute()
self.post_data(info)
def collect(self):
host_list = self.get_hosts()
p = ThreadPoolExecutor(10)
for hostname in host_list:
p.submit(self.run,hostname)
__init__,py中的代码:
import importlib
import traceback
from lib.config.settings import settings
#插件管理类
class PluginsManager():
def __init__(self,hostname=None):
self.mode = settings.MODE
self.hostname = hostname
self.debug = settings.DEBUG
if self.mode == 'ssh':
self.ssh_port = settings.SSH_PORT
self.ssh_username = settings.SSH_USERNAME
self.ssh_pwd = settings.SSH_PWD
def execute(self):
#1.获取配置
self.pluginSettings = settings.PLUGINS_DICT
response = {}
#2.循环执行
for k,v in self.pluginSettings.items():
ret = {'status':1,'data':None}
#k:basic v:src.plugins.basic.Basic
try:
module_name,class_name = v.rsplit('.',1)
m = importlib.import_module(module_name)
cls = getattr(m,class_name)
res = cls().process(self.command,self.debug)
ret['data'] = res
response[k] = ret
except Exception as e:
ret['code'] = 2
ret['data'] = "[%s] 模式下的主机 [%s] 采集[%s]出错,出错信息是:[%s]" % (self.mode, self.hostname if self.hostname else 'agent', k, traceback.format_exc())
response[k] = ret
return response
def command(self,cmd):
if self.mode == 'agent':
return self.__agent(cmd)
elif self.mode == 'ssh':
return self.__ssh(cmd)
elif self.mode == 'salt':
return self.__salt(cmd)
else:
return '只支持采集的模式为:agent/ssh/salt 模式'
#私有方法,外部的类无法调用
def __agent(self,cmd):
import subprocess
res = subprocess.getoutput(cmd)
return res
def __ssh(self,cmd):
import paramiko
# 创建SSH对象
ssh = paramiko.SSHClient()
# 允许连接不在know_hosts文件中的主机
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
# 连接服务器
ssh.connect(hostname=self.hostname, port=self.ssh_port, username=self.ssh_username, password=self.ssh_pwd)
# 执行命令
stdin, stdout, stderr = ssh.exec_command(cmd)
# 获取命令结果
result = stdout.read()
# 关闭连接
ssh.close()
return result
def __salt(self,cmd):
import subprocess
res_cmd = "salt '%s' cmd.run '%s'" % (self.hostname,cmd)
res = subprocess.getoutput(res_cmd)
return res
2.API获取数据并清洗入DB
数据库的设计:
重点: 表和表之间的关系
资产:
现在只收集服务器的信息
将来回收集交换机还有路由器等网络设备
目录结构:
创建新app:python3 manage.py startapp backend
python3 manage.py startapp repository
api : 只负责数据的接收和清洗, 并入库
backend : 负责后台数据的管理和记录
repository: 负责数据模型的创建
API的验证:
1.较简单的验证
client.py
import requests
token = 'asdfghjkl'
#将token值发送
requests.get('https://127.0.0.1.8000/asset',headers={'Token':token})
server.py
server_token = 'asdfghjkl'
#接受客户端的token值
client_token = request.META.get('HTTP_TOKEN')
#两者进行比较
if server_token != client_token:
return HttpResponse('非法请求')
问题:黑客可以通过抓包获取到url和token
2.加密验证
client.py
import requests,time,hashlib
token = 'asdfghjkl'
client_time = time.time()
tmp = '%s|%s'%(client_time,token)
m = hashlib.md5()
m.update(bytes(tmp,encoding='utf-8'))
time_token = m.hexdigest()
#将token和时间都发送过去
client_token = "%s|%s"%(time_token,client_time)
requests.get('http://127.0.0.1:8000/asset',headers={'Token':client_token})
server.py
server_token = 'asdfghjkl'
info = request.META.get('HTTP_TOKEN')
client_token , client_time = info.split('|')
#md5加密
m = hashlib.md5()
tmp = '%s|%s' %(client_time,server_token)
m.update(bytes(tmp,encoding='utf-8'))
server_ctoken = m.hexdigest()
if client_token != server_ctoken:
return HttpResponse('非法请求!')
问题:每次请求都会产生一条请求,这样就会产生成千上万条请求,如果加密方式不变,黑客抓取其中一条一直进行请求,总会获取到数据
3.最终版本
client.py
import requests,time,hashlib
token = 'asdfghjkl'
client_time = time.time()
tmp = '%s|%s'%(client_time,token)
m = hashlib.md5()
m.update(bytes(tmp,encoding='utf-8'))
time_token = m.hexdigest()
#将token和时间都发送过去
client_token = "%s|%s"%(time_token,client_time)
requests.get('http://127.0.0.1:8000/asset',headers={'Token':client_token})
server.py
key_record = {}
def asset():
#最终方案
server_token = 'asdfghjkl'
info = request.META.get('HTTP_TOKEN')
client_token , client_time = info.split('|')
# 1.超时时间验证
client_time = float(client_time)
server_time = time.time()
if server_time - client_time >30:
return HttpResponse('第一关超时了!')
#2.md5加密验证
m = hashlib.md5()
tmp = '%s|%s' %(client_time,server_token)
m.update(bytes(tmp,encoding='utf-8'))
server_ctoken = m.hexdigest()
if client_token != server_ctoken:
return HttpResponse('第二关 md5编码错误!')
#3.client_token 只能用一次 (可以使用redis数据库进行设置,将client_token设为k,client_time设为v,过期时间设为10秒)
for k in list(key_record.keys()):
v = key_record[k]
if server_time > v:
del key_record[k]
if client_token in key_record:
return HttpResponse('第三关 已经访问过了')
else:
key_record[client_token] = client_time + 10
return HttpResponse('重要的数据')
数据清洗入库
以disk表为例:
新的数据:
{
'0': {'slot': '0', 'pd_type': 'SAS', 'capacity': '279.396', 'model': 'SEAGATE ST300MM0006 LS08S0K2B5NV'},
'1': {'slot': '1', 'pd_type': 'SAS', 'capacity': '279.396', 'model': 'SEAGATE ST300MM0006 LS08S0K2B5AH'},
'2': {'slot': '2', 'pd_type': 'SATA', 'capacity': '476.939', 'model': 'S1SZNSAFA01085L Samsung SSD 850 PRO 512GB EXM01B6Q'},
'3': {'slot': '3', 'pd_type': 'SAS', 'capacity': '476.939', 'model': 'S1AXNSAF912433K Samsung SSD 840 PRO Series DXM06B0Q'},
'4': {'slot': '4', 'pd_type': 'SATA', 'capacity': '476.939', 'model': 'S1AXNSAF303909M Samsung SSD 840 PRO Series DXM05B0Q'},
'5': {'slot': '5', 'pd_type': 'SATA', 'capacity': '476.939', 'model': 'S1AXNSAFB00549A Samsung SSD 840 PRO Series DXM06B0Q'}
}
new_disk_data = info['disk']['data']
new_disk_slot = list(info['disk']['data'].keys())
旧的数据:
'''
[
{'slot':1, 'pd_type':'sas'},
{'slot':2, 'pd_type':'sas'},
....
]
'''
通过server_obj从数据库中获取相应的disk表中的数据
#old_disk_data = models.Disk.object.filter(server_obj = server_obj).all()
old_disk_data = server_obj.disk.all()
old_disk_solt = []
for i in old_disk_data:
old_disk_solt.append(item.solt)
增加:
add_slot = set(new_disk_solt).difference(set(old_disk_slot))
if add_slot:
change_log = []
for slot in add_slot:
disk_res = new_disk_data[slot]
disk_res['server_obj'] = server_obj
models.Disk.objects.create(**disk_res)
#信息
tmp = "增加硬盘 插槽是{slot}, 类型{pd_type}, 容量{capacity}, 型号{model}".format(**disk_res)
change_log.append(tmp)
#列表转换为字符串
content = ';'.join(change_log)
#将信息内容存入资产变更表
models.AssetRecord.objects.create(asset_obj=server_obj.asset, content=content)
删除:
del_solt = set(old_disk_slot).difference(set(new_disk_slot))
if del_slot:
# 到数据库中删除
change_log = []
for slot in del_slot:
models.Disk.objects.filter(slot=slot, server_obj=server_obj).delete()
tmp = "删除服务器%s 上的硬盘, 插槽%s" % (hostname, slot)
change_log.append(tmp)
#列表转换为字符串
content = ';'.join(change_log)
#将信息内容存入资产变更表
models.AssetRecord.objects.create(asset_obj=server_obj.asset, content=content)
更新:
up_slot = set(old_disk_slot).intersection(set(new_disk_slot))
if up_slot:
change_log = []
for slot in up_slot:
'''
{'slot': '4', 'pd_type': 'SATA', 'capacity': '476.939', 'model': 'Samsung SSD 840 PRO SeriesDXM05B0Q'},
'''
new_disk_row = new_disk_data[slot]
'''
{'slot':'4', 'pd_type':'sas', 'capacity':512, 'model':'Samsung SSD 840 PRO SeriesDXM05B0Q'}
'''
old_disk_row = models.Disk.objects.filter(slot=slot, server_obj=server_obj).first()
for k, new_v in new_disk_row.items():
'''
K: slot, pd_type, capacity, model
new_v: 4 ,sata, 476.939, Samsung
'''
#通过getattr方法拿到k对应的old_v
old_v = getattr(old_disk_row, k)
if str(new_v) != str(old_v):
#将old_disk_row的k值替换成new_v
setattr(old_disk_row, k, new_v)
tmp = "变更硬盘: 槽位 %s, %s 由 %s 变成了 %s" % (slot, key_map[k], old_v, new_v)
change_log.append(tmp)
#old_disk_row直接进行保存
old_disk_row.save()
content = ';'.join(change_log)
models.AssetRecord.objects.create(content=content, asset_obj=server_obj.asset)
3.web管理界面的展示
后台:
def server(request):
return render(request, 'server.html')
def server_ajax(request):
table_config = [
{
'field' : 'id', # 数据库中的一个字段
'text' : 'ID', # 表头中显示的内容
'display' : 1, #控制显示
},
{
'field': 'hostname', # 数据库中的一个字段
'text': '主机名', # 表头中显示的内容
'display' : 1
},
{
'field': 'sn', # 数据库中的一个字段
'text': 'sn号', # 表头中显示的内容
'display':1
},
{
'field': 'asset__business_unit__name', # 数据库中的一个字段,可以连表查询
'text': '产品线', # 表头中显示的内容
'display': 0
}
]
filed_list = []
for item in table_config:
if item['display']:
filed_list.append(item['field'])
data_list = models.Server.objects.values(*filed_list)
ret = {
'data_list' : list(data_list),
'table_config' : table_config
}
return HttpResponse(json.dumps(ret))
前端:server.html
<table>
<thead id="mythead">
</thead>
<tbody id="mybody">
</tbody>
</table>
<script>
$.ajax({
url : '/backend/server_ajax/',
type: 'GET',
dataType: 'json', // 相当于 JSON.parse()
success: function (msg) {
// console.log(JSON.parse(msg)); 第一种转换方式
console.log(msg);
initThead(msg.table_config);
initTbody(msg.data_list, msg.table_config);
}
});
function initThead(table_config){
/*
table_config = [
{
'field' : 'id', # 数据库中的一个字段
'text' : 'ID',
'display' : 1,
},
]
*/
var tr = document.createElement('tr'); // <tr></tr>
$.each(table_config, function (k, v) {
if(v['display']){
var th = document.createElement('th'); // <th>dsads</th>
th.innerHTML = v['text'];
$(tr).append($(th));
}
});
/*
<tr>
<th>dbsjada</th>
</tr>
*/
$('#mythead').append($(tr));
}
function initTbody(data_list, table_config) {
/**
* data_list = [{id: 1, hostname: "c1.com", sn: "gytrytrytr43543543"}]
*
* table_config = [
{
'field' : 'id', # 数据库中的一个字段
'text' : 'ID',
'display' : 1,
},
]
*/
$.each(data_list, function (k, value) {
var tr = document.createElement('tr');
$.each(table_config, function (k1, item) {
var td = document.createElement('td');
td.innerHTML = value[item['field']];
$(tr).append($(td));
});
$('#mybody').append($(tr));
});
}
</script>
前端界面框架: x-admin layui
前端图表:移动端:ANTV PC端:HCharts ECharts