为什么要设计CMDB?
目标是搭建自动化平台,包括装机,软件,平台,代码,基础是资产管理,
运维平台, 首要目的是对资产管理,自动更新数据,记录变更记录
相对于
1.Excel表格,麻烦
2.一套系统,自动更新数据,拿到资产变更记录
CMDB 是运维自动化项目
运维管理服务器,绝大多数运维人员都不会开发
运维工作:采购服务器,采购器件
运维自动化项目
减少人工干预,降低人员成本
- 资产管理
- 操作管理
CMDB -- 存储基础信息
- 运维自动化基础
- 资产管理,,对于资产信息,没必要实时更新,只需要隔一段时间再去取就行了
电脑--装机--系统--软件(环境),CMDB 工具使得可以快速自动化完成
多个系统部署
公司里指定自动化运维时,先制定规则,
规定,公司的服务器系统版本,内核,各个模版,
主机名不能重复,
主板的sn号,每个主机的主板sn号都是唯一的,
--不过,如果主机开启虚拟机,
--虚拟机的sn号跟主机是相同的,
java等强类型语言是编译运行的,也就是整个运行之前先编译一遍,看看哪里有问题,一旦出现问题就会运行不起来。
而python是弱类型语言,边解释边运行,程序出现问题只会在程序运行到那个部分时才报错,要不然不会报错。
对于记录日志,日志路径最好写成可修改形式的,也就是配置文件内,
日志不要和程序放在一起,因为日志文件会变得越来越大,之后迁移很麻烦
采集资产
--1.agent 形式
--2.ssh 类方式
--3.saltstack 方式
agent 形式,特定:需要agent客户端
import subprocess
import requests
# pip3 install requests
# ################## 采集数据 ##################
# result = subprocess.getoutput('ipconfig')
# result正则处理获取想要数据
# 整理资产信息
# data_dict ={
# 'nic': {},
# 'disk':{},
# 'mem':{}
# }
# ################## 发送数据 ##################
# requests.post('http://www.127.0.0.1:8000/assets.html',data=data_dict)
# 这里只是简写,具体还要完善
ssh 类方式,特点:慢
# 基于paramiko模块, pip3 install paramiko
import requests
import paramiko
# ################## 获取今日未采集主机名 ##################
#result = requests.get('http://www.127.0.0.1:8000/assets.html')
# result = ['c1.com','c2.com']
# ################## 通过paramiko连接远程服务器,执行命令 ##################
# 创建SSH对象
ssh = paramiko.SSHClient()
# 允许连接不在know_hosts文件中的主机
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
# 连接服务器
ssh.connect(hostname='192.168.14.36', port=22, username='wupeiqi', password='123')
# 执行命令
# stdin, stdout, stderr = ssh.exec_command('df')
# 获取命令结果
# result = stdout.read()
# 关闭连接
# ssh.close()
# print(result)
# data_dict = {result}
# ################## 发送数据 ##################
# requests.post('http://www.127.0.0.1:8000/assets.html',data=data_dict)
saltstack 方式,很多都在用
我在Centos 7 安装 saltstack 的过程中,发现 rpm 添加不了,可以换一种方式安装,
参考:https://blog.csdn.net/wanglei_storage/article/details/50574621
这里下面的只是供参考,,
# 1. 安装saltstack
# rpm --import https://repo.saltstack.com/yum/redhat/6/x86_64/latest/SALTSTACK-GPG-KEY.pub
#
#
"""
Master: yum install salt-master
Master准备:
a. 配置文件,监听本机IP
vim /etc/salt/master
interface: 本机IP地址
b. 启动master
/etc/init.d/salt-master start
Slave: yum install salt-minion
Slave准备:
a. 配置文件,连接那个master
vim /etc/salt/minion
master: 远程master地址
b. 启动slave
/etc/init.d/salt-minion start
2. 创建关系
查看
Master:salt-key -L
Accepted Keys:
Denied Keys:
Unaccepted Keys:
c1.com
c2.com
c3.com
Rejected Keys:
接受
Master:salt-key -a c1.com
Accepted Keys:
c1.com
c2.com
Denied Keys:
Unaccepted Keys:
c3.com
Rejected Keys:
3. 执行命令
master:
salt 'c1.com' cmd.run 'ifconfig'
import salt.client
local = salt.client.LocalClient()
result = local.cmd('c2.salt.com', 'cmd.run', ['ifconfig'])
"""
# ################## 获取今日未采集主机名 ##################
#result = requests.get('http://www.127.0.0.1:8000/assets.html')
# result = ['c1.com','c2.com']
# ################## 远程服务器执行命令 ##################
# import subprocess
# result = subprocess.getoutput("salt 'c1.com' cmd.run 'ifconfig'")
#
# import salt.client
# local = salt.client.LocalClient()
# result = local.cmd('c2.salt.com', 'cmd.run', ['ifconfig'])
# ################## 发送数据 ##################
# requests.post('http://www.127.0.0.1:8000/assets.html',data=data_dict)
链接:https://pan.baidu.com/s/1EtOF1t_9sA3Ey6WWJjQNdQ 密码:snsc
告别数据库的CURD
也就是用这一套模版,就可以实现各种models的操作,对各个不同的models,可以很快的做出一个增删改查的页面出来
CMDB总结:
- 三种采集资产方式
--唯一标识- API
--API验证(tornado源码,加密cookie+时间限制+访问记录)
--数据库表结构- 后台管理
--告别CURD,公共组件(前端+后端配置)
运维自动化项目
减少人工干预,降低人员成本
-- 资产管理
-- 操作管理
CMDB
- 运维自动化基础
- 资产管理
功能:
---- Agent或中控机
---- 字典套字典 =》 数据
---- 认证API
-------------a. 时间超时
-------------b. 访问记录
-------------c. 加密后进行比较
====> Tornado:源码中来了 <====
- API
- 后台管理(10分钟完成CURD)
- 对于数据库表数据进行增删改查
==》 组件:基本增删改查 《===
通用的数据库增删改查的插件,只需要修改config配置列表 ,就可以实现对各种models的操作
注意:在js文件中,我没有写添加和删除事件,添加可以另起一个html页面,需要自定制,删除只需要传递一个id到后台就行了,,,
保存传递到django中的数据也需要修改一下,,,也就是PUT那里。。。。
models.py 文件
class Asset(models.Model):
"""
资产信息表,所有资产公共信息(交换机,服务器,防火墙等)
"""
device_type_choices = (
(1, '服务器'),
(2, '交换机'),
(3, '防火墙'),
)
device_status_choices = (
(1, '上架'),
(2, '在线'),
(3, '离线'),
(4, '下架'),
)
device_type_id = models.IntegerField(choices=device_type_choices, default=1)
device_status_id = models.IntegerField(choices=device_status_choices, default=1)
cabinet_num = models.CharField('机柜号', max_length=30, null=True, blank=True)
cabinet_order = models.CharField('机柜中序号', max_length=30, null=True, blank=True)
idc = models.ForeignKey('IDC', verbose_name='IDC机房', null=True, blank=True,on_delete=models.CASCADE)
business_unit = models.ForeignKey('BusinessUnit', verbose_name='属于的业务线', null=True, blank=True,on_delete=models.CASCADE)
tag = models.ManyToManyField('Tag')
latest_date = models.DateField(null=True)
create_at = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name_plural = "资产表"
def __str__(self):
return "%s-%s-%s" % (self.idc.name, self.cabinet_num, self.cabinet_order)
views.py 文件
from django.shortcuts import render,HttpResponse
from django.views import View
import json
class AssetView(View):
def get(self,request,*args,**kwargs):
# 数据库中获取数据
return render(request,'asset.html')
class AssetJsonView(View):
def get(self,request,*args,**kwargs):
# 数据库中获取数据
table_config = [
{
'q': None,
'title': "选项",
'display': True, # content 中的值会赋给td标签
'text': {'content': "<input type='checkbox' />","kwargs": {}},
'attrs': {}
},
{
'q': 'id',
'title': 'ID',
'display': False, # 不显示
'text':{},
'attrs': {}
},
{
'q': 'device_type_id', # 这个字段用于去数据库中取值
'title': '资产类型', # 显示的字段名
'display': True, # 两个@ 表示去全局变量中取值,@@之后的字符串是全局变量名称
'text': {'content': "{n}", 'kwargs': {'n': "@@device_type_choices"}},
'attrs': {} # attrs 中的数据会称为td的属性
},
{
'q': 'device_status_id',
'title': '状态',
'display': True,
'text': {'content': "{n}", 'kwargs': {'n': "@@device_status_choices"}},
'attrs': {'name':'device_status_id','origin':"@device_status_id",'edit-enable': 'true', 'edit-type': 'select',"global-name": 'device_status_choices'}
},
{
'q': 'idc__id',
'title': 'IDC',
'display': False,
'text': {},
'attrs': {}
},
{
'q': 'idc__name',
'title': 'IDC',
'display': True, # 一个@ 代表从当前的一组数据中取值
'text': {'content': "{n}", 'kwargs': {'n': "@idc__name"}},
'attrs': {'name':'idc_id','origin':"@idc__id",'edit-enable': 'true', 'edit-type': 'select',"global-name": 'idc_choices'}
},
{
'q': 'cabinet_order',
'title': '机柜位置',
'display': True,
'text': {'content': "{n}",'kwargs': {'n': "@cabinet_order"}},
'attrs': {'name':'cabinet_order','origin':"@cabinet_order",'edit-enable': 'true', 'edit-type': 'input'}
},
{
'q': 'cabinet_num',
'title': '机柜号',
'display': True,
'text': {'content': "{n}", 'kwargs': {'n': "@cabinet_num"}},
'attrs': {},
},
{
'q': None,
'title': '操作',
'display': True,
'text': {'content': "<a href='/assetdetail-{m}.html'>{n}</a>", 'kwargs': {'n': '查看详细','m': '@id'}},
'attrs': {},
}
]
q_list = []
for i in table_config: # 去数据库中取值
if not i['q']:
continue
q_list.append(i['q'])
from repository import models
# 分页组件用户获取数据
data_list = models.Asset.objects.all().values(*q_list)
data_list = list(data_list)
result = {
'table_config':table_config,
'data_list':data_list,
'global_dict': {
'device_type_choices': models.Asset.device_type_choices,
'device_status_choices': models.Asset.device_status_choices,
'idc_choices': list(models.IDC.objects.values_list('id','name'))
},
# 分页组件生成页码信息
'pager': """<li><a>1</a></li><li><a>2</a></li><li><a>3</a></li><li><a>4</a></li><li><a>5</a></li>"""
}
return HttpResponse(json.dumps(result))
def put(self,request,*args,**kwargs):
content = request.body
v = json.loads(str(content,encoding='utf-8'))
print(v)
ret = {
'status':True
}
return HttpResponse(json.dumps(ret))
所定制的 js,全部都封装到里面了,而我们引用时只需要引入js文件,并传入url路径就行了,而所操作的表格就需要在view中修改了,
文档中有很多要注意的点,留作以后参考
nbList.js 文件
(function () {
var requestUrl = null;
function bindChangePager() {
$('#idPagination').on('click','a',function () { // 为页码绑定点击事件
var num = $(this).text(); // 事件为获取页码中li标签的text,也就是1,2,3,4,5
init(num); // 执行 init 发送ajax,并更新table
})
}
function bindSave() { // 绑定保存按钮
$('#idSave').click(function () {
var postList = [];
//找到已经编辑过的tr,tr has-edit='true'
$('#table_tb').find('tr[has-edit="true"]').each(function () {
// $(this) => tr
var temp = {};
var id = $(this).attr('row-id');
temp['id'] = id;
// 找到编辑过的tr标签,查找其中修改的值,原值存放在origin中,修改的值放在new-val
$(this).children('[edit-enable="true"]').each(function () {
// $(this) = > td
var name = $(this).attr('name');
var origin = $(this).attr('origin');
var newVal = $(this).attr('new-val');
if (origin != newVal){ // 修改过的,才发送到服务器
temp[name] = newVal;
}
});
postList.push(temp);
});
$.ajax({
url:requestUrl,
type: 'PUT',
data: {'post_list': JSON.stringify(postList)}, //json 发送不了字典,得首先格式字符串
dataType: 'JSON',
success:function (arg) {
if(arg.status){
init(1);
}else{
alert(arg.error);
}
}
})
})
}
function bindReverseAll() { // 绑定反选按钮
$('#idReverseAll').click(function () {
$('#table_tb').find(':checkbox').each(function () {
// $(this) => checkbox
if($('#idEditMode').hasClass('btn-warning')) { // 进入编辑模式的状态
if($(this).prop('checked')){ // 获取checkbox的状态
$(this).prop('checked',false); // 设定checkbox 为不选中
trOutEditMode($(this).parent().parent()); // 必须传递tr标签过去
}else{
$(this).prop('checked',true);
trIntoEditMode($(this).parent().parent());
}
}else{ // 未进入编辑模式,就只有checkbox的反向选择了
if($(this).prop('checked')){
$(this).prop('checked',false);
}else{
$(this).prop('checked',true);
}
}
})
})
}
function bindCancelAll() { // 绑定取消按钮
$('#idCancelAll').click(function () {
$('#table_tb').find(':checked').each(function () { // 找到所有选中的checkbox
// $(this) => checkbox ,,,使之变为false
if($('#idEditMode').hasClass('btn-warning')){
$(this).prop('checked',false);
// 退出编辑模式
trOutEditMode($(this).parent().parent());
}else{
$(this).prop('checked',false);
}
});
})
}
function bindCheckAll() { // 绑定全选按钮
$('#idCheckAll').click(function () {
$('#table_tb').find(':checkbox').each(function () {
// $(this) = checkbox
if($('#idEditMode').hasClass('btn-warning')){
if($(this).prop('checked')){
// 当前行已经进入编辑模式了
}else{
// 进入编辑模式
var $currentTr = $(this).parent().parent();
trIntoEditMode($currentTr);
$(this).prop('checked',true);
}
}else{
$(this).prop('checked',true);
}
})
})
}
function bindEditMode() { // 绑定进入编辑模式按钮
$('#idEditMode').click(function () {
var editing = $(this).hasClass('btn-warning');
if(editing){
// 退出编辑模式
$(this).removeClass('btn-warning');
$(this).text('进入编辑模式');
$('#table_tb').find(':checked').each(function () { //找到所有选中的checkbox进入编辑模式
var $currentTr = $(this).parent().parent();
trOutEditMode($currentTr);
})
}else{
// 进入编辑模式
$(this).addClass('btn-warning');
$(this).text('退出编辑模式');
$('#table_tb').find(':checked').each(function () { //找到所有选中的checkbox退出编辑模式
var $currentTr = $(this).parent().parent();
trIntoEditMode($currentTr);
})
}
})
}
function bindCheckbox() { // 为每一个checkbox绑定事件,这里为了避免之后添加的checkbox没有点击事件
// $('#table_tb').find(':checkbox').click(),,(这个程序没有添加之后的checkbox),采用事件委托的方式绑定事件
$('#table_tb').on('click',':checkbox',function () {
if($('#idEditMode').hasClass('btn-warning')){ // 要一直检测是否进入了编辑模式
var ck = $(this).prop('checked');
var $currentTr = $(this).parent().parent();
if(ck){
// 进入编辑模式
trIntoEditMode($currentTr);
}else{
// 退出编辑模式
trOutEditMode($currentTr)
}
}
})
}
function trIntoEditMode($tr) { // $tr 是tr标签
$tr.addClass('success'); // success 是颜色效果
$tr.attr('has-edit','true'); // 设置编辑过的标志
$tr.children().each(function () {
// $(this) => td
var editEnable = $(this).attr('edit-enable');
var editType = $(this).attr('edit-type');
if(editEnable=='true'){
if(editType == 'select'){ // select 标签,在views中必须要把choice传递过来
var globalName = $(this).attr('global-name'); // "device_status_choices"
var origin = $(this).attr('origin'); // 1
// 生成select标签
var sel = document.createElement('select');
sel.className = "form-control";
$.each(window[globalName],function(k1,v1){
var op = document.createElement('option');
op.setAttribute('value',v1[0]);
op.innerHTML = v1[1];
$(sel).append(op);
});
$(sel).val(origin); // 这样可以设定select的选定值****
$(this).html(sel);
// 下拉框
/*
* <select>
* <option value='1'>在线</option>
* <option value='2'>下线</option>
* <option value='3'>离线</option>
* </select>
*
* */
// 在线
}else if(editType == 'input'){
// input文本框
// *******可以进入编辑模式*******
var innerText = $(this).text();
var tag = document.createElement('input');
tag.className = "form-control";
tag.value = innerText;
$(this).html(tag);
}
}
})
}
function trOutEditMode($tr){ // 退出编辑模式
$tr.removeClass('success');
$tr.children().each(function () {
// $(this) => td
var editEnable = $(this).attr('edit-enable');
var editType = $(this).attr('edit-type');
if(editEnable=='true'){
if (editType == 'select'){
// 获取正在编辑的select对象
var $select = $(this).children().first();
// 获取选中的option的value
var newId = $select.val();
// 获取选中的option的文本内容
var newText = $select[0].selectedOptions[0].innerHTML;
// 在td中设置文本内容
$(this).html(newText);
$(this).attr('new-val',newId); // 设置属性中更新的值
}else if(editType == 'input') {
// *******可以退出编辑模式*******
var $input = $(this).children().first();
var inputValue = $input.val();
$(this).html(inputValue);
$(this).attr('new-val',inputValue);
}
}
})
}
String.prototype.format = function (kwargs) { // 自定制的string 的 format 方法
// this ="laiying: {age} - {gender}";
// kwargs = {'age':18,'gender': '女'}
var ret = this.replace(/\{(\w+)\}/g,function (km,m) {
return kwargs[m];
});
return ret;
};
function init(pager) {
$.ajax({
url: requestUrl,
type: 'GET',
data: {'pager':pager},
dataType: 'JSON',
success:function (result) {
initGlobalData(result.global_dict);
initHeader(result.table_config);
initBody(result.table_config,result.data_list);
initPager(result.pager);
}
})
}
function initPager(pager){
$('#idPagination').html(pager); // 添加中views 传递过来的页码,也就是那一堆li标签
}
function initHeader(table_config) {
/*
table_config = [
{
'q': 'id',
'title': 'ID',
'display':false
},
{
'q': 'name',
'title': '随便',
'display': true
}
]
*/
/*
<tr>
<th>ID</th>
<th>用户名</th>
</tr>
*/
var tr = document.createElement('tr');
$.each(table_config,function (k,item) {
if(item.display){ // display 的值就是view中传递过来,确定是否展示
var th = document.createElement('th');
th.innerHTML = item.title;
$(tr).append(th);
}
});
$('#table_th').empty(); // 添加之前需要先清空之前的值
$('#table_th').append(tr);
}
function initBody(table_config,data_list){ // 添加数据,用键值对的方式,list 列表有序
$('#table_tb').empty();
for(var i=0;i<data_list.length;i++){
var row = data_list[i];
// row = {'cabinet_num': '12B', 'cabinet_order': '1', 'id': 1},
var tr = document.createElement('tr');
tr.setAttribute('row-id',row['id']);
$.each(table_config,function (i,colConfig) {
if(colConfig.display){
var td = document.createElement('td');
/* 生成文本信息 */
var kwargs = {};
$.each(colConfig.text.kwargs,function (key,value) {
if(value.substring(0,2) == '@@'){ // 两个@ 代表去全局变量中取值
var globalName = value.substring(2,value.length); // 全局变量的名称
var currentId = row[colConfig.q]; // 获取的数据库中存储的数字类型值
var t = getTextFromGlobalById(globalName,currentId);
kwargs[key] = t;
}
else if (value[0] == '@'){ // 一个@ 代表从当前 的一组数据中取值
kwargs[key] = row[value.substring(1,value.length)]; //cabinet_num
}else{
kwargs[key] = value;
}
});
var temp = colConfig.text.content.format(kwargs); // format 是自定制的string方法
td.innerHTML = temp; // 格式化字符串,
// 'content': "{n}", 'kwargs': {'n': "@@device_type_choices"}
// 如同上面的字符串,用kwargs 中的 n 代替 {n}
/* 属性colConfig.attrs = {'edit-enable': 'true','edit-type': 'select'} */
$.each(colConfig.attrs,function (kk,vv) {
if(vv[0] == '@'){ //还必须为td设置属性,标识
td.setAttribute(kk,row[vv.substring(1,vv.length)]);
}else{
td.setAttribute(kk,vv);
}
});
$(tr).append(td);
}
});
$('#table_tb').append(tr);
}
}
function initGlobalData(global_dict) {
$.each(global_dict,function (k,v) {
// k = "device_type_choices"
// v= [[0,'xx'],[1,'xxx']]
// device_type_choices = 123;
window[k] = v; // k 是一个字符串,这里是定义为全局变量,需要加 window
}) // 全局变量用来存储select_choice,foreignkey choice
}
function getTextFromGlobalById(globalName,currentId) { // 从全局变量中获取值
// globalName = "device_type_choices"
// currentId = 1
var ret = null;
$.each(window[globalName],function (k,item) {
if(item[0] == currentId){
ret = item[1];
return
}
});
return ret;
}
jQuery.extend({
'NB': function (url) {
requestUrl = url;
init();
bindEditMode();
bindCheckbox();
bindCheckAll();
bindCancelAll();
bindReverseAll();
bindSave();
bindChangePager();
},
'changePager': function (num) {
init(num);
// 页码
}
})
})();
// 整个封装到js内部空间,不为外部展示
使用时非常简便,
template 模版文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" href="/static/bootstrap-3.3.7-dist/css/bootstrap.css" />
<style>
</style>
</head>
<body>
{# <select id="i1">#}
{# <option>上阿海</option>#}
{# <option>北京</option>#}
{# <option>广州</option>#}
{# </select>#}
<div style="width: 800px;margin: 0 auto;">
<h1>资产列表</h1>
<div class="btn-group" role="group" aria-label="...">
<button id="idCheckAll" type="button" class="btn btn-default">全选</button>
<button id="idReverseAll" type="button" class="btn btn-default">反选</button>
<button id="idCancelAll" type="button" class="btn btn-default">取消</button>
<button id="idEditMode" type="button" class="btn btn-default">进入编辑模式</button>
<button type="button" class="btn btn-default">批量删除</button>
<button id="idSave" type="button" class="btn btn-default">保存</button>
<a id="idAdd" href="/web/asset-add.html" class="btn btn-default">添加</a>
{# 添加按钮需要自定制#}
</div>
<table class="table table-bordered">
<thead id="table_th"></thead>
<tbody id="table_tb"></tbody>
</table>
// 这里是页码
<ul id="idPagination" class="pagination">
</ul>
</div>
<script src="/static/js/jquery-3.1.1.js"></script>
<script src="/static/js/nbList.js"></script>
<script>
$(function () {
$.NB("/web/asset-json.html"); // 这样就行了
});
</script>
</body>
</html>
最终的效果图:
cmdb,就到这里结束了,,,,
分享文件:
cmdb--告别CURD
链接:https://pan.baidu.com/s/1UHnJtC6xQAFWBk2QxQs9TQ 密码:hi3s
cmdb--LNH
链接:https://pan.baidu.com/s/1ZWOd2z4hadlEYjzvyjXk8Q 密码:tle2
模版文件效果图: