python自动化运维学习第四十四天--发布系统

本文详细介绍了一个自动化代码发布系统的搭建过程,包括代码库管理、环境配置、主机信息维护、代码版本控制及部署流程自动化。系统利用Django框架进行前端展示与后台逻辑处理,采用Celery实现定时任务调度,如定时发布代码。同时,文中还介绍了如何结合SaltStack进行代码推送及远程执行脚本,确保代码在目标环境中的正确部署。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

cmdb和权限系统弄好以后,下面就是代码发布系统。

分析

发布系统需要几个方面,代码库(git/svn)、代码路径、代码版本号、应用、环境等。一个应用可能有好几个小应用组成,每个小应用对应一个代码库;还有可能只有一个单独的应用对应一个代码库,没有其他小应用。应用有测试环境和生产环境,每个环境对应一台或多台主机。发布时需要指定应用、版本号、环境,到代码指定路径下载代码,并通过工具发送到指定环境主机指定路径下。

表设计

models中发布系统相关表的设计

class Host(models.Model):
    hostname = models.CharField(max_length=64, blank=True, null=True, verbose_name='salt_id')
    ip = models.CharField(max_length=32,verbose_name='IP')
    ...  #省略了其他字段
    def __str__(self):
        return self.eth0_network
    class Meta:
        verbose_name_plural = "主机表"

class Package(models.Model):
    name = models.CharField(max_length=32, blank=True, null=True, verbose_name='包名/版本号/需求编号等')
    pack_path = models.CharField(max_length=64, blank=True, null=True, verbose_name='包路径')
    # project = models.ForeignKey(to='App', blank=True, verbose_name='所属项目', related_name='packapp')
    def __str__(self):
        return self.name
    class Meta:
        verbose_name_plural = "代码"

class App(models.Model):
    name = models.CharField(max_length=32, blank=True, null=True, verbose_name='应用名')
    path = models.CharField(max_length=64, blank=True, null=True, verbose_name='应用路径')
    environment = models.ForeignKey(to='RecordEnv', blank=True, verbose_name='环境')
    hosts = models.ManyToManyField(to='Host', blank=True, verbose_name='对应主机', related_name='apphost')
    _script = models.CharField(max_length=64, blank=True, null=True, verbose_name='部署脚本')
    package = models.ForeignKey(to='Package', blank=True, verbose_name='代码', related_name='apppack')
    _app = models.ForeignKey(to='App', blank=True, verbose_name='上级应用')
    def __str__(self):
        return  self.name
    class Meta:
        verbose_name_plural = "项目表"

class RecordEnv(models.Model):
    name = models.CharField(max_length=64, blank=True, null=True, verbose_name='环境名')
    def __str__(self):
        return self.name
    class Meta:
        verbose_name_plural = "环境"

class Record(models.Model):
    timestamp = models.CharField(max_length=64, blank=True, null=True, verbose_name='时间')
    project = models.ForeignKey(to='App', blank=True, verbose_name='项目', related_name='proj')
    package = models.ManyToManyField(to='Package', blank=True, verbose_name='包', related_name='pack')
    env = models.ForeignKey(to='RecordEnv', blank=True, verbose_name='环境', related_name='env')
    def __str__(self):
        return self.timestamp
    class Meta:
        verbose_name_plural = "部署记录"

在主机表中,发布系统使用saltstack来推送代码,saltstack要使用salt-id来识别主机,所以hostname为salt_id.
应用表关联环境表、主机表、代码表和自己本身。关联本身是由于一个应用可能有子应用;关联主机是多对多关系,一个应用部署在多个主机上,一个主机部署着多个应用;每个应用对应一个代码库。
部署记录表相当于日志表,记录每次发布的相关信息。

伪代码

表设计完成了,下面就是代码的实现
views中的代码

def fabu(request):
    if request.method == 'GET':
        env = models.RecordEnv.objects.all()
        return render(request, 'fabu.html', locals())
    else:
        env = request.POST.get('env')
        app = request.POST.get('app')
        obj_li = models.App.objects.filter(name=app, environment=env)
    #部分代码没有写,下面有说明这部分需要实现的功能
    return HttpResponse("ok")

fabu.html代码

<body>

<form method="post">
    <label>应用</label>
    <input type="text" name="app">
      <select class="form-control" id="numbers" name="env">
          {% for item in env %}
              <option value="{{ item.name }}">{{ item.name }}</option>
          {% endfor %}
      </select>

    <input type="submit" value="提交">
</form>

</body>

