agent/plugins/updateec2config/updateec2config.go (475 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 updateec2config implements the UpdateEC2Config plugin.
package updateec2config
import (
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"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/fileutil/artifact"
"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"
"github.com/aws/amazon-ssm-agent/agent/plugins/pluginutil"
"github.com/aws/amazon-ssm-agent/agent/s3util"
"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/versionutil"
)
var (
ec2ConfigVersionPattern = regexp.MustCompile("[.0-9]+")
sourcePattern = regexp.MustCompile(`^https?:[\/][\/][\w.-]+:?[\/\w.-]+$`)
)
// Plugin is the type for the RunCommand plugin.
type Plugin struct {
context context.T
// Manifest location
ManifestLocation string
}
// 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:"-"`
}
// UpdatePluginConfig is used for initializing update agent plugin with default values
type UpdatePluginConfig struct {
ManifestLocation string
}
type updateManager struct {
context context.T
}
// TODO move the interface and structs into a separate file to reduce the size of this main file
// pluginHelper is a interface that has helper functions for update manager
type pluginHelper interface {
generateSetupUpdateCmd(log log.T,
manifest *Manifest,
pluginInput *UpdatePluginInput,
region string,
updaterPath string,
messageID string,
agentVersion string) (cmd string, err error)
generateUpdateCmd(log log.T,
updaterPath string) (cmd string, err error)
downloadManifest(context context.T,
util updateutil.T,
pluginInput *UpdatePluginInput,
info updateinfo.T,
out iohandler.IOHandler) (manifest *Manifest, err error)
downloadUpdater(context context.T,
region string,
updaterPackageName string,
manifest *Manifest,
out iohandler.IOHandler,
info updateinfo.T) (version string, err error)
validateUpdate(log log.T,
pluginInput *UpdatePluginInput,
info updateinfo.T,
manifest *Manifest,
out iohandler.IOHandler, version string) (noNeedToUpdate bool, err error)
loadUpdateContext(log log.T,
path string) (updateContext *UpdateContextFile, err error)
}
// Assign method to global variables to allow unittest to override
var fileDownload = artifact.Download
var fileUncompress = fileutil.Uncompress
var updateAgent = runUpdateAgent
var mkDirAll = os.MkdirAll
// NewPlugin returns a new instance of the plugin.
func NewPlugin(context context.T, updatePluginConfig UpdatePluginConfig) (*Plugin, error) {
return &Plugin{
context,
updatePluginConfig.ManifestLocation,
}, nil
}
// getEC2ConfigCurrentVersion gets the current version of EC2 config installed on the platform
func getEC2ConfigCurrentVersion(log log.T) (res string, err error) {
ec2ConfigCLICmd := filepath.Join(os.Getenv("ProgramFiles"), "Amazon", "Ec2ConfigService", "ec2config-cli.exe")
var cmdOut []byte
cmdArgs := []string{"--ec2config-version"}
if cmdOut, err = exec.Command(ec2ConfigCLICmd, cmdArgs...).CombinedOutput(); err != nil {
log.Errorf("There was an error running %v %v. Error = %s", ec2ConfigCLICmd, cmdArgs, err)
return res, err
}
data := string(cmdOut)
matches := ec2ConfigVersionPattern.FindStringSubmatch(data)
if matches != nil {
version := matches[0]
log.Debugf("GetEC2ConfigCurrentVersion: version after trimming space is %s", version)
return version, err
}
return res, fmt.Errorf("Ec2Config version cannot be determined")
}
// TODO add to update manager to merge codes between update agent and update ssm agent to avoid duplication
// updateAgent downloads the installation packages and update the agent
func runUpdateAgent(
p *Plugin,
config contracts.Configuration,
context context.T,
manager pluginHelper,
util updateutil.T,
rawPluginInput interface{},
output iohandler.IOHandler,
startTime time.Time) {
var pluginInput UpdatePluginInput
var updatecontext *UpdateContextFile = new(UpdateContextFile)
var log log.T = context.Log()
var err error
var info updateinfo.T
var region string
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
}
log.Debugf("[runUpdateAgent]: Will now create the instance info")
if info, err = updateinfo.New(context); err != nil {
output.MarkAsFailed(err)
return
}
log.Debugf("[runUpdateAgent]: Will now get region information")
if region, err = context.Identity().Region(); err != nil {
output.MarkAsFailed(err)
return
}
//Use default manifest location is the override is not present
if len(pluginInput.Source) == 0 {
pluginInput.Source = p.ManifestLocation
}
//Calculate manifest location base on current instance's region
pluginInput.Source = strings.Replace(pluginInput.Source, updateconstants.RegionHolder, region, -1)
//Check the format of the source input and revert to default if not
if err = validateSource(pluginInput); err != nil {
log.Warnf("Invalid url provided at %v (url must be https/http link) falling back to default manifest location", pluginInput.Source)
pluginInput.Source = p.ManifestLocation
pluginInput.Source = strings.Replace(pluginInput.Source, updateconstants.RegionHolder, region, -1)
}
//Calculate updater package name base on agent name
pluginInput.UpdaterName = pluginInput.AgentName + updateconstants.UpdaterPackageNamePrefix
//Generate update output
targetVersion := pluginInput.TargetVersion
if len(targetVersion) == 0 {
targetVersion = "latest"
}
var agentVersion string
log.Debugf("[runUpdateAgent]: getting the current version of ec2config ")
if agentVersion, err = getEC2ConfigCurrentVersion(log); err != nil {
output.MarkAsFailed(err)
return
}
// 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); err == nil && !isDiskSpaceSufficient {
output.MarkAsFailed(errors.New("Insufficient available disk space"))
return
}
output.AppendInfof("Updating %v from %v to %v",
pluginInput.AgentName,
agentVersion,
targetVersion)
if updatecontext, err = manager.loadUpdateContext(log, appconfig.UpdateContextFilePath); err != nil {
log.Error("Update context load error: ", err)
}
//Update only when no other update process is running
if updatecontext.UpdateState != notStarted && updatecontext.UpdateState != completed { //update process is running
output.MarkAsFailed(fmt.Errorf("Another update in progress, try again later"))
} else { //if update process is not running
//Download manifest file
manifest, downloadErr := manager.downloadManifest(context, util, &pluginInput, info, output)
if downloadErr != nil {
output.MarkAsFailed(downloadErr)
return
}
//Validate update details
noNeedToUpdate := false
if noNeedToUpdate, err = manager.validateUpdate(log, &pluginInput, info, manifest, output, agentVersion); noNeedToUpdate {
if err != nil {
output.MarkAsFailed(err)
}
return
}
//Download updater and retrieve the version number
updaterVersion := ""
if updaterVersion, err = manager.downloadUpdater(
context, region, pluginInput.UpdaterName, manifest, output, info); err != nil {
output.MarkAsFailed(err)
return
}
//Generate update command base on the update detail
cmd := ""
if cmd, err = manager.generateSetupUpdateCmd(log,
manifest,
&pluginInput,
region,
UpdaterFilePath(appconfig.EC2UpdateArtifactsRoot, pluginInput.UpdaterName, updaterVersion),
config.MessageId,
agentVersion); err != nil {
output.MarkAsFailed(err)
return
}
log.Debugf("Setup 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.EC2UpdateArtifactsRoot, updatePluginResult); err != nil {
output.MarkAsFailed(err)
return
}
workDir := updateutil.UpdateArtifactFolder(
appconfig.EC2UpdateArtifactsRoot, pluginInput.UpdaterName, updaterVersion)
//Command to setup the installation
commandInput := &updateutil.CommandExecutionSettings{
Log: log,
Cmd: strings.Fields(cmd),
WorkingDir: workDir,
UpdaterRoot: appconfig.EC2UpdateArtifactsRoot,
StdOut: pluginConfig.StdoutFileName,
StdErr: pluginConfig.StderrFileName,
IsAsync: false,
}
if _, _, err = util.ExeCommand(commandInput); err != nil {
output.MarkAsFailed(err)
return
}
cmd = ""
log.Infof("Start Installation")
log.Infof("Hand over update process to %v", pluginInput.UpdaterName)
//Execute updater, hand over the update process
if cmd, err = manager.generateUpdateCmd(log,
UpdaterFilePath(appconfig.EC2UpdateArtifactsRoot, pluginInput.UpdaterName, updaterVersion)); err != nil {
output.MarkAsFailed(err)
return
}
log.Debugf("Setup update command %v", cmd)
asyncCommandInput := &updateutil.CommandExecutionSettings{
Log: log,
Cmd: strings.Fields(cmd),
WorkingDir: workDir,
UpdaterRoot: appconfig.EC2UpdateArtifactsRoot,
StdOut: pluginConfig.StdoutFileName,
StdErr: pluginConfig.StderrFileName,
IsAsync: true,
}
if _, _, err = util.ExeCommand(asyncCommandInput); err != nil {
output.MarkAsFailed(err)
return
}
output.MarkAsInProgress()
}
return
}
// TODO Create a command package for command execution
// generateSetupUpdateCmd generates cmd to setup the installation process
func (m *updateManager) generateSetupUpdateCmd(log log.T,
manifest *Manifest,
pluginInput *UpdatePluginInput,
region string,
updaterPath string,
messageID string,
agentVersion string) (cmd string, err error) {
cmd = updaterPath + SetupInstallCmd //Command sent to updater to setup the installation
source := ""
hash := ""
//Get download url and hash value from for the current version of ssm agent
if source, hash, err = manifest.DownloadURLAndHash(
EC2ConfigAgentName, agentVersion, EC2SetupFileName, S3Format, HTTPFormat, region); err != nil {
return
}
cmd = updateutil.BuildUpdateCommand(cmd, SourceVersionCmd, agentVersion)
cmd = updateutil.BuildUpdateCommand(cmd, SourceLocationCmd, source)
cmd = updateutil.BuildUpdateCommand(cmd, SourceHashCmd, hash)
//Get download url and hash value from for the target version of ssm agent
if source, hash, err = manifest.DownloadURLAndHash(
EC2ConfigAgentName, pluginInput.TargetVersion, EC2SetupFileName, S3Format, HTTPFormat, region); err != nil {
return
}
cmd = updateutil.BuildUpdateCommand(cmd, TargetVersionCmd, pluginInput.TargetVersion)
cmd = updateutil.BuildUpdateCommand(cmd, TargetLocationCmd, source)
cmd = updateutil.BuildUpdateCommand(cmd, TargetHashCmd, hash)
cmd = updateutil.BuildUpdateCommand(cmd, "-"+updateconstants.PackageNameCmd, EC2ConfigAgentName)
//messageID obtained from ssm is in the format = aws.ssm.{message-id}.{instance-id}. Parsing for use here
messageinfo := strings.Split(messageID, ".")
cmd = updateutil.BuildUpdateCommand(cmd, MessageIDCmd, messageID)
cmd = updateutil.BuildUpdateCommand(cmd, DocumentIDCmd, messageID)
cmd = updateutil.BuildUpdateCommand(cmd, HistoryCmd, numHistories)
appConfig := m.context.AppConfig()
cmd = updateutil.BuildUpdateCommand(cmd, MdsEndpointCmd, appConfig.Mds.Endpoint)
cmd = updateutil.BuildUpdateCommand(cmd, InstanceID, messageinfo[3])
cmd = updateutil.BuildUpdateCommand(cmd, RegionIDCmd, region)
user_agent := "EC2Config" + "/" + agentVersion
cmd = updateutil.BuildUpdateCommand(cmd, UserAgentCmd, user_agent)
cmd = cmd + UpdateHealthCmd //sends command to update health information after setting up installation
log.Debug("Setup installation command is ", cmd)
return
}
// generateUpdateCmd generates the command to perform update
func (m *updateManager) generateUpdateCmd(log log.T,
updaterPath string) (cmd string, err error) {
cmd = updaterPath + UpdateCmd //argument provided to the updater to perform update
cmd = updateutil.BuildUpdateCommand(cmd, HistoryCmd, numHistories)
appConfig := m.context.AppConfig()
cmd = updateutil.BuildUpdateCommand(cmd, MdsEndpointCmd, appConfig.Mds.Endpoint)
log.Debug("Update command is ", cmd)
return
}
// createUpdateDownloadFolder creates folder for storing update downloads
func createUpdateDownloadFolder() (folder string, err error) {
root := filepath.Join(appconfig.EC2UpdaterDownloadRoot, "update")
if err = mkDirAll(root, os.ModePerm|os.ModeDir); err != nil {
return "", err
}
return root, nil
}
// downloadManifest downloads manifest file from s3 bucket
func (m *updateManager) downloadManifest(context context.T,
util updateutil.T,
pluginInput *UpdatePluginInput,
info updateinfo.T,
out iohandler.IOHandler) (manifest *Manifest, err error) {
//Download source
var updateDownload = ""
if updateDownload, err = createUpdateDownloadFolder(); err != nil {
return nil, err
}
downloadInput := artifact.DownloadInput{
SourceURL: pluginInput.Source,
DestinationDirectory: updateDownload,
}
downloadOutput, downloadErr := fileDownload(context, downloadInput)
if downloadErr != nil ||
downloadOutput.IsHashMatched == false ||
downloadOutput.LocalFilePath == "" {
return nil, downloadErr
}
out.AppendInfof("Successfully downloaded %v", downloadInput.SourceURL)
return ParseManifest(context.Log(), downloadOutput.LocalFilePath)
}
// downloadUpdater downloads updater from the s3 bucket
func (m *updateManager) downloadUpdater(
context context.T,
region string,
updaterPackageName string,
manifest *Manifest,
out iohandler.IOHandler,
info updateinfo.T) (version string, err error) {
var hash = ""
var source = ""
var log = context.Log()
if version, err = manifest.LatestVersion(log, info); err != nil {
return
}
if source, hash, err = manifest.DownloadURLAndHash(EC2UpdaterPackageName, version, EC2UpdaterFileName, HTTPFormat, S3Format, region); err != nil {
return
}
var updateDownloadFolder = ""
if updateDownloadFolder, err = createUpdateDownloadFolder(); err != nil {
return
}
downloadInput := artifact.DownloadInput{
SourceURL: source,
SourceChecksums: map[string]string{
updateconstants.HashType: hash,
},
DestinationDirectory: updateDownloadFolder,
}
downloadOutput, downloadErr := fileDownload(context, downloadInput)
if downloadErr != nil ||
downloadOutput.IsHashMatched == false ||
downloadOutput.LocalFilePath == "" {
errMessage := fmt.Sprintf("failed to download file reliably, %v", downloadInput.SourceURL)
if downloadErr != nil {
errMessage = fmt.Sprintf("%v, %v", errMessage, downloadErr.Error())
}
// delete downloaded file, if it exists
pluginutil.CleanupFile(log, downloadOutput.LocalFilePath)
return version, errors.New(errMessage)
}
out.AppendInfof("Successfully downloaded %v", downloadInput.SourceURL)
if uncompressErr := fileUncompress(
log,
downloadOutput.LocalFilePath,
updateutil.UpdateArtifactFolder(appconfig.EC2UpdateArtifactsRoot, updaterPackageName, version)); uncompressErr != nil {
// delete downloaded file, if it exists
pluginutil.CleanupFile(log, downloadOutput.LocalFilePath)
return version, fmt.Errorf("failed to uncompress updater package, %v, %v",
downloadOutput.LocalFilePath,
uncompressErr.Error())
}
// delete downloaded file, if it exists
pluginutil.CleanupFile(log, downloadOutput.LocalFilePath)
return version, nil
}
// validateUpdate validates manifest against update request
func (m *updateManager) validateUpdate(log log.T,
pluginInput *UpdatePluginInput,
info updateinfo.T,
manifest *Manifest,
out iohandler.IOHandler, currentVersion string) (noNeedToUpdate bool, err error) {
var allowDowngrade = false
if len(pluginInput.TargetVersion) == 0 {
if pluginInput.TargetVersion, err = manifest.LatestVersion(log, info); err != nil {
return true, err
}
}
if allowDowngrade, err = strconv.ParseBool(pluginInput.AllowDowngrade); err != nil {
return true, err
}
var versionCmpVal int
if versionCmpVal, err = versionutil.VersionCompare(pluginInput.TargetVersion, currentVersion); err != nil {
return true, err
}
if versionCmpVal == 0 {
out.AppendInfof("%v %v has already been installed, update skipped",
pluginInput.AgentName,
currentVersion)
out.MarkAsSucceeded()
return true, nil
}
if versionCmpVal < 0 && !allowDowngrade {
return true,
fmt.Errorf(
"updating %v to an older version, please enable allow downgrade to proceed",
pluginInput.AgentName)
}
if !manifest.HasVersion(info, pluginInput.TargetVersion) {
return true,
fmt.Errorf(
"%v version %v is unsupported",
pluginInput.AgentName,
pluginInput.TargetVersion)
}
if !manifest.HasVersion(info, currentVersion) {
return true,
fmt.Errorf(
"%v current version %v is unsupported on current platform",
pluginInput.AgentName,
currentVersion)
}
return false, nil
}
// TODO Make common methods go into utility/helper/common package. Check if Execute can be added to that package
// Execute runs multiple sets of commands and returns their outputs.
func (p *Plugin) Execute(config contracts.Configuration, cancelFlag task.CancelFlag, output iohandler.IOHandler) {
log := p.context.Log()
log.Info("RunCommand started with update configuration for EC2 config update ", config)
util := new(updateutil.Utility)
util.Context = p.context
manager := &updateManager{
p.context,
}
if cancelFlag.ShutDown() {
output.MarkAsShutdown()
} else if cancelFlag.Canceled() {
output.MarkAsCancelled()
} else {
updateAgent(p,
config,
p.context,
manager,
util,
config.Properties,
output,
time.Now())
}
return
}
// Name returns the plugin name
func Name() string {
return appconfig.PluginEC2ConfigUpdate
}
// GetUpdatePluginConfig returns the default values for the update plugin
func GetUpdatePluginConfig(context context.T) UpdatePluginConfig {
var manifestURL string
dynamicS3Endpoint := context.Identity().GetServiceEndpoint("s3")
if dynamicS3Endpoint != "" {
manifestURL = "https://" + dynamicS3Endpoint + ManifestPath
} else {
region, _ := context.Identity().Region()
if strings.HasPrefix(region, s3util.ChinaRegionPrefix) {
manifestURL = ChinaManifestURL
} else {
manifestURL = CommonManifestURL
}
}
return UpdatePluginConfig{
ManifestLocation: manifestURL,
}
}
func validateSource(pluginInput UpdatePluginInput) (err error) {
//This pattern allows for http/https links to local hosts and s3 buckets
validSourceValue := sourcePattern
if !validSourceValue.MatchString(pluginInput.Source) {
return errors.New("Invalid source")
}
return err
}