github.com/jlmeeker/kismatic@v1.10.1-0.20180612190640-57f9005a1f1a/pkg/ansible/runner.go (about) 1 package ansible 2 3 import ( 4 "fmt" 5 "io" 6 "io/ioutil" 7 "log" 8 "math/rand" 9 "os" 10 "os/exec" 11 "path/filepath" 12 "strings" 13 "syscall" 14 "time" 15 ) 16 17 const ( 18 // RawFormat is the raw Ansible output formatting 19 RawFormat = OutputFormat("raw") 20 // JSONLinesFormat is a JSON Lines representation of Ansible events 21 JSONLinesFormat = OutputFormat("json_lines") 22 ) 23 24 // OutputFormat is used for controlling the STDOUT format of the Ansible runner 25 type OutputFormat string 26 27 // Runner for running Ansible playbooks 28 type Runner interface { 29 // StartPlaybook runs the playbook asynchronously with the given inventory and extra vars. 30 // It returns a read-only channel that must be consumed for the playbook execution to proceed. 31 StartPlaybook(playbookFile string, inventory Inventory, cc ClusterCatalog) (<-chan Event, error) 32 // WaitPlaybook blocks until the execution of the playbook is complete. If an error occurred, 33 // it is returned. Otherwise, returns nil to signal the completion of the playbook. 34 WaitPlaybook() error 35 // StartPlaybookOnNode runs the playbook asynchronously with the given inventory and extra vars 36 // against the specific node. 37 // It returns a read-only channel that must be consumed for the playbook execution to proceed. 38 StartPlaybookOnNode(playbookFile string, inventory Inventory, cc ClusterCatalog, node ...string) (<-chan Event, error) 39 } 40 41 type runner struct { 42 // Out is the stdout writer for the Ansible process 43 out io.Writer 44 // ErrOut is the stderr writer for the Ansible process 45 errOut io.Writer 46 47 pythonPath string 48 ansibleDir string 49 runDir string 50 waitPlaybook func() error 51 namedPipe string 52 } 53 54 // NewRunner returns a new runner for running Ansible playbooks. 55 func NewRunner(out, errOut io.Writer, ansibleDir string, runDir string) (Runner, error) { 56 // Ansible depends on python 2.7 being installed and on the path as "python". 57 // Validate that it is available 58 if _, err := exec.LookPath("python"); err != nil { 59 return nil, fmt.Errorf("Could not find 'python' in the PATH. Ensure that python 2.7 is installed and in the path as 'python'.") 60 } 61 62 ppath, err := getPythonPath() 63 if err != nil { 64 return nil, err 65 } 66 67 return &runner{ 68 out: out, 69 errOut: errOut, 70 pythonPath: ppath, 71 ansibleDir: ansibleDir, 72 runDir: runDir, 73 }, nil 74 } 75 76 // WaitPlaybook blocks until the ansible process running the playbook exits. 77 // If the process exits with a non-zero status, it will return an error. 78 func (r *runner) WaitPlaybook() error { 79 if r.waitPlaybook == nil { 80 return fmt.Errorf("wait called, but playbook not started") 81 } 82 execErr := r.waitPlaybook() 83 // Process exited, we can clean up named pipe 84 removeErr := os.Remove(r.namedPipe) 85 if removeErr != nil && execErr != nil { 86 return fmt.Errorf("an error occurred running ansible: %v. Removing named pipe at %q failed: %v", execErr, r.namedPipe, removeErr) 87 } 88 if removeErr != nil { 89 return fmt.Errorf("failed to clean up named pipe at %q: %v", r.namedPipe, removeErr) 90 } 91 if execErr != nil { 92 return fmt.Errorf("error running ansible: %v", execErr) 93 } 94 return nil 95 } 96 97 // RunPlaybook with the given inventory and extra vars 98 func (r *runner) StartPlaybook(playbookFile string, inv Inventory, cc ClusterCatalog) (<-chan Event, error) { 99 return r.startPlaybook(playbookFile, inv, cc) // Don't set the --limit arg 100 } 101 102 // StartPlaybookOnNode runs the playbook asynchronously with the given inventory and extra vars 103 // against the specific node. 104 // It returns a read-only channel that must be consumed for the playbook execution to proceed. 105 func (r *runner) StartPlaybookOnNode(playbookFile string, inv Inventory, cc ClusterCatalog, nodes ...string) (<-chan Event, error) { 106 // set the --limit arg to the node we want to target 107 return r.startPlaybook(playbookFile, inv, cc, nodes...) 108 } 109 110 func (r *runner) startPlaybook(playbookFile string, inv Inventory, cc ClusterCatalog, nodes ...string) (<-chan Event, error) { 111 playbook := filepath.Join(r.ansibleDir, "playbooks", playbookFile) 112 if _, err := os.Stat(playbook); os.IsNotExist(err) { 113 return nil, fmt.Errorf("playbook %q does not exist", playbook) 114 } 115 116 yamlBytes, err := cc.ToYAML() 117 if err != nil { 118 return nil, fmt.Errorf("error writing cluster catalog data to yaml: %v", err) 119 } 120 clusterCatalogFile := filepath.Join(r.ansibleDir, "clustercatalog.yaml") 121 if err = ioutil.WriteFile(clusterCatalogFile, yamlBytes, 0644); err != nil { 122 return nil, fmt.Errorf("error writing cluster catalog file to %q: %v", clusterCatalogFile, err) 123 } 124 125 inventoryFile := filepath.Join(r.ansibleDir, "inventory.ini") 126 if err := ioutil.WriteFile(inventoryFile, inv.ToINI(), 0644); err != nil { 127 return nil, fmt.Errorf("error writing inventory file to %q: %v", inventoryFile, err) 128 } 129 130 if err := copyFileContents(clusterCatalogFile, filepath.Join(r.runDir, "clustercatalog.yaml")); err != nil { 131 return nil, fmt.Errorf("error copying clustercatalog.yaml to %q: %v", r.runDir, err) 132 } 133 if err := copyFileContents(inventoryFile, filepath.Join(r.runDir, "inventory.ini")); err != nil { 134 return nil, fmt.Errorf("error copying inventory.ini to %q: %v", r.runDir, err) 135 } 136 137 cmd := exec.Command(filepath.Join(r.ansibleDir, "bin", "ansible-playbook"), "-i", inventoryFile, "-s", playbook, "--extra-vars", "@"+clusterCatalogFile) 138 cmd.Stdout = r.out 139 cmd.Stderr = r.errOut 140 141 log.SetOutput(r.out) 142 143 limitArg := strings.Join(nodes, ",") 144 if limitArg != "" { 145 cmd.Args = append(cmd.Args, "--limit", limitArg) 146 } 147 148 // We always want the most verbose output from Ansible. If it's not going to 149 // stdout, it's going to a log file. 150 cmd.Args = append(cmd.Args, "-vvvv") 151 152 // Create named pipe 153 np, err := createTempNamedPipe() 154 if err != nil { 155 return nil, err 156 } 157 r.namedPipe = np 158 159 os.Setenv("PYTHONPATH", r.pythonPath) 160 os.Setenv("ANSIBLE_CALLBACK_PLUGINS", filepath.Join(r.ansibleDir, "playbooks", "callback")) 161 os.Setenv("ANSIBLE_CALLBACK_WHITELIST", "json_lines") 162 os.Setenv("ANSIBLE_CONFIG", filepath.Join(r.ansibleDir, "playbooks", "ansible.cfg")) 163 os.Setenv("ANSIBLE_JSON_LINES_PIPE", r.namedPipe) 164 165 // Print Ansible command 166 fmt.Fprintf(r.out, "export PYTHONPATH=%v\n", os.Getenv("PYTHONPATH")) 167 fmt.Fprintf(r.out, "export ANSIBLE_CALLBACK_PLUGINS=%v\n", os.Getenv("ANSIBLE_CALLBACK_PLUGINS")) 168 fmt.Fprintf(r.out, "export ANSIBLE_CALLBACK_WHITELIST=%v\n", os.Getenv("ANSIBLE_CALLBACK_WHITELIST")) 169 fmt.Fprintf(r.out, "export ANSIBLE_CONFIG=%v\n", os.Getenv("ANSIBLE_CONFIG")) 170 fmt.Fprintf(r.out, "export ANSIBLE_JSON_LINES_PIPE=%v\n", os.Getenv("ANSIBLE_JSON_LINES_PIPE")) 171 fmt.Fprintln(r.out, strings.Join(cmd.Args, " ")) 172 173 // Starts async execution of ansible, which will block until 174 // we start reading from the named pipe 175 err = cmd.Start() 176 if err != nil { 177 return nil, fmt.Errorf("error running playbook: %v", err) 178 } 179 r.waitPlaybook = cmd.Wait 180 181 // Create the event stream out of the named pipe 182 eventStreamFile, err := os.OpenFile(r.namedPipe, os.O_RDWR, os.ModeNamedPipe) 183 if err != nil { 184 return nil, fmt.Errorf("error openning event stream pipe: %v", err) 185 } 186 eventStream := EventStream(eventStreamFile) 187 return eventStream, nil 188 } 189 190 // create a named pipe for getting json events out of ansible. 191 // add random int to file name to avoid collision. 192 func createTempNamedPipe() (string, error) { 193 start := time.Now() 194 np := filepath.Join(os.TempDir(), fmt.Sprintf("ansible-pipe-%d-%s", rand.Int(), start.Format("2006-01-02-15-04-05.99999"))) 195 if err := syscall.Mkfifo(np, 0644); err != nil { 196 return "", fmt.Errorf("error creating named pipe %q: %v", np, err) 197 } 198 return np, nil 199 } 200 201 func getPythonPath() (string, error) { 202 wd, err := os.Getwd() 203 if err != nil { 204 return "", fmt.Errorf("error getting working dir: %v", err) 205 } 206 lib := filepath.Join(wd, "ansible", "lib", "python2.7", "site-packages") 207 lib64 := filepath.Join(wd, "ansible", "lib64", "python2.7", "site-packages") 208 return fmt.Sprintf("%s:%s", lib, lib64), nil 209 } 210 211 func copyFileContents(src, dst string) (err error) { 212 in, err := os.Open(src) 213 if err != nil { 214 return err 215 } 216 defer in.Close() 217 out, err := os.Create(dst) 218 if err != nil { 219 return err 220 } 221 defer out.Close() 222 if _, err = io.Copy(out, in); err != nil { 223 return err 224 } 225 return out.Sync() 226 }