agent/longrunning/plugin/cloudwatch/cloudwatch_windows.go (238 lines of code) (raw):
// Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"). You may not
// use this file except in compliance with the License. A copy of the
// License is located at
//
// http://aws.amazon.com/apache2.0/
//
// or in the "license" file accompanying this file. This file is distributed
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
// either express or implied. See the License for the specific language governing
// permissions and limitations under the License.
//
//go:build windows
// +build windows
// Package cloudwatch implements cloudwatch plugin and its configuration
package cloudwatch
import (
"bytes"
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/aws/amazon-ssm-agent/agent/appconfig"
"github.com/aws/amazon-ssm-agent/agent/context"
"github.com/aws/amazon-ssm-agent/agent/executers"
"github.com/aws/amazon-ssm-agent/agent/fileutil"
"github.com/aws/amazon-ssm-agent/agent/framework/processor/executer/iohandler"
"github.com/aws/amazon-ssm-agent/agent/jsonutil"
"github.com/aws/amazon-ssm-agent/agent/log/logger"
"github.com/aws/amazon-ssm-agent/agent/plugins/pluginutil"
"github.com/aws/amazon-ssm-agent/agent/task"
)
// Plugin is the type for the Cloudwatch plugin.
type Plugin struct {
Context context.T
CommandExecuter executers.T
Process *os.Process
WorkingDir string
ExeLocation string
Name string
DefaultHealthCheckOrchestrationDir string
}
const (
//TODO: Change the way the output is being returned to return exit codes
IsProcessRunning = "$ProcessActive = Get-Process -Name %v -ErrorAction SilentlyContinue ; $ProcessActive -ne $null"
GetPidOfExe = "Get-Process -Name %v -ErrorAction SilentlyContinue | Select ProcessName, Id | ConvertTo-Json"
ProcessNotFound = "Process not found"
// CloudWatchProcessName represents CloudWatch Exe Absolute Path
CloudWatchProcessName = "AWS.CloudWatch"
// CloudWatchExeName represents the name of the executable file of cloud watch
CloudWatchExeName = "AWS.CloudWatch.exe"
// CloudWatchFolderName represents the default folder name for cloud watch plugin
CloudWatchFolderName = "awsCloudWatch"
)
// CloudwatchProcessInfo is a structure for info returned by Cloudwatch process
type CloudwatchProcessInfo struct {
ProcessName string `json:"ProcessName"`
PId int `json:"Id"`
}
// Assign method to global variables to allow unittest to override
// TODO change these to deps.go later
var fileExist = fileutil.Exists
var exec = executers.ShellCommandExecuter{}
var findProcess = os.FindProcess
var killProcess = func(process *os.Process) error {
return process.Kill()
}
// var createScript = pluginutil.CreateScriptFile
// todo: honor cancel flag for Start
// todo: honor cancel flag for Stop
// todo: Start,Stop -> should return plugin.result or error as well -> so that caller can report the results/errors accordingly.
// NewPlugin returns a new instance of Cloudwatch plugin
func NewPlugin(context context.T, pluginConfig iohandler.PluginConfig) (*Plugin, error) {
//Note: This is a wrapper on top of cloudwatch.exe - basically this executes the exe in a separate process.
var plugin Plugin
plugin.Context = context
plugin.WorkingDir = fileutil.BuildPath(appconfig.DefaultPluginPath, CloudWatchFolderName)
plugin.ExeLocation = filepath.Join(plugin.WorkingDir, CloudWatchExeName)
plugin.Name = Name()
//health check specific stuff will be done here
instanceId, _ := context.Identity().ShortInstanceID()
plugin.DefaultHealthCheckOrchestrationDir = fileutil.BuildPath(appconfig.DefaultDataStorePath,
instanceId,
appconfig.LongRunningPluginsLocation,
appconfig.LongRunningPluginsHealthCheck,
plugin.Name)
_ = fileutil.MakeDirsWithExecuteAccess(plugin.DefaultHealthCheckOrchestrationDir)
plugin.CommandExecuter = exec
return &plugin, nil
}
// Name returns the plugin name
func Name() string {
return appconfig.PluginNameCloudWatch
}
// IsRunning returns if the said plugin is running or not
func (p *Plugin) IsRunning() bool {
//working directory here doesn't really matter much since we run a powershell script to determine if exe is running
return p.IsCloudWatchExeRunning(p.DefaultHealthCheckOrchestrationDir, p.DefaultHealthCheckOrchestrationDir, task.NewChanneledCancelFlag())
}
// Start starts the executable file and returns encountered errors
func (p *Plugin) Start(configuration string, orchestrationDir string, cancelFlag task.CancelFlag, out iohandler.IOHandler) (err error) {
log := p.Context.Log()
logFormatConfig := logger.PrintCWConfig(configuration, log)
log.Infof("CloudWatch Configuration to be applied - %s ", logFormatConfig)
//check if the exe is located
if !fileExist(p.ExeLocation) {
errorMessage := "unable to locate cloudwatch.exe"
log.Errorf(errorMessage)
return errors.New(errorMessage)
}
//if no orchestration directory specified, create temp directory
var useTempDirectory = (orchestrationDir == "")
var tempDir string
//var err error
if useTempDirectory {
if tempDir, err = ioutil.TempDir("", "Ec2RunCommand"); err != nil {
log.Error(err)
return
}
orchestrationDir = tempDir
}
//workingDirectory -> is the location where the exe runs from -> for cloudwatch this is where all configurations are present
orchestrationDir = fileutil.BuildPath(orchestrationDir, p.Name)
log.Debugf("Cloudwatch specific commands will be run in workingDirectory %v; orchestrationDir %v ", p.WorkingDir, orchestrationDir)
// create orchestration dir if needed
if !fileExist(orchestrationDir) {
if err = fileutil.MakeDirsWithExecuteAccess(orchestrationDir); err != nil {
log.Errorf("Encountered error while creating orchestrationDir directory %s:%s", orchestrationDir, err.Error())
return
}
}
//check if cloudwatch.exe is already running or not
if p.IsCloudWatchExeRunning(p.DefaultHealthCheckOrchestrationDir, p.DefaultHealthCheckOrchestrationDir, cancelFlag) {
log.Debug("Cloudwatch executable is already running. Starting to terminate the process")
p.Stop(cancelFlag)
}
/*
In general exec.Execute -> waits for the command to finish with added attribute to timeout and cancel the command
We don't want that for Cloudwatch.exe -> because we simply launch the exe and forget about it, hence we are using
exec.StartExe that just launches an exe.
Also, for aws:runPowerShellScript, aws:psModule & aws:applications plugins -> we create a powershellscript which
has all commands expressed as []string and then we execute that script. For cloudwatch we directly invoke the exe,
and that's why we don't have to create any powershellscript.
*/
//construct command name and arguments that will be run by executer
commandName := p.ExeLocation
var commandArguments []string
var instanceId, instanceRegion string
if instanceId, err = p.Context.Identity().InstanceID(); err != nil {
log.Error("Cannot get the current instance ID")
return
}
if instanceRegion, err = p.Context.Identity().Region(); err != nil {
log.Error("Cannot get the current instance region information")
return
}
commandArguments = append(commandArguments, instanceId, instanceRegion, getFileName())
value, _, err := pluginutil.LocalRegistryKeyGetStringsValue(appconfig.ItemPropertyPath, appconfig.ItemPropertyName)
if err != nil {
log.Debug("Cannot find customized proxy setting.")
}
// if user has customized proxy setting
if (err == nil) && (len(value) != 0) {
url, noProxy := pluginutil.GetProxySetting(value)
if (len(url) != 0) && (len(noProxy) != 0) {
commandArguments = append(commandArguments, url, noProxy)
} else if len(url) != 0 {
commandArguments = append(commandArguments, url)
}
}
log.Debugf("commandName: %s", commandName)
log.Debugf("arguments passed: %s", commandArguments)
//start the new process
stdoutFilePath := filepath.Join(orchestrationDir, "stdout")
stderrFilePath := filepath.Join(orchestrationDir, "stderr")
//remove previous output log files if they are present
fileutil.DeleteFile(stdoutFilePath)
fileutil.DeleteFile(stderrFilePath)
process, exitCode, err := p.CommandExecuter.StartExe(p.Context, p.WorkingDir, out.GetStdoutWriter(), out.GetStderrWriter(), cancelFlag, commandName, commandArguments)
if err != nil || exitCode != 0 {
return fmt.Errorf("Errors occurred while starting Cloudwatch exit code %v, error %v", exitCode, err)
}
// Cloudwatch process details
p.Process = process
log.Infof("Process id of cloudwatch.exe -> %v", p.Process.Pid)
return nil
}
// Stop returns true if it successfully killed the cloudwatch exe or else it returns false
func (p *Plugin) Stop(cancelFlag task.CancelFlag) (err error) {
log := p.Context.Log()
var cwProcInfo []CloudwatchProcessInfo
if cwProcInfo, err = p.GetProcInfoOfCloudWatchExe(
p.DefaultHealthCheckOrchestrationDir,
p.DefaultHealthCheckOrchestrationDir,
task.NewChanneledCancelFlag()); err != nil {
log.Errorf("Can't stop cloudwatch because unable to find Pid of cloudwatch.exe : %v", err)
return err
}
log.Info("The number of cloudwatch processes running are ", len(cwProcInfo))
var processKillError error
var currentProcess *os.Process
processKillError = nil
//Iterating through the cwProcess info to in case multiple Cloudwatch processes are running.
//All existing processes must be killed
for _, cloudwatchInfo := range cwProcInfo {
//Assigning existing cloudwatch process Id to currentProcess in order to kill that process.
log.Debug("PID of Cloudwatch is ", cloudwatchInfo.PId)
if currentProcess, err = findProcess(cloudwatchInfo.PId); err != nil {
err = fmt.Errorf("failed to find process CloudWatch process with pid %v. Err: %w", cloudwatchInfo.PId, err)
log.Error(err)
processKillError = err
continue
}
if err = killProcess(currentProcess); err != nil {
// Continuing here without returning to kill whatever processes can be killed even if something
// goes wrong. Return on error later
log.Errorf("Encountered error while trying to kill the process %v : %v", currentProcess.Pid, err)
processKillError = err
} else {
log.Infof("Successfully killed the process %v", currentProcess.Pid)
}
}
if p.IsRunning() || processKillError != nil {
log.Errorf("There was an error while killing Cloudwatch: %v", processKillError)
return processKillError
} else {
log.Infof("All existing Cloudwatch processes killed successfully.")
}
return nil
}
// IsCloudWatchExeRunning runs a powershell script to determine if the given process is running
func (p *Plugin) IsCloudWatchExeRunning(workingDirectory, orchestrationDir string, cancelFlag task.CancelFlag) bool {
/*
Since most functions in "os" package in GoLang isn't implemented for Windows platform, we run a powershell
script (using Get-Process) to get process details in Windows.
*/
log := p.Context.Log()
//constructing the powershell command to execute
var commandArguments []string
var err error
cloudwatchProcessName := CloudWatchProcessName
cmdIsExeRunning := fmt.Sprintf(IsProcessRunning, cloudwatchProcessName)
log.Debugf("Final cmd to check if process is still running is", cmdIsExeRunning)
commandArguments = append(commandArguments, cmdIsExeRunning)
// execute the command
var commandOutput string
if commandOutput, err = p.runPowerShell(workingDirectory, cancelFlag, commandArguments); err != nil {
//TODO Returning false here because we are unsure if Cloudwatch is running. Trying to kill PID will lead to error. Handle this situation
return false
}
log.Debugf("The output of IsCloudwatchExeRunning is %s", commandOutput)
//Get-Process returned the Pid -> means it was not null
if strings.Contains(commandOutput, "True") {
log.Infof("Process %s is running", cloudwatchProcessName)
return true
} else if !strings.Contains(commandOutput, "False") {
log.Infof("Multiple processes of %s running. Command output is ", cloudwatchProcessName, commandOutput)
return true
}
log.Infof("Process %s is not running", cloudwatchProcessName)
return false
}
// GetProcInfoOfCloudWatchExe runs a powershell script to determine the process ID of the Cloudwatch process. It should be called only after confirming that cloudwatch is running
func (p *Plugin) GetProcInfoOfCloudWatchExe(orchestrationDir, workingDirectory string, cancelFlag task.CancelFlag) (cwProcInfo []CloudwatchProcessInfo, err error) {
log := p.Context.Log()
//constructing the powershell command to execute
var commandArguments []string
cmdGetPidOfCW := fmt.Sprintf(GetPidOfExe, CloudWatchProcessName)
log.Debugf("Command to get the PID info is ", cmdGetPidOfCW)
commandArguments = append(commandArguments, cmdGetPidOfCW)
// execute the command
var commandOutput string
if commandOutput, err = p.runPowerShell(workingDirectory, cancelFlag, commandArguments); err != nil {
return cwProcInfo, err
}
//Since output is returned as a Json, checking to see if output is not in the form of an array
//Output will be in the form of an array only in case of multiple Cloudwatch instances running
if !strings.HasPrefix(commandOutput, "[") && !strings.HasSuffix(commandOutput, "]") {
commandOutput = "[" + commandOutput + "]"
}
//Unmarshal the result into json obj.
if err = jsonutil.Unmarshal(commandOutput, &cwProcInfo); err != nil {
log.Errorf("Error unmarshalling Cloudwatch process information is %v", err)
return cwProcInfo, err
}
return cwProcInfo, err
}
// runPowerShell is a wrapper around Execute command to run powershell script
func (p *Plugin) runPowerShell(workingDirectory string, cancelFlag task.CancelFlag, commandArguments []string) (commandOutput string, err error) {
log := p.Context.Log()
commandName := pluginutil.GetShellCommand()
log.Infof("commandName: %s", commandName)
log.Infof("arguments passed: %s", commandArguments)
//If the stdoutFile and stderrFile path is empty, p.CommandExecuter.Execute return the output as a buffer
stdoutFilePath := ""
stderrFilePath := ""
//executionTimeout -> determining if a process is running or not shouldn't take more than 60 seconds
executionTimeout := pluginutil.ValidateExecutionTimeout(log, 60)
//execute the command
stdout, stderr, exitCode, errs := p.CommandExecuter.Execute(p.Context, workingDirectory, stdoutFilePath,
stderrFilePath, cancelFlag, executionTimeout, commandName, commandArguments, make(map[string]string))
stdOutBuf := new(bytes.Buffer)
stdOutBuf.ReadFrom(stdout)
commandOutput = stdOutBuf.String()
stdErrBuf := new(bytes.Buffer)
stdErrBuf.ReadFrom(stderr)
commandOutputError := stdErrBuf.String()
//We don't expect any errors because the powershell script that we run has error action set as SilentlyContinue
if commandOutputError != "" {
log.Errorf("Powershell script to get process ID of the Cloudwatch executable currently running failed with error - %v", commandOutputError)
}
log.Debugf("exitCode - %v", exitCode)
log.Debugf("errs - %v", errs)
return commandOutput, nil
}