介绍
研发人员提交代码
运维人员Jenkins选择分支进行构建
Jenkins服务器进行maven打包
mvn打包同时进行sonar代码检测
mvn打包完成后生成sonar检测报告
判断sonar检测结果,ERROR状态进行企业微信告警
Jenkins将jar包打成镜像或者同步到目标主机上
启动jar包或者启动pod
以上是Jenkins集成sonar的一个完整的过程
编写企业微信告警脚本
# cat sonar.py
#!/usr/bin/env python37
# -*- coding: utf-8 -*-
# @Time : 2019/8/30 12:41
# @Author : chenfei
# @FileName: sonar.py
# @Software: PyCharm
# @Blog :https://www.devilf.cc/
import requests
import json
import time
import sys
import smtplib
from email.mime.text import MIMEText
def Mail(receivers,content,title):
'''
发送邮件
:param receivers:
:param content:
:param title:
:return:
'''
mail_host = 'smtp.qiye.163.com'
mail_user = 'xxxx@xxxxx.com'
mail_pass = 'xxxxxxxxx'
mail_sender = 'xxxx@xxxxx.com' #可以同mail_user一样
msg = MIMEText(content, 'html', 'utf-8')
msg['From'] = "{}".format(mail_sender)
msg['To'] = "".join(receivers)
msg['Subject'] = title
try:
smtpObj = smtplib.SMTP_SSL(mail_host, 465)
smtpObj.login(mail_user, mail_pass)
smtpObj.sendmail(mail_sender, receivers, msg.as_string())
print('mail send successful.')
except smtplib.SMTPException as e:
print(e)
def GetSonarReport(project_name, code_user, info_type='text', sonar_url='https://xxx.xxxxxxxxxxx.com/'):
'''
从sonar获取指定项目的分析报告,包括关键指标信息和链接地址
:param project_name:
:param code_user:
:param type:
:param sonar_url:
:return:
'''
#定义sonar检测时获取的指标信息
select_content = 'alert_status%2Cbugs%2Creliability_rating' \
'%2Cvulnerabilities%2Csecurity_rating%2Ccode_smells' \
'%2Csqale_rating%2Cduplicated_lines_density%2Ccoverage' \
'%2Cncloc%2Cncloc_language_distribution'
#设置用户名和密码
auth = {
'login': 'sonar',
'password': 'Sonar@123'
}
#设置header信息
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:44.0) Gecko/20100101 Firefox/44.0"
}
#sonar完整的查询各指标信息的请求URL
url = sonar_url + 'api/measures/search' + '?' + 'projectKeys=' + project_name + '&' + 'metricKeys=' + select_content
#sonar检测完毕后的报告链接
report_url = sonar_url + 'dashboard?id=' + project_name
#sonar登录接口
login_url = 'https://xxx.xxxxxxxxxxx.com/api/authentication/login'
#使用session
session = requests.session()
login = session.post(login_url,headers=headers, data=auth)
response = session.get(url).text
result = json.loads(response)
for item in result['measures']:
# 检测状态
if item['metric'] == 'alert_status':
Status = item['value']
# Bug数
elif item['metric'] == 'bugs':
Bugs = item['value']
# 可能存在的问题行数
elif item['metric'] == 'code_smells':
Smells = item['value']
# 漏洞数
elif item['metric'] == 'vulnerabilities':
Vulnerabilities = item['value']
# 重复率
elif item['metric'] == 'duplicated_lines_density':
Duplicated_lines_density = item['value']
# 覆盖率
elif item['metric'] == 'coverage':
Coverage = item['value']
#markdown格式的提示信息
text_info = '检测目标项目:' + ' ' + project_name + '\n' \
+ '代码作者: ' + code_user + '\n' \
+ '>' + '状态为:' + '<font color=\"warning\">' + Status + '</font>' + '\n' \
+ '>' + 'Bug数量:' + '<font color=\"warning\">' + Bugs + '</font>' + '\n' \
+ '>' + '漏洞数:' + '<font color=\"warning\">' + Vulnerabilities + '</font>' + '\n' \
+ '>' + '重复率:' + '<font color=\"warning\">' + Duplicated_lines_density + '</font>' + '\n' \
+ '>' + '覆盖率:' + '<font color=\"warning\">' + Coverage + '</font>' + '\n' \
+ '>' + '可能存在的问题行数:' + '<font color=\"warning\">' + Smells + '</font>' + '\n' + '\n'\
+ '[详情可查看分析报告]' + '(' + report_url + ')'
#HTML格式的提示信息
html_info = '''<html>
<head>
<title>Evernote Export</title>
<basefont face="微软雅黑" size="2" />
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
<meta name="exporter-version" content="YXBJ Windows/600753 (zh-CN, DDL); Windows/10.0.0 (Win64);"/>
<meta name="content-class" content="yinxiang.markdown"/>
</head>
<body>
<a name="3004"/>
<span><div style="font-size: 14px; margin: 0; padding: 0; width: 100%;"><h3 style="line-height: 160%; box-sizing: content-box; font-weight: 700; font-size: 27px; color: #333;">Sonar检测报告</h3>
<blockquote style="line-height: 160%; box-sizing: content-box; margin: 15px 0; border-left: 4px solid #ddd; padding: 0 15px; color: #777;">
<p style="line-height: 160%; box-sizing: content-box; margin: 10px 0; color: #333; margin-top: 0; margin-bottom: 0;">状态为:{}<br/>
Bug数量:{}<br/>
漏洞数:{}<br/>
重复率:{}<br/>
覆盖率:{}<br/>
可能存在的问题行数:{}</p>
</blockquote>
<p style="line-height: 160%; box-sizing: content-box; margin: 10px 0; color: #333;"><a href="{}" style="line-height: 160%; box-sizing: content-box; text-decoration: underline; color: #5286bc;">点击查看报告</a></p>
<hr style="line-height: 160%; box-sizing: content-box; border-top: 1px solid #eee; margin: 16px 0;"/>
<p style="line-height: 160%; box-sizing: content-box; margin: 10px 0; color: #333;">运维中心发</p>
</body></html>'''.format(Status, Bugs, Vulnerabilities, Duplicated_lines_density, Coverage, Smells, report_url)
if info_type == 'text':
return text_info
elif info_type == 'html':
return html_info
else:
return None
def SendToWx(project_name, send_who):
'''
通过企业微信机器人发送sonar检查报告,并@相关负责人。
:param project_name:
:return:
'''
#企业微信机器人链接,通过该链接完成一系列的请求
robot_wx_url = 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=5xxxxxxxxxxxxxxxxxxxx7868'
username = send_who.split('@')[0]
who = [username]
content = GetSonarReport(project_name,username,info_type='text')
data_user = {
"msgtype": "text",
"text": {
"content": "代码作者:%s"%(who[0]),
"mentioned_list": who
}
}
data_info = {
"msgtype": "markdown",
"markdown": {
"content": content
}
}
count = int(5)
flag = False
while not flag:
try:
SendPostUser = requests.post(robot_wx_url,json=data_user).text
SendPostInfo = requests.post(robot_wx_url,json=data_info).text
return SendPostUser, SendPostInfo
except requests.exceptions.ConnectTimeout:
print('请求超时,3秒后重试')
count-=1
time.sleep(3)
except requests.exceptions.ConnectionError:
print('连接错误,3秒后重试')
count-=1
time.sleep(3)
except:
print('Unknow Err, retry after 3 seconds!!!')
count-=1
time.sleep(3)
if count == 0:
flag = True
def SendMail(project_name, email_addr):
'''
发html类型的邮件,默认不用指定发送的文本类型,邮件默认为html类型的邮件
:param project_name:
:param email_addr:
:return:
'''
content = GetSonarReport(project_name, info_type='html')
title = 'Sonar检测报告:{}'.format(project_name)
send = Mail(email_addr,content,title)
return send
if __name__ == "__main__":
project_name = sys.argv[1]
send_who = sys.argv[2]
# project_name = 'lorehouse-base-interface'
# send_who = 'chenfei@cdvcloud.com'
SendToWx(project_name, send_who)
SendMail(project_name, send_who)
企业微信机器人接口文档:机器人 执行该脚本只需传入两个参数即可,一个是sonar检测的项目的项目名,另一个是要发的邮件地址
def gettags = ("git ls-remote -h git@coxxxxxxxxxxxxxxxxxx01/empapi.git").execute()
a=gettags.text.readLines().collect { it.split()[1].replaceAll('refs/heads/', '') }.unique()
return a
pipeline {
agent any
environment {
def registry = "192.168.66.169"
def base_img_addr = "'${registry}/cicd/base-jdk-img:v1.0.1'"
def ops_git_addr = "git@codxxxxxxxxxxxxxxxxxxx01/phoenix-sample.git"
def git_address = "git@codexxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxg00001/empapi.git"
def git_auth = "998dc514-4b0c-4194-80f6-5393da0cb710"
// Pod的名称
def app_names = "emp-api"
def replica_numbers = "1"
def max_cpus = "2"
def max_mems = "4096Mi"
def cpus = "100m"
def mems = "100Mi"
def target_ports = "8080"
def inner_ports = "8080"
def out_ports = "32029"
def nacos_url = "xxx.xxxxxxx.com"
def nacos_group = "PLAN_A"
def k8s_group = "k8s-server-test"
}
stages('Begin Deploy or Rollback') {
stage('set namespace') {
steps {
script {
if (env.EnvName == 'alpha') {
nacos_namespace = "5f9a9xxxxxxxxxxxc340e9"
} else {
nacos_namespace = "6484xxxxxxxxxxxxxxebe028"
}
}
}
}
stage('拉取代码') {
steps {
script {
deleteDir()
checkout([$class: 'GitSCM', branches: [[name: '${Branch}']], userRemoteConfigs: [[credentialsId: "${git_auth}", url: "${git_address}"]]])
}
}
}
stage('deploy') {
steps {
script {
if (env.Action == 'Deploy') {
git_cm_id = sh(script: "git rev-parse --short HEAD", returnStdout: true).trim()
pom = readMavenPom file: 'pom.xml'
date_time = sh(script: "date +%Y%m%d%H%M", returnStdout: true).trim()
img_group = "testhuawei"
img_name = "${registry}/${img_group}/${pom.artifactId}"
cur_version = "${pom.version}"
whole_img_name = "${img_name}:${date_time}-${cur_version}-${git_cm_id}"
tag_info = "${date_time}-${cur_version}-${git_cm_id}"
jar_file_info = "${pom.artifactId}-${pom.version}.jar"
app_name_info = "${pom.artifactId}"
kuber_yml_dirs = "/root/leishengchan-k8s-yaml/${app_name_info}"
project_name = sh(script: "echo ${git_address} | awk -F '/' '{print \$2}' | awk -F '.' '{print \$1}'", returnStdout: true).trim()
project_version = "${pom.version}"
email_addr = sh(script: "git log -1 --pretty=format:'%ce'", returnStdout: true).trim()
withSonarQubeEnv('SonarQube') {
sh "mvn package install -Dmaven.test.skip=true sonar:sonar -Dsonar.projectKey=${project_name} -Dsonar.projectName=${project_name} -Dsonar.projectVersion=${project_version} -Dsonar.sourceEncoding=UTF-8 -Dsonar.exclusions=**/*.js,**/*.html,**/*.css -Dsonar.sources=src/ -Dsonar.java.binaries=target/classes -Dsonar.host.url=https://sonar.yunshicloud.com -Dsonar.login=a0994774ef9e11c652e9b7545401394d0bae6a00"
}
timeout(10) {
def qg = waitForQualityGate('SonarQube')
if (qg.status == 'OK') {
sh "/data/jenkins/workspace/scripts/sonar.py ${project_name} ${email_addr}"
error "未通过Sonarqube的代码质量阈检查,请及时修改!failure: ${qg.status}"
}
}
//sh "mvn package install -Dmaven.test.skip=true"
sh "rm -rf jenkins-script && mkdir jenkins-script && cd jenkins-script && git clone ${ops_git_addr} && cp -r ${WORKSPACE}/jenkins-script/phoenix-sample/deploy-jenkins/* ${WORKSPACE}/target/"
withCredentials([usernamePassword(credentialsId: 'ed481948-5714-4d16-93b3-57023c4a5fbd', passwordVariable: 'dockerpasswd', usernameVariable: 'dockeruser')]) {
dir('target') {
sh "cp -r docker-build/* ."
sh "cp -r rollback/* ."
sh "./rollback.sh ${tag_info}"
sh "./docker.sh --app_name=${app_name_info} --jar_file=${jar_file_info} --img_info=${whole_img_name} --nacos_url=${nacos_url} --nacos_namespace=${nacos_namespace} --nacos_group=${nacos_group} --envname=${EnvName} --base_img=${base_img_addr}"
}
}
dir('target') {
sh "./k8s.sh --img_secrets=kit-new40 --app_name=${app_names} --replica_number=${replica_numbers} --image_address=${whole_img_name} --mem=${mems} --cpu=${cpus} --max_mem=${max_mems} --max_cpu=${max_cpus} --inner_port=${inner_ports} --target_port=${target_ports} --out_port=${out_ports} --kuber_yml_dir=${kuber_yml_dirs} --group_name=${k8s_group}"
}
} else {
sh "echo Stop."
}
}
}
}
stage('rollback') {
steps {
script {
if (env.Action == "Rollback") {
poms = readMavenPom file: 'pom.xml'
img_groups = "testhuawei"
img_names = "${registry}/${img_groups}/${poms.artifactId}"
whole_img_name_rollback = "${img_names}:${RollbackFile}"
app_name_info_rollback = "${poms.artifactId}"
kuber_yml_dirs_rollback = "/root/leishengchan-k8s-yaml/${app_name_info_rollback}"
sh "rm -rf jenkins-script && mkdir jenkins-script && cd jenkins-script && git clone ${ops_git_addr} && cp -r ${WORKSPACE}/jenkins-script/phoenix-sample/deploy-jenkins/* ${WORKSPACE}/"
dir("${WORKSPACE}") {
sh "./k8s.sh --img_secrets=kit-new40 --app_name=${app_names} --replica_number=${replica_numbers} --image_address=${whole_img_name_rollback} --mem=${mems} --cpu=${cpus} --max_mem=${max_mems} --max_cpu=${max_cpus} --inner_port=${inner_ports} --target_port=${target_ports} --out_port=${out_ports} --kuber_yml_dir=${kuber_yml_dirs_rollback} --group_name=${k8s_group}"
}
} else {
sh "echo stop."
}
}
}
}
}
}
以上是jenkins构建时需要设置的一些参数,和结合sonar的pipeline写法。
当然也可以回滚,回滚是通过脚本,将tag记录到Jenkins本地的一个文件中
supervisor文件参考
$ cat /etc/supervisord.d/dazzle-interface.ini
[program:dazzle-interface]
command=/usr/bin/java -jar -Xms1024m -Xmx3072m -XX:PermSize=256M -XX:MaxPermSize=512m dazzle-interface-v4.0.0.0.1909220104.jar --spring.cloud.nacos.config.server-addr=xxx.xxxxxx.com --spring.cloud.nacos.config.namespace=dbaxxxxxxxxxxxxxxxx3 --spring.cloud.nacos.config.group=PLAN_A --spring.profiles.active=prod
directory=/usr/local/dazzle-interface
autostart=true
autorestart=true
startsecs=5
priority=1
redirect_stderr=true
stdout_logfile=/usr/local/dazzle-interface/logs/dazzle-interface.log
stopasgroup=true
killasgroup=true
cat docker.sh
#!/bin/bash
ARGS=`getopt -a -o n:j:i?️:u:s:g:e: --long app_name:,jar_file:,img_info:,base_img::,nacos_url:,nacos_namespace:,nacos_group:,envname:, -- "$@"`
if [ $? != 0 ];then
echo "Terminating..."
exit 1
fi
eval set -- "${ARGS}"
while :
do
case $1 in
-n|--app_name)
app_name=$2
shift
;;
-j|--jar_file)
jar_file=$2
shift
;;
-i|--img_info)
img_info=$2
shift
;;
-b|--base_img)
base_img=$2
shift
;;
-u|--nacos_url)
nacos_url=$2
shift
;;
-s|--nacos_namespace)
nacos_namespace=$2
shift
;;
-g|--nacos_group)
nacos_group=$2
shift
;;
-e|--envname)
envname=$2
shift
;;
--)
shift
break
;;
*)
echo "Internal error!"
exit 1
;;
esac
shift
done
SedFile() {
sed -i "s#\$JarName#$jar_file#g" Dockerfile
sed -i "s#\$AppName#$app_name#g" Dockerfile
sed -i "s#\$JarName#$jar_file#g" entrypoints.sh
sed -i "s#\$NacosUrl#$nacos_url#g" entrypoints.sh
sed -i "s#\$NacosNamespace#$nacos_namespace#g" entrypoints.sh
sed -i "s#\$NacosGroup#$nacos_group#g" entrypoints.sh
sed -i "s#\$EnvName#$envname#g" entrypoints.sh
sed -i "s#\$BaseimgAddr#$base_img#g" Dockerfile
}
start() {
docker build -t ${img_info} .
docker push ${img_info}
}
SedFile
start
dockerifle参考
FROM $BaseimgAddr
MAINTAINER chenfei
COPY entrypoints.sh /
RUN mkdir -p /data/$AppName/logs && \
chmod +x /entrypoints.sh
COPY $JarName /data/$AppName/
WORKDIR /data/$AppName
ENTRYPOINT ["/entrypoints.sh"]
entrypoint.sh参考
#!/bin/bash
echo '
nameserver 172.18.1.14
nameserver 10.96.0.10
search docker-new40.svc.cluster.local svc.cluster.local cluster.local openstacklocal
options ndots:5
' > /etc/resolv.conf
java -jar $JarName --spring.cloud.nacos.config.server-addr=$NacosUrl --spring.cloud.nacos.config.namespace=$NacosNamespace --spring.cloud.nacos.config.group=$NacosGroup --spring.profiles.active=$EnvName
回滚时将tag写入文件的脚本
#!/bin/bash
export PATH=$PATH
#最多保存5个tag
backup_num="5"
#存放tag的文件路径
rollbackfile="/data/jenkins/workspace/rollback/RollBackFile.txt"
#当前jenkins执行的任务名
job_name=${JOB_NAME}
#传入参数:tag
img_tag_info="$1"
#检查当前任务名是否存在,存在即为非零
job_name_stat=$(grep -c ${job_name} ${rollbackfile})
#插入的tag,以逗号结尾
add_content_info="${img_tag_info},"
#检查当前有多少个tag
tag_num=$(awk "/$job_name/" ${rollbackfile} | awk -F '=' '{print $2}' | awk -F ',' '{print NF}')
if [ ${job_name_stat} -eq 0 ];then
echo "${job_name}=${img_tag_info}" >> ${rollbackfile}
else
if [ $(grep -c "${img_tag_info}" ${rollbackfile}) -eq 0 ];then
if [ "${tag_num}" -lt "${backup_num}" ];then
sed -i "s#^${job_name}=#&${add_content_info}#g" ${rollbackfile}
elif [ "${tag_num}" -ge "${backup_num}" ];then
while [ "${tag_num}" -ge "${backup_num}" ]
do
del_field=$(awk "/$job_name/" ${rollbackfile} | awk -F '=' '{print $2}' | awk -F ',' '{print $NF}')
sed -i "s#,${del_field}##g" ${rollbackfile}
let tag_num--
done
sed -i "s#^${job_name}=#&${add_content_info}#g" ${rollbackfile}
fi
fi
fi
执行ansible时的playbook参考 入口文件
cat setup-k8s.yaml
- hosts: $GroupName
roles:
- role: deploy-server-k8s
vars:
app_pod_name: "$AppPodName"
replica_num: "$ReplicaNum"
harbor_image_address: "$HarborImageAddress"
max_cpu_num: "$MaxCpuNum"
max_mem_size: "$MaxMemSize"
cpu_num: "$CpuNum"
mem_size: "$MemSize"
target_server_port: "$TargetServerPort"
inner_server_port: "$InnerServerPort"
out_server_port: "$OutServerPort"
k8s_dst_dir: "$KuberDstDir"
img_secret: "$ImgSecret"
启动pod的模板文件
cat roles/deploy-server-k8s/templates/app-deployment.yaml.j2
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: {{ app_pod_name }}
labels:
name: {{ app_pod_name }}
spec:
replicas: {{ replica_num }}
strategy:
rollingUpdate:
maxSurge: 1
maxUnavailable: 1
type: RollingUpdate
selector:
matchLabels:
name: {{ app_pod_name }}
template:
metadata:
labels:
name: {{ app_pod_name }}
spec:
containers:
- name: {{ app_pod_name }}
image: {{ harbor_image_address }}
imagePullPolicy: IfNotPresent
resources:
limits:
cpu: {{ max_cpu_num }}
memory: {{ max_mem_size }}
requests:
cpu: {{ cpu_num }}
memory: {{ mem_size }}
imagePullSecrets:
- name: {{ img_secret}}
cat roles/deploy-server-k8s/templates/app-service.yaml.j2
apiVersion: v1
kind: Service
metadata:
name: {{ app_pod_name }}
labels:
name: {{ app_pod_name }}
spec:
ports:
- name: {{ app_pod_name }}
protocol: TCP
targetPort: {{ target_server_port }}
port: {{ inner_server_port }}
nodePort: {{ out_server_port }}
selector:
name: {{ app_pod_name }}
sessionAffinity: None
type: NodePort
主要的main文件
cat roles/deploy-server-k8s/tasks/main.yml
- name: Check dst dir is already exists.
stat:
path: "{{ k8s_dst_dir }}"
register: dst_dir
- name: Check yaml file is already exists.
stat:
path: "{{ k8s_dst_dir }}/app-service.yaml"
register: yml_file
- name: Create k8s dst dir
file:
path: "{{ k8s_dst_dir }}"
state: directory
owner: root
group: root
mode: 0755
when: dst_dir.stat.exists == False
- name: Copy local deployment yaml file to k8s server
template:
src: app-deployment.yaml.j2
dest: "{{ k8s_dst_dir }}/app-deployment.yaml"
owner: root
group: root
mode: 0644
- name: Copy local service yaml file to k8s server
template:
src: app-service.yaml.j2
dest: "{{ k8s_dst_dir }}/app-service.yaml"
owner: root
group: root
mode: 0644
- name: Start pod when first deploy
shell: "kubectl apply -f ."
args:
chdir: "{{ k8s_dst_dir }}"
when: yml_file.stat.exists == False
#- name: Stop pod
# shell: "kubectl delete -f . || sleep 3 ; kubectl delete -f . "
# args:
# chdir: "{{ k8s_dst_dir }}"
# when: yml_file.stat.exists == True
- name: Start pod
shell: "kubectl apply -f . || kubectl delete -f . ; sleep 3 ; kubectl apply -f ."
args:
chdir: "{{ k8s_dst_dir }}"
when: yml_file.stat.exists == True
#- name: Create pod
# shell: "kubectl apply -f ."
# args:
# chdir: "{{ k8s_dst_dir }}"
# register: result
# until: result.stdout.find("created") == 2
# retries: 3
# delay: 3