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模块,需要提前安装好这个模块。具体的实现代码这里不做介绍,网上有很多相关文章,根据自己的环境做些修改即可。