views中很多代码没有写,这里对还需要实现那些功能做一些说明:
1、通过获取的环境和应用从数据库中查询对应主机及应用代码路径
2、在cmdb的salt-master上下载代码,通过subprocess下载代码及打包代码
3、备份以前的代码,并推送新代码。写salt的state.sls规则的yml文件,再使用python的salt api调用state触发动作
4、最后执行远端应用启动等相关命令

定时任务

代码需要定时发布时,就会用到celery,celery不止是可以定时发布代码,还可以定时更新cmdb等操作。
下面是一个使用celery的例子
1、需要安装一个djcelery的包
2、在项目同名的app下创建一个celery.py文件,如创建的Django项目名为cmdb,那么在cmdb项目下会有一个cmdb的app,在这个app下创建一个celery.py文件,内容如下

from __future__ import absolute_import, unicode_literals
import os
from celery import Celery
from django.conf import settings

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'cmdb.settins')    #其中cmdb为app名,下同
app = Celery('cmdb')

app.config_from_object('django.conf:settings')

app.autodiscover_tasks(lambda: settings.INSTALLED_APPS )


@app.task(bind=True)
def debug_task(self):
    print('Request: (0!x)'.format(self.request))

在同app下的settings中添加如下内容:

import djcelery
from celery.schedules import crontab
from datetime import timedelta
djcelery.setup_loader()
CELERY_TIMEZONE = TIME_ZONE
BROKER_URL = 'redis://0.0.0.0'     #redis地址,根据自己redis地址修改
CELERY_RESULT_BACKEND = 'redis://0.0.0.0'        #redis地址
CELERY_ACCEPT_CONTENT = ['application/json']
CELERY_TASK_SERIALIZER = 'json'
CELERY_RESULT_SERIALIZER = 'json'
CELERY_TIMEZONE = 'Africa/Nairobi'
CELERY_IMPORTS = ['demo1.task']        #执行的模块路径,根据实际情况修改
CELERY_MAX_TASKS_PER_CHILD = 3
CELERY_SCHEDULER = 'djcelery.schedulers.DatabaseScheduler'

在demo1的app下创建task.py文件,跟settings中设置的CELERY_IMPORTS 对应。
task.py文件内容如下

#/usr/bin/env python

from __future__ import absolute_import, unicode_literals
import time
import requests
from celery import shared_task
from django.views.decorators.csrf import csrf_exempt, csrf_protect
from django.shortcuts import render, HttpResponse, redirect

@shared_task
def add(x,y):
    return x+y

@shared_task
def mul(x,y):
    print(x*y)
    return x*y

@shared_task
def xsum(numbers):
    print(sum(numbers))
    return sum(numbers)

这个task文件是个简单的例子,需要执行的定时任务的功能都可以写在这个文件中,如发布代码、更新cmdb数据等。
在主views中添加如下代码

def celery_status(request):
    import datetime
    import json
    from demo1.task import add
    from cmdb.celery import app
    from celery.result import AsyncResult
    if request.method=='GET':
        if request.GET.get('x') and request.GET.get('y'):
            if request.GET.get('after'):
                ctime = datetime.datetime.now()
                utc_ctime = datetime.datetime.utcfromtimestamp(ctime.timestamp())
                s1 = datetime.timedelta(seconds=int(request.GET.get('after'))*60)
                ctime_x = utc_ctime + s1
            #使用apply_async并设定时间
            year = request.GET.get('year')
            mouth = request.GET.get('month')
            day = request.GET.get('day')
            hour = request.GET.get('hour')
            minute = request.GET.get('minute')
            if year and mouth and day and hour and minute:
                ctime = datetime.datetime(year=int(year), mouth=int(mouth), day=int(day), hour=int(hour), minute=int(minute))
                #把当前本地时间转换为UTC时间
                ctime_x = datetime.datetime.utcfromtimestamp(ctime.timestamp())
            if ctime_x:
                ret = add.apply_async(args=[int(request.GET.get('x')), int(request.GET.get('y'))], eta=ctime_x)
                num = ret.id
        if request.GET.get('cancel'):
            async  = AsyncResult(id=request.GET.get('cancel'), app=app)
            async.revoke(terminate=True)
            cancel_tag = '取消成功'
        if request.GET.get('stop'):
            async = AsyncResult(id=request.GET.get('stop'), app=app)
            async.revoke()
            stop_tag='中止成功'
        return render(request, 'celery.html', locals())
    else:
        ret = request.POST.get('id','')
        data = ''
        if ret:
            async = AsyncResult(id=ret,app=app)
            if async.successful():
                data = "执行成功,数据是:"+str(async.get())
                async.forget()
            elif async.failed():
                data = "执行失败"
            elif async.status == 'PENDING':
                data = "等待被执行"
            elif async.status == 'RETRY':
                data = "任务异常正在重试"
            elif async.status == 'STARTED':
                data = "任务正在执行"
            else:
                data == '未知'
        return render(request, 'celery.html', locals())

