playbook_session.go 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. // Copyright 2019 Yunion
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. package ansiblev2
  15. import (
  16. "context"
  17. "fmt"
  18. "io"
  19. "os"
  20. "os/exec"
  21. "path"
  22. "path/filepath"
  23. "strings"
  24. "sync"
  25. "github.com/go-yaml/yaml"
  26. "yunion.io/x/pkg/errors"
  27. yerrors "yunion.io/x/pkg/util/errors"
  28. )
  29. type IPlaybookSession interface {
  30. GetPrivateKey() string
  31. GetPlaybook() string
  32. GetPlaybookPath() string
  33. GetInventory() string
  34. IsKeepTmpdir() bool
  35. GetConfigs() map[string]interface{}
  36. GetRequirements() string
  37. GetFiles() map[string][]byte
  38. GetOutputWriter() io.Writer
  39. GetRolePublic() bool
  40. GetTimeout() int
  41. CheckAndSetRunning() bool
  42. SetStopped()
  43. GetConfigYaml() string
  44. }
  45. type runnable struct {
  46. IPlaybookSession
  47. }
  48. type multiWriter struct {
  49. data []byte
  50. writer io.Writer
  51. }
  52. func (w *multiWriter) Write(p []byte) (n int, err error) {
  53. w.data = append(w.data, p...)
  54. return w.writer.Write(p)
  55. }
  56. func (w multiWriter) String() string {
  57. info := strings.Split(string(w.data), "\n")
  58. data := []string{}
  59. for _, v := range info {
  60. if strings.Contains(v, "FAILED! =>") || strings.HasPrefix(v, "fatal:") {
  61. data = append(data, v)
  62. }
  63. }
  64. if len(data) > 0 {
  65. return strings.Join(data, "\n")
  66. }
  67. return string(w.data)
  68. }
  69. func (r runnable) Run(ctx context.Context) (err error) {
  70. var (
  71. tmpdir string
  72. )
  73. has := r.CheckAndSetRunning()
  74. if has {
  75. return errors.Errorf("playbook is already running")
  76. }
  77. defer r.SetStopped()
  78. // make tmpdir
  79. tmpdir, err = os.MkdirTemp("", "onecloud-ansiblev2")
  80. if err != nil {
  81. err = errors.Wrap(err, "making tmp dir")
  82. return
  83. }
  84. defer func() {
  85. if r.IsKeepTmpdir() {
  86. return
  87. }
  88. if err1 := os.RemoveAll(tmpdir); err1 != nil {
  89. err = errors.Wrapf(err1, "removing %q", tmpdir)
  90. }
  91. }()
  92. // write out inventory
  93. inventory := filepath.Join(tmpdir, "inventory")
  94. err = os.WriteFile(inventory, []byte(r.GetInventory()), os.FileMode(0600))
  95. if err != nil {
  96. err = errors.Wrapf(err, "writing inventory %s", inventory)
  97. return
  98. }
  99. // write out playbook
  100. playbook := r.GetPlaybookPath()
  101. if len(playbook) == 0 {
  102. playbook = filepath.Join(tmpdir, "playbook")
  103. err = os.WriteFile(playbook, []byte(r.GetPlaybook()), os.FileMode(0600))
  104. if err != nil {
  105. err = errors.Wrapf(err, "writing playbook %s", playbook)
  106. return
  107. }
  108. }
  109. // write out private key
  110. var privateKey string
  111. if len(r.GetPrivateKey()) > 0 {
  112. privateKey = filepath.Join(tmpdir, "private_key")
  113. err = os.WriteFile(privateKey, []byte(r.GetPrivateKey()), os.FileMode(0600))
  114. if err != nil {
  115. err = errors.Wrapf(err, "writing private key %s", privateKey)
  116. return
  117. }
  118. }
  119. // write out requirements
  120. var requirements string
  121. if len(r.GetRequirements()) > 0 {
  122. requirements = filepath.Join(tmpdir, "requirements.yml")
  123. err = os.WriteFile(requirements, []byte(r.GetRequirements()), os.FileMode(0600))
  124. if err != nil {
  125. err = errors.Wrapf(err, "writing requirements %s", requirements)
  126. return
  127. }
  128. }
  129. // write out files
  130. for name, content := range r.GetFiles() {
  131. path := filepath.Join(tmpdir, name)
  132. dir := filepath.Dir(path)
  133. err = os.MkdirAll(dir, os.FileMode(0700))
  134. if err != nil {
  135. err = errors.Wrapf(err, "mkdir -p %s", dir)
  136. return
  137. }
  138. err = os.WriteFile(path, content, os.FileMode(0600))
  139. if err != nil {
  140. err = errors.Wrapf(err, "writing file %s", name)
  141. return
  142. }
  143. }
  144. // write out configs
  145. var config string
  146. if r.GetConfigs() != nil {
  147. yml, err := yaml.Marshal(r.GetConfigs())
  148. if err != nil {
  149. return errors.Wrap(err, "unable to marshal map to yaml")
  150. }
  151. config = filepath.Join(tmpdir, "config")
  152. err = os.WriteFile(config, yml, os.FileMode(0600))
  153. if err != nil {
  154. return errors.Wrapf(err, "unable to write config to file %s", config)
  155. }
  156. } else if r.GetConfigYaml() != "" {
  157. yml := r.GetConfigYaml()
  158. config = filepath.Join(tmpdir, "config")
  159. err = os.WriteFile(config, []byte(yml), os.FileMode(0600))
  160. if err != nil {
  161. return errors.Wrapf(err, "unable to write config to file %s", config)
  162. }
  163. }
  164. // run modules one by one
  165. var errs []error
  166. defer func() {
  167. if len(errs) > 0 {
  168. err = yerrors.NewAggregate(errs)
  169. }
  170. }()
  171. // install required roles
  172. if len(requirements) > 0 {
  173. args := []string{
  174. "install", "-r", requirements,
  175. }
  176. if !r.GetRolePublic() {
  177. args = append(args, "-p", tmpdir)
  178. }
  179. cmd := exec.CommandContext(ctx, "ansible-galaxy", args...)
  180. stdout, _ := cmd.StdoutPipe()
  181. stderr, _ := cmd.StderrPipe()
  182. if err1 := cmd.Start(); err1 != nil {
  183. errs = append(errs, errors.Wrap(err1, "start ansible-galaxy install roles"))
  184. return
  185. }
  186. // Mix stdout, stderr
  187. if writer := r.GetOutputWriter(); writer != nil {
  188. go io.Copy(writer, stdout)
  189. go io.Copy(writer, stderr)
  190. }
  191. if err1 := cmd.Wait(); err1 != nil {
  192. errs = append(errs, errors.Wrap(err1, "wait ansible-galaxy install roles"))
  193. }
  194. }
  195. // run playbook
  196. {
  197. args := []string{
  198. "--inventory", inventory, "--timeout", fmt.Sprintf("%d", r.GetTimeout()),
  199. }
  200. if config != "" {
  201. args = append(args, "-e", "@"+config)
  202. }
  203. if privateKey != "" {
  204. args = append(args, "--private-key", privateKey)
  205. }
  206. args = append(args, playbook)
  207. cmd := exec.CommandContext(ctx, "ansible-playbook", args...)
  208. cmd.Dir = tmpdir
  209. cmd.Env = os.Environ()
  210. cmd.Env = append(cmd.Env, "ANSIBLE_HOST_KEY_CHECKING=False")
  211. // for debug
  212. os.WriteFile(path.Join(tmpdir, "run_cmd"), []byte(cmd.String()), os.ModePerm)
  213. stdout, _ := cmd.StdoutPipe()
  214. stderr, _ := cmd.StderrPipe()
  215. if err1 := cmd.Start(); err1 != nil {
  216. errs = append(errs, errors.Wrapf(err1, "start playbook %s", playbook))
  217. return
  218. }
  219. // Mix stdout, stderr
  220. writer := r.GetOutputWriter()
  221. w := &multiWriter{
  222. data: []byte{},
  223. writer: writer,
  224. }
  225. if writer != nil {
  226. go io.Copy(w, stdout)
  227. go io.Copy(w, stderr)
  228. }
  229. if err1 := cmd.Wait(); err1 != nil {
  230. errs = append(errs, errors.Wrapf(err1, "wait playbook %s %s", playbook, w.String()))
  231. }
  232. }
  233. return nil
  234. }
  235. type PlaybookSessionBase struct {
  236. privateKey string
  237. inventory string
  238. outputWriter io.Writer
  239. stateMux *sync.Mutex
  240. isRunning bool
  241. keepTmpdir bool
  242. rolePublic bool
  243. timeout int
  244. }
  245. func NewPlaybookSessionBase() PlaybookSessionBase {
  246. return PlaybookSessionBase{
  247. stateMux: &sync.Mutex{},
  248. timeout: 10,
  249. }
  250. }
  251. func (pb *PlaybookSessionBase) GetPrivateKey() string {
  252. return pb.privateKey
  253. }
  254. func (pb *PlaybookSessionBase) IsKeepTmpdir() bool {
  255. return pb.keepTmpdir
  256. }
  257. func (pb *PlaybookSessionBase) GetOutputWriter() io.Writer {
  258. return pb.outputWriter
  259. }
  260. func (pb *PlaybookSessionBase) CheckAndSetRunning() bool {
  261. pb.stateMux.Lock()
  262. if pb.isRunning {
  263. return true
  264. }
  265. pb.isRunning = true
  266. pb.stateMux.Unlock()
  267. return false
  268. }
  269. func (pb *PlaybookSessionBase) SetStopped() {
  270. pb.stateMux.Lock()
  271. pb.isRunning = false
  272. pb.stateMux.Unlock()
  273. }
  274. func (pb *PlaybookSessionBase) GetPlaybook() string {
  275. return ""
  276. }
  277. func (pb *PlaybookSessionBase) GetPlaybookPath() string {
  278. return ""
  279. }
  280. func (pb *PlaybookSessionBase) GetInventory() string {
  281. return pb.inventory
  282. }
  283. func (pb *PlaybookSessionBase) GetConfigs() map[string]interface{} {
  284. return nil
  285. }
  286. func (pb *PlaybookSessionBase) GetConfigYaml() string {
  287. return ""
  288. }
  289. func (pb *PlaybookSessionBase) GetRequirements() string {
  290. return ""
  291. }
  292. func (pb *PlaybookSessionBase) GetFiles() map[string][]byte {
  293. return nil
  294. }
  295. func (pb *PlaybookSessionBase) GetRolePublic() bool {
  296. return pb.rolePublic
  297. }
  298. func (pb *PlaybookSessionBase) GetTimeout() int {
  299. return pb.timeout
  300. }