playbook.go 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  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 ansible
  15. import (
  16. "context"
  17. "io"
  18. "io/ioutil"
  19. "os"
  20. "os/exec"
  21. "path/filepath"
  22. "strings"
  23. "sync"
  24. "github.com/pkg/errors"
  25. "yunion.io/x/pkg/gotypes"
  26. yerrors "yunion.io/x/pkg/util/errors"
  27. )
  28. type pbState int
  29. const (
  30. pbStateInit pbState = iota
  31. pbStateRunning
  32. pbStateStopped
  33. )
  34. func (pbs pbState) String() string {
  35. switch pbs {
  36. case pbStateInit:
  37. return "init"
  38. case pbStateRunning:
  39. return "running"
  40. case pbStateStopped:
  41. return "stopped"
  42. }
  43. return "unknown"
  44. }
  45. type Playbook struct {
  46. Inventory Inventory
  47. Modules []Module
  48. PrivateKey []byte
  49. Files map[string][]byte
  50. tmpdir string
  51. noCleanOnExit bool
  52. outputWriter io.Writer
  53. state pbState
  54. stateMux *sync.Mutex
  55. }
  56. func NewPlaybook() *Playbook {
  57. pb := &Playbook{
  58. state: pbStateInit,
  59. stateMux: &sync.Mutex{},
  60. }
  61. return pb
  62. }
  63. func (pb *Playbook) Copy() *Playbook {
  64. pb1 := NewPlaybook()
  65. pb1.Inventory = gotypes.DeepCopy(pb.Inventory).(Inventory)
  66. pb1.Modules = gotypes.DeepCopy(pb.Modules).([]Module)
  67. pb1.PrivateKey = gotypes.DeepCopy(pb.PrivateKey).([]byte)
  68. pb1.Files = gotypes.DeepCopy(pb.Files).(map[string][]byte)
  69. return pb1
  70. }
  71. // CleanOnExit decide whether temporary workdir will be cleaned up after Run
  72. func (pb *Playbook) CleanOnExit(b bool) {
  73. pb.noCleanOnExit = !b
  74. }
  75. // State returns current state of the playbook
  76. func (pb *Playbook) State() pbState {
  77. pb.stateMux.Lock()
  78. defer pb.stateMux.Unlock()
  79. return pb.state
  80. }
  81. // Runnable returns whether the playbook is in a state feasible to be run
  82. func (pb *Playbook) Runnable() bool {
  83. return pb.state == pbStateInit
  84. }
  85. // Running returns whether the playbook is currently running
  86. func (pb *Playbook) Running() bool {
  87. return pb.state == pbStateRunning
  88. }
  89. // Run runs the playbook
  90. func (pb *Playbook) Run(ctx context.Context) (err error) {
  91. var (
  92. tmpdir string
  93. )
  94. pb.stateMux.Lock()
  95. if pb.state != pbStateInit {
  96. return errors.Errorf("playbook state %s, want %s",
  97. pb.state, pbStateInit)
  98. }
  99. pb.state = pbStateRunning
  100. pb.stateMux.Unlock()
  101. defer func() {
  102. pb.stateMux.Lock()
  103. pb.state = pbStateStopped
  104. pb.stateMux.Unlock()
  105. }()
  106. if pb.Inventory.IsEmpty() {
  107. return errors.New("empty inventory")
  108. }
  109. // make tmpdir
  110. tmpdir, err = ioutil.TempDir("", "onecloud-ansible")
  111. if err != nil {
  112. err = errors.WithMessage(err, "making tmp dir")
  113. return
  114. }
  115. pb.tmpdir = tmpdir
  116. defer func() {
  117. if pb.noCleanOnExit {
  118. return
  119. }
  120. if err1 := os.RemoveAll(tmpdir); err1 != nil {
  121. err = errors.WithMessagef(err1, "removing %q", tmpdir)
  122. }
  123. }()
  124. // write out inventory
  125. inventory := filepath.Join(tmpdir, "inventory")
  126. err = ioutil.WriteFile(inventory, pb.Inventory.Data(), os.FileMode(0600))
  127. if err != nil {
  128. err = errors.WithMessagef(err, "writing inventory %s", inventory)
  129. return
  130. }
  131. // write out private key
  132. var privateKey string
  133. if len(pb.PrivateKey) > 0 {
  134. privateKey = filepath.Join(tmpdir, "private_key")
  135. err = ioutil.WriteFile(privateKey, pb.PrivateKey, os.FileMode(0600))
  136. if err != nil {
  137. err = errors.WithMessagef(err, "writing private key %s", privateKey)
  138. return
  139. }
  140. }
  141. // write out files
  142. for name, content := range pb.Files {
  143. path := filepath.Join(tmpdir, name)
  144. dir := filepath.Dir(path)
  145. err = os.MkdirAll(dir, os.FileMode(0700))
  146. if err != nil {
  147. err = errors.WithMessagef(err, "mkdir -p %s", dir)
  148. return
  149. }
  150. err = ioutil.WriteFile(path, content, os.FileMode(0600))
  151. if err != nil {
  152. err = errors.WithMessagef(err, "writing file %s", name)
  153. return
  154. }
  155. }
  156. // run modules one by one
  157. var errs []error
  158. defer func() {
  159. if len(errs) > 0 {
  160. err = yerrors.NewAggregate(errs)
  161. }
  162. }()
  163. for _, m := range pb.Modules {
  164. select {
  165. case <-ctx.Done():
  166. err = ctx.Err()
  167. return
  168. default:
  169. }
  170. modArgs := strings.Join(m.Args, " ")
  171. args := []string{
  172. "--inventory", inventory,
  173. "--module-name", m.Name,
  174. "--args", modArgs,
  175. "all",
  176. }
  177. if privateKey != "" {
  178. args = append(args, "--private-key", privateKey)
  179. }
  180. cmd := exec.CommandContext(ctx, "ansible", args...)
  181. cmd.Dir = pb.tmpdir
  182. cmd.Env = os.Environ()
  183. cmd.Env = append(cmd.Env, "ANSIBLE_HOST_KEY_CHECKING=False")
  184. stdout, _ := cmd.StdoutPipe()
  185. stderr, _ := cmd.StderrPipe()
  186. if err1 := cmd.Start(); err1 != nil {
  187. errs = append(errs, errors.WithMessagef(err1, "run module %q, args %q", m.Name, modArgs))
  188. return
  189. }
  190. // Mix stdout, stderr
  191. if pb.outputWriter != nil {
  192. go io.Copy(pb.outputWriter, stdout)
  193. go io.Copy(pb.outputWriter, stderr)
  194. }
  195. if err1 := cmd.Wait(); err1 != nil {
  196. errs = append(errs, errors.WithMessagef(err1, "wait module %q, args %q", m.Name, modArgs))
  197. // continue to next
  198. }
  199. }
  200. return nil
  201. }
  202. func (pb *Playbook) OutputWriter(w io.Writer) {
  203. pb.outputWriter = w
  204. }