介绍
在用 jenkins 实现远程发布的时候,除了通过 ansible 外,还可以通过自定义接口的方式来实现文件传输与版本发布,本例子演示通过 golang 自定义接口来实现文件发布。
实现原理
golang 服务端定义两个接口/upload、/fparam,其中/upload接口主要用于将本地文件发送到远程机器上,/fparam 接口用于调用远程机器上的命令和脚本来实现发布,详细步骤如下:
用户端调用服务端的/upload接口将要发布的文件传送到服务端(远程机器) 服务端在接收到文件后,储存起来并计算文件的md5值,计算后将此md5的值返回给用户端,用户端通过返回的md5值和自己发送的文件作对比,如果一样,说明文件发送过程中无损失 用户端再次调用/fpram接口,将刚才发送的文件名再次发送给服务端,服务端接收到文件名后,根据名字去调用函数,函数中通过文件名已经路径并使用命令将此文件发送到要发布的机器上,发送方式有ansible和scp两种方式(纯内网发送一般不会存在文件不完整),发送后再次调用脚本执行备份和发布操作
package mainimport ("crypto/md5""encoding/hex""errors""fmt""github.com/gin-gonic/gin""io""log""os""os/exec")// 获取文件的md5值func md5Value(filename string) string {f, err := os.Open(filename)if err != nil {log.Fatal(err)}defer f.Close()hash := md5.New()if _, err := io.Copy(hash, f); err != nil {log.Fatal(err)}hashBytes := hash.Sum(nil)hashString := hex.EncodeToString(hashBytes)return hashString}// 通过scp和ssh方式func execAnsible(hostname, srcpath, destpath string) error {scpSlice := []string{fmt.Sprintf("%s", srcpath), fmt.Sprintf("%s:%s", hostname, destpath)}backSlice := []string{"-t", fmt.Sprintf("%s", hostname), "/data/script/backup.sh"}deploySlice := []string{"-t", fmt.Sprintf("%s", hostname), "/data/script/deploy.sh"}mapp := make(map[int][]string)mapp[0] = scpSlicemapp[1] = backSlicemapp[2] = deploySlicefor k, v := range mapp {if k == 0 {cmd := exec.Command("scp", v...)_, err := cmd.CombinedOutput()if err != nil {return err}} else {cmd := exec.Command("ssh", v...)_, err := cmd.CombinedOutput()if err != nil {return err}}}return nil}// 通过ansible方式,跟上面的通过scp和ssh方式一样,根据情况选择//// func execAnsible(hostname, srcpath, destpath string) error {// cmdSlice := [][]string{// {fmt.Sprintf("%s", hostname), "-m", "copy", "-a", fmt.Sprintf("src=%s dest=%s", srcpath, destpath)},// {fmt.Sprintf("%s", hostname), "-m", "shell", "-a", "bash data/script/backup.sh"},// {fmt.Sprintf("%s", hostname), "-m", "shell", "-a", "bash data/script/deploy.sh"},// }// for _, v := range cmdSlice {// //v是一个一维切片,...可以将切片中元素拆出来作为参数传递给Command,Command至少需要两个参数// cmd := exec.Command("ansible", v...)// _, err := cmd.CombinedOutput()// if err != nil {// fmt.Println("errrs:", err)// return err// }// // continue// }// return nil// }func checkFilename(filename string) error {if filename == "api-order.jar" {//api-order.jar如何部署在了多个节点上,那么就循环节点信息,将文件发送到多个节点,app0和app2表示节点主机名orderSlice := []string{"app0", "app2"}for _, node := range orderSlice {err := execAnsible(node, "/data/deploy/api-order.jar", "/data/deploy")if err != nil {return err}}return nil} else if filename == "a.jar" {err := execAnsible("192.168.49.186", "/tmp/a.jar", "/data/linshi")if err != nil {return err}return nil} else {return errors.New("不存在这个文件!!!")}}func main() {router := gin.Default()router.POST("/upload", func(c *gin.Context) {_, file, err := c.Request.FormFile("filename")if err != nil {log.Fatal("err:", err)}err = c.SaveUploadedFile(file, "/data/deploy/"+file.Filename)if err != nil {log.Fatal(err)}//获取md5值后返回给客户端做对比md5v := md5Value(fmt.Sprintf("/data/deploy/%s", file.Filename))c.JSON(200, gin.H{"code": 200,"data": md5v,})})//客户端传递文件名过来,根据文件名在服务端做文件copy和备份发布操作router.POST("/fparam", func(c *gin.Context) {filename := c.PostForm("fname")err := checkFilename(filename)if err != nil {c.JSON(200, gin.H{"code": 100, "data": err})} else {c.JSON(200, gin.H{"code": 200, "data": filename,})}})router.Run(":8888")}
注意:checkFilename函数中,根据传入的文件名不同,来判断发送到不同的主机上,可根据需要添加多个
用户端脚本deployagent.go内容如下:
package mainimport ("bytes""crypto/md5""encoding/hex""encoding/json""fmt""io""io/ioutil""log""mime/multipart""net/http""os""path/filepath"// "time")type jtos struct {Code int `json: code` 结构体标签加和不加,影响不大Data string `json: data`}// 将json反序列化到结构体中,为了获取json中的data内容,也就是远程文件的md5值func jsonToStruct(j string) string {jts := jtos{}err := json.Unmarshal([]byte(j), &jts)if err != nil {log.Fatal(err)}return jts.Data}func md5Value(filename string) string {f, err := os.Open(filename)if err != nil {log.Fatal(err)}defer f.Close()hash := md5.New()if _, err := io.Copy(hash, f); err != nil {log.Fatal(err)}hashBytes := hash.Sum(nil)hashString := hex.EncodeToString(hashBytes)return hashString}func dproject(filename string) {client := &http.Client{} //创建一个http的client实例,用于发送http请求bf := &bytes.Buffer{} //创建Buffer实例,用于在内存中存储发送的nultipart数据writer := multipart.NewWriter(bf) //创建writer实例,写入到buffer中,writer后续用于构建multipart/form-data请求体req, err := http.NewRequest("POST", "http://127.0.0.1:8888/upload", nil)if err != nil {log.Fatal(err)}//设置请求头,writer.FormDataContentType方法返回一个Content-Type,格式为multipart/form-data;boundary=<boundary>,<boundary> 是一个随机生成的唯一字符串,用于分隔请求体中的不同部分req.Header.Set("Content-Type", writer.FormDataContentType())md5value := md5Value(filename)file, err := os.Open(filename)if err != nil {log.Fatal(err)}defer file.Close()//创建表单文件字段,字段名为filename,filepath.Base可以截取路径最后的文件名part, err := writer.CreateFormFile("filename", filepath.Base(filename))if err != nil {log.Fatal(err)}//将文件数据流复制到表单字段part中_, err = io.Copy(part, file)if err != nil {log.Fatal(err)}err = writer.Close()if err != nil {log.Fatal(err)}//writer.WriteField("fieldName","fieldValue") //添加其他字段req.Body = io.NopCloser(bf)resp, err := client.Do(req)if err != nil {log.Fatal("cuowu:", err)}defer resp.Body.Close()bodys, err := ioutil.ReadAll(resp.Body)if err != nil {log.Fatal(err)}r := jsonToStruct(string(bodys))//发送之前的文件和发送到远程机器文件的md5值相同,说明文件传输正常if md5value == r {data := fmt.Sprintf("fname=%s",filepath.Base(filename))requestBody := bytes.NewBuffer([]byte(data))reqq,err := http.NewRequest("POST","http://127.0.0.1:8888/fparam",requestBody)if err != nil {log.Fatal(err)}reqq.Header.Set("Content-Type","application/x-www-form-urlencoded")resps,err := client.Do(reqq)if err != nil {log.Fatal(err)}defer reqq.Body.Close()bodyss,err := ioutil.ReadAll(resps.Body)if err != nil {log.Fatal(err)}fmt.Println(string(bodyss))}}func main() {s := os.Args[1:]for _, v := range s {dproject(v)}}
上面的接口地址,根据需要替换为实际ip或者域名即可
用法
deployserver.go编译为二进制后在服务端启动,如下:
go build deployserver.gonohup ./deployserver &
deployagent.go编译为二进制后启动(可与构建后的包在一台机器或者自定义机器),如下:
go build deployagent.go./deployagent b.jar //后面要跟上要发送到远程机器的包
文章转载自运维DevOps,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。




