agent/plugins/updatessmagent/updateagent.go (263 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.
// Package updatessmagent implements the UpdateSsmAgent plugin.
package updatessmagent
import (
"errors"
"fmt"
"math/rand"
"strconv"
"time"
"github.com/aws/amazon-ssm-agent/agent/appconfig"
"github.com/aws/amazon-ssm-agent/agent/context"
"github.com/aws/amazon-ssm-agent/agent/contracts"
"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/task"
"github.com/aws/amazon-ssm-agent/agent/updateutil"
"github.com/aws/amazon-ssm-agent/agent/updateutil/updateconstants"
"github.com/aws/amazon-ssm-agent/agent/updateutil/updateinfo"
"github.com/aws/amazon-ssm-agent/agent/updateutil/updatemanifest"
"github.com/aws/amazon-ssm-agent/agent/updateutil/updates3util"
"github.com/aws/amazon-ssm-agent/agent/version"
"github.com/aws/amazon-ssm-agent/core/executor"
"github.com/nightlyone/lockfile"
)
const (
noOfRetries = 2
updateRetryDelayBase = 1000 // 1000 millisecond
updateRetryDelay = 500 // 500 millisecond
)
// Plugin is the type for the RunCommand plugin.
type Plugin struct {
Context context.T
}
// UpdatePluginInput represents one set of commands executed by the UpdateAgent plugin.
type UpdatePluginInput struct {
contracts.PluginInput
AgentName string `json:"agentName"`
AllowDowngrade string `json:"allowDowngrade"`
TargetVersion string `json:"targetVersion"`
Source string `json:"source"`
UpdaterName string `json:"-"`
}
// Assign method to global variables to allow unittest to override
var currentAgentVersion = version.Version
var updateAgent = runUpdateAgent
var getLockObj = lockfile.New
var updateUtilRef updateutil.T // added mainly for testing
// NewPlugin returns a new instance of the plugin.
func NewPlugin(context context.T) (*Plugin, error) {
return &Plugin{
context,
}, nil
}
// updateAgent downloads the installation packages and update the agent
func runUpdateAgent(
config contracts.Configuration,
context context.T,
util updateutil.T,
s3util updates3util.T,
manifest updatemanifest.T,
rawPluginInput interface{},
output iohandler.IOHandler,
startTime time.Time,
exec executor.IExecutor,
downloadFolder string) (pid int) {
log := context.Log()
var pluginInput UpdatePluginInput
var err error
pluginConfig := iohandler.DefaultOutputConfig()
if err = jsonutil.Remarshal(rawPluginInput, &pluginInput); err != nil {
output.MarkAsFailed(fmt.Errorf("invalid format in plugin properties %v;\nerror %v", rawPluginInput, err))
return
}
//Calculate updater package name base on agent name
pluginInput.UpdaterName = pluginInput.AgentName + updateconstants.UpdaterPackageNamePrefix
// if TargetVersion is Empty, set to None and it will be resolved in the updater
if len(pluginInput.TargetVersion) == 0 {
pluginInput.TargetVersion = "None"
}
// If disk space is not sufficient, fail the update to prevent installation and notify user in output
// If loading disk space fails, continue to update (agent update is backed by rollback handler)
log.Infof("Checking available disk space ...")
if isDiskSpaceSufficient, err := util.IsDiskSpaceSufficientForUpdate(log); !isDiskSpaceSufficient || err != nil {
if err != nil {
output.MarkAsFailed(err)
return
}
output.MarkAsFailed(errors.New("Insufficient available disk space"))
return
}
//Download manifest file and populate manifest object
if downloadErr := s3util.DownloadManifest(manifest, pluginInput.Source); downloadErr != nil && downloadErr.Error != nil {
output.MarkAsFailed(downloadErr.Error)
return
}
output.AppendInfo("Successfully downloaded manifest\n")
//Download updater and retrieve the version number
updaterVersion := ""
if updaterVersion, err = s3util.DownloadUpdater(manifest, pluginInput.UpdaterName, downloadFolder); err != nil {
output.MarkAsFailed(err)
return
}
output.AppendInfof("Successfully downloaded updater version %s\n", updaterVersion)
//Generate update command base on the update detail
cmd, err := generateUpdateCmd(
&pluginInput,
updaterVersion,
config.MessageId,
config.UpstreamServiceName,
pluginConfig.StdoutFileName,
pluginConfig.StderrFileName,
fileutil.BuildS3Path(output.GetIOConfig().OutputS3KeyPrefix, config.PluginID),
output.GetIOConfig().OutputS3BucketName)
if err != nil {
output.MarkAsFailed(err)
return
}
log.Debugf("Update command %v", cmd)
//Save update plugin result to local file, updater will read it during agent update
updatePluginResult := &updateutil.UpdatePluginResult{
StandOut: output.GetStdout(),
StartDateTime: startTime,
}
if err = util.SaveUpdatePluginResult(log, appconfig.UpdaterArtifactsRoot, updatePluginResult); err != nil {
output.MarkAsFailed(err)
return
}
log.Infof("Start Installation")
log.Infof("Hand over update process to %v", pluginInput.UpdaterName)
//Execute updater, hand over the update process
workDir := updateutil.UpdateArtifactFolder(
appconfig.UpdaterArtifactsRoot, pluginInput.UpdaterName, updaterVersion)
commandInput := &updateutil.CommandExecutionSettings{
Log: log,
Cmd: cmd,
WorkingDir: workDir,
UpdaterRoot: appconfig.UpdaterArtifactsRoot,
StdOut: pluginConfig.StdoutFileName,
StdErr: pluginConfig.StderrFileName,
IsAsync: true,
}
for retryCounter := 1; retryCounter <= noOfRetries; retryCounter++ {
pid, _, err = util.ExeCommandWithSlice(commandInput)
if err == nil {
break
}
if retryCounter < noOfRetries {
time.Sleep(time.Duration(updateRetryDelayBase+rand.Intn(updateRetryDelay)) * time.Millisecond)
}
}
if err != nil {
output.MarkAsFailed(err)
return
}
// Sleep for 1 second and verify updater is running
time.Sleep(time.Second)
isRunning, procErr := exec.IsPidRunning(pid)
if procErr != nil {
log.Warnf("Failed to check if updater process is running: %s", err)
} else {
if !isRunning {
errMsg := "Updater died before updating, make sure your system is supported"
log.Error(errMsg)
output.MarkAsFailed(fmt.Errorf(errMsg))
exec.Kill(pid)
return
} else {
log.Info("Updater is running")
}
}
output.MarkAsInProgress()
return
}
func (p *Plugin) Execute(config contracts.Configuration, cancelFlag task.CancelFlag, output iohandler.IOHandler) {
log := p.Context.Log()
log.Info("RunCommand started with configuration ", config)
if updateUtilRef == nil {
updateUtilRef = &updateutil.Utility{
Context: p.Context,
}
}
executor := executor.NewProcessExecutor(log)
updateInfo, err := updateinfo.New(p.Context)
if err != nil {
log.Warnf("Failed to create update info object: %s", err)
output.MarkAsFailed(fmt.Errorf("Failed to create update info object: %s", err))
return
}
manifest := updatemanifest.New(p.Context, updateInfo, "")
updateS3Util := updates3util.New(p.Context)
if cancelFlag.ShutDown() {
output.MarkAsShutdown()
} else if cancelFlag.Canceled() {
output.MarkAsCancelled()
} else {
// create update directory before creating locks
var downloadFolder string
var directoryErr error
if downloadFolder, directoryErr = updateUtilRef.CreateUpdateDownloadFolder(); directoryErr != nil {
log.Warnf("error while creating update directory: %v", directoryErr)
}
// First check if lock is locked by anyone
lock, _ := getLockObj(appconfig.UpdaterPidLockfile)
err = lock.TryLockExpireWithRetry(updateconstants.UpdateLockFileMinutes)
if err != nil {
if err == lockfile.ErrBusy {
log.Warnf("Failed to lock update lockfile, another update is in progress: %s", err)
output.MarkAsFailed(fmt.Errorf("Another update in progress, try again later"))
return
} else {
log.Warnf("Proceeding update process with new lock. Failed to lock update lockfile: %s", err)
}
}
defer func() {
if err := recover(); err != nil {
// If we panic, we want to release the lock.
log.Errorf("UpdateAgent panicked with error '%s'. Unlocking lockfile", err)
_ = lock.Unlock()
if output.GetStatus() != contracts.ResultStatusFailed {
output.MarkAsFailed(fmt.Errorf("Panic with error: '%s'", err))
}
}
}()
pid := updateAgent(
config,
p.Context,
updateUtilRef,
updateS3Util,
manifest,
config.Properties,
output,
time.Now(),
executor,
downloadFolder)
// If starting update fails, we unlock
if output.GetStatus() != contracts.ResultStatusInProgress {
err = lock.Unlock()
if err != nil {
log.Warnf("Failed to unlock update lockfile: %s", err)
}
return
}
// We need to change ownership to the updater processes because
// the document worker dies right after this function
// If we don't change ownership, other updates can start before before the updater has finished
err = lock.ChangeOwner(pid)
if err != nil {
log.Warnf("failed to transfer ownership of update lockfile to updater, unlocking: %s", err)
_ = lock.Unlock()
}
}
}
// Name returns the plugin name
func Name() string {
return appconfig.PluginNameAwsAgentUpdate
}
func generateUpdateCmd(
pluginInput *UpdatePluginInput,
updaterVersion string,
messageID string,
upstreamServiceName contracts.UpstreamServiceName,
stdout string,
stderr string,
keyPrefix string,
bucketName string) (cmd []string, err error) {
// quick return
allowDowngrade, err := strconv.ParseBool(pluginInput.AllowDowngrade)
if err != nil {
return cmd, err
}
updaterPath := updateutil.UpdaterFilePath(appconfig.UpdaterArtifactsRoot, pluginInput.UpdaterName, updaterVersion)
cmd = append(cmd, updaterPath)
cmd = append(cmd, "-update")
// Tell the updater if downgrade is not allowed
if !allowDowngrade {
cmd = append(cmd, "-"+updateconstants.DisableDowngradeCmd)
}
cmd = append(cmd, "-"+updateconstants.SourceVersionCmd, currentAgentVersion)
cmd = append(cmd, "-"+updateconstants.TargetVersionCmd, pluginInput.TargetVersion)
cmd = append(cmd, "-"+updateconstants.PackageNameCmd, pluginInput.AgentName)
cmd = append(cmd, "-"+updateconstants.MessageIDCmd, messageID)
cmd = append(cmd, "-"+updateconstants.StdoutFileName, stdout)
cmd = append(cmd, "-"+updateconstants.StderrFileName, stderr)
cmd = append(cmd, "-"+updateconstants.OutputKeyPrefixCmd, keyPrefix)
cmd = append(cmd, "-"+updateconstants.OutputBucketNameCmd, bucketName)
cmd = append(cmd, "-"+updateconstants.ManifestFileUrlCmd, pluginInput.Source)
cmd = append(cmd, "-"+updateconstants.UpstreamServiceName, string(upstreamServiceName))
return
}