CMDB后端开发(上)
企业项目开发流程
项目背景
目前运维管理存在痛点:随着业务增长,服务器数量越来越多,资产信息通过Excel记录,人工管理低效,易于出错。
CMDB介绍
-
配置管理数据库(Configuration Management Database,CMDB),是一个逻辑数据库,包含了应用生命周期的信息,例如服务器、物理关系、通信关系、依赖关系等。
-
CMDB存储与管理企业IT架构中设备的各种配置信息,它与所有运维服务和应用发布流程都紧密相联,支持这些流程的运转、发挥配置信息的价值,同时依赖于相关流程保证数据的准确性。CMDB可以实现高度的自动化,减少人为错误的发生、降低人员成本,CMDB是实现运维自动化的基础。
-
CMDB资产
CMDB数据存储需要注意的事项
- CMDB的目的是为了在其他流程或应用之间共享数据的,如果一个应用或流程需要对某类数据单独使用的话,则不建议将这类数据存入CMDB中,存在自身应用即可。
- 动态数据不建议存储在CMDB中,例如CPU使用率、内存使用率,因为这类数据更新过于频繁。
- 如果没有任何流程、应用及人员,需要对特定的数据进行使用,则没有必要放到CMDB中存储。
技术选型
- 前端技术栈
- Vue3
- Vue-router
- Element Plus
- Axios
- 后端技术栈
- python
- Django DRF
- mysql
整体设计
数据库设计
机房管理
表名:cmdb_idc
字段 | 类型 | 空 | 名称 |
---|---|---|---|
id | INTEGER | 否 | 自增长ID |
name | VARCHAR(30)(unique) | 否 | 机房名称 |
city | VARCHAR(20) | 否 | 城市 |
provider | VARCHAR(20) | 否 | 运营商 |
note | TEXT | 是 | 备注 |
create_time | DATETIME | 否 | 创建时间 |
主机分组
表名:cmdb_server_group
字段 | 类型 | 空 | 名称 |
---|---|---|---|
id | INTEGER | 否 | 自增长ID |
name | VARCHAR(30)(unique) | 否 | 分组名称 |
note | TEXT | 是 | 备注 |
create_time | DATETIME | 否 | 创建时间 |
主机管理
字段 | 类型 | 空 | 名称 |
---|---|---|---|
id | INTEGER | 否 | 自增长ID |
idc | IDC表一对多关系 | 否 | IDC机房 |
server_group | 分组表多对多关系,默认“Default”组 | 否 | 主机分组 |
credential | 凭据表一对多 | 否 | 凭据ID |
name | VARCHAR(30) | 否 | 名称,默认与主机名一样 |
hostname | VARCHAR(30),unique(唯一索引) | 否 | 主机名,唯一标识符 |
ssh_ip | VARCHAR(40) | 否 | SSH IP |
ssh_port | INTEGER | 否 | SSH端口 |
machine_type | VARCHAR(20) | 是 | 机器类型(虚拟机、云主机、物理机) |
os_version | VARCHAR(30) | 是 | 系统版本 |
public_ip | JSON | 是 | 公网IP(列表存储,会有多个ip) |
private_ip | JSON | 否 | 内网IP(列表存储) |
cpu_num | VARCHAR(10) | 是 | CPU数量 |
cpu_model | VARCHAR(100) | 是 | CPU型号 |
memory | VARCHAR(30) | 是 | 内存 |
disk | JSON | 是 | 硬盘(列表存储,包含设备、容量、硬盘类型) |
put_shelves_date | DATE | 是 | 上架日期,默认为系统启动时间 |
off_shelves_date | DATE | 是 | 下架日期 |
expire_datetime | DATETIME | 是 | 租约过期时间 |
is_verified | VARCHAR(10) | 是 | SSH验证状态(已验证,未验证) |
note | TEXT | 是 | 备注 |
update_time | DATETIME | 是 | 更新时间 |
create_time | DATETIME | 否 | 创建时间 |
系统配置: 凭据管理
表名:system_config_credential
字段 | 类型 | 空 | 名称 |
---|---|---|---|
id | INTEGER | 否 | 自增长ID |
name | VARCHAR(30) | 否 | 名称 |
auth_mode | VARCHAR(30) | 否 | 认证方式,key、pass |
username | VARCHAR(20) | 否 | 用户名 |
password | VARCHAR(30) | 是 | 密码 |
private_key | TEXT | 是 | 私钥 |
note | TEXT | 是 | 备注 |
update_time | DATETIME | 否 | 更新时间 |
create_time | DATETIME | 否 | 创建时间 |
API 平台开发
接口设计
请求路径 | http方法 | 功能 | 备注 |
---|---|---|---|
/api/cmdb/idc/ | get,post,put,delete | 查看,创建,更新,删除 | IDC机房 |
/api/cmdb/server_group/ | get,post,put,delete | 查看,创建,更新,删除 | 主机分组 |
/api/cmdb/server/ | get,post,put,delete | 查看,创建,更新,删除 | 服务器 |
/api/cmdb/create_host | post | 创建 | 新建主机 |
/api/cmdb/host_collect | get | SSH连接采集主机配置 | SSH连接采集主机配置 |
/api/cmdb/excel_create_host | get,post | 下载excel模板文件,提交文件 | excel导入主机 |
/api/cmdb/tencent_cloud | get | 调用腾讯云ECS API获取 | 腾讯云云主机导入 |
/api/cmdb/aliyun_cloud | get | 调用阿里云ECS API获取 | 阿里云云主机导入 |
API平台雏形(上)
基础准备
- pip需要安装的包
pip3 install django==3.2
pip3 install pymysql
pip3 install djangorestframework
pip3 install django-rest-swagger
pip3 install django-filter
pip3 install Markdown
- Pycharm创建项目
- 创建应用
wanghui@kkkk devops_api % python3 manage.py startapp cmdb
wanghui@kkkk devops_api % python3 manage.py startapp system_config
- 调整settings.py 配置
- 本地安装并启动mysql,设置好数据库,用户
wanghui@kkkk ~ % mysql -uroot -p123456
mysql> create database devops_backend;
- 配置mysql数据库
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'devops_backend',
'USER': 'root',
'PASSWORD': '123456',
'HOST': '127.0.0.1',
'PORT': '3306',
}
}
- Devops_api/init.py 配置默认的pymysql为驱动
cmdb数据库model和system_config数据库model设置
- system_config model配置: devops_api/system_config/models.py
from django.db import models
class Credential(models.Model):
auth_choice = (
(1, "密码"),
(2, "秘钥")
)
name = models.CharField(max_length=30, verbose_name="凭据名称")
username = models.CharField(max_length=20, verbose_name="用户名")
auth_mode = models.IntegerField(choices=auth_choice, default=1, verbose_name="认证方式")
password = models.CharField(max_length=50, blank=True, verbose_name="密码")
private_key = models.TextField(blank=True, verbose_name="私钥")
note = models.TextField(blank=True, verbose_name="备注")
create_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
update_time = models.DateTimeField(auto_now=True, verbose_name="更新时间")
class Meta:
db_table = "system_config_credential"
verbose_name_plural = "凭据管理"
ordering = ('-id',)
def __str__(self):
return self.name
- cmdb model配置: devops_api/cmdb/models.py
from django.db import models
from system_config.models import Credential
class Idc(models.Model):
'''
idc表
'''
name = models.CharField(max_length=64,unique=True,verbose_name="idc名称")
city = models.CharField(max_length=64, verbose_name="城市")
provider = models.CharField(max_length=64,verbose_name="提供商")
note = models.TextField(null=True, blank=True, verbose_name="备注")
create_time = models.DateTimeField(auto_now_add=True,verbose_name="创建时间")
class Meta:
db_table = 'devops_idc'
verbose_name_plural = 'idc机房'
ordering = ('-id',)
def __str__(self):
return self.name
class ServerGroup(models.Model):
'''
主机分组
'''
name = models.CharField(max_length=64,unique=True,verbose_name="分组名称")
note = models.TextField(null=True,blank=True,verbose_name="备注")
create_time = models.DateTimeField(auto_now_add=True,verbose_name="创建时间")
class Meta:
db_table = "devops_server_group"
verbose_name_plural = "服务器分组"
ordering = ('-id',)
def __str__(self):
return self.name
class Server(models.Model):
'''
服务器组
'''
idc = models.ForeignKey(Idc, on_delete=models.PROTECT, verbose_name="IDC机房")
server_group = models.ManyToManyField(ServerGroup, default="Default", verbose_name="主机分组")
credential = models.ForeignKey(Credential,on_delete=models.PROTECT, verbose_name="SSH凭据")
hostname = models.CharField(max_length=30, unique=True, verbose_name="主机名")
name = models.CharField(max_length=30, unique=True, verbose_name="名称")
ssh_ip = models.GenericIPAddressField(verbose_name="SSH IP")
ssh_port = models.IntegerField(verbose_name="SSH端口")
note = models.TextField(blank=True, null=True, verbose_name="备注")
machine_type = models.CharField(max_length=30, blank=True,
choices=(('vm', '虚拟机'), ('cloud_vm', '云主机'), ('physical_machine', '物理机')),
verbose_name="机器类型")
os_version = models.CharField(max_length=50, blank=True, null=True, verbose_name="系统版本")
public_ip = models.JSONField(max_length=100, blank=True, null=True, verbose_name="公网IP")
private_ip = models.JSONField(max_length=100, blank=True, null=True, verbose_name="内网IP")
cpu_num = models.CharField(max_length=10, blank=True, null=True, verbose_name="CPU")
cpu_model = models.CharField(max_length=100, blank=True, null=True, verbose_name="CPU型号")
memory = models.CharField(max_length=30, blank=True, null=True, verbose_name="内存")
disk = models.JSONField(max_length=200, blank=True, null=True, verbose_name="硬盘")
put_shelves_date = models.DateField(null=True, blank=True, verbose_name="上架日期")
off_shelves_date = models.DateField(null=True, blank=True, verbose_name="下架日期")
expire_datetime = models.DateTimeField(blank=True, null=True, verbose_name="租约过期时间")
is_verified = models.CharField(max_length=10, blank=True, choices=(('verified', '已验证'), ('unverified', '未验证')),
default='unverified', verbose_name="SSH验证状态")
update_time = models.DateTimeField(auto_now_add=True, verbose_name="更新时间")
create_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
class Meta:
db_table = "cmdb_server"
verbose_name_plural = "主机管理"
ordering = ('-id',)
def __str__(self):
return self.hostname
- 同步数据
wanghui@kkkk devops_api % python3 manage.py makemigrations
wanghui@kkkk devops_api % python3 manage.py migrate
定义序列化器
- cmdb序列化器的定义: devops_api/cmdb/serializers.py
from .models import Idc,ServerGroup,Server
from rest_framework import serializers
class IdcSerializers(serializers.ModelSerializer):
class Meta:
model = Idc
fields = '__all__'
read_only_fields = ("id",)
class ServerGroupSerializers(serializers.ModelSerializer):
class Meta:
model = ServerGroup
fields = '__all__'
read_only_fields = ("id",)
class ServerSerializers(serializers.ModelSerializer):
class Meta:
model = Server
fields = '__all__'
read_only_fields = ("id",)
- System_config序列化器的定义: devops_api/system_config/serializers.py
from .models import Credential
from rest_framework import serializers
class CredentialSerializer(serializers.ModelSerializer):
class Meta:
model = Credential
fields = '__all__'
read_only_fields = ("id",)
定义视图
- 定义cmdb视图: devops_api/cmdb/views.py
from cmdb.models import Idc,ServerGroup,Server
from cmdb.serializers import IdcSerializers,ServerGroupSerializers,ServerSerializers
from rest_framework.viewsets import ModelViewSet
class IdcViewSet(ModelViewSet):
queryset = Idc.objects.all()
serializer_class = IdcSerializers
class ServerGroupViewSet(ModelViewSet):
queryset = ServerGroup.objects.all()
serializer_class = ServerGroupSerializers
class ServerViewSet(ModelViewSet):
queryset = Server.objects.all()
serializer_class = ServerSerializers
- 定义system_config视图: devops_api/system_config/views.py
from rest_framework.viewsets import ModelViewSet
from system_config.models import Credential
from system_config.serializers import CredentialSerializer
class CredentialViewSet(ModelViewSet):
queryset = Credential.objects.all()
serializer_class = CredentialSerializer
动态路由
- 注册cmdb和credentials的路由: devops_api/devops_api/urls.py
from django.contrib import admin
from django.urls import path, include
from cmdb.views import IdcViewSet, ServerGroupViewSet, ServerViewSet
from system_config.views import CredentialViewSet
from rest_framework import routers
router = routers.DefaultRouter()
router.register(r'cmdb/idc', IdcViewSet, basename="idc")
router.register(r'cmdb/server_group', ServerGroupViewSet, basename="server_group")
router.register(r'cmdb/server', ServerViewSet, basename="server")
router.register(r'config/credential', CredentialViewSet, basename="credential")
urlpatterns = [
path('admin/', admin.site.urls),
]
urlpatterns += [
path('api/', include(router.urls))
]
- 查看接口
新增接口数据
- idc数据: http://127.0.0.1:8000/api/cmdb/idc/
- Server-group数据:http://127.0.0.1:8000/api/cmdb/server_group/
- credential数据:http://127.0.0.1:8000/api/config/credential/
- server数据:http://127.0.0.1:8000/api/cmdb/server/
API平台雏形(下)
分页
- 定义分页lib,devops_api/libs/pagination.py
from rest_framework.pagination import PageNumberPagination
from rest_framework.response import Response
from collections import OrderedDict
class MyPagination(PageNumberPagination):
page_size = 6 # 默认每页显示多少条
page_query_param = 'page_num' # 指定查询第几页(页码),默认 page
page_size_query_param = 'page_size' # 定义每页显示多少条
max_page_size = 50 # 每页最多显示多少条
def get_paginated_response(self, data):
code = 200
msg = "成功"
return Response(OrderedDict([
('code', code),
('msg', msg),
('count', self.page.paginator.count),
# ('next', self.get_next_link()),
# ('previous', self.get_previous_link()),
('data', data)
]))
- settings配置引用自定义分页(最后追加): devops_api/devops_api/settings.py
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'libs.pagination.MyPagination'
}
过滤,搜索和排序
- Cmdb视图中新增搜索和排序的字段: devops_api/cmdb/views.py
from cmdb.models import Idc,ServerGroup,Server
from cmdb.serializers import IdcSerializers,ServerGroupSerializers,ServerSerializers
from rest_framework.viewsets import ModelViewSet
from rest_framework import filters
from django_filters.rest_framework import DjangoFilterBackend
class IdcViewSet(ModelViewSet):
queryset = Idc.objects.all()
serializer_class = IdcSerializers
filter_backends = [filters.SearchFilter,filters.OrderingFilter,DjangoFilterBackend]
search_fields = ("name",)
filterset_fields = ("city",)
ordering_fields = ("id",)
class ServerGroupViewSet(ModelViewSet):
queryset = ServerGroup.objects.all()
serializer_class = ServerGroupSerializers
filter_backends = [filters.SearchFilter, filters.OrderingFilter, DjangoFilterBackend]
search_fields = ("name",)
filterset_fields = ("name",)
ordering_fields = ("id",)
class ServerViewSet(ModelViewSet):
queryset = Server.objects.all()
serializer_class = ServerSerializers
filter_backends = [filters.SearchFilter, filters.OrderingFilter, DjangoFilterBackend]
search_fields = ("hostname",)
filterset_fields = ("hostname",)
ordering_fields = ("id",)
- credentials视图中新增搜索,过滤,排序字段:devops_api/system_config/views.py
from rest_framework.viewsets import ModelViewSet
from system_config.models import Credential
from system_config.serializers import CredentialSerializer
from rest_framework import filters
from django_filters.rest_framework import DjangoFilterBackend
class CredentialViewSet(ModelViewSet):
queryset = Credential.objects.all()
serializer_class = CredentialSerializer
filter_backends = [filters.SearchFilter, filters.OrderingFilter, DjangoFilterBackend]
search_fields = ("name",)
filterset_fields = ("name",)
ordering_fields = ("id",)
-
测试case
- 搜索:http://127.0.0.1:8000/api/cmdb/idc/?search=电信
b. 过滤: http://127.0.0.1:8000/api/cmdb/idc/?city=北京
c. 排序(逆序): http://127.0.0.1:8000/api/cmdb/idc/?ordering=-id
启用token认证
- app中安装: settings配置中app下新增:
"rest_framework.authtoken"
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'django_filters',
'cmdb',
'system_config',
'rest_framework.authtoken'
]
- settings中配置认证和权限: devops_api/devops_api/settings.py
REST_FRAMEWORK = {
#分页
'DEFAULT_PAGINATION_CLASS': 'libs.pagination.MyPagination',
# 认证
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.TokenAuthentication',
],
# 权限
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated' # 登录后就能访问所有API
],
}
- 重新操作数据库:migrate
wanghui@kkkk devops_api % python3 manage.py migrate
- 自定义token认证返回值: devops_api/libs/token_auth.py
from rest_framework.authtoken.views import ObtainAuthToken
from rest_framework.authtoken.models import Token
from rest_framework.response import Response
class CustomAuthToken(ObtainAuthToken):
def post(self, request, *args, **kwargs):
serializer = self.serializer_class(data=request.data,
context={'request': request})
if serializer.is_valid():
user = serializer.validated_data['user']
token, created = Token.objects.get_or_create(user=user)
res = {'code': 200,'msg': '认证成功','token': token.key,'username': user.username,}
return Response(res)
else:
res = {'code': 500,'msg': '用户名或密码错误!'}
return Response(res)
- 定义路由: devops_api/devops_api/urls.py
from django.contrib import admin
from django.urls import path, include,re_path
from cmdb.views import IdcViewSet, ServerGroupViewSet, ServerViewSet
from system_config.views import CredentialViewSet
from rest_framework import routers
router = routers.DefaultRouter()
#cmdb项目相关接口
router.register(r'cmdb/idc', IdcViewSet, basename="idc")
router.register(r'cmdb/server_group', ServerGroupViewSet, basename="server_group")
router.register(r'cmdb/server', ServerViewSet, basename="server")
#credentials项目接口
router.register(r'config/credential', CredentialViewSet, basename="credential")
#token接口
from libs.token_auth import CustomAuthToken
urlpatterns = [
path('admin/', admin.site.urls),
re_path('^api/login/$', CustomAuthToken.as_view())
]
urlpatterns += [
path('api/', include(router.urls))
]
- 创建django superuser
wanghui@kkkk devops_api % python3 manage.py createsuperuser
- 浏览器请求测试
- apipost请求测试
- 根据token访问接口测试
修改密码接口
- 路由配置: devops_api/urls.py
from libs import token_auth
re_path('^api/change_password/$', token_auth.ChangeUserPasswordView.as_view()),
我们是基于django用户认证实现登录,因此判断密码是否正确和修改密码需要使用django自带的make_password加密和check_password验证。
- devops_api/libs/token_auth.py
from rest_framework.views import APIView
from django.contrib.auth.models import User
from django.contrib.auth.hashers import make_password, check_password
from rest_framework.permissions import AllowAny
class ChangeUserPasswordView(APIView):
permission_classes = (AllowAny,) # AllowAny 允许所有用户(登录不需要身份认证)
def post(self, request):
# username = request.user
username = 'aliang'
old_password = request.data.get("old_password")
new_password = request.data.get("new_password")
try:
user = User.objects.get(username=username)
except:
res = {'code': 500, 'msg': '用户不存在!'}
return Response(res)
if check_password(old_password, user.password):
user.password = make_password(new_password)
user.save()
res = {'code': 200, 'msg': '修改密码成功'}
else:
res = {'code': 500, 'msg': '原密码不正确!'}
return Response(res)
- 修改密码接口测试: 需要在post的header种加上Authentication 认证头信息,否则就是403
API平台雏形(下)
服务器信息采集
- 前端采集功能实现流程图
- 服务器自动上报或主动采集工作流程
采集方式
-
Agent方式:在每台服务器部署,周期采集并提交API。也可以下发任务。速度快。
- 缺点:提前部署
- 应用场景:适合服务器数量多
-
SH访问:通过paramiko连接各个机器,执行命令,获取数据并提交API
- 缺点:慢
- 应用场景:适合服务器数量少
-
Ansible类工具:也是基于ssh通信,功能完善,速度快,开发成本低。
- 缺点:依赖工具
- 应用场景:适合熟悉ansible的
服务器配置采集脚本
- 采集内容
字段 | 名称 | 获取方式 |
---|---|---|
hostname | 主机名 | socket模块获取 |
machine_type | 机器类型(虚拟机、云主机、物理机) | 从dmesg中提取标识 |
os_version | 系统版本 | /etc/issue |
public_ip | 公网IP地址 | 调用接口判断公网还是内网(用户也会传递),云主机无需判断 |
[intranet](javascript:;)_ip | 内网IP地址 | |
cpu_num | CPU数量 | /proc/cpuinfo |
cpu_model | CPU型号 | /proc/cpuinfo |
memory | 内存 | /proc/meminfo |
disk | 硬盘 | lsblk |
put_shelves_date | 上架日期 | 默认以系统启动时间,后期人工再改 |
- 采集脚本
#!/usr/bin/python
# coding: utf-8
# describe:CMDB采集脚本,对python版本和执行用户没要求
# 解决python执行编码问题
import sys
try:
reload(sys) # py3没有
sys.setdefaultencoding('utf8')
except:
pass
import socket, fcntl, struct
from datetime import datetime, date, timedelta
import os, json
try:
from urllib import request
except:
import urllib2 as request
import logging
# 当前目录
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
# 日志配置
log_file = os.path.join(BASE_DIR, "collect.log")
logging.basicConfig(level=logging.INFO,filename=log_file,format="%(asctime)s - [%(levelname)s] %(message)s")
class GetData():
def __init__(self):
self.result = {}
def hostname(self):
hostname = socket.gethostname()
hostname_backup = '/tmp/.hostname'
if os.path.isfile(hostname_backup) and os.path.getsize(hostname_backup) != 0:
with open(hostname_backup) as f:
hostname = f.read().strip()
else:
with open(hostname_backup, 'w') as f:
f.write(hostname)
return hostname
def machine_type(self):
result = os.popen("dmesg |grep -i virtual |grep -ci hardware")
if int(result.read()) >= 1:
type = "physical_machine" # 物理机
else:
result = os.popen("dmesg |grep -i virtual |grep -ci kvm")
if int(result.read()) >= 1:
type = "cloud_vm" # 云主机
else:
type = "vm" # 虚拟机
return type
# 获取系统版本,兼容centos7和Ubuntu
def os_version(self):
with open("/etc/issue") as f:
if f.readline().strip() == "\S":
with open("/etc/redhat-release") as f:
os_version = f.readline().strip()
else:
os_version = f.readline().strip()
return os_version
# 系统启动时间
def system_up_time(self):
with open("/proc/uptime") as f:
s = f.read().split(".")[0] # 启动有多少秒
up_time = datetime.now() - timedelta(seconds=float(s)) # 当前时间减去启动秒
return date.strftime(up_time, '%Y-%m-%d')
def public_ip(self):
private_ip = self.private_ip()
ip_api_url = ['http://ifconfig.me/ip', 'http://ip.renfei.net']
ip_list = []
try:
req = request.Request(url=ip_api_url[0])
res = request.urlopen(req)
ip = res.read().decode()
except:
req = request.Request(url=ip_api_url[1])
res = request.urlopen(req)
ip = json.loads(res.read().decode())['clientIP']
ip_list.append(ip)
return ip_list
def private_ip(self):
nic_prefix = ['eth', 'en', 'em'] # 常见网卡名前缀
ip_list = []
with open("/proc/net/dev") as f:
for s in f.readlines():
name = s.split(':')[0].strip()
for p in nic_prefix:
if name.startswith(p):
result = os.popen("ip addr show %s |awk -F'[ /]' '/inet /{print $6}'" %name)
ip_list.append(result.read().strip())
return ip_list
# 解析文件
def parse_file(self, file, name):
with open(file) as f:
for line in f.readlines():
key, value = line.split(":")
key = key.strip()
value = value.strip()
if key == name:
return value
def cpu_num(self):
cpu = self.parse_file("/proc/cpuinfo", "cpu cores")
return "%s核" %cpu
def cpu_model(self):
model = self.parse_file("/proc/cpuinfo", "model name")
return model
def memory(self):
total = self.parse_file("/proc/meminfo", "MemTotal")
total = round(float(total.split()[0]) / 1024 / 1024, 1) # 转GB单位
return "%sG" %total
def disk(self):
disk = []
result = os.popen("lsblk |awk '$6~/disk/{print $1,$4,$5}'")
for d in result.read().strip().split('\n'):
d = d.split()
device = d[0]
size = d[1]
type = "HDD" if d[2] == 0 else "SSD"
disk.append({'device': '/dev/%s' %device, 'size': size, 'type': type})
return disk
def get_all(self):
"""
这里字段必须与API对应
"""
self.result = {
"hostname": self.hostname(),
"machine_type": self.machine_type(),
"os_version": self.os_version(),
"public_ip": self.public_ip(),
"private_ip": self.private_ip(),
"cpu_num": self.cpu_num(),
"cpu_model": self.cpu_model(),
"memory": self.memory(),
"disk": self.disk(),
"put_shelves_date": self.system_up_time(), # 上架时间默认设置系统启动时间
}
json_data = json.dumps(self.result)
return json_data
if __name__ == "__main__":
data = GetData()
try:
print(data.get_all())
except Exception as e:
result = {'code': 500, 'msg': '采集脚本执行失败!错误:%s' %e}
print(json.dumps(result))
采集脚本什么时候工作
- 新建主机并同步实现(SSH)
填写基本信息,确保主机名与目标主机一致->点击确认->请求测试接口(带上凭据id),不通先关闭窗口,提示要操作什么,例如检查ip和端口,通的话修改数据库字段已验证,请求调用采集接口自动上报。
-
管理员点击同步实现(SSH)
-
周期性自动执行上报(Agent)
- 在第一次新建主机时候上传脚本并配置定时任务。
- 在装机后系统初始化自动配置
- 后期用ansible批量主机配置
在远程主机执行命令和上传文件
有了采集脚本,接下来就是如何让脚本能目标主机执行进行采集并获取入库。
这里采用paramiko实现ssh连接目标主机并执行采集脚本。
paramiko模块是基于Python实现的SSH远程安全连接,用于SSH远程执行命令、文件传输等功能。
首先pip安装:
pip3 install paramiko
为更好学习该模块,我们下面写几个具体的示例来熟悉它的常用用法。
拓扑图:
SSH密码认证远程执行命令
import paramiko
hostname = '10.0.1.66'
port = 22
username = 'root'
password = '123456'
# 绑定实例
ssh = paramiko.SSHClient()
# AutoAddPolicy()自动添加主机keys
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
# 连接主机信息
ssh.connect(hostname, port, username, password, timeout=5)
# 执行Shell命令,结果分别保存在标准输入,标准输出和标准错误
stdin, stdout, stderr = ssh.exec_command('ls -l')
stdout = stdout.read()
error = stderr.read()
# 判断stderr输出是否为空,为空则打印运行结果,不为空打印报错信息
if not error:
print(stdout)
else:
print(error)
ssh.close()
SSH密钥认证远程执行命令
口令是普遍的鉴权策略,为了提高安全性,还会用密钥对认证。
首选生成秘钥对, 并将公钥加到目标机器的~/.ssh/authorized_keys中
import paramiko
import sys
hostname = '10.0.1.66'
port = 22
username = 'root'
key_file = '/Users/wanghui/.ssh/id_rsa'
# 将列表元素以空格拼接
cmd = " ".join(sys.argv[1:])
def ssh_command(command):
ssh = paramiko.SSHClient()
# 指定key文件
key = paramiko.RSAKey.from_private_key_file(key_file)
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
# 使用key登录
ssh.connect(hostname, port, username, pkey=key)
stdin, stdout, stderr = ssh.exec_command(command)
result = stdout.read()
error = stderr.read()
if not error:
print(result)
else:
print(error)
ssh.close()
if __name__ == "__main__":
ssh_command (cmd)
上传文件到远程服务器
- 代码示例:
import paramiko
hostname = '10.0.1.66'
port = 22
username = 'root'
password = '123456'
local_path = './ssh_key.py'
remote_path = '/tmp/ssh_key.py'
try:
s = paramiko.Transport((hostname, port))
s.connect(username = username, password=password)
#key = paramiko.RSAKey.from_private_key(key_file)
#transport.connect(username=username, pkey=key)
except Exception as e:
print(e)
sftp = paramiko.SFTPClient.from_transport(s)
# 使用put()方法把本地文件上传到远程服务器
sftp.put(local_path, remote_path)
封装ssh模块到django模块验证
- 在devops_api项目路径下创建
ssh.py
模块文件
import paramiko
from io import StringIO # py2 from StringIO import StringIO
import os
class SSH():
def __init__(self, ip, port, username, password=None, key=None):
self.ip = ip
self.port = port
self.username = username
self.password = password
self.key = key
def command(self, shell):
# 绑定实例
ssh = paramiko.SSHClient()
# 允许连接不在known_hosts文件上的主机
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
try:
if self.password:
ssh.connect(hostname=self.ip, port=self.port, username=self.username, password=self.password)
else:
cache = StringIO(self.key) # 将字符串通过StringIO转为file对象(self.key内容是从数据库查询的文本)
key = paramiko.RSAKey.from_private_key(cache) # 接收file对象
# 使用key登录
ssh.connect(hostname=self.ip, port=self.port, username=self.username, pkey=key)
except Exception as e:
return {'code': 500, 'msg': 'ssh连接失败%s' % e}
# 执行Shell命令,结果分别保存在标准输入,标准输出和标准错误
stdin, stdout, stderr = ssh.exec_command(shell)
stdout = stdout.read()
error = stderr.read()
# 判断stderr输出是否为空,为空则打印运行结果,不为空打印报错信息
if not error:
ssh.close()
return {'code': 200, 'msg': '执行命令成功', 'data': stdout}
else:
ssh.close()
return {'code': 500, 'msg': '执行命令失败, 错误信息%s' % error}
def scp(self, local_file, remote_file):
# 绑定实例
s = paramiko.Transport((self.ip, self.port))
try:
if self.password:
s.connect(username=self.username, password=self.password)
else:
cache = StringIO(self.key)
key = paramiko.RSAKey.from_private_key(cache)
s.connect(username=self.username, pkey=key)
except Exception as e:
return {'code': 500, 'msg': 'SSH连接失败, 错误信息%s' % e}
sftp = paramiko.SFTPClient.from_transport(s)
try:
sftp.put(local_file, remote_file)
s.close()
return {'code': 200, 'msg': '上传文件成功'}
except Exception as e:
s.close()
return {'code': 500, 'msg': '上传文件失败, 错误信息%s' % e}
def test(self):
result = self.command('ls')
return result
if __name__ == '__main__':
ssh = SSH('10.0.1.66', 22, 'root', '123456')
#local_file=os.path.join(os.getcwd(),'client_collect_agent.py')
res = ssh.test()
# result = ssh.scp(local_file, '/tmp/agent.py')
print(res)
新建主机功能
表单新建主机接口
- 基本流程
- 创建主机视图: devops_api/cmdb/views.py
from cmdb.models import Idc,ServerGroup,Server
from cmdb.serializers import IdcSerializers,ServerGroupSerializers,ServerSerializers
from rest_framework.viewsets import ModelViewSet
from rest_framework import filters
from django_filters.rest_framework import DjangoFilterBackend
class IdcViewSet(ModelViewSet):
queryset = Idc.objects.all()
serializer_class = IdcSerializers
filter_backends = [filters.SearchFilter,filters.OrderingFilter,DjangoFilterBackend]
search_fields = ("name",)
filterset_fields = ("city",)
ordering_fields = ("id",)
class ServerGroupViewSet(ModelViewSet):
queryset = ServerGroup.objects.all()
serializer_class = ServerGroupSerializers
filter_backends = [filters.SearchFilter, filters.OrderingFilter, DjangoFilterBackend]
search_fields = ("name",)
filterset_fields = ("name",)
ordering_fields = ("id",)
class ServerViewSet(ModelViewSet):
queryset = Server.objects.all()
serializer_class = ServerSerializers
filter_backends = [filters.SearchFilter, filters.OrderingFilter, DjangoFilterBackend]
search_fields = ("hostname","public_ip","private_ip")
filterset_fields = ("idc","server_group")
ordering_fields = ("id",)
from rest_framework.views import APIView
import os
import json
from rest_framework.response import Response
from system_config.models import Credential
from libs.ssh import SSH
class CreateHostView(APIView):
def post(self,request):
# idc_id = int(request.data.get('idc')) # 机房id
# server_group_id_list = request.data.get('server_group') # 分组id
# name = request.data.get('name')
# hostname = request.data.get('hostname')
ssh_ip = request.data.get('ssh_ip')
ssh_port = int(request.data.get('ssh_port'))
credential_id = int(request.data.get('credential'))
# note = request.data.get('note')
# 通过凭据ID获取用户名信息
credential = Credential.objects.get(id=credential_id)
username = credential.username
print(username)
if credential.auth_mode ==1:
password = credential.password
ssh = SSH(ssh_ip,ssh_port,username,password)
else:
private_key = credential.private_key
ssh = SSH(ssh_ip,ssh_port,username,key=private_key)
# 测试ssh链接是否成功
result = ssh.test()
print(result)
- 创建路由: devops_api/devops_api/urls.py
from django.contrib import admin
from django.urls import path, include,re_path
from cmdb.views import IdcViewSet, ServerGroupViewSet, ServerViewSet
from cmdb.views import CreateHostView
from system_config.views import CredentialViewSet
from rest_framework import routers
router = routers.DefaultRouter()
#cmdb项目相关接口
router.register(r'cmdb/idc', IdcViewSet, basename="idc")
router.register(r'cmdb/server_group', ServerGroupViewSet, basename="server_group")
router.register(r'cmdb/server', ServerViewSet, basename="server")
#credentials项目接口
router.register(r'config/credential', CredentialViewSet, basename="credential")
#token接口
from libs.token_auth import CustomAuthToken
from libs import token_auth
urlpatterns = [
path('admin/', admin.site.urls),
re_path('^api/login/$', CustomAuthToken.as_view()),
re_path('^api/change_password/$', token_auth.ChangeUserPasswordView.as_view()),
re_path('^api/cmdb/create_host/$', CreateHostView.as_view())
]
urlpatterns += [
path('api/', include(router.urls))
]
- apiPost 测试
- 查看接口日志
- 创建主机接口改造: devops_api/cmdb/views.py
....
from rest_framework.views import APIView
import os
from django.conf import settings
import json
from rest_framework.response import Response
from system_config.models import Credential
from libs.ssh import SSH
class CreateHostView(APIView):
def post(self,request):
idc_id = int(request.data.get('idc')) # 机房id
server_group_id_list = request.data.get('server_group') # 分组id
name = request.data.get('name')
hostname = request.data.get('hostname')
ssh_ip = request.data.get('ssh_ip')
ssh_port = int(request.data.get('ssh_port'))
credential_id = int(request.data.get('credential'))
note = request.data.get('note')
# 通过凭据ID获取用户名信息
credential = Credential.objects.get(id=credential_id)
username = credential.username
if credential.auth_mode == 1:
password = credential.password
ssh = SSH(ssh_ip,ssh_port,username,password)
else:
private_key = credential.private_key
ssh = SSH(ssh_ip,ssh_port,username,key=private_key)
# 测试ssh链接是否成功
result = ssh.test()
if result['code'] == 200:
local_file = os.path.join(settings.BASE_DIR, 'cmdb','files','host_collect.py')
remote_file = os.path.join('/tmp/host_collect.py')
ssh.scp(local_file,remote_file)
result = ssh.command('python %s' %remote_file)
print(result)
# 服务器信息采集成功,入库
if result['code'] == 200:
idc = Idc.objects.get(id=idc_id)
print(idc)
server_obj = Server.objects.create(
idc = idc,
name = name,
credential = credential,
hostname = hostname,
ssh_ip = ssh_ip,
ssh_port = ssh_port,
is_verified = 'verified',
note = note
)
print(server_obj)
for group_id in server_group_id_list:
group = ServerGroup.objects.get(id=group_id)
server_obj.server_group.add(group)
# 服务器配置信息入库
data = json.loads(result['data'])
Server.objects.filter(hostname=hostname).update(**data)
res = {'code': 200, 'msg': '添加主机成功,并同步配置'}
else:
res = {'code': 500, 'msg': '主机配置信息同步失败,错误信息: %s' %result['msg']}
else:
res = {'code': 500, 'msg': '错误信息: %s' % result['msg']}
return Response(res)
- Apipost 测试
Excel 新建主机功能
- 根据excl模板下载,填写之后再导入Excel
- 需要安装xlrd:
pip3 install xlrd
- excel导入视图: devops_api/cmdb/views.py
...
#Excel创建主机接口
from django.http import FileResponse
import xlrd
class ExcelCreateHostView(APIView):
#下载主机模板文件
def get(self,request):
file_name = 'host.xlsx'
file_path = os.path.join(settings.BASE_DIR,'cmdb','files',file_name)
response = FileResponse(open(file_path,'rb'))
response['Content-Type'] = 'application/octet-stream'
response['Content-Disposition'] = 'attachment;filename=%s'%file_name
return response
#导入文件创建主机
def post(self,request):
excel_file_obj = request.data.get('file')
idc_id = int(request.data.get('idc'))
server_group_id = int(request.data.get('server_group'))
try:
data = xlrd.open_workbook(filename=None,file_contents=excel_file_obj.read())
except Exception:
result = {'code':500, 'msg': '请上传文件'}
return Response(result)
table = data.sheets()[0]
#获取行数
nrows = table.nrows
idc = Idc.objects.get(id=idc_id)
server_group = ServerGroup.objects.get(id=server_group_id)
try:
for i in range(nrows):
if i != 0: #跳过标题行
name = table.row_values(i)[0]
hostname = table.row_values(i)[1]
ssh_ip = table.row_values(i)[2]
ssh_port = table.row_values(i)[3]
note = table.row_values(i)[4]
server_obj = Server.objects.create(
idc=idc,
name= name,
hostname = hostname,
ssh_ip=ssh_ip,
ssh_port=ssh_port,
note=note
)
server_obj.server_group.add(server_group)
result = {'code': 200 , 'msg': 'excel导入主机成功'}
except Exception as e:
result = {'code': 500 , 'msg': 'excel导入主机异常%s'%e}
return Response(result)
- 调整cmdb的creadential字段可为空: devops_api/cmdb/models.py
class Server(models.Model):
'''
服务器组
'''
idc = models.ForeignKey(Idc, on_delete=models.PROTECT, verbose_name="IDC机房")
server_group = models.ManyToManyField(ServerGroup, default="Default", verbose_name="主机分组")
credential = models.ForeignKey(Credential,on_delete=models.PROTECT, blank=True, null=True,verbose_name="SSH凭据")
...
- 设置路由: devops_api/devops_api/urls.py
...
from cmdb.views import CreateHostView,ExcelCreateHostView
urlpatterns = [
path('admin/', admin.site.urls),
re_path('^api/login/$', CustomAuthToken.as_view()),
re_path('^api/change_password/$', token_auth.ChangeUserPasswordView.as_view()),
re_path('^api/cmdb/create_host/$', CreateHostView.as_view()),
re_path('^api/cmdb/excel_create_host/$', ExcelCreateHostView.as_view())
]
...
- Apipost 测试
云主机导入功能
云主机采集很方便,无需agent脚本,直接通过云平台API获取即可。
阿里云
- 在线API调试平台:https://api.aliyun.com/
- 获取AceessKey文档:https://help.aliyun.com/document_detail/175967.html
- 获取AceessKey地址:https://ram.console.aliyun.com/manage/ak
- 安装sdk:
pip install aliyun-python-sdk-ecs -i https://mirrors.aliyun.com/pypi/simple
- 阿里云信息获取模块脚本: devops_api/libs/aliyun_cloud.py
from aliyunsdkcore.client import AcsClient
from aliyunsdkecs.request.v20140526 import DescribeRegionsRequest, DescribeInstancesRequest, DescribeZonesRequest, DescribeDisksRequest
import json
class AliCloud():
def __init__(self, secret_id, secret_key):
self.secret_id = secret_id
self.secret_key = secret_key
def region_list(self):
client = AcsClient(self.secret_id, self.secret_key)
req = DescribeRegionsRequest.DescribeRegionsRequest() # 获取地区
try:
resp = client.do_action_with_exception(req)
resp = json.loads(resp.decode())
resp = {'code': 200, 'data': resp}
return resp
except Exception as e:
return {'code': '500', 'msg': e.get_error_msg()}
def zone_list(self, region_id):
client = AcsClient(self.secret_id, self.secret_key)
req = DescribeZonesRequest.DescribeZonesRequest()
req.add_query_param('RegionId', region_id)
try:
resp = client.do_action_with_exception(req)
resp = json.loads(resp.decode())
resp = {'code': 200, 'data': resp}
return resp
except Exception as e:
return {'code': '500', 'msg': e.get_error_msg()}
def instance_list(self, region_id):
client = AcsClient(self.secret_id, self.secret_key)
req = DescribeInstancesRequest.DescribeInstancesRequest()
req.add_query_param('RegionId', region_id)
try:
resp = client.do_action_with_exception(req)
resp = json.loads(resp.decode())
resp = {'code': 200, 'data': resp}
return resp
except Exception as e:
return {'code': '500', 'msg': e.get_error_msg()}
def instance_disk(self, instance_id):
client = AcsClient(self.secret_id, self.secret_key)
req = DescribeDisksRequest.DescribeDisksRequest()
req.add_query_param('InstanceId', instance_id)
try:
resp = client.do_action_with_exception(req)
resp = json.loads(resp.decode())
resp = {'code': 200, 'data': resp}
return resp
except Exception as e:
return {'code': '500', 'msg': e.get_error_msg()}
if __name__ == '__main__':
# 找个测试ak可以试试
cloud = AliCloud("LTAI5tEDbr3UPGWrUeQbPfKo", "OGn0ESXEHKR29iNES8bCjibw1jo9sa")
result = cloud.region_list()
result = cloud.zone_list('cn-hangzhou')
result = cloud.instance_list("cn-hangzhou")
result = cloud.instance_disk("i-bp1g28isv8irjtrwdxf4")
print(result)
- 获取aliyun region 信息接口,提交服务器配置入库接口: devops_api/cmdb/views.py
...
from libs.aliyun_cloud import AliCloud
import time
class AliyunCloudView(APIView):
def get(self,request):
# 获取region信息
secret_id = request.query_params.get('secret_id')
secret_key = request.query_params.get('secret_key')
cloud = AliCloud(secret_id,secret_key)
region_result = cloud.region_list()
code = region_result['code']
if code == 200:
# 二次处理,固定字段名
region= []
for r in region_result['data']['Regions']['Region']:
region.append({"region_id": r['RegionId'], 'region_name': r['LocalName']})
res = {'code': 200, 'msg': '获取区域列表成功', 'data': region}
else:
res = {'code': 500, 'msg': '获取区域列表成功失败:%s' %region_result['msg']}
return Response(res)
def post(self,request):
"""
根据区域名称创建机房,再导入云主机(绑定机房)到数据库
"""
# 凭据、IDC机房、主机分组、SSH连接地址(IP、端口)
secret_id = request.data.get('secret_id')
secret_key = request.data.get('secret_key')
server_group_id = int(request.data.get('server_group'))
region_id = request.data.get('region') # 区域用于机房里的城市
ssh_ip = request.data.get('ssh_ip') # 用户选择使用内网(private)还是公网(public),下面判断对应录入
ssh_port = int(request.data.get('ssh_port'))
cloud = AliCloud(secret_id, secret_key)
instance_result = cloud.instance_list(region_id)
instance_list = []
if instance_result['code'] == 200:
instance_list = instance_result['data']['Instances']['Instance']
if len(instance_list) == 0:
res = {'code': 500, 'msg': '该区域未发现云主机,请重新选择!'}
return Response(res)
elif instance_result['code'] == 500:
res = {'code': 500, 'msg': '%s' % instance_result['msg']}
return Response(res)
# InstanceSet中可用区字段值是英文,例如 ap-beijing-1
# 先获取可用区英文与中文对应,下面遍历主机再获取中文名
zone_result = cloud.zone_list(region_id)
zone_dict = {}
for z in zone_result['data']['Zones']['Zone']:
zone_dict[z['ZoneId']] = z['LocalName']
# 获取主机所在可用区
# 可用区用于机房里的机房名称
zone_set = set()
for host in instance_list:
zone = host['ZoneId'] # 可用区,例如 ap-beijing-1
zone_set.add(zone_dict[zone]) # 获取中文名
# 根据可用区创建机房
for zone in zone_set:
# 如果存在不创建
idc = Idc.objects.filter(name=zone)
if not idc:
city = ""
region_list = cloud.region_list()['data']['Regions']['Region']
for r in region_list: # 获取区域对应中文名
if r['RegionId'] == region_id:
city = r['LocalName']
Idc.objects.create(
name=zone,
city=city,
provider="阿里云"
)
# 导入云主机信息到数据库
for host in instance_list:
zone = host['ZoneId']
instance_id = host['InstanceId'] # 实例ID
# hostname = host['HostName']
instance_name = host['InstanceName'] # 机器名称
os_version = host['OSName']
private_ip_list = host['NetworkInterfaces']['NetworkInterface'][0]['PrivateIpSets']['PrivateIpSet']
private_ip = []
for ip in private_ip_list:
private_ip.append(ip['PrivateIpAddress'])
public_ip = host['PublicIpAddress']['IpAddress']
cpu = "%s核" % host['Cpu']
memory = "%sG" % (int(host['Memory']) / 1024)
# 硬盘信息需要单独获取
disk = []
disk_list = cloud.instance_disk(instance_id)['data']['Disks']['Disk']
for d in disk_list:
disk.append({'device': d['Device'], 'size': '%sG' % d['Size'], 'type': None})
create_date = time.strftime("%Y-%m-%d", time.strptime(host['CreationTime'], "%Y-%m-%dT%H:%MZ"))
# 2022-01-30T04:51Z 需要转换才能存储
expired_time = time.strftime("%Y-%m-%d %H:%M:%S", time.strptime(host['ExpiredTime'], "%Y-%m-%dT%H:%MZ"))
# 创建服务器
idc_name = zone_dict[zone]
idc = Idc.objects.get(name=idc_name) # 一对多
if ssh_ip == "public":
ssh_ip = public_ip[0]
elif ssh_ip == "private":
ssh_ip = private_ip[0]
data = {'idc': idc,
'name': instance_name,
'hostname': instance_id,
'ssh_ip': ssh_ip,
'ssh_port': ssh_port,
'machine_type': 'cloud_vm',
'os_version': os_version,
'public_ip': public_ip,
'private_ip': private_ip,
'cpu_num': cpu,
'memory': memory,
'disk': disk,
'put_shelves_date': create_date,
'expire_datetime': expired_time,
'is_verified': 'verified'}
# 如果instance_id不存在才创建
server = Server.objects.filter(hostname=instance_id)
if not server:
server = Server.objects.create(**data)
# 分组多对多
group = ServerGroup.objects.get(id=server_group_id) # 根据id查询分组
server.server_group.add(group) # 将服务器添加到分组
else:
server.update(**data)
res = {'code': 200, 'msg': '导入云主机成功'}
return Response(res)
- 添加路由: devops_api/devops_api/urls.py
...
from cmdb.views import CreateHostView,ExcelCreateHostView,AliyunCloudView
urlpatterns = [
path('admin/', admin.site.urls),
re_path('^api/login/$', CustomAuthToken.as_view()),
re_path('^api/change_password/$', token_auth.ChangeUserPasswordView.as_view()),
re_path('^api/cmdb/create_host/$', CreateHostView.as_view()),
re_path('^api/cmdb/excel_create_host/$', ExcelCreateHostView.as_view()),
re_path('^api/cmdb/aliyun_cloud/$', AliyunCloudView.as_view())
]
...
- Apipost get 接口测试
- Apipost post 接口测试
- 阿里云创建资源测试:创建一个只读全局的账号,并获取access id ,secert key
- apipost post 测试
- server接口查询: http://127.0.0.1:8000/api/cmdb/server/11/
- Idc 接口查询: http://127.0.0.1:8000/api/cmdb/idc/