diff --git a/README.md b/README.md index 6c08436..1120525 100644 --- a/README.md +++ b/README.md @@ -1 +1,54 @@ -# kenaito-ansible-go-example \ No newline at end of file +# go-ansible库应用 + +## thank you for go-ansible authors + +https://github.com/apenella/go-ansible + +## 执行结果参考 + +#### 执行成功 + +```json +{ + "Command": "ansible all --args 'ping 127.0.0.1 -c 2' --inventory 192.168.0.1,192.168.0.2 --module-name command --connection local", + "Success": true, + "Data": { + "192.168.0.1": { + "changed": 1, + "failures": 0, + "ignored": 0, + "ok": 1, + "rescued": 0, + "skipped": 0, + "unreachable": 0 + }, + "192.168.0.2": { + "changed": 1, + "failures": 0, + "ignored": 0, + "ok": 1, + "rescued": 0, + "skipped": 0, + "unreachable": 0 + } + }, + "ErrMsg": "", + "StartTime": "2025-07-03 21:17:26", + "EndTime": "2025-07-03 21:17:28", + "Duration": 1566 +} +``` + +#### 执行失败 + +```json +{ + "Command": "ansible all --args 'pingxxxx 127.0.0.1 -c 2' --inventory 192.168.0.1,192.168.0.2 --module-name command --connection local", + "Success": false, + "Data": null, + "ErrMsg": "Error during command execution.\n Command executed: ansible all --args 'pingxxxx 127.0.0.1 -c 2' --inventory 192.168.0.1,192.168.0.2 --module-name command --connection local\n\n Environment variables:\nANSIBLE_LOAD_CALLBACK_PLUGINS=1\nANSIBLE_STDOUT_CALLBACK=json\n\n\texit status 2", + "StartTime": "2025-07-03 21:20:07", + "EndTime": "2025-07-03 21:20:08", + "Duration": 497 +} +``` \ No newline at end of file diff --git a/core/ansible_adhoc.go b/core/ansible_adhoc.go new file mode 100644 index 0000000..7905bfc --- /dev/null +++ b/core/ansible_adhoc.go @@ -0,0 +1,105 @@ +package core + +import ( + "bytes" + "context" + "fmt" + "github.com/apenella/go-ansible/v2/pkg/adhoc" + "github.com/apenella/go-ansible/v2/pkg/execute" + results "github.com/apenella/go-ansible/v2/pkg/execute/result/json" + "io" + "kenaito-ansible-go-example/model" + "strings" + "time" +) + +// ExecuteCommand +// mode 模式。local、ssh、smart +// inventoryList 主机列表。 +// moduleName 模块名称。 +// args 模块参数。 +// extraVars 变量。 +func ExecuteCommand(mode string, inventoryList []string, moduleName string, args string, extraVars map[string]interface{}) model.CsExecResult { + ansibleAdhocOptions := &adhoc.AnsibleAdhocOptions{ + Connection: mode, + Inventory: strings.Join(inventoryList, ","), + ModuleName: moduleName, + Args: args, + ExtraVars: extraVars, + } + /** + "all" 表示该命令将作用于 Inventory(清单)中的所有主机。 + "webservers"(针对 webservers 组的主机) + "192.168.1.1"(针对特定 IP 的主机) + "db*"(匹配所有以 db 开头的主机名) + */ + adhocCmd := adhoc.NewAnsibleAdhocCmd( + adhoc.WithPattern("all"), + adhoc.WithAdhocOptions(ansibleAdhocOptions), + ) + + result := model.CsExecResult{} + result.Command = adhocCmd.String() + + buff := new(bytes.Buffer) + defaultExecute := execute.NewDefaultExecute( + execute.WithCmd(adhocCmd), + execute.WithEnvVars(map[string]string{ + "ANSIBLE_LOAD_CALLBACK_PLUGINS": "1", + "ANSIBLE_STDOUT_CALLBACK": "json", + }), + execute.WithWrite(buff), + ) + startTime := time.Now() + timeLayout := "2006-01-02 15:04:05" + result.StartTime = startTime.Format(timeLayout) + err := defaultExecute.Execute(context.TODO()) + if err != nil { + fmt.Printf("ansible命令行参数异常: %v\n", err) + result.Success = false + result.ErrMsg = err.Error() + endTime := time.Now() + result.EndTime = endTime.Format(timeLayout) + result.Duration = int(endTime.Sub(startTime).Milliseconds()) + return result + } + + // 解析的结果有问题 + res, err := results.ParseJSONResultsStream(io.Reader(buff)) + if err != nil { + fmt.Printf("执行结果解析异常: %v\n", err) + result.Success = false + result.ErrMsg = err.Error() + endTime := time.Now() + result.EndTime = endTime.Format(timeLayout) + result.Duration = int(endTime.Sub(startTime).Milliseconds()) + return result + } + result.Success = true + result.Data = res.Stats + endTime := time.Now() + result.EndTime = endTime.Format(timeLayout) + result.Duration = int(endTime.Sub(startTime).Milliseconds()) + return result + + // 自行解析,也有问题,降级吧 + //var data map[string]interface{} + //jsonStr, err := util.ReaderToString(io.Reader(buff)) + //fmt.Printf("jsonStr => %s\n\n", jsonStr) + //err = json.Unmarshal([]byte(jsonStr), &data) + //if err != nil { + // fmt.Printf("执行结果解析异常: %v\n", err) + // result.Success = false + // result.ErrMsg = err.Error() + // endTime := time.Now() + // result.EndTime = endTime.Format(timeLayout) + // result.Duration = int(endTime.Sub(startTime).Milliseconds()) + // return result + //} + //result.Success = true + //result.Data = data["stats"] + //endTime := time.Now() + //result.EndTime = endTime.Format(timeLayout) + //result.Duration = int(endTime.Sub(startTime).Milliseconds()) + //return result +} diff --git a/core/ansible_playbook.go b/core/ansible_playbook.go new file mode 100644 index 0000000..d37f814 --- /dev/null +++ b/core/ansible_playbook.go @@ -0,0 +1,85 @@ +package core + +import ( + "bytes" + "context" + "fmt" + "github.com/apenella/go-ansible/v2/pkg/execute" + "github.com/apenella/go-ansible/v2/pkg/execute/measure" + results "github.com/apenella/go-ansible/v2/pkg/execute/result/json" + "github.com/apenella/go-ansible/v2/pkg/execute/stdoutcallback" + "github.com/apenella/go-ansible/v2/pkg/playbook" + "io" + "kenaito-ansible-go-example/model" + "kenaito-ansible-go-example/util" + "strings" + "time" +) + +// ExecutePlaybook +// mode 模式。local、ssh、smart +// inventoryList 主机列表。 +// playbookList playbook脚本列表。 +// extraVars 变量。 +func ExecutePlaybook(mode string, inventoryList []string, playbookList []string, extraVars map[string]interface{}) model.CsExecResult { + ansiblePlaybookOptions := &playbook.AnsiblePlaybookOptions{ + Connection: mode, + Inventory: strings.Join(inventoryList, ","), + ExtraVars: extraVars, + } + /** + "all" 表示该命令将作用于 Inventory(清单)中的所有主机。 + "webservers"(针对 webservers 组的主机) + "192.168.1.1"(针对特定 IP 的主机) + "db*"(匹配所有以 db 开头的主机名) + */ + playbookCmd := playbook.NewAnsiblePlaybookCmd( + playbook.WithPlaybooks(playbookList...), + playbook.WithPlaybookOptions(ansiblePlaybookOptions), + ) + + result := model.CsExecResult{} + result.Command = playbookCmd.String() + + buff := new(bytes.Buffer) + exec := measure.NewExecutorTimeMeasurement( + stdoutcallback.NewJSONStdoutCallbackExecute( + execute.NewDefaultExecute( + execute.WithCmd(playbookCmd), + execute.WithErrorEnrich(playbook.NewAnsiblePlaybookErrorEnrich()), + execute.WithWrite(io.Writer(buff)), + ), + ), + ) + + startTime := time.Now() + result.StartTime = util.FormatDateTime(startTime) + err := exec.Execute(context.TODO()) + if err != nil { + fmt.Printf("ansible-playbook命令行参数异常: %v\n", err) + result.Success = false + result.ErrMsg = err.Error() + endTime := time.Now() + result.EndTime = util.FormatDateTime(endTime) + result.Duration = int(endTime.Sub(startTime).Milliseconds()) + return result + } + + res, err := results.ParseJSONResultsStream(io.Reader(buff)) + if err != nil { + fmt.Printf("执行结果解析异常: %v\n", err) + result.Success = false + result.ErrMsg = err.Error() + endTime := time.Now() + result.EndTime = util.FormatDateTime(endTime) + result.Duration = int(endTime.Sub(startTime).Milliseconds()) + return result + } + + result.Success = true + result.Data = res.Stats + endTime := time.Now() + result.EndTime = util.FormatDateTime(endTime) + result.Duration = int(endTime.Sub(startTime).Milliseconds()) + return result +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e8e77a3 --- /dev/null +++ b/go.mod @@ -0,0 +1,17 @@ +module kenaito-ansible-go-example + +go 1.23.3 + +require github.com/apenella/go-ansible/v2 v2.2.0 + +require ( + github.com/apenella/go-common-utils/data v0.0.0-20220913191136-86daaa87e7df // indirect + github.com/apenella/go-common-utils/error v0.0.0-20220913191136-86daaa87e7df // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect + github.com/stretchr/testify v1.9.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..1d452f6 --- /dev/null +++ b/go.sum @@ -0,0 +1,22 @@ +github.com/apenella/go-ansible/v2 v2.2.0 h1:IldFymiRy9hc+rCyZ0dj4jJfLUmW9wjzU4JxoOMy9KE= +github.com/apenella/go-ansible/v2 v2.2.0/go.mod h1:G3JNiTazO5t/PEriJFywV/4RFu2RtzAxxR7Z5SoYF78= +github.com/apenella/go-common-utils/data v0.0.0-20220913191136-86daaa87e7df h1:sEikY2P+NZK/7VZUwIsnXIGElhsuFDSxh1bZYwHxdcI= +github.com/apenella/go-common-utils/data v0.0.0-20220913191136-86daaa87e7df/go.mod h1:cLVL6GjUiKG/WyBzX+KD6h/XRV/HnNZIZbMNNiBgQ9o= +github.com/apenella/go-common-utils/error v0.0.0-20220913191136-86daaa87e7df h1:SvlYbjlsSQDS7hbVT1h012/zdgvcwWJ+Yd9XRiiY/8s= +github.com/apenella/go-common-utils/error v0.0.0-20220913191136-86daaa87e7df/go.mod h1:+3dyIlHX350xJIUIffwMLswZXU+N2FwDE05VuKqxYdw= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..d68985d --- /dev/null +++ b/main.go @@ -0,0 +1,41 @@ +package main + +import ( + "encoding/json" + "fmt" + "kenaito-ansible-go-example/core" +) + +func main() { + testExecuteCommand() + //testExecutePlaybook() +} +func testExecuteCommand() { + command := core.ExecuteCommand( + "local", + []string{"192.168.150.170", "192.168.205.234"}, + "command", + "pingxxxx 127.0.0.1 -c 2", + nil, + ) + marshal, err := json.Marshal(command) + if err != nil { + fmt.Println("Error marshalling to JSON:", err) + return + } + fmt.Printf("Execute Result: %s\n", string(marshal)) +} +func testExecutePlaybook() { + command := core.ExecutePlaybook( + "local", + []string{"192.168.150.170", "192.168.205.234"}, + []string{"site1.yml", "site2.yml", "site3.yml"}, + nil, + ) + marshal, err := json.Marshal(command) + if err != nil { + fmt.Println("Error marshalling to JSON:", err) + return + } + fmt.Printf("Execute Playbook Result: %s\n", string(marshal)) +} diff --git a/model/cs_exec_result.go b/model/cs_exec_result.go new file mode 100644 index 0000000..7655174 --- /dev/null +++ b/model/cs_exec_result.go @@ -0,0 +1,24 @@ +package model + +type CsExecResult struct { + // 执行的ansible命令 + Command string + // 执行成功与否 + Success bool + // 执行结果 + //ok 表示任务执行成功,没有做任何修改; + //changed 表示任务执行成功,并且做了一些修改; + //failed 表示任务执行失败; + //unreachable 表示任务无法连接到目标主机。 + Data interface{} + // 异常信息 + ErrMsg string + // 开始时间 + //StartTime time.Time + StartTime string + // 结束时间 + //EndTime time.Time + EndTime string + // 执行耗时 + Duration int +} diff --git a/site1.yml b/site1.yml new file mode 100644 index 0000000..16f29a0 --- /dev/null +++ b/site1.yml @@ -0,0 +1,29 @@ +--- + +- hosts: all + gather_facts: false + vars: + array: + - Your are running + - 'ansibleplaybook-json-stdout' + - first example + map: + intro: Your are running + what: ansibleplaybook-json-stdout + outro: first example + + tasks: + - name: ansibleplaybook-json-stdout + debug: + msg: | + Your are running + 'ansibleplaybook-json-stdout' + first example + + - name: ansibleplaybook-json-stdout from array + debug: + msg: "{{ array }}" + + - name: ansibleplaybook-json-stdout from map + debug: + msg: "{{ map }}" diff --git a/site2.yml b/site2.yml new file mode 100644 index 0000000..5f6fd9b --- /dev/null +++ b/site2.yml @@ -0,0 +1,29 @@ +--- + +- hosts: all + gather_facts: false + vars: + array: + - Your are running + - 'ansibleplaybook-json-stdout' + - second example + map: + intro: Your are running + what: ansibleplaybook-json-stdout + outro: second example + + tasks: + - name: ansibleplaybook-json-stdout + debug: + msg: | + Your are running + 'ansibleplaybook-json-stdout' + second example + + - name: ansibleplaybook-json-stdout from array + debug: + msg: "{{ array }}" + + - name: ansibleplaybook-json-stdout from map + debug: + msg: "{{ map }}" diff --git a/site3.yml b/site3.yml new file mode 100644 index 0000000..e5c377f --- /dev/null +++ b/site3.yml @@ -0,0 +1,29 @@ +--- + +- hosts: all + gather_facts: false + vars: + array: + - Your are running + - 'ansibleplaybook-json-stdout' + - third example + map: + intro: Your are running + what: ansibleplaybook-json-stdout + outro: third example + + tasks: + - name: ansibleplaybook-json-stdout + debug: + msg: | + Your are running + 'ansibleplaybook-json-stdout' + third example + + - name: ansibleplaybook-json-stdout from array + debug: + msg: "{{ array }}" + + - name: ansibleplaybook-json-stdout from map + debug: + msg: "{{ map }}" diff --git a/util/bytes.go b/util/bytes.go new file mode 100644 index 0000000..30d2ff9 --- /dev/null +++ b/util/bytes.go @@ -0,0 +1,14 @@ +package util + +import ( + "io" + "io/ioutil" +) + +func ReaderToString(reader io.Reader) (string, error) { + data, err := ioutil.ReadAll(reader) + if err != nil { + return "", err + } + return string(data), nil +} diff --git a/util/date.go b/util/date.go new file mode 100644 index 0000000..ce2a0d8 --- /dev/null +++ b/util/date.go @@ -0,0 +1,9 @@ +package util + +import "time" + +const layoutDateTime = "2006-01-02 15:04:05" + +func FormatDateTime(time time.Time) string { + return time.Format(layoutDateTime) +}