agent/taskengine/host/processor.go (387 lines of code) (raw):
package host
import (
"errors"
"fmt"
"io"
"os"
"path/filepath"
"regexp"
"time"
"github.com/aliyun/aliyun_assist_client/thirdparty/sirupsen/logrus"
"github.com/hectane/go-acl"
"github.com/aliyun/aliyun_assist_client/agent/flagging"
"github.com/aliyun/aliyun_assist_client/agent/log"
"github.com/aliyun/aliyun_assist_client/agent/metrics"
"github.com/aliyun/aliyun_assist_client/agent/pluginmanager/acspluginmanager"
"github.com/aliyun/aliyun_assist_client/agent/taskengine/models"
"github.com/aliyun/aliyun_assist_client/agent/taskengine/scriptmanager"
"github.com/aliyun/aliyun_assist_client/agent/taskengine/taskerrors"
"github.com/aliyun/aliyun_assist_client/agent/util/errnoutil"
"github.com/aliyun/aliyun_assist_client/agent/util/powerutil"
"github.com/aliyun/aliyun_assist_client/agent/util/process"
"github.com/aliyun/aliyun_assist_client/common/executil"
"github.com/aliyun/aliyun_assist_client/common/langutil"
"github.com/aliyun/aliyun_assist_client/common/pathutil"
)
type HostProcessor struct {
TaskId string
// Fundamental properties of command process
CommandType string
CommandContent string
InvokeVersion int
Repeat models.RunTaskRepeatType
Timeout int
TerminationMode string
// Additional attributes for command process in host
CommandName string
WorkingDirectory string
Username string
WindowsUserPassword string
// Launcher params
Launcher string
// Detected properties for command process in host
envHomeDir string
realWorkingDir string
// Generated variables to invoke command process
scriptFilePath string
invokeCommand string
invokeCommandArgs []string
// Object for command process
processCmd *process.ProcessCmd
// Generated variables from invoked command process
exitCode int
resultStatus int
canceled bool
}
var (
launcherParamRe = regexp.MustCompile(`'[^']*'|"[^"]*"|{{.*}}|\S+`)
scriptFileRe = regexp.MustCompile(`(?i){{\s*ACS\s*::\s*ScriptFileName\s*(?:\|\s*ext\s*\(([\w-.]+)\)\s*)?\s*}}`)
environmentParamRe = regexp.MustCompile(`\{\{(.+?)\}\}`)
// scriptFileRe = regexp.MustCompile(`{{\s*([\w-.]+)\s*::\s*([\w-.]+)\s*(?:\|\s*([\w-.]+)\s*\(\s*([\w-.]+)\s*\)\s*)*}}`)
pluginRe = regexp.MustCompile(`@(\S+)`)
)
func (p *HostProcessor) PreCheck() (string, error) {
taskLogger := log.GetLogger().WithFields(logrus.Fields{
"TaskId": p.TaskId,
"Phase": "HostProcessor-PreCheck",
})
if len(p.Username) > 0 {
if _, err := p.checkCredentials(); err != nil {
if validationErr, ok := err.(taskerrors.NormalizedValidationError); ok {
return validationErr.Param(), err
} else {
return "UsernameOrPasswordInvalid", err
}
}
}
var err error
p.envHomeDir, err = p.checkHomeDirectory()
if err != nil {
taskLogger.WithError(err).Warningln("Invalid HOME directory for invocation")
}
p.realWorkingDir, err = p.checkWorkingDirectory()
if err != nil {
return "workingDirectory", err
}
return "", nil
}
func (p *HostProcessor) Prepare(commandContent string) error {
taskLogger := log.GetLogger().WithFields(logrus.Fields{
"TaskId": p.TaskId,
"Phase": "HostProcessor-Preparing",
})
if langutil.NeedTransformEncoding() {
commandContent = langutil.UTF8ToLocal(commandContent)
}
p.CommandContent = commandContent
var processCmd *process.ProcessCmd
var processCmdErr error
switch p.TerminationMode {
case "ProcessTree":
groupName := fmt.Sprintf("%s-%d", p.TaskId, time.Now().Unix())
processCmd, processCmdErr = process.NewProcessCmdWithProcessTree(groupName)
case "", "Process":
processCmd = process.NewProcessCmd()
default:
processCmdErr = fmt.Errorf("unknown terminate mode %s", p.TerminationMode)
}
taskLogger.Info("Terminate mode: ", p.TerminationMode)
if processCmdErr != nil {
return taskerrors.NewCreateProcessCollectionError(processCmdErr)
}
p.processCmd = processCmd
useScriptFile := true
var whyNoScriptFile error
defer func() {
if !useScriptFile {
metrics.GetTaskWarnEvent(
"taskid", p.TaskId,
"warnmsg", "NotUseScriptFile",
"reason", whyNoScriptFile.Error(),
).ReportEvent()
}
}()
var scriptDir string
var scriptFileExtension string
switch p.CommandType {
case "RunBatScript":
scriptFileExtension = ".bat"
case "RunPowerShellScript":
scriptFileExtension = ".ps1"
case "RunShellScript":
scriptFileExtension = ".sh"
if p.Username != "" {
scriptDir = "/tmp"
}
default:
return taskerrors.NewUnknownCommandTypeError()
}
var launcherParams []string
// If launcher exists, scriptFileExtension is different
environmentParamExist := false
if p.Launcher != "" {
// If exists {{ACS::ScriptFileName}}, while without Ext
extensionMatches := scriptFileRe.FindStringSubmatch(p.Launcher)
if len(extensionMatches) == 2 {
scriptFileExtension = extensionMatches[1]
}
fileNameMatches := scriptFileRe.FindAllString(p.Launcher, -1)
environmentMatches := environmentParamRe.FindAllString(p.Launcher, -1)
// Launcher has other environment parameter and it's not {{ACS::ScriptFileName}} or {{ACS::ScriptFileName|Ext}}
if len(fileNameMatches) == 1 {
environmentParamExist = true
}
if len(fileNameMatches) > 1 || len(environmentMatches) != len(fileNameMatches) {
taskLogger.Errorf("Invalid match when resolving environment parameter in launcher %s", p.Launcher)
return taskerrors.NewInvalidEnvironmentParameterError(fmt.Sprintf(`Invalid match when resolving environment parameter in launcher %s`, p.Launcher))
}
}
if scriptDir == "" {
var err error
scriptDir, err = pathutil.GetScriptPath()
if err != nil {
useScriptFile = false
if errnoutil.IsNoEnoughSpaceError(err) {
whyNoScriptFile = taskerrors.NewNoEnoughSpaceError(err)
} else {
whyNoScriptFile = taskerrors.NewGetScriptPathError(err)
}
taskLogger.Error("Get script dir path error: ", whyNoScriptFile)
}
}
if useScriptFile {
iVersion := ""
commandName := ""
if p.InvokeVersion > 0 {
iVersion = fmt.Sprintf(".iv%d", p.InvokeVersion)
}
if len(p.CommandName) > 0 {
commandName = p.CommandName + "-"
}
p.scriptFilePath = filepath.Join(scriptDir, fmt.Sprintf("%s%s%s%s", commandName, p.TaskId, iVersion, scriptFileExtension))
// In english environment PowerShell scripts need to be saved in utf8-bom format,
// otherwise no-ASCII characters will not be recognized
commandContent = p.CommandContent
if p.CommandType == "RunPowerShellScript" && !langutil.NeedTransformEncoding() {
commandContent = string(append([]byte{0xEF, 0xBB, 0xBF}, []byte(commandContent)...))
}
if err := scriptmanager.SaveScriptFile(p.scriptFilePath, commandContent); err != nil {
// NOTE: Only non-repeated tasks need to check whether command script
// file exists.
taskLogger.Error("Save script file error: ", err)
if errors.Is(err, scriptmanager.ErrScriptFileExists) {
if p.Repeat == models.RunTaskOnce || p.Repeat == models.RunTaskNextRebootOnly {
return taskerrors.NewScriptFileExistedError(p.scriptFilePath, err)
}
} else if errnoutil.IsNoEnoughSpaceError(err) {
useScriptFile = false
whyNoScriptFile = taskerrors.NewNoEnoughSpaceError(err)
} else {
useScriptFile = false
whyNoScriptFile = taskerrors.NewSaveScriptFileError(err)
}
// If save script into file failed, file may be created but is invalid
if !useScriptFile {
os.Remove(p.scriptFilePath)
}
}
}
if p.Launcher != "" {
if !useScriptFile {
metrics.GetTaskFailedEvent(
"taskid", p.TaskId,
"errormsg", fmt.Sprintf("Can not use script file, so Launcher script cannot run"),
"reason", whyNoScriptFile.Error(),
).ReportEvent()
return whyNoScriptFile
}
launcher := scriptFileRe.ReplaceAllString(p.Launcher, p.scriptFilePath)
launcherParams = launcherParamRe.FindAllString(launcher, -1)
if !environmentParamExist {
launcherParams = append(launcherParams, p.scriptFilePath)
}
}
taskLogger.Infof("Launcher params: %v", launcherParams)
if useScriptFile {
if p.CommandType == "RunShellScript" {
if err := acl.Chmod(p.scriptFilePath, 0755); err != nil {
useScriptFile = false
whyNoScriptFile = taskerrors.NewSetExecutablePermissionError(err)
}
} else {
if p.Username != "" {
if err := acl.Chmod(p.scriptFilePath, 0755); err != nil {
useScriptFile = false
whyNoScriptFile = taskerrors.NewSetWindowsPermissionError(err)
}
}
}
}
p.invokeCommand = p.scriptFilePath
p.invokeCommandArgs = []string{}
if p.CommandType == "RunShellScript" {
p.invokeCommand = "sh"
if useScriptFile {
p.invokeCommandArgs = []string{"-c", p.scriptFilePath}
} else {
p.invokeCommandArgs = []string{"-c", p.CommandContent}
}
if _, err := executil.LookPath(p.invokeCommand); err != nil {
return taskerrors.NewSystemDefaultShellNotFoundError(err)
}
} else if p.CommandType == "RunPowerShellScript" {
p.invokeCommand = "powershell"
if useScriptFile {
p.invokeCommandArgs = []string{"-file", p.scriptFilePath}
} else {
p.invokeCommandArgs = []string{"-command", p.CommandContent}
}
if _, err := executil.LookPath(p.invokeCommand); err != nil {
return taskerrors.NewPowershellNotFoundError(err)
}
tempProcessCmd := process.NewProcessCmd()
if err := tempProcessCmd.SyncRunSimple("powershell.exe", []string{"Set-ExecutionPolicy", "RemoteSigned"}, 10); err != nil {
taskLogger.WithError(err).Warningln("Failed to set powershell execution policy")
}
} else if !useScriptFile {
metrics.GetTaskFailedEvent(
"taskid", p.TaskId,
"errormsg", fmt.Sprintf("Can not use script file, so this type[%s] script cannot run", p.CommandType),
"reason", whyNoScriptFile.Error(),
).ReportEvent()
return whyNoScriptFile
}
// For Launcher: change invokeCommand or invokeCommandArgs
if p.Launcher != "" && len(launcherParams) > 0 {
var cmdPath string
// Check if it's plugin or local launcher
if launcherParams[0][0] == '@' && len(launcherParams[0]) > 1 && launcherParams[0][1] != '@' {
//It's a plugin and install it
pluginName := matchPlugin(p.Launcher)
pluginManager, err := acspluginmanager.NewPluginManager(false)
if err != nil {
return taskerrors.NewPluginLoadFailedError(fmt.Errorf("Broken plugin management: %w", err))
}
pluginInfo, err := acspluginmanager.QueryPluginFromLocal(pluginName, "")
if pluginInfo == nil || err != nil {
err1 := err
pluginInfo, err = acspluginmanager.QueryPluginFromOnline(pluginName, "", "")
// Online query nil
if pluginInfo == nil || err != nil {
taskLogger.Error("Plugin not found online and local: ", launcherParams[0])
return taskerrors.NewPluginLoadFailedError(fmt.Errorf("query from local failed,%s; query from online failed,%s", err1, err))
}
err = pluginManager.InstallPluginFromOnline(pluginInfo, 20)
if err != nil {
taskLogger.Error("Plugin install error: ", err)
return taskerrors.NewPluginLoadFailedError(fmt.Errorf("Plugin install error: %s", err))
}
}
cmdPath = pluginManager.GetPluginCommandPath(pluginInfo)
if _, err := executil.LookPath(cmdPath); err != nil {
taskLogger.Error("LookPath error: ", err)
return taskerrors.NewPluginLoadFailedError(err)
}
} else {
if len(launcherParams[0]) > 1 && launcherParams[0][0] == '@' && launcherParams[0][1] == '@' {
// local launcher start with @
cmdPath = launcherParams[0][1:]
} else {
cmdPath = launcherParams[0]
}
if _, err := executil.LookPath(cmdPath); err != nil {
taskLogger.Error("LookPath error: ", err)
return taskerrors.NewLauncherNotFoundError(err)
}
}
if useScriptFile {
p.invokeCommand = cmdPath
p.invokeCommandArgs = launcherParams[1:]
taskLogger.Infof("InvokeCommand: %s ; InvokeCommandArgs: %s", p.invokeCommand, p.invokeCommandArgs)
} else {
metrics.GetTaskFailedEvent(
"taskid", p.TaskId,
"errormsg", fmt.Sprintf("Can not use script file, so Launcher script cannot run"),
"reason", whyNoScriptFile.Error(),
).ReportEvent()
return whyNoScriptFile
}
}
return nil
}
func matchPlugin(launcher string) string {
match := pluginRe.FindStringSubmatch(launcher)
if len(match) > 1 {
result := match[1]
return result
} else {
return ""
}
}
func (p *HostProcessor) SyncRun(
stdoutWriter io.Writer,
stderrWriter io.Writer,
stdinReader io.Reader) (int, int, error) {
if p.Username != "" {
p.processCmd.SetUserInfo(p.Username)
}
if p.WindowsUserPassword != "" {
p.processCmd.SetPasswordInfo(p.WindowsUserPassword)
}
// Fix $HOME environment variable undex *nix
if p.envHomeDir != "" {
p.processCmd.SetHomeDir(p.envHomeDir)
}
var err error
p.exitCode, p.resultStatus, err = p.processCmd.SyncRun(p.realWorkingDir, p.invokeCommand, p.invokeCommandArgs, stdoutWriter, stderrWriter, stdinReader, nil, p.Timeout)
if p.resultStatus == process.Fail && err != nil {
err = taskerrors.NewExecuteScriptError(err)
}
return p.exitCode, p.resultStatus, err
}
func (p *HostProcessor) Cancel() error {
if p.canceled {
return nil
}
p.canceled = true
// For periodic task, script is deleted when task canceled
if p.isPeriodic() && !flagging.GetTaskKeepScriptFile() {
os.Remove(p.scriptFilePath)
}
// For periodic task, p.Cancel() may be called before task.Run().
if p.processCmd == nil {
return nil
}
if err := p.processCmd.Cancel(); err != nil {
return err
}
p.processCmd = nil
return nil
}
func (p *HostProcessor) Cleanup(removeScriptFile bool) error {
// For no-periodic task, script is deleted when task finished
if !p.isPeriodic() && removeScriptFile {
if err := os.Remove(p.scriptFilePath); err != nil {
return err
}
}
p.processCmd = nil
return nil
}
func (p *HostProcessor) SideEffect() error {
taskLogger := log.GetLogger().WithFields(logrus.Fields{
"TaskId": p.TaskId,
"Phase": "HostProcessor-SideEffect",
})
// Perform instructed poweroff/reboot action after task finished
if p.resultStatus == process.Success {
if p.exitCode == exitcodePoweroff {
taskLogger.Infof("Poweroff the instance due to the special task exitcode %d", p.exitCode)
powerutil.Shutdown(false)
} else if p.exitCode == exitcodeReboot {
taskLogger.Infof("Reboot the instance due to the special task exitcode %d", p.exitCode)
powerutil.Shutdown(true)
}
}
return nil
}
func (p *HostProcessor) ExtraLubanParams() string {
return ""
}
func (p *HostProcessor) isPeriodic() bool {
return (p.Repeat == models.RunTaskCron || p.Repeat == models.RunTaskRate || p.Repeat == models.RunTaskAt)
}