接上篇《授之以漁-運(yùn)維平臺發(fā)布模塊三(Jenkins篇)》,今天介紹下針對Jenkins pipeline+saltstack的發(fā)布改造姐呐。
一、 Jenkins Pipeline的總體介紹
- Pipeline徒欣,簡而言之饭望,就是一套運(yùn)行于Jenkins上的工作流框架悔政,將原本獨立運(yùn)行于單個或者多個節(jié)點的任務(wù)連接起來翁涤,實現(xiàn)單個任務(wù)難以完成的復(fù)雜流程編排與可視化桥言。
- Pipeline是Jenkins2.X的最核心的特性,幫助Jenkins實現(xiàn)從CI到CD與DevOps的轉(zhuǎn)變
- Pipeline是一組插件迷雪,讓Jenkins可以實現(xiàn)持續(xù)交付管道的落地和實施。
- 持續(xù)交付管道(CD Pipeline)是將軟件從版本控制階段到交付給用戶或客戶的完整過程的自動化表現(xiàn)虫蝶。軟件的每一次更改(提交到源代碼管理系統(tǒng))都要經(jīng)過一個復(fù)雜的過程才能被發(fā)布章咧。
- Pipeline提供了一組可擴(kuò)展的工具,通過Pipeline Domain Specific Language(DSL)syntax可以達(dá)到Pipeline as Code(Jenkinsfile存儲在項目的源代碼庫)的目的能真。
- Stage:階段赁严,一個Pipeline可以劃分成若干個Stage,每個Stage代表一組操作粉铐,例如:“Build”疼约,“Test”,“Deploy”蝙泼。
二程剥、 早期設(shè)計思路
在Jenkins早期版本中,還沒有Saltstack插件汤踏,所以項目在Jenkins構(gòu)建后织鲸,只能是通過Jenkins調(diào)用SSH或者SFTP將項目傳到目標(biāo)主機(jī)上舔腾。但我們公司由于有堡壘機(jī)的存在,SSH的唯一入口只能是堡壘機(jī)搂擦,傳統(tǒng)的方法不可行稳诚。于是就有了《授之以漁-運(yùn)維平臺發(fā)布模塊一(Jenkins篇)》http://www.reibang.com/p/0e052e79e134。
上文中那個可以遠(yuǎn)程回調(diào)的Saltstack接口瀑踢,小名叫Salt_jenkins_post
君扳还,Jenkins上的回調(diào)參數(shù)如下curl -d "job=JOB_NAME" http://xxxx/salt_jenkins_post
。是的橱夭,沒錯氨距,只需要回傳項目名字即可,后續(xù)的一些針對這個項目的要做的一系列動作都存在一個叫做Release
表中徘钥,他包含了如下幾個字段:
class Release (models.Model):
class Meta:
verbose_name = '項目發(fā)布'
verbose_name_plural = verbose_name
ordering = ['-release_time']
release_name = models.CharField('項目名稱',max_length=40)
release_time = models.DateTimeField('項目發(fā)布時間')
release_svn_address = models.CharField('項目SVN地址',max_length=170)
release_no = models.CharField('項目版本號',max_length=10,blank=True)
release_next_no = models.CharField('項目下次版本號',max_length=10,blank=True)
release_hosts = models.TextField('發(fā)布主機(jī)',blank=True,max_length=1000)
release_path = models.CharField('發(fā)布路徑',max_length=50)
release_fail_hosts_count = models.PositiveIntegerField('成功主機(jī)數(shù)',blank=True)
release_success_hosts_count = models.PositiveIntegerField('失敗數(shù)字?jǐn)?shù)',blank=True)
release_release_dir = models.TextField('發(fā)布路徑',blank=True,max_length=1000)
release_reboot_sync = models.CharField('同步重啟',default='0',blank=True,max_length=5)
release_monitor_process = models.CharField('監(jiān)測進(jìn)程',blank=True,max_length=150)
release_monitor_dir = models.CharField('監(jiān)測目錄',blank=True,max_length=150)
release_purge = models.CharField('清除緩存',default='0',blank=True,max_length=5)
release_purge_name = models.CharField('清除緩存項目',blank=True,max_length=40)
release_purge_dir = models.CharField('清除緩存項目路徑',default='',blank=True,max_length=40)
release_pipeline = models.CharField('項目流水線',default='',blank=True,max_length=40)
release_after_commands = models.TextField('項目前置命令',blank=True,max_length=1000)
release_before_commands = models.TextField('項目后置命令',blank=True,max_length=1000)
Salt_jenkins_post君的作用就是在接到Jenkins構(gòu)建后(Batch tasks
)的回調(diào)命令衔蹲,會根據(jù)Jenkins發(fā)布目標(biāo)的JOB_NAME
找到發(fā)布主機(jī)release_hosts
,最后通過在通過Saltstack調(diào)用目標(biāo)發(fā)布主機(jī)執(zhí)行pkg.upgrade_available
呈础、pkg.mod_repo
舆驶、pkg.get_repo
、pkg.install
來安裝RPM包而钞,最后在根據(jù)配置信息決定是否調(diào)用release_before_commands
執(zhí)行一些后置命令沙廉,如重啟,刪除及release_purge
域名刷新推送臼节,如CDN撬陵。
優(yōu)點:一站式服務(wù),全含
缺點:無法拆分每一步网缝,無法做到發(fā)布步驟的獨立步驟的重放
三巨税、 Pipeline帶來的新的設(shè)計思路
利用Stage:階段,一個Pipeline可以劃分成若干個Stage粉臊,每個Stage代表一組操作草添,于是Release_pipeline_model
君誕生了。
class Release_pipeline_model (models.Model):
class Meta:
verbose_name = '項目發(fā)布流水線模塊'
verbose_name_plural = verbose_name
release_pipeline_model_name = models.CharField('流水線模塊', max_length=40)
release_pipeline_model_id = models.CharField('流水線模塊簡稱', max_length=40)
release_pipeline_responsive_name = models.CharField('流水線Responsive名稱', max_length=40)
release_pipeline_model_edit_menu = models.CharField('流水線重放可視',default='0',max_length=40)
release_pipeline_model_template = models.TextField('流水線模塊模板', blank=True, max_length=1000)
release_pipeline_model_change_at = models.DateTimeField('最后更改時間', auto_now=True)
def __unicode__(self):
return str(self.release_pipeline_model_name)
他的究極完成體如下:存放著名稱扼仲,標(biāo)識远寸,可視化按鈕及具體的Stage內(nèi)容(里面的部分內(nèi)容采用變量,根據(jù)不同項目生成不同的Stage信息屠凶,Pipeline語法詳見https://jenkins.io/doc/book/pipeline/)
Salt_jenkins_post
君也被拆分成了Salt_jenkins_after_commands
君驰后、Salt_jenkins_before_commands
君、Salt_jenkins_getrepo
君矗愧、Salt_jenkins_install
君灶芝、Salt_jenkins_purge
君、Salt_jenkins_reboote
君、Salt_jenkins_upgradeavailable
君监署。每人負(fù)責(zé)一部分颤专,且在Release
表的release_pipeline
記錄著他們的排序。
最后的系統(tǒng)會根據(jù)
release_pipeline
的排序生成Jenkins調(diào)用的Pipeline script钠乏,效果如下:
pipeline{
agent any
stages {
stage('遷出代碼') {
steps{
checkout([$class: 'SubversionSCM', additionalCredentials: [], excludedCommitMessages: '', excludedRegions: '', excludedRevprop: '', excludedUsers: '', filterChangelog: false, ignoreDirPropChanges: false, includedRegions: '', locations: [[cancelProcessOnExternalsFail: true, credentialsId: 'f999860f-8121-4367-913b-21cf61969200', depthOption: 'infinity', ignoreExternalsOption: true, local: '.', remote: "http://172.17.130.96/svndata/dz.m_youth/trunk"]], quietOperation: true, workspaceUpdater: [$class: 'UpdateUpdater']])
}
}
stage('創(chuàng)建目錄') {
steps{
sh 'mkdir -p /home/release/$JOB_NAME'
}
}
stage('打FPM包') {
steps{
sh 'fpm -s dir -x .svn -t rpm -n $JOB_NAME -v $BUILD_NUMBER --prefix /home/dz -C /var/lib/jenkins/workspace/$JOB_NAME -p /home/release/$JOB_NAME ./'
}
}
stage('更新YUM源') {
steps{
sh 'createrepo --update /home/release/$JOB_NAME/'
}
}
stage('客戶端驗證YUM源是否存在') {
steps{
script {
try{
out = sh(script: 'curl -d "job_id=$JOB_NAME" http://veronica.youth.cn/cmdb/salt_jenkins_getrepo/', returnStdout: true)
if (out == '{"status":1}'){
echo 'salt_jenkins_getrepo ok'
}else{
sh 'exit 1'
}
}catch(Exception e){
error("salt_jenkins_getrepo not ok")
}
}
}
}
stage('客戶端檢測YUM源是否更新') {
steps{
script {
try{
out = sh(script: 'curl -d "job_id=$JOB_NAME" http://veronica.youth.cn/cmdb/salt_jenkins_upgradeavailable/', returnStdout: true)
if (out == '{"status":1}'){
echo 'salt_jenkins_upgradeavailable ok'
}else{
sh 'exit 1'
}
}catch(Exception e){
error("salt_jenkins_upgradeavailable not ok")
}
}
}
}
stage('客戶端RPM包安裝') {
steps{
script {
try{
out = sh(script: 'curl -d "job_id=$JOB_NAME" http://veronica.youth.cn/cmdb/salt_jenkins_install/', returnStdout: true)
if (out == '{"status":1}'){
echo 'salt_jenkins_install ok'
}else{
sh 'exit 1'
}
}catch(Exception e){
error("salt_jenkins_install not ok")
}
}
}
}
stage('客戶端后置命令') {
steps{
sh 'curl -d "job_id=$JOB_NAME" http://veronica.youth.cn/cmdb/salt_jenkins_before_commands/'
}
}
}
}
前臺的展示是這樣的:用的jquery.nestable.js
部分JS也雙手奉上:
jQuery(function($){
$('.dd').nestable({
maxDepth: 1,
});
var aa=$('#nestable_list_1').nestable('serialize')
$('#release_pipeline').val(JSON.stringify(aa))
$('.dd').nestable({'group':1}).on('change', function() {
var r=$('#nestable_list_1').nestable('serialize')
console.log(JSON.stringify(r))
$('#release_pipeline').val(JSON.stringify(r))
}
);
});
function reboot_sync_hidden(obj){
if($("#release_reboot_sync_type").val()=="0"){
document.getElementById("monitor_process").style.display ="none";
document.getElementById("monitor_dir").style.display ="none";
}
else {
document.getElementById("monitor_process").style.display ="block";
document.getElementById("monitor_dir").style.display ="block";
}
}
//獲取URL參數(shù)栖秕,?id=xx
function getQueryString(name) {
var reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)', 'i');
var r = window.location.search.substr(1).match(reg);
if (r != null) {
return unescape(r[2]);
}
return null;
}
function release_pipeline_edit(){
csrftokens = $.cookie('csrftoken')
$('#responsive_pipeline_ediit form').submit(function() {
$.ajax({
type: "POST",
url: "../release_pipeline_edit/",
data: $('#responsive_pipeline_ediit form').serialize(),
headers: {"X-CSRFtoken": csrftokens},
async: true,
cache: false,
dataType: "json",
beforeSend: function () {
Metronic.blockUI({animate: true});
},
complete: function () {
Metronic.unblockUI();
},
success: function (obj) {
if (obj['status'] == 999) {
alert(obj['err'])
$('#responsive_pipeline_ediit').modal('hide');
$('#responsive_pipeline_ediit form')[0].reset();
} else {
alert("流水線修改完成")
window.location.href = "../release_list";
}
}
});
return false;
})
}
$(document).ready(function(){
release_pipeline_edit();
});
function purge_hidden(obj){
if($("#release_purge_type").val()=="0"){
document.getElementById("purge_name").style.display ="none";
document.getElementById("purge_dir").style.display ="none";
}
else {
document.getElementById("purge_name").style.display ="block";
document.getElementById("purge_dir").style.display ="block";
}
}
$("#reboot").bind("click", release_pipeline_reboot_edit);
function release_pipeline_reboot_edit(){
var id = getQueryString("id");
console.log(id)
$.ajax({
type: "GET",
url: "../release_pipeline_reboot_edit_ajax/?id=" + id,
async: true,
cache: false,
dataType: "json",
beforeSend: function () {
Metronic.blockUI({animate: true});
},
complete: function () {
Metronic.unblockUI();
},
success: function (obj) {
if (obj['status']==999){
alert(obj['err'])
$('#responsive_reboot').modal('hide');
$('#responsive_reboot form')[0].reset();
}else{
var monitor_process = obj['monitor_process']
var monitor_dir = obj['monitor_dir']
var reboot_sync = obj['reboot_sync']
if (reboot_sync == "1") {
$("#release_reboot_sync_type").val(["1"]).trigger('change');
document.getElementById("monitor_process").style.display = "block";
document.getElementById("monitor_dir").style.display = "block";
$("#release_monitor_process").val(monitor_process)
$("#release_monitor_dir").val(monitor_dir)
} else if (reboot_sync == "2") {
$("#release_reboot_sync_type").val(["2"]).trigger('change');
document.getElementById("monitor_process").style.display = "block";
document.getElementById("monitor_dir").style.display = "block";
$("#release_monitor_process").val(monitor_process)
$("#release_monitor_dir").val(monitor_dir)
}
$("#job_id").val(id)
}}
})
}
$('#responsive_reboot form').submit(function(){
var id = $('#job_id').val();
var release_reboot_sync_type = $("#release_reboot_sync_type").val();
var release_monitor_process = $("#release_monitor_process").val();
var release_monitor_dir = $("#release_monitor_dir").val();
var jsonData = {
"id":id,
"release_reboot_sync":release_reboot_sync_type,
"release_monitor_process":release_monitor_process,
"release_monitor_dir":release_monitor_dir,
}
csrftokens = $.cookie('csrftoken')
$.ajax({
type: "POST",
data: jsonData,
url: "../release_pipeline_reboot_edit_ajax/",
headers:{ "X-CSRFtoken":csrftokens},
async:true,
cache: false,
dataType: "json",
beforeSend:function(){
Metronic.blockUI({animate: true});
},
complete: function() {
Metronic.unblockUI();
},
success: function(obj) {
if (obj['status']==999){
alert(obj['err'])
$('#responsive_reboot').modal('hide');
$('#responsive_reboot form')[0].reset();
}else if (obj['status'] == "1"){
$('#responsive_reboot').modal('hide');
$('#responsive_reboot form')[0].reset();
alert ("修改完成");
} else {
alert ("修改失敗");
}
},
});
return false;
});
$('#responsive_reboot').on('hide.bs.modal', function () {
location.reload();
});
$("#purge").bind("click", release_purge_edit);
function release_purge_edit(){
var id = getQueryString("id");
console.log(id)
$.ajax({
type: "GET",
url: "../release_pipeline_purge_edit_ajax/?id=" + id,
async: true,
cache: false,
dataType: "json",
beforeSend: function () {
Metronic.blockUI({animate: true});
},
complete: function () {
Metronic.unblockUI();
},
success: function (obj) {
if (obj['status']==999){
alert(obj['err'])
$('#responsive_purge').modal('hide');
$('#responsive_purge form')[0].reset();
}else{
var purge_name = obj['purge_name']
var purge_dir = obj['purge_dir']
var purge = obj['purge']
if (purge == "1") {
$("#release_purge_type").val(["1"]).trigger('change');
document.getElementById("purge_name").style.display = "block";
document.getElementById("purge_dir").style.display = "block";
$("#release_purge_name").val(purge_name)
$("#release_purge_dir").val(purge_dir)
}
$("#job_id").val(id)
}}
})
}
$('#responsive_purge form').submit(function(){
var id = $('#job_id').val();
var release_purge_type = $("#release_purge_type").val();
var release_purge_name = $("#release_purge_name").val();
var release_purge_dir = $("#release_purge_dir").val();
var jsonData = {
"release_purge":release_purge_type,
"release_purge_name":release_purge_name,
"release_purge_dir":release_purge_dir,
"id":id
}
csrftokens = $.cookie('csrftoken')
$.ajax({
type: "POST",
data: jsonData,
url: "../release_pipeline_purge_edit_ajax/",
headers:{ "X-CSRFtoken":csrftokens},
async:true,
cache: false,
dataType: "json",
beforeSend:function(){
Metronic.blockUI({animate: true});
},
complete: function() {
Metronic.unblockUI();
},
success: function(obj) {
if (obj['status']==999){
alert(obj['err'])
$('#responsive_purge').modal('hide');
$('#responsive_purge form')[0].reset();
}else if (obj['status'] == "1"){
$('#responsive_purge').modal('hide');
$('#responsive_purge form')[0].reset();
alert ("修改完成");
} else {
alert ("修改失敗");
}
},
});
return false;
});
$('#responsive_purge').on('hide.bs.modal', function () {
location.reload();
});