CMDB后端开发(上)

企业项目开发流程

image-20230228200754851

项目背景

目前运维管理存在痛点:随着业务增长,服务器数量越来越多,资产信息通过Excel记录,人工管理低效,易于出错。

CMDB介绍

  • 配置管理数据库(Configuration Management Database,CMDB),是一个逻辑数据库,包含了应用生命周期的信息,例如服务器、物理关系、通信关系、依赖关系等。

  • CMDB存储与管理企业IT架构中设备的各种配置信息,它与所有运维服务和应用发布流程都紧密相联,支持这些流程的运转、发挥配置信息的价值,同时依赖于相关流程保证数据的准确性。CMDB可以实现高度的自动化,减少人为错误的发生、降低人员成本,CMDB是实现运维自动化的基础。

  • CMDB资产

image-20230228201611653

CMDB数据存储需要注意的事项

  • CMDB的目的是为了在其他流程或应用之间共享数据的,如果一个应用或流程需要对某类数据单独使用的话,则不建议将这类数据存入CMDB中,存在自身应用即可。
  • 动态数据不建议存储在CMDB中,例如CPU使用率、内存使用率,因为这类数据更新过于频繁。
  • 如果没有任何流程、应用及人员,需要对特定的数据进行使用,则没有必要放到CMDB中存储。

技术选型

image-20230228201743863

  • 前端技术栈
    • Vue3
    • Vue-router
    • Element Plus
    • Axios
  • 后端技术栈
    • python
    • Django DRF
    • mysql

整体设计

image-20230228202104427

数据库设计

image-20230228203446921

机房管理

表名: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平台雏形(上)

基础准备
  1. pip需要安装的包
pip3 install django==3.2
pip3 install pymysql
pip3 install djangorestframework
pip3 install django-rest-swagger
pip3 install django-filter
pip3 install Markdown
  1. Pycharm创建项目

image-20230306194519769

  1. 创建应用
wanghui@kkkk devops_api % python3 manage.py startapp cmdb
wanghui@kkkk devops_api % python3 manage.py startapp system_config

image-20230306194716166

  1. 调整settings.py 配置

image-20230306194904600

  1. 本地安装并启动mysql,设置好数据库,用户

image-20230306200621828

wanghui@kkkk ~ % mysql -uroot -p123456
mysql> create database devops_backend;
  1. 配置mysql数据库

image-20230306200906460

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'devops_backend',
        'USER': 'root',
        'PASSWORD': '123456',
        'HOST': '127.0.0.1',
        'PORT': '3306',
    }
}
  1. Devops_api/init.py 配置默认的pymysql为驱动

image-20230306201230717

cmdb数据库model和system_config数据库model设置
  1. 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
  1. 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
  1. 同步数据
wanghui@kkkk devops_api % python3 manage.py makemigrations 
wanghui@kkkk devops_api % python3 manage.py migrate
定义序列化器
  1. 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",)
  1. 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",)
  定义视图
  1. 定义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
  1. 定义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
动态路由
  1. 注册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))
]
  1. 查看接口

image-20230306232224454

新增接口数据
  • idc数据: http://127.0.0.1:8000/api/cmdb/idc/

image-20230306230951257

  • Server-group数据:http://127.0.0.1:8000/api/cmdb/server_group/

image-20230306231251954

  • credential数据:http://127.0.0.1:8000/api/config/credential/

image-20230306232029358

  • server数据:http://127.0.0.1:8000/api/cmdb/server/

image-20230306232351164

API平台雏形(下)

分页
  1. 定义分页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)
        ]))
  1. settings配置引用自定义分页(最后追加): devops_api/devops_api/settings.py
REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'libs.pagination.MyPagination'
}
过滤,搜索和排序
  1. 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",)
  1. 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",)
  1. 测试case

    1. 搜索:http://127.0.0.1:8000/api/cmdb/idc/?search=电信

    ​ b. 过滤: http://127.0.0.1:8000/api/cmdb/idc/?city=北京

    image-20230306234944263

    ​ c. 排序(逆序): http://127.0.0.1:8000/api/cmdb/idc/?ordering=-id

    image-20230306235316944

启用token认证
  1. 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'
]
  1. 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
    ],
}
  1. 重新操作数据库:migrate
wanghui@kkkk devops_api % python3 manage.py migrate 
  1. 自定义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)
  1. 定义路由: 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))
]
  1. 创建django superuser
wanghui@kkkk devops_api % python3 manage.py createsuperuser
  1. 浏览器请求测试

image-20230307001038869

  1. apipost请求测试

image-20230416170034660

  1. 根据token访问接口测试

image-20230416170316379

修改密码接口
  1. 路由配置: 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验证。

  1. 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)
  1. 修改密码接口测试: 需要在post的header种加上Authentication 认证头信息,否则就是403

image-20230416171702205

API平台雏形(下)

服务器信息采集
  • 前端采集功能实现流程图

image-20230416171958294

  • 服务器自动上报或主动采集工作流程

采集方式
  • 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))
采集脚本什么时候工作
  1. 新建主机并同步实现(SSH)

填写基本信息,确保主机名与目标主机一致->点击确认->请求测试接口(带上凭据id),不通先关闭窗口,提示要操作什么,例如检查ip和端口,通的话修改数据库字段已验证,请求调用采集接口自动上报。

  1. 管理员点击同步实现(SSH)

  2. 周期性自动执行上报(Agent)

    1. 在第一次新建主机时候上传脚本并配置定时任务。
    2. 在装机后系统初始化自动配置
    3. 后期用ansible批量主机配置

在远程主机执行命令和上传文件

有了采集脚本,接下来就是如何让脚本能目标主机执行进行采集并获取入库。

这里采用paramiko实现ssh连接目标主机并执行采集脚本。

paramiko模块是基于Python实现的SSH远程安全连接,用于SSH远程执行命令、文件传输等功能。

首先pip安装:

pip3 install paramiko

为更好学习该模块,我们下面写几个具体的示例来熟悉它的常用用法。

拓扑图:

image-20230417203601642

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模块验证
  1. 在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)

新建主机功能

表单新建主机接口
  1. 基本流程

image-20230501140416005

  1. 创建主机视图: 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)
  1. 创建路由: 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))
]
  1. apiPost 测试

image-20230501145136427

  1. 查看接口日志

image-20230501145216899

  1. 创建主机接口改造: 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)
  1. Apipost 测试

image-20230501162224242

Excel 新建主机功能
  • 根据excl模板下载,填写之后再导入Excel
  • 需要安装xlrd: pip3 install xlrd
  1. 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)
  1. 调整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凭据")
...
  1. 设置路由: 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())
]
...

  1. Apipost 测试

image-20230501171052922

云主机导入功能

云主机采集很方便,无需agent脚本,直接通过云平台API获取即可。

image-20230501171504172

阿里云
  • 在线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
  1. 阿里云信息获取模块脚本: 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)
  1. 获取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)
  1. 添加路由: 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())
]
...
  1. Apipost get 接口测试

image-20230503122213173

  1. Apipost post 接口测试

image-20230503122951012

  1. 阿里云创建资源测试:创建一个只读全局的账号,并获取access id ,secert key
  2. apipost post 测试

image-20230503124529480

  1. server接口查询: http://127.0.0.1:8000/api/cmdb/server/11/

image-20230503124613611

  1. Idc 接口查询: http://127.0.0.1:8000/api/cmdb/idc/

image-20230503124658843