func (a *Ansible) Execute(plan *testers.Plan, parser chan<- parsers.Input) error {
	for round, tasks := range plan.Commands {
		a.logger.Infof("running commands round %d of %d", round+1, len(plan.Commands))
		for i, task := range tasks {
			if task.Sleep != 0 {
				a.logger.Infof("waiting %s to pass before continuing next round", task.Sleep.String())
				time.Sleep(task.Sleep)
				continue
			}
			a.logger.Infof("running task round %d of %d", i+1, len(tasks))
			if err := a.runTasks(round, task, plan.TestStartTime, plan.Tester, util.GetTaskName(plan.Tester, plan.TestStartTime), parser); err != nil {
				if !*plan.RunOptions.ContinueOnError {
					return err
				}
				a.logger.Warnf("continuing after err. %+v", err)
			}
		}
	}
	return nil
}
Execute run the given commands and return the logs of it and / or error
package ansible
import (
	"bufio"
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"os/exec"
	"regexp"
	"strings"
	"sync"
	"syscall"
	"time"
	"github.com/cloudical-io/ancientt/parsers"
	"github.com/cloudical-io/ancientt/pkg/ansible"
	"github.com/cloudical-io/ancientt/pkg/cmdtemplate"
	"github.com/cloudical-io/ancientt/pkg/config"
	"github.com/cloudical-io/ancientt/pkg/executor"
	"github.com/cloudical-io/ancientt/pkg/util"
	"github.com/cloudical-io/ancientt/runners"
	"github.com/cloudical-io/ancientt/testers"
	"github.com/sirupsen/logrus"
	log "github.com/sirupsen/logrus"
)
const (
	Name = "ansible"
)
var (
	jsonHeadCleanRegex = regexp.MustCompile(`(?sm)^.*(=> \{| >>$\n\{)`)
	jsonTailCleanRegex = regexp.MustCompile(`(?sm)(^\}.*)`)
)
func init() {
	runners.Factories[Name] = NewRunner
}
type Ansible struct {
	runners.Runner
	logger         *log.Entry
	config         *config.RunnerAnsible
	runOptions     config.RunOptions
	executor       executor.Executor
	additionalInfo string
}
func NewRunner(cfg *config.Config) (runners.Runner, error) {
	conf := cfg.Runner.Ansible
	return &Ansible{
		logger:   log.WithFields(logrus.Fields{"runner": Name, "inventoryfile": cfg.Runner.Ansible.InventoryFilePath}),
		config:   conf,
		executor: executor.NewCommandExecutor("runner:ansible"),
	}, nil
}
func (a *Ansible) GetHostsForTest(test *config.Test) (*testers.Hosts, error) {
	ctx, cancel := context.WithTimeout(context.Background(), a.config.Timeouts.CommandTimeout)
	defer cancel()
	out, err := a.executor.ExecuteCommandWithOutputByte(ctx, "runner:ansible: list hosts from inventory", a.config.AnsibleInventoryCommand, []string{
		fmt.Sprintf("--inventory=%s", a.config.InventoryFilePath),
		"--list",
	}...)
	if err != nil {
		return nil, err
	}
	inv, err := ansible.Parse(cleanAnsibleOutput(out))
	if err != nil {
		return nil, err
	}
	servers := inv.GetHostsForGroup(a.config.Groups.Server)
	clients := inv.GetHostsForGroup(a.config.Groups.Clients)
	hosts := map[string]*testers.Host{}
	var lock sync.Mutex
	var wg sync.WaitGroup
	inCh := make(chan string)
	retCh := make(chan error)
	cmdCtx, cmdCancel := context.WithCancel(context.Background())
	defer cmdCancel()
	for i := 0; i < *a.config.ParallelHostFactCalls; i++ {
		wg.Add(1)
		go func(in chan string, retCh chan error) {
			defer wg.Done()
			for host := range in {
				addresses, err := a.getHostNetworkAddress(cmdCtx, host)
				if err != nil {
					retCh <- err
					return
				}
				lock.Lock()
				hosts[host] = &testers.Host{
					Name:      host,
					Labels:    map[string]string{},
					Addresses: addresses,
				}
				lock.Unlock()
				retCh <- nil
			}
			retCh <- nil
		}(inCh, retCh)
	}
	go func() {
		uniqHosts := util.UniqueStringSlice(servers, clients)
		for _, h := range uniqHosts {
			inCh <- h
		}
		close(inCh)
		wg.Wait()
		close(retCh)
	}()
	var errs []string
	for err := range retCh {
		if err != nil {
			errs = append(errs, err.Error())
		}
	}
	if len(errs) > 0 {
		retErr := fmt.Errorf("errors in retrieving ansible host facts. %+v", strings.Join(errs, " "))
		return nil, retErr
	}
	sHosts, err := getHosts(servers, hosts)
	if err != nil {
		return nil, err
	}
	cHosts, err := getHosts(clients, hosts)
	if err != nil {
		return nil, err
	}
	return &testers.Hosts{
		Servers: sHosts,
		Clients: cHosts,
	}, nil
}
func getHosts(in []string, list map[string]*testers.Host) (map[string]*testers.Host, error) {
	hosts := map[string]*testers.Host{}
	for _, h := range in {
		if v, ok := list[h]; ok {
			hosts[h] = v
		} else {
			return nil, fmt.Errorf("server %q not found in ansible hosts list, this should not have happened", h)
		}
	}
	return hosts, nil
}
type facts struct {
	AnsibleFacts networkInterface `json:"ansible_facts"`
}
type networkInterface struct {
	AnsibleDefaultIPv4 networkInterfaceAddress `json:"ansible_default_ipv4"`
	AnsibleDefaultIPv6 networkInterfaceAddress `json:"ansible_default_ipv6"`
}
type networkInterfaceAddress struct {
	Address string `json:"address"`
}
func (a *Ansible) getHostNetworkAddress(bctx context.Context, host string) (*testers.IPAddresses, error) {
	a.logger.WithField("hostname", host).Debug("retrieving ansible host facts")
	ctx, cancel := context.WithTimeout(bctx, a.config.Timeouts.CommandTimeout)
	defer cancel()
	out, err := a.executor.ExecuteCommandWithOutputByte(ctx, "runner:ansible: list hosts from inventory", a.config.AnsibleCommand, []string{
		fmt.Sprintf("--inventory=%s", a.config.InventoryFilePath),
		host,
		"--module-name=setup",
		"--args=gather_subset=!all,!any,network",
	}...)
	if err != nil {
		return nil, err
	}
	out = cleanAnsibleOutput(out)
	facts := &facts{}
	if err := json.Unmarshal(out, facts); err != nil {
		return nil, err
	}
	addresses := &testers.IPAddresses{}
	if facts.AnsibleFacts.AnsibleDefaultIPv4.Address != "" {
		addresses.IPv4 = []string{
			facts.AnsibleFacts.AnsibleDefaultIPv4.Address,
		}
	}
	if facts.AnsibleFacts.AnsibleDefaultIPv6.Address != "" {
		addresses.IPv6 = []string{
			facts.AnsibleFacts.AnsibleDefaultIPv6.Address,
		}
	}
	if facts.AnsibleFacts.AnsibleDefaultIPv4.Address == "" && facts.AnsibleFacts.AnsibleDefaultIPv6.Address == "" {
		return nil, fmt.Errorf("no default IP addresses for ansible host %s", host)
	}
	a.logger.WithField("hostname", host).Debug("retrieved ansible host facts")
	return addresses, nil
}
func (a *Ansible) Prepare(runOpts config.RunOptions, plan *testers.Plan) error {
	a.runOptions = runOpts
	ctx, cancel := context.WithTimeout(context.Background(), a.config.Timeouts.CommandTimeout)
	defer cancel()
	out, err := a.executor.ExecuteCommandWithOutput(ctx, "runner:ansible: get ansible version", a.config.AnsibleCommand, "--version")
	if err != nil {
		return err
	}
	scanner := bufio.NewScanner(strings.NewReader(out))
	for scanner.Scan() {
		line := scanner.Text()
		if strings.HasPrefix(line, "ansible ") {
			a.additionalInfo = line
			break
		}
	}
	return nil
}