celery.html文件内容如下

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>定时</title>
</head>
<body>

<form method="post">
    {% csrf_token %}
    id:<input type="text" name="id">
    结果:<input type="text" value="{{ data }}">
    <input type="submit" value="提交">
</form>
<br>
<hr>

<form method="get">
    x:<input type="text" name="x">+
    y:<input type="text" name="y">
    <br>
    年:<input type="text" name="year">
    月:<input type="text" name="month">
    日:<input type="text" name="day">
    时:<input type="text" name="hour">
    分:<input type="text" name="minute">
    <br>
    几分钟后:<input type="text" name="after">
    <br>
    取消这个任务:<input type="text" name="cancel">
    结果:<input type="text" name="{{ cancel_tag }}">
    <br>
    中止这个任务:<input type="text" name="stop">
    结果:<input type="text" name="{{ stop_tag }}">
    <br>
    <hr>
    结果:<input type="text" name="{{ num }}">
    <input type="submit" value="提交">

</form>

</body>
</html>

配置urls,启动项目,然后在terminal中执行celery worker -A cmdb -l debug,celery必须单独启动,否则无法使用
访问效果如下
在这里插入图片描述
定时任务的功能有了以后,发布功能也有了,只要不发布功能的部分代码放到task文件中,在celery中调用这部分功能就可以实现定时发布了。

补充views中的代码
def sub_run(command):
    return subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

class MainSalt(object):
    def __init__(self, tgt='*'):
        self.local = sc.LocalClient()
        self.tgt = tgt

    def get_cache_returns(self, func):
        while not self.local.get_cache_returns(func):
            time.sleep(1)
        return self.local.get_cache_returns(func)

    def cmd_run(self, run_func):
        if not isinstance(run_func, list):
            raise TypeError(AttributeError)
        cmd_id = self.local.cmd_async(self.tgt, 'cmd.run', run_func)
        ret_cmd = self.get_cache_returns(cmd_id)
        return ret_cmd

    def state(self, salt_fun, tag=''):
        if tag:
            disk_id = self.local.cmd_async(self.tgt, 'state.sls', [salt_fun, tag])
        else:
            disk_id = self.local.cmd_async(self.tgt, 'state.sls', [salt_fun,])
        ret_disk_data = self.get_cache_returns(disk_id)
        return ret_disk_data
    def push_package(self, pillar_dir):
        tag = 'pillar={0}'.format(json.dumps(pillar_dir))
        salt_fun = 'test2'
        return self.state(salt_fun, tag)

def fabu(request):
    if request.method == 'GET':
        env = models.RecordEnv.objects.all()
        return render(request, 'fabu.html', locals())
    else:
        env = request.POST.get('env')
        app = request.POST.get('app')
        obj_li = models.App.objects.filter(name=app, environment=env)

        # 通过环境和应用从数据库中查询对应主机及应用代码路劲
        app_name = 'payment'
        host_li = [{'id':'kvm-10.120', 'path':'/opt/pay_www'},]
        package = 'ssh://git@github.com/***/***.git'

    # 然后进行如下操作
    # 1、在cmdb的salt-master上下载代码,通过subprocess执行下载代码及打包代码
    for host in host_li:
        path = os.getcwd()+ r'/project_path'    #项目下载代码的目录
        ret = sub_run('cd {0} && mkdir {1} &&cd {2} && git clone {3}'.format(path,app_name,app_name,package))
    # 2、备份以前的代码,并推送新代码。写salt的state.sls规则的yml文件,再使用python的salt api调用state触发推送
        a_salt = MainSalt()
        pillar_dir = {
            'path':host.get('path')+'/'+app_name,
            'app':app_name
        }
        salt_ret = a_salt.push_package(pillar_dir)
    # 3、执行远端代码,启动服务
        a_salt.cmd_run('cd {0} && python manage.py runserver 8080'.format(host.get('path')+'/'+app_name))
    return HttpResponse("已存入队列")

代码部分基本就是这些,主要的功能都实现了。

结合Jenkins

上面介绍的是自己写的发布系统,如果公司已经使用了Jenkins作为发布系统,那么可以直接用Jenkins-api来结合cmdb实现发布功能,省去了单独开发发布系统。
使用Jenkins-api需要用到python的python-jenkins模块,需要提前安装好这个模块。具体的实现代码这里不做介绍,网上有很多相关文章,根据自己的环境做些修改即可。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值