Semaphore/api/tasks/runner.go

578 lines
14 KiB
Go
Raw Normal View History

package tasks
import (
2018-03-16 02:26:25 +01:00
"bytes"
"database/sql"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"os"
"os/exec"
2017-08-19 10:45:01 +02:00
"regexp"
"strconv"
"strings"
"time"
"github.com/ansible-semaphore/semaphore/api/helpers"
"github.com/ansible-semaphore/semaphore/db"
log "github.com/Sirupsen/logrus"
"github.com/ansible-semaphore/semaphore/util"
)
const (
taskFailStatus = "error"
2018-06-07 09:29:55 +02:00
taskTypeID = "task"
)
type task struct {
store db.Store
task db.Task
template db.Template
sshKey db.AccessKey
inventory db.Inventory
repository db.Repository
environment db.Environment
2016-04-17 12:41:36 +02:00
users []int
projectID int
hosts []string
alertChat string
alert bool
prepared bool
}
2016-04-17 02:20:23 +02:00
func (t *task) fail() {
t.task.Status = taskFailStatus
t.updateStatus()
2017-02-22 09:46:42 +01:00
t.sendMailAlert()
2017-03-22 08:22:09 +01:00
t.sendTelegramAlert()
2016-04-17 02:20:23 +02:00
}
func (t *task) prepareRun() {
t.prepared = false
defer func() {
log.Info("Stopped preparing task " + strconv.Itoa(t.task.ID))
2021-03-12 21:20:18 +01:00
log.Info("Release resource locker with task " + strconv.Itoa(t.task.ID))
2018-06-07 09:29:55 +02:00
resourceLocker <- &resourceLock{lock: false, holder: t}
objType := taskTypeID
desc := "Task ID " + strconv.Itoa(t.task.ID) + " (" + t.template.Alias + ")" + " finished - " + strings.ToUpper(t.task.Status)
_, err := t.store.CreateEvent(db.Event{
ProjectID: &t.projectID,
ObjectType: &objType,
ObjectID: &t.task.ID,
Description: &desc,
})
if err != nil {
t.panicOnError(err, "Fatal error inserting an event")
}
}()
t.log("Preparing: " + strconv.Itoa(t.task.ID))
err := checkTmpDir(util.Config.TmpPath)
if err != nil {
t.log("Creating tmp dir failed: " + err.Error())
t.fail()
return
}
if err := t.populateDetails(); err != nil {
t.log("Error: " + err.Error())
t.fail()
return
}
objType := taskTypeID
desc := "Task ID " + strconv.Itoa(t.task.ID) + " (" + t.template.Alias + ")" + " is preparing"
_, err = t.store.CreateEvent(db.Event{
ProjectID: &t.projectID,
ObjectType: &objType,
ObjectID: &t.task.ID,
Description: &desc,
})
if err != nil {
t.log("Fatal error inserting an event")
panic(err)
}
t.log("Prepare task with template: " + t.template.Alias + "\n")
if err := t.installKey(t.repository.SSHKey); err != nil {
t.log("Failed installing ssh key for repository access: " + err.Error())
2016-04-17 02:20:23 +02:00
t.fail()
return
}
if err := t.updateRepository(); err != nil {
t.log("Failed updating repository: " + err.Error())
2016-04-17 02:20:23 +02:00
t.fail()
return
}
if err := t.installInventory(); err != nil {
t.log("Failed to install inventory: " + err.Error())
2016-04-17 02:20:23 +02:00
t.fail()
return
}
if err := t.installRequirements(); err != nil {
t.log("Running galaxy failed: " + err.Error())
t.fail()
return
}
// todo: write environment
2018-03-16 03:00:14 +01:00
if stderr, err := t.listPlaybookHosts(); err != nil {
2018-03-16 02:26:25 +01:00
t.log("Listing playbook hosts failed: " + err.Error() + "\n" + stderr)
t.fail()
return
}
t.prepared = true
}
func (t *task) run() {
defer func() {
log.Info("Stopped running task " + strconv.Itoa(t.task.ID))
log.Info("Release resource locker with task " + strconv.Itoa(t.task.ID))
2017-08-19 10:45:01 +02:00
resourceLocker <- &resourceLock{lock: false, holder: t}
now := time.Now()
t.task.End = &now
t.updateStatus()
objType := taskTypeID
desc := "Task ID " + strconv.Itoa(t.task.ID) + " (" + t.template.Alias + ")" + " finished - " + strings.ToUpper(t.task.Status)
_, err := t.store.CreateEvent(db.Event{
ProjectID: &t.projectID,
ObjectType: &objType,
ObjectID: &t.task.ID,
Description: &desc,
})
if err != nil {
t.log("Fatal error inserting an event")
panic(err)
}
}()
{
now := time.Now()
t.task.Status = "running"
t.task.Start = &now
t.updateStatus()
}
objType := taskTypeID
desc := "Task ID " + strconv.Itoa(t.task.ID) + " (" + t.template.Alias + ")" + " is running"
_, err := t.store.CreateEvent(db.Event{
ProjectID: &t.projectID,
ObjectType: &objType,
ObjectID: &t.task.ID,
Description: &desc,
})
if err != nil {
t.log("Fatal error inserting an event")
panic(err)
}
t.log("Started: " + strconv.Itoa(t.task.ID))
t.log("Run task with template: " + t.template.Alias + "\n")
if err := t.runPlaybook(); err != nil {
t.log("Running playbook failed: " + err.Error())
2016-04-17 02:20:23 +02:00
t.fail()
return
}
2016-04-17 02:20:23 +02:00
t.task.Status = "success"
t.updateStatus()
}
func (t *task) prepareError(err error, errMsg string) error {
if err == sql.ErrNoRows {
t.log(errMsg)
return err
}
if err != nil {
t.fail()
panic(err)
}
return nil
}
//nolint: gocyclo
func (t *task) populateDetails() error {
// get template
var err error
t.template, err = t.store.GetTemplate(t.projectID, t.task.TemplateID)
if err != nil {
return t.prepareError(err, "Template not found!")
}
2017-04-18 16:36:09 +02:00
// get project alert setting
project, err := t.store.GetProject(t.template.ProjectID)
if err != nil {
return t.prepareError(err, "Project not found!")
2017-03-10 07:25:42 +01:00
}
t.alert = project.Alert
t.alertChat = project.AlertChat
2016-04-17 12:41:36 +02:00
// get project users
users, err := t.store.GetProjectUsers(t.template.ProjectID, db.RetrieveQueryParams{})
2021-03-12 21:30:17 +01:00
if err != nil {
return t.prepareError(err, "Users not found!")
}
2016-04-17 12:41:36 +02:00
t.users = []int{}
for _, user := range users {
t.users = append(t.users, user.ID)
}
// get access key
t.sshKey, err = t.store.GetAccessKey(t.template.ProjectID, t.template.SSHKeyID)
if err != nil {
return t.prepareError(err, "Template AccessKey not found!")
}
if t.sshKey.Type != "ssh" {
t.log("Non ssh-type keys are currently not supported: " + t.sshKey.Type)
return errors.New("unsupported SSH Key")
}
// get inventory
t.inventory, err = t.store.GetInventory(t.template.ProjectID, t.template.InventoryID)
if err != nil {
return t.prepareError(err, "Template Inventory not found!")
}
// get repository
t.repository, err = t.store.GetRepository(t.template.ProjectID, t.template.RepositoryID)
if err != nil {
return err
}
if t.repository.SSHKey.Type != "ssh" {
t.log("Repository Access Key is not 'SSH': " + t.repository.SSHKey.Type)
return errors.New("unsupported SSH Key")
}
// get environment
if len(t.task.Environment) == 0 && t.template.EnvironmentID != nil {
t.environment, err = t.store.GetEnvironment(t.template.ProjectID, *t.template.EnvironmentID)
if err != nil {
return err
}
} else if len(t.task.Environment) > 0 {
t.environment.JSON = t.task.Environment
}
return nil
}
func (t *task) installKey(key db.AccessKey) error {
t.log("access key " + key.Name + " installed")
2017-04-18 16:21:20 +02:00
path := key.GetPath()
2017-02-23 12:21:18 +01:00
if key.Key != nil {
2017-04-18 16:21:20 +02:00
if err := ioutil.WriteFile(path+"-cert.pub", []byte(*key.Key), 0600); err != nil {
return err
2017-02-23 12:21:18 +01:00
}
}
2017-04-18 16:21:20 +02:00
return ioutil.WriteFile(path, []byte(*key.Secret), 0600)
}
func (t *task) updateRepository() error {
repoName := "repository_" + strconv.Itoa(t.repository.ID)
_, err := os.Stat(util.Config.TmpPath + "/" + repoName)
2018-06-07 09:29:55 +02:00
cmd := exec.Command("git") //nolint: gas
cmd.Dir = util.Config.TmpPath
gitSSHCommand := "ssh -o StrictHostKeyChecking=no -i " + t.repository.SSHKey.GetPath()
2017-04-18 16:21:20 +02:00
cmd.Env = t.envVars(util.Config.TmpPath, util.Config.TmpPath, &gitSSHCommand)
repoURL, repoTag := t.repository.GitURL, "master"
2016-11-22 02:07:00 +01:00
if split := strings.Split(repoURL, "#"); len(split) > 1 {
repoURL, repoTag = split[0], split[1]
}
if err != nil && os.IsNotExist(err) {
2016-11-22 02:07:00 +01:00
t.log("Cloning repository " + repoURL)
cmd.Args = append(cmd.Args, "clone", "--recursive", "--branch", repoTag, repoURL, repoName)
} else if err != nil {
return err
} else {
2016-11-22 02:07:00 +01:00
t.log("Updating repository " + repoURL)
cmd.Dir += "/" + repoName
2016-11-22 02:07:00 +01:00
cmd.Args = append(cmd.Args, "pull", "origin", repoTag)
}
t.logCmd(cmd)
return cmd.Run()
}
func (t *task) installRequirements() error {
requirementsFilePath := fmt.Sprintf("%s/repository_%d/roles/requirements.yml", util.Config.TmpPath, t.repository.ID)
requirementsHashFilePath := fmt.Sprintf("%s/repository_%d/requirements.md5", util.Config.TmpPath, t.repository.ID)
if _, err := os.Stat(requirementsFilePath); err != nil {
t.log("No roles/requirements.yml file found. Skip galaxy install process.\n")
return nil
}
if hasRequirementsChanges(requirementsFilePath, requirementsHashFilePath) {
if err := t.runGalaxy([]string{
"install",
"-r",
"roles/requirements.yml",
"--force",
}); err != nil {
return err
}
if err := writeMD5Hash(requirementsFilePath, requirementsHashFilePath); err != nil {
return err
}
} else {
t.log("roles/requirements.yml has no changes. Skip galaxy install process.\n")
}
return nil
}
func (t *task) runGalaxy(args []string) error {
2018-06-07 09:29:55 +02:00
cmd := exec.Command("ansible-galaxy", args...) //nolint: gas
cmd.Dir = util.Config.TmpPath + "/repository_" + strconv.Itoa(t.repository.ID)
gitSSHCommand := "ssh -o StrictHostKeyChecking=no -i " + t.repository.SSHKey.GetPath()
2017-04-18 16:21:20 +02:00
cmd.Env = t.envVars(util.Config.TmpPath, cmd.Dir, &gitSSHCommand)
t.logCmd(cmd)
return cmd.Run()
}
2018-03-16 03:00:14 +01:00
func (t *task) listPlaybookHosts() (string, error) {
2018-06-07 09:29:55 +02:00
if util.Config.ConcurrencyMode == "project" {
return "", nil
}
args, err := t.getPlaybookArgs()
if err != nil {
2018-03-16 03:00:14 +01:00
return "", err
}
args = append(args, "--list-hosts")
2018-06-07 09:29:55 +02:00
cmd := exec.Command("ansible-playbook", args...) //nolint: gas
cmd.Dir = util.Config.TmpPath + "/repository_" + strconv.Itoa(t.repository.ID)
cmd.Env = t.envVars(util.Config.TmpPath, cmd.Dir, nil)
2018-03-16 02:26:25 +01:00
var errb bytes.Buffer
cmd.Stderr = &errb
out, err := cmd.Output()
2018-03-16 02:26:25 +01:00
re := regexp.MustCompile(`(?m)^\\s{6}(.*)$`)
matches := re.FindAllSubmatch(out, 20)
hosts := make([]string, len(matches))
for i := range matches {
hosts[i] = string(matches[i][1])
}
t.hosts = hosts
2018-03-20 01:12:43 +01:00
return errb.String(), err
}
func (t *task) runPlaybook() error {
args, err := t.getPlaybookArgs()
if err != nil {
return err
}
2018-06-07 09:29:55 +02:00
cmd := exec.Command("ansible-playbook", args...) //nolint: gas
cmd.Dir = util.Config.TmpPath + "/repository_" + strconv.Itoa(t.repository.ID)
cmd.Env = t.envVars(util.Config.TmpPath, cmd.Dir, nil)
t.logCmd(cmd)
cmd.Stdin = strings.NewReader("")
return cmd.Run()
}
//nolint: gocyclo
func (t *task) getPlaybookArgs() ([]string, error) {
playbookName := t.task.Playbook
if len(playbookName) == 0 {
playbookName = t.template.Playbook
}
2018-03-16 02:26:25 +01:00
var inventory string
2017-10-26 09:00:48 +02:00
switch t.inventory.Type {
case "file":
inventory = t.inventory.Inventory
default:
inventory = util.Config.TmpPath + "/inventory_" + strconv.Itoa(t.task.ID)
}
args := []string{
2017-10-26 09:00:48 +02:00
"-i", inventory,
}
if t.inventory.SSHKeyID != nil {
args = append(args, "--private-key="+t.inventory.SSHKey.GetPath())
}
if t.task.Debug {
args = append(args, "-vvvv")
}
2016-06-30 16:57:45 +02:00
if t.task.DryRun {
args = append(args, "--check")
}
if len(t.environment.JSON) > 0 {
var js map[string]interface{}
2017-04-18 15:58:48 +02:00
err := json.Unmarshal([]byte(t.environment.JSON), &js)
if err != nil {
t.log("JSON is not valid")
return nil, err
}
extraVar, err := removeCommandEnvironment(t.environment.JSON, js)
if err != nil {
t.log("Could not remove command environment, if existant it will be passed to --extra-vars. This is not fatal but be aware of side effects")
}
args = append(args, "--extra-vars", extraVar)
}
var templateExtraArgs []string
if t.template.Arguments != nil {
err := json.Unmarshal([]byte(*t.template.Arguments), &templateExtraArgs)
if err != nil {
t.log("Could not unmarshal arguments to []string")
return nil, err
}
}
var taskExtraArgs []string
if t.task.Arguments != nil {
err := json.Unmarshal([]byte(*t.task.Arguments), &taskExtraArgs)
if err != nil {
t.log("Could not unmarshal arguments to []string")
return nil, err
}
}
if t.template.OverrideArguments {
args = templateExtraArgs
} else {
args = append(args, templateExtraArgs...)
args = append(args, taskExtraArgs...)
args = append(args, playbookName)
}
return args, nil
}
2017-04-18 16:21:20 +02:00
func (t *task) envVars(home string, pwd string, gitSSHCommand *string) []string {
env := os.Environ()
2017-02-08 13:36:44 +01:00
env = append(env, fmt.Sprintf("HOME=%s", home))
env = append(env, fmt.Sprintf("PWD=%s", pwd))
env = append(env, fmt.Sprintln("PYTHONUNBUFFERED=1"))
//env = append(env, fmt.Sprintln("GIT_FLUSH=1"))
env = append(env, extractCommandEnvironment(t.environment.JSON)...)
2017-04-18 16:21:20 +02:00
if gitSSHCommand != nil {
env = append(env, fmt.Sprintf("GIT_SSH_COMMAND=%s", *gitSSHCommand))
}
return env
}
func hasRequirementsChanges(requirementsFilePath string, requirementsHashFilePath string) bool {
oldFileMD5HashBytes, err := ioutil.ReadFile(requirementsHashFilePath)
if err != nil {
return true
}
newFileMD5Hash, err := helpers.GetMD5Hash(requirementsFilePath)
if err != nil {
return true
}
return string(oldFileMD5HashBytes) != newFileMD5Hash
}
func writeMD5Hash(requirementsFile string, requirementsHashFile string) error {
newFileMD5Hash, err := helpers.GetMD5Hash(requirementsFile)
if err != nil {
return err
}
return ioutil.WriteFile(requirementsHashFile, []byte(newFileMD5Hash), 0644)
}
// extractCommandEnvironment unmarshalls a json string, extracts the ENV key from it and returns it as
// []string where strings are in key=value format
2018-03-15 00:52:37 +01:00
func extractCommandEnvironment(envJSON string) []string {
env := make([]string, 0)
var js map[string]interface{}
2018-03-15 00:52:37 +01:00
err := json.Unmarshal([]byte(envJSON), &js)
if err == nil {
if cfg, ok := js["ENV"]; ok {
switch v := cfg.(type) {
case map[string]interface{}:
for key, val := range v {
env = append(env, fmt.Sprintf("%s=%s", key, val))
}
}
}
}
return env
}
// removeCommandEnvironment removes the ENV key from task environments and returns the resultant json encoded string
// which can be passed as the --extra-vars flag values
2018-03-15 00:52:37 +01:00
func removeCommandEnvironment(envJSON string, envJs map[string]interface{}) (string, error) {
if _, ok := envJs["ENV"]; ok {
delete(envJs, "ENV")
ev, err := json.Marshal(envJs)
if err != nil {
2018-03-15 00:52:37 +01:00
return envJSON, err
}
2018-03-15 00:52:37 +01:00
envJSON = string(ev)
}
2018-03-15 00:52:37 +01:00
return envJSON, nil
}
// checkTmpDir checks to see if the temporary directory exists
// and if it does not attempts to create it
func checkTmpDir(path string) error {
var err error
if _, err = os.Stat(path); err != nil {
if os.IsNotExist(err) {
2018-02-28 10:02:54 +01:00
return os.MkdirAll(path, 0700)
}
}
return err
}