2.5 使用Django快速构建CMDB系统
2.5.1 Django介绍
Django是一个免费的、开源的Web框架,由Python语言编写,由于其是在一个快节奏的新闻编译室环境中开发出来的,因此它的设计目的是让普通开发者的工作变得简单。Django遵循模型-视图-控制器(MVC)框架模式,目前由一个非盈利的独立组织的软件基金会(DSF)维持。
Django鼓励快速开发和干净实用的设计。Django可以更容易更快速地构建更好的Web应用程序。它是由经验丰富的开发人员来创建的,省去了Web开发的很多麻烦,因此你可以专注地开发应用程序而不需要去白费力气地重复工作。
Django目前已经被运维圈广泛使用,本文在此不会详细介绍Django的基础知识,有兴趣的朋友可以去Django官网查看更为详细的介绍,同时也有Django中文文档可供学习。
2.5.2 Django安装
Django的安装分为4个步骤,下面以Django 1.7.1、CentOS 6.5 x86_64为例进行讲解,详细步骤如下。
1.?安装Python 2.7.x
用CentOS 7以下版本的朋友需要将Python升级到2.7.x以上,Django对Python版本存在依赖,具体如图2-7所示。
编译步骤如下:
# yum install -y zlib-dev openssl-devel sqlite-devel bzip2-devel
# wget http://www.python.org/ftp/python/2.7.6/Python-2.7.6.tgz
# tar zxf Python-2.7.6.tgz
# cd Python-2.7.6
# ./configure --prefix=/usr/local
# make && make altinstall
(1)安装easy_install工具,操作命令如下:
$ wget https://bootstrap.pypa.io/ez_setup.py -O -| python
(2)安装Django,使用easy_install来安装,安装的版本为1.7.1,具体命令如下:
$ easy_install django==1.7.1
(3)测试Django的安装,操作命令如下:
$ easy_install django==1.7.1
$ django-admin –version
1.7.1
2.?MySQL安装
本文推荐使用yum命令进行安装,并设置MySQL root密码,创建cmdbtest数据库,具体安装步骤如下:
$ yum -y install mysql mysql-server
$ mysql_install_db --user=mysql
$ /etc/init.d/mysqld start
$ mysqladmin -u root password 'cmdbtest'
$ mysql -u root -pcmdbtest -e 'create database if not exists cmdbtest'
2.5.3 Django常用命令
完成Django的安装后,可以通过如下命令快速熟悉Django的操作,以便快速创建一个CMDB App,如果你对如下这些Django命令很熟悉,可以直接跳过。
(1)新建一个django-project:
$ django-admin startproject project-name
(2)新建App:
$ django-admin startapp app-name
(3)同步数据库:
$ python manage.py syncdb
(4)启动Django服务器:
$ python manage.py runserver
(5)Django Shell调试:
$ python manage.py shell
(6)帮助:
$ django-admin --help
$ python manage.py --help
2.5.4 Django的配置
1.?环境准备
笔者准备了两台测试机器用来进行代码测试,测试机器的环境信息分别如下。
(1)服务端机器信息:
IP: 10.20.122.100
Role: puppet server + cmdb
System OS: CentOS release 6.5 x86_64
Python version: 2.7.8 Django version: 1.7.1
Mysql version: 5.1.73
(2)客户端机器信息:
IP: 10.20.122.111
Role: puppet agent
System OS: CentOS release 6.5 x86_64
2.?软件安装
前几节已经对所需要的环境进行了安装,在这里我们再回顾一下:
(1)master安装Puppet Server。
(2)master安装Python。
(3)master安装MySQL。
(4)master安装Django。
(5)master安装项目依赖的Python模块。
(6)Agent安装Puppet Agent。
3.?创建CMDB项目
创建CMDB项目的同时,在这个项目中创建一个CMDB App,登录10.20.122.100,运行如下命令。
(1)创建一个Project:
$ django-admin startproject myproject
(2)进入myproject目录:
$ cd myproject
(3)创建一个CMDB App:
$ django-admin startapp cmdb
(4)创建一个存放静态文件和模板的目录:
$ mkdir static templates
运行成功后使用11命令就可以看到如图2-8所示的目录结构。
4.?配置CMDB项目信息
在图2-8中我们可以在myproject目录下看到settings.py的全局配置文件,Django在运行时会默认先加载此配置文件,因此我们需要先对它进行定义,需要配置如下6个地方,操作步骤具体如下。
(1)修改数据库设置:
DATABASES = {
'default': { 'ENGINE': 'django.db.backends.mysql',
'NAME': 'cmdbtest',
'HOST': 'localhost',
'USER': 'root',
'PASSWORD': 'cmdbtest',
'PORT': '3306',
'OPTIONS': {'init_command': 'SET storage_engine=INNODB', 'charset': 'utf8', }
} }
(2)设置App,把我们新建的CMDB App加到末尾,代码如下:
NSTALLED_APPS = ('django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles', 'cmdb',
)
(3)设置静态文件存放目录:
STATICFILES_DIRS = ( os.path.join(BASE_DIR, 'static/'), )
(4)设置模板文件存放目录:
TEMPLATE_DIRS = [ os.path.join(BASE_DIR, 'templates'), ]
(5)设置登录URL:
LOGIN_URL = '/cmdb/login/'
(6)设置其他参数,可以根据自己的需求进行设置:
TEMPLATE_CONTEXT_PROCESSORS = ( 'django.core.context_processors.static', 'cmdb.context_processors.menu', "django.contrib.auth.context_processors.auth", "django.core.context_processors.request",
) CMDB_VERSION = '1.0' CMDB_NAME = u'测试 CMDB' LOGIN_REDIRECT_URL = '/home/'
到此为止基础环境已经准备完毕,接下来需要设计数据库,并定义好视图。
5.?数据表设计
Django遵循MVC设计,其中M(模型)就是数据库模型,也就是App中的models.py文件的设置,Django自带数据库的ORM(Object Relational Mapping)架构,这使得我们不用再需要学习复杂的数据库操作,只需要通过定义models即可。下面是我设置的最简单的CMDB数据结构:
# 指定解析器为Python
# !/usr/bin/env python
# 指定字符编码为utf8
# encoding:utf8
# 从django.db中导入models模块
from django.db import models
# 导入User模块
from django.contrib.auth.models import User
# Create your models here.
# 定义一个Server_Group类,从models.Model中继承,这里就是所谓的数据表结构
class Server_Group(models.Model):
# 定义主机组名称字段
name = models.CharField(u'主机组', max_length=255, unique=True)
# 关联的项目字段,这里是关联一个外键
project = models.ForeignKey("Project", verbose_name='项目名称')
# 备注字段
memo = models.CharField(u'备注', max_length=255, blank=True)
# unicode返回值
def __unicode__(self):
# 返回的格式
return '%s-%s' % (self.project.name, self.name)
# 定义Meta属性
class Meta:
# 数据库中的表名
db_table = 'server_group'
# 存储的时候需要确认组合键是唯一的
unique_together = (("name", "project"),)
# 定义一个IDC类,主要存储IDC信息,数据表结构有2个字段
class IDC(models.Model):
# 定义IDC的名称字段
name = models.CharField(u'IDC名称', max_length=255, unique=True)
memo = models.CharField(u'备注', max_length=255, blank=True)
def __unicode__(self):
return self.name
class Meta:
db_table = 'idc'
# 定义一个Project类,主要存储项目信息,数据表结构有2个字段
class Project(models.Model):
name = models.CharField(u'项目名称', max_length=255, unique=True)
memo = models.CharField(u'备注', max_length=255, blank=True)
def __unicode__(self):
return self.name
class Meta:
db_table = 'project'
# 定义一个Server_Role类,主要存储服务器角色信息,数据表结构有3个字段
class Server_Role(models.Model):
name = models.CharField(u'角色', max_length=255)
# 关联Server_Group,也就是服务器组
group = models.ForeignKey("Server_Group", verbose_name='项目组')
memo = models.CharField(u'备注', max_length=255, blank=True)
def __unicode__(self):
return '%s-%s-%s' % (self.group.project.name, self.group.name, self.name)
class Meta:
# 设置数据库表名
db_table = 'server_role'
# 存储的时候需要确认组合键是唯一的
unique_together = (("name", "group"),)
# CMDB核心数据表结构,用来存储服务器系统信息
class Server_Device(models.Model):
# 服务器状态选择,具体的字段存储数据为0~3的int数字
SERVER_STATUS = (
(0, u'下线'),
(1, u'在线'),
(2, u'待上线'),
(3, u'测试'),
)
# 定义一个名称字段,若blank没有设置则默认为False,不能为空,且unique=True必须唯一
name = models.CharField(u'主机名称', max_length=100, unique=True)
# 定义SN编号字段, blank=True,可以为空
sn = models.CharField(u'SN号', max_length=200, blank=True)
# 公网IP字段,可以为空
public_ip = models.CharField(u'外网IP', max_length=200, blank=True)
# 私网IP字段,可以为空
private_ip = models.CharField(u'内网IP', max_length=200, blank=True)
# 定义MAC地址字段
mac = models.CharField(u'MAC地址', max_length=200, blank=True)
# 定义操作系统字段
os = models.CharField(u'操作系统', max_length=200, blank=True)
# 定义磁盘信息字段
disk = models.CharField(u'磁盘', max_length=200, blank=True)
# 定义内存信息字段
mem = models.CharField(u'内存', max_length=200, blank=True)
# 定义CPU信息字段
cpu = models.CharField(u'CPU', max_length=200, blank=True)
# 关联IDC信息
idc = models.ForeignKey(IDC, max_length=255, blank=True, null=True, verbose_
name='机房名称')
# 定义一个多对多字段,一台服务器可以对应多个角色
role = models.ManyToManyField("Server_Role", verbose_name='角色', blank=True)
# 机器状态,默认都为在线状态
status = models.SmallIntegerField(verbose_name='机器状态', choices=SERVER_STATUS,
default=1)
# 管理用户信息
admin = models.ForeignKey('auth.User', verbose_name='管理员', null=True, blank=True)
# 定义备注字段
memo = models.CharField(u'备注', max_length=200, blank=True)
def __unicode__(self):
return self.name
class Meta:
db_table = 'server_device'
初始化数据库,同时设置登录所需要的username和password,命令如下:
$ python manage.py syncdb
Operations to perform:
Apply all migrations: admin, contenttypes, auth, sessions
Running migrations:
Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
Applying admin.0001_initial... OK
Applying sessions.0001_initial... OK
You have installed Django's auth system, and don't have any superusers defined.
Would you like to create one now? (yes/no): yes
# 这里输入用户名
Username (leave blank to use 'root'): admin
Email address:
# 这里输入密码
Password:
# 重复输入密码
Password (again):
Superuser created successfully.
在命令行登录数据库,并查看数据库信息,就能看到如图2-9所示的内容,说明数据库已创建成功。
6.?视图设置
上文中我们已经成功设置了Django的M(Models,模型),下面我们来设置V(View,视图),如下代码是一个登出页面和一个home页面的View:
# encoding:utf8
# Create your views here.
# 导入需要使用的模块
from django.template import RequestContext
from django.shortcuts import render_to_response
from django.contrib.auth.decorators import login_required
from django.contrib.auth.views import logout_then_login
# 判断用户是否登录
@login_required
# 登出时的调用
def logout_view(request):
return logout_then_login(request)
# 判断用户是否登录
@login_required
# 登录后调用home页所展示的页面,template为home.html
def home(request):
return render_to_response('home.html', locals(), context_instance=RequestCo
ntext(request))
URL设置(这里直接使用了Django自带的login函数,所以不需要自己写login view):
# 设置字符编码
# encoding:utf8
# 从urls中导入patterns、include、url模块
from django.conf.urls import patterns, include, url
# 从contrib中导入admin模块
from django.contrib import admin
# 从http中导入HttpResponseRedirect模块
from django.http import HttpResponseRedirect
# 设置前端访问的URL对应的后端视图
urlpatterns = patterns('',
# url什么参数都不带时,直接重定向到login
url(r'^$', lambda x: HttpResponseRedirect('/login/')),
# 登出对应的视图为cmdb.views.logout_view
url(r'^logout/$','cmdb.views.logout_view', name='cmdb_logout'),
# 登录对应的view为django.contrib.auth.views.login,对应的
template为login.html
url(r'^login/$','django.contrib.auth.views.login', {'template_
name': 'login.html'},
name='cmdb_login'),
# home页面,对应的view为cmdb.views.home
url(r'^home/$', 'cmdb.views.home', name='home'),
)
通过如上定义,现在就启动Django服务,登录后即可看到如图2-10所示的界面。代码已托管至Github网站https://github.com/oysterclub/open-cmdb,有兴趣的朋友可以去复制下来查看、修改或使用。下面就来展示一下登录界面的效果图(注,前端框架为bootstrap)。
利用python manage.py syncdb命令输入的用户名和密码登录。登录后的页面为home空白页(见图2-11),具体如下(home空白页主要是为了以后做导向流页面或数据图表展示页面,这里先留空):
图2-11 系统登录后的界面
7.?使用Python程序获取Facts数据
通过如上定义,我们已经完成了视图、数据表结构的定义。而数据的来源既可以通过添加,也可以通过Facter工具来获取。下面我们就来讲讲如何自动获取Agent机器的系统数据(如果想要充分了解Facter工具,可以参考阅读《Puppet实战》的第9章“Facter介绍”)。Facter工具会在Puppet Agent与Puppet Master通信的时候把获取到的Agent主机系统信息和自己定义的Facts信息汇报给Puppet Master,生成一个hostname.yaml格式的文件,文件存放在/var/lib/puppet/yaml/facts目录下,文件的格式如图2-12所示,其中的values数据:domain、ipaddress、macaddress等正是构建CMDB所需要的系统数据,因此我们可以通过一个Python程序来处理这些数据,并录入MySQL中,最终通过Django来实现前端展示。因此一个最简单的CMDB系统就构建完成了。
图2-12 Facter上报至Puppet Master后的yaml部分信息
我们先来具体看一下facter_message.py程序(Python程序处理Facter数据),完整代码如下:
#!/usr/bin/env python
# encoding: utf8
__authors__ = ['liuyu', 'chenlijun']
__version__ = 1.0
__date__ = '2015-09-06 14:58:23'
__licence__ = 'GPL licence'
# 导入模块
import yaml
import os
# IPy主要用来判断IP类型,IPy.IP('ip').iptype()
import IPy
# yaml文件目录
yaml_dir = '/var/lib/puppet/yaml/facts'
# 结果集,结果集的格式{'cmdb_agent':()}
all_host_facter_message = {}
# 结果列表
result_list = ['name',
'SN',
'public_ip',
'private_ip',
'mac',
'os',
'disk',
'mem',
'cpu',
'idc',
'role',
'status',
'admin',
'memo']
# db对应的Facter字段,需要获取其他的字段时可以一一对应
list_field = {'name': 'fqdn',
'public_ip': 'ipaddress__interfaces',
'private_ip': 'ipaddress__interfaces',
'mac': 'macaddress__interfaces',
'os': ['operatingsystem', 'operatingsystemrelease', 'hardwaremodel'],
'disk': 'blockdevice__blockdevices',
'mem': 'memorysize',
'cpu': ['processorcount', 'processor0']}
# ruby objectobjectconstruct
def construct_ruby_object(loader, suffix, node):
return loader.construct_yaml_map(node)
def construct_ruby_sym(loader, node):
return loader.construct_yaml_str(node)
# 读取数据
def yaml_file_handle(filename):
stream = open(filename)
mydata = yaml.load(stream)
return mydata
# 获取IP的类型
def get_ip_type(ip):
try:
return IPy.IP(ip).iptype().lower()
except Exception, e:
print e
# 处理单个Agent的数据
def handle_facter_message(data):
# 定义一个结果字典,字段和db一样,处理完的结果和db中的一样
result_dict = {}
# 对结果进行处理
for db_field in result_list:
# 定义一个字段结果字符
value = ''
# result_list中的字段是否存在于我们需要的Facter取值列表中,如果存在
if db_field in list_field:
facter_field = list_field[db_field]
# 先判断facter_field的类型,然后进行处理
if type(facter_field) == type([]):
for tag in facter_field:
if data.get(tag):
value += data[tag] + ' '
else:
# 由于disk、IP等需要进一步处理,所以用了一个__来分隔,然后再进行处理
field_tmp = facter_field.split("__")
if len(field_tmp) == 2:
if db_field == 'disk':
for tag in data[field_tmp[1]].split(","):
# 对磁盘进行处理, 由于磁盘的字段为blockdevice_type_size,
所以需要单独进行处理
f = field_tmp[0] + '_' + tag + '_' + 'size'
if data.get(f):
# 去除sr0 tag的字段
if tag != 'sr0':
# 结果字符串
value += tag + ':' + str(int(data[f]) / 1024 /
1024 / 1024) + 'G' + ' '
# 对外网IP进行处理
elif db_field == 'public_ip':
for tag in data[field_tmp[1]].split(","):
f = field_tmp[0] + '_' + tag
if data.get(f):
# 去除lo tag的字段
if tag != 'lo':
if get_ip_type(data[f]) == 'public':
# 结果字符串
value += data[f] + ' '
# 对内外IP进行处理
elif db_field == 'private_ip':
for tag in data[field_tmp[1]].split(","):
f = field_tmp[0] + '_' + tag
if data.get(f):
# 去除lo tag的字段
if tag != 'lo':
if get_ip_type(data[f]) == 'private':
# 结果字符串
value += data[f] + ' '
else:
# 其他的字段直接就处理了
for tag in data[field_tmp[1]].split(","):
f = field_tmp[0] + '_' + tag
if data.get(f):
# 去除lo tag的字段
if tag != 'lo':
# 结果字符串
value += tag + ':' + data[f] + ' '
else:
if data.get(facter_field):
# 结果字符串
value = data[facter_field]
# 将结果添加到result列表中
result_dict[db_field] = value.strip()
# 如果不存在
else:
result_dict[db_field] = ''
# 返回结果字典
return result_dict
# 定义获取facter的函数
def get_all_host_facter_message():
# 由于Puppet的yaml文件是Ruby格式的,因此需要进行转换
yaml.add_multi_constructor(u"!ruby/object:", construct_ruby_object)
yaml.add_constructor(u"!ruby/sym", construct_ruby_sym)
# 获取所有有Facter信息的主机文件名称
for dirpath, dirnames, filenames in os.walk(yaml_dir):
# 只需要处理yaml目录下以yaml结尾的文件
if dirpath == yaml_dir:
for file in filenames:
file_name, file_ext = os.path.splitext(file)
if file_ext == '.yaml':
host_yaml_path = yaml_dir + '/' + file
# 得到yaml文件的内容, 字典形式
host_yaml_result_dict = yaml_file_handle(host_yaml_path)
# 对单个Agent的数据进行处理
if host_yaml_result_dict:
# 由于有key为facts,所以可以直接查找facts key的值
if host_yaml_result_dict.has_key('facts'):
data_dict = host_yaml_result_dict['facts']['values']
# 没有的就直接取
else:
data_dict = host_yaml_result_dict['values']
# 现在就可以对data进行处理,获取我们所需要的数据了
result_dict = handle_facter_message(data_dict)
all_host_facter_message[file_name] = result_dict
#返回我们最终的数据结果集
return all_host_facter_message
以上程序可以过滤Facter中我们想要得到的Agent数据,运行facter_message.py程序,结果输出如下:
$ python facter_message.py
{'puppetclient.domain.com':
{'status': '',
'name': 'puppetclient.domain.com',
'mem': '1.83 GB',
'memo': '',
'idc': '',
'public_ip': '',
'admin': '',
'mac': 'eth0:00:1A:4A:25:E2:12 eth1:00:1A:4A:25:E2:13',
'role': '',
'private_ip': '10.20.122.111',
'disk': 'vda:20G vdb:30G',
'os': 'CentOS 6.5 x86_64',
'cpu': '2 Intel Core 2 Duo P9xxx (Penryn Class Core 2)',
'SN': ''}
}
到这里,我们能够看到facter_message.py得到的数据字段和models.py中数据结构的字段正好一样,下一步我们就可以直接将facter_message.py的数据导入到数据库中了,具体程序如下:
# 检测用户是否登录
@login_required
# 定义一个views,用来处理导入信息
def import_data(request, model):
# 导入计数器
import_num = 0
# 查看model是否存在于定义的模板中
if model in BASE_ADMIN:
# 获取tag名称
tag_name = BASE_ADMIN[model]['name']
# 获取model名称
model_name = BASE_ADMIN[model]['model']
# 这里只处理server_device的导入信息
if model == 'server_device':
server_device_data = get_all_host_facter_message()
# 进行数据入库处理
for hostname, facter_message in server_device_data.items():
# 主机名处理,判断facter_message中name key是否有值,
if facter_message['name']:
# 如果有值,name就使用该值
name = facter_message['name']
# 如果没有这个值
else:
# 就使用hostname
name = hostname
# 对于IDC信息、User信息、项目角色信息的处理都需要自己去写Facter插件,不
写的都为空,然后进行处理
# IDC关联处理,如果facter_message中的idc key有值
if facter_message['idc']:
# idc_name就为该值
idc_name = facter_message['idc']
# 同时处理该IDC信息是否存在于IDC表中,如果有则取出ID
if IDC.objects.filter(name=idc_name):
idc_id = IDC.objects.get(name=idc_name).id
# 如果没有,则进行保存,然后取出ID
else:
idc_sql = IDC(name=idc_name)
try:
idc_sql.save()
# 取出ID
idc_id = IDC.objects.get(name=idc_name).id
except Exception, e:
return e
# 如果idc key没有值,则为None
else:
idc_id = None
# 管理员信息关联处理,如果用户存在则关联,不存在则跳过
if facter_message['admin']:
admin_name = facter_message['admin']
# 如果用户存在User表中则取ID,若没有则为空
if User.objects.filter(username=admin_name):
user_id = User.objects.get(username=admin_name).id
else:
user_id = None
# 没有就为空
else:
user_id = None
# 这里还有一个角色多对多关系的处理,由于这里没有定义机器角色,因此此处不处理
角色信息
# 判断主机是否存在于server_device表中,如果不存在则添加
if not model_name.objects.filter(name=name):
import_sql = model_name(name=name,
sn=facter_message['sn'],
public_ip=facter_message['public_ip'],
private_ip=facter_message['private_ip'],
mac=facter_message['mac'],
idc=idc_id,
os=facter_message['os'],
disk=facter_message['disk'],
mem=facter_message['mem'],
cpu=facter_message['cpu'],
admin=user_id,
memo=facter_message['memo'],
)
try:
# 保存
import_sql.save()
except Exception, e:
return e
# 如果有了,则查询数据,若信息不对则更新
elif not model_name.objects.filter(name=name,
sn=facter_message['sn'],
public_ip=facter_message['public_ip'],
private_ip=facter_message['private_ip'],
mac=facter_message['mac'],
os=facter_message['os'],
disk=facter_message['disk'],
mem=facter_message['mem'],
cpu=facter_message['cpu'],
memo=facter_message['memo']):
try:
# 更新数据库
model_name.objects.filter(name=name).update(sn=
facter_message['sn'],
public_ip=facter_message['public_ip'],
private_ip=facter_message['private_ip'],
mac=facter_message['mac'],
os=facter_message['os'],
disk=facter_message['disk'],
mem=facter_message['mem'],
cpu=facter_message['cpu'],
memo=facter_message['memo'],
)
except Exception, e:
return e
# 如果有了,且信息ok,则跳过
else:
continue
return HttpResponseRedirect('/cmdb/%s/show/' % model)
return render_to_response('all_data_show.html', locals(), context_instance=RequestContext(request))
重新登录CMDB之后的页面,有一个导入主机的按钮,点击导入主机按钮,就可以自动导入通过Facter获取的Agent主机信息了,如图2-13所示。
我们还可以通过添加的方式来维护CMDB的内容,到目前为止我们已经完成了使用Python和Puppet来构建一个小型的、简单的CMDB系统。
CMDB是需要我们定期进行维护和更新的,因此它还需要提供历史查看、API等更实用的功能,为此在2.6节中我们将介绍一下Django提供的几个好用的功能模块。
图2-13 信息导入界面