agent/plugins/inventory/inventory.go (528 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 inventory contains implementation of aws:softwareInventory plugin
package inventory
import (
"encoding/json"
"fmt"
"path/filepath"
"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/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/inventory/datauploader"
"github.com/aws/amazon-ssm-agent/agent/plugins/inventory/gatherers"
"github.com/aws/amazon-ssm-agent/agent/plugins/inventory/gatherers/application"
"github.com/aws/amazon-ssm-agent/agent/plugins/inventory/gatherers/awscomponent"
"github.com/aws/amazon-ssm-agent/agent/plugins/inventory/gatherers/billinginfo"
"github.com/aws/amazon-ssm-agent/agent/plugins/inventory/gatherers/custom"
"github.com/aws/amazon-ssm-agent/agent/plugins/inventory/gatherers/file"
"github.com/aws/amazon-ssm-agent/agent/plugins/inventory/gatherers/instancedetailedinformation"
"github.com/aws/amazon-ssm-agent/agent/plugins/inventory/gatherers/network"
"github.com/aws/amazon-ssm-agent/agent/plugins/inventory/gatherers/registry"
"github.com/aws/amazon-ssm-agent/agent/plugins/inventory/gatherers/role"
"github.com/aws/amazon-ssm-agent/agent/plugins/inventory/gatherers/service"
"github.com/aws/amazon-ssm-agent/agent/plugins/inventory/gatherers/windowsUpdate"
"github.com/aws/amazon-ssm-agent/agent/plugins/inventory/model"
"github.com/aws/amazon-ssm-agent/agent/sdkutil"
"github.com/aws/amazon-ssm-agent/agent/task"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/ssm"
)
// TODO: add more unit tests.
var fileutilExists = fileutil.Exists
var fileutilReadAllText = fileutil.ReadAllText
const (
errorMsgForMultipleAssociations = "%v detected multiple inventory configurations associated with one instance. Each instance can be associated with just one inventory configuration. Conflicting inventory configuration IDs: %v and %v"
errorMsgForInvalidInventoryInput = "invalid or unrecognized input was received for %v plugin"
errorMsgForExecutingInventoryViaAssociate = "%v plugin can only be invoked via ssm-associate"
errorMsgForUnableToDetectInvocationType = "it could not be detected if %v plugin was invoked via ssm-associate because - %v"
errorMsgForInabilityToSendDataToSSM = "inventory data could not be uploaded to Systems Manager. Additional troubleshooting information - %v"
errorMsgForInabilityToSendFileDataToSSM = "File inventory data could not be uploaded to Systems Manager. Additional troubleshooting information - %v"
msgWhenNoDataToReturnForInventoryPlugin = "Inventory policy has been successfully applied but there is no inventory data to upload to SSM"
successfulMsgForInventoryPlugin = "Inventory policy has been successfully applied and collected inventory data has been uploaded to SSM"
largeSizeItem = 1024 * 1024 //1MB
fileInventoryItemName = "AWS:File"
)
// PluginInput represents configuration which is applied to inventory plugin during execution.
type PluginInput struct {
contracts.PluginInput
Applications string
AWSComponents string
NetworkConfig string
BillingInfo string
Files string
WindowsRoles string
Services string
WindowsRegistry string
WindowsUpdates string
InstanceDetailedInformation string
CustomInventory string
CustomInventoryDirectory string
}
// Plugin encapsulates the logic of configuring, starting and stopping inventory plugin
type Plugin struct {
context context.T
stopPolicy *sdkutil.StopPolicy
//supportedGatherers is a map of all inventory gatherers supported by current OS
// (e.g. WindowsUpdateGatherer is not included when running on Unix based systems)
supportedGatherers gatherers.SupportedGatherer
//installedGatherers is a map of gatherers that can run on all platforms
installedGatherers gatherers.InstalledGatherer
//uploader handles uploading inventory data to SSM.
uploader datauploader.T
// machineID of the machine where agent is running - useful during command detection
machineID string
}
// Name returns the plugin name
func Name() string {
return appconfig.PluginNameAwsSoftwareInventory
}
// NewPlugin creates a new inventory worker plugin.
func NewPlugin(context context.T) (*Plugin, error) {
var err error
var p = Plugin{}
c := context.With("[" + Name() + "]")
log := c.Log()
//get machineID - return if not able to detect machineID
if p.machineID, err = context.Identity().InstanceID(); err != nil {
err = fmt.Errorf("Unable to detect machineID because of %v - this will hamper execution of inventory plugin",
err.Error())
return &p, err
}
p.context = c
p.stopPolicy = sdkutil.NewStopPolicy(Name(), model.ErrorThreshold)
//loads all registered gatherers (for now only a dummy application gatherer is loaded in memory)
p.supportedGatherers, p.installedGatherers = gatherers.InitializeGatherers(p.context)
//initializes SSM Inventory uploader
if p.uploader, err = datauploader.NewInventoryUploader(c); err != nil {
err = log.Errorf("Unable to configure SSM Inventory uploader - %v", err.Error())
}
return &p, err
}
// ApplyInventoryPolicy applies given inventory policy regarding which gatherers to run
func (p *Plugin) ApplyInventoryPolicy(inventoryInput PluginInput, output iohandler.IOHandler) {
log := p.context.Log()
var optimizedInventoryItems, nonOptimizedInventoryItems []*ssm.InventoryItem
var items []model.Item
var err error
var uploadFlag bool
//map of all valid gatherers & respective configs to run
var gatherers map[gatherers.T]model.Config
//validate all gatherers
if gatherers, err = p.ValidateInventoryInput(p.context, inventoryInput); err != nil {
log.Info(err.Error())
output.SetExitCode(1)
output.AppendError(err.Error())
return
}
//execute all eligible gatherers with their respective config
if items, err = p.RunGatherers(gatherers); err != nil {
log.Info(err.Error())
output.SetExitCode(1)
output.AppendError(err.Error())
return
}
//check if there is data to send to SSM
if len(items) == 0 {
//no data to send to ssm - no need to call PutInventory API
log.Info(msgWhenNoDataToReturnForInventoryPlugin)
output.SetExitCode(0)
output.AppendInfo(msgWhenNoDataToReturnForInventoryPlugin)
return
}
//log collected data before sending
d, _ := json.Marshal(items)
log.Debugf("Collected Inventory data: %v", string(d))
if optimizedInventoryItems, nonOptimizedInventoryItems, err = p.uploader.ConvertToSsmInventoryItems(items); err != nil {
log.Infof("Encountered error in converting data to SSM InventoryItems - %v. Skipping upload to SSM", err.Error())
output.SetExitCode(1)
output.AppendError(err.Error())
return
}
log.Debugf("Optimized data - \n%v \n Non-optimized data - \n%v",
optimizedInventoryItems,
nonOptimizedInventoryItems)
// uploadItemsToSSM uploads collected inventory data to SSM and returns true if the upload was successful
// else returns false.
if uploadFlag = p.uploadItemsToSSM(nonOptimizedInventoryItems, optimizedInventoryItems, output); uploadFlag != true {
output.SetExitCode(1)
return
}
log.Infof("%v uploaded inventory data to SSM", Name())
output.SetExitCode(0)
output.AppendInfo(successfulMsgForInventoryPlugin)
return
}
// uploadItemsToSSM uploads inventory data to SSM and returns boolean flag based on whether upload was successful or not.
func (p *Plugin) uploadItemsToSSM(nonOptimizedInventoryItems []*ssm.InventoryItem,
optimizedInventoryItems []*ssm.InventoryItem, output iohandler.IOHandler) bool {
/*
In order to optimize PutInventory calls to SSM, we use following algo:
if collected data is < 1 MB, we send all data in 1 API call.
if collected data is > 1 MB and has AWS:File data in it, we make multiple PutInventory calls with different data-sets:
1st call - with just AWS:File data
2nd call - with all other collected data.
*/
var err error
log := p.context.Log()
var inventoryItemIndex int
dataUploadStatus := true
var optimizedFileItems, nonOptimizedFileItems, optimizedNonFileItems, nonOptimizedNonFileItems []*ssm.InventoryItem
optimizedNonFileItems = optimizedInventoryItems
nonOptimizedNonFileItems = nonOptimizedInventoryItems
inventoryItemIndex, err = p.getLargeItemIndex(nonOptimizedInventoryItems, fileInventoryItemName)
log.Debugf("inventoryItemIndex %v", inventoryItemIndex)
if err != nil {
log.Errorf("Encountered error. Skipping upload to SSM %v", err)
output.AppendError(err.Error())
return false
}
// inventoryItemIndex is the index of the AWS:File item in the optimizedInventoryItems list,
// Default value -1 indicates we're not splitting calls and uploading all data in one putInventory api call.
if inventoryItemIndex != -1 {
nonOptimizedFileItems, optimizedFileItems, nonOptimizedNonFileItems, optimizedNonFileItems =
extractFileItems(nonOptimizedInventoryItems, optimizedInventoryItems, inventoryItemIndex)
// uploading AWS:File inventory data.
if err = p.uploadDataToSSM(nonOptimizedFileItems, optimizedFileItems, output); err != nil {
log.Errorf("Encountered error %v. Skip uploading %v to SSM", err, fileInventoryItemName)
dataUploadStatus = false
message := fmt.Sprintf(errorMsgForInabilityToSendFileDataToSSM, err.Error())
log.Info(message)
output.AppendError(message)
} else {
log.Debugf("uploaded File inventory data to SSM")
}
}
// uploading non-file inventory data
if err = p.uploadDataToSSM(nonOptimizedNonFileItems, optimizedNonFileItems, output); err != nil {
log.Errorf("error uploading inventory data %v", err)
dataUploadStatus = false
message := fmt.Sprintf(errorMsgForInabilityToSendDataToSSM, err.Error())
log.Info(message)
output.AppendError(message)
} else {
log.Debugf("uploaded inventory data to SSM")
}
return dataUploadStatus
}
// extractFileItems returns copies of optimized and non-optimized items list after removing File Item from it.
func extractFileItems(nonOptimizedInventoryItems, optimizedInventoryItems []*ssm.InventoryItem,
ItemIndex int) (nonOptimizedFileData,
optimizedFileData, nonOptimizedNonFileData,
optimizedNonFileData []*ssm.InventoryItem) {
// removing FileItem from the optimizedInventoryItems list based on it's index.
optimizedNewInventoryItem := optimizedInventoryItems[ItemIndex]
optimizedFileData = append(optimizedFileData, optimizedNewInventoryItem)
// Adjusting optimizedInventoryItems after removing FileItem
copy(optimizedInventoryItems[ItemIndex:], optimizedInventoryItems[ItemIndex+1:])
optimizedInventoryItems[len(optimizedInventoryItems)-1] = nil
optimizedNonFileData = optimizedInventoryItems[:len(optimizedInventoryItems)-1]
// removing FileItem from the NonOptimizedInventoryItems list.
nonOptimizedNewInventoryItem := nonOptimizedInventoryItems[ItemIndex]
nonOptimizedFileData = append(nonOptimizedFileData, nonOptimizedNewInventoryItem)
// Adjusting nonOptimizedInventoryItems after removing FileItem
copy(nonOptimizedInventoryItems[ItemIndex:], nonOptimizedInventoryItems[ItemIndex+1:])
nonOptimizedInventoryItems[len(nonOptimizedInventoryItems)-1] = nil
nonOptimizedNonFileData = nonOptimizedInventoryItems[:len(nonOptimizedInventoryItems)-1]
return
}
// uploadDataToSSM uploads inventory data to SSM. First it tries to upload with optimizedInventoryItems
// If that fails, it retries upload to SSM with the nonOptimizedInventoryItems.
func (p *Plugin) uploadDataToSSM(nonOptimizedInventoryItems []*ssm.InventoryItem,
optimizedInventoryItems []*ssm.InventoryItem, output iohandler.IOHandler) error {
var err error
log := p.context.Log()
//first send data in optimized fashion
if err = p.uploader.SendDataToSSM(optimizedInventoryItems); err != nil {
if shouldRetryWithNonOptimizedData(err, log) {
//call putinventory again with non-optimized dataset
if err = p.uploader.SendDataToSSM(nonOptimizedInventoryItems); err != nil {
//sending non-optimized data also failed
return err
}
} else {
//some other error happened for which there is no need to retry - upload failed
return err
}
}
return err
}
// getLargeItemIndex returns index of the inventoryItem if inventoryItem is present in nonOptimizedInventoryItems
// If not it returns default -1.
func (p *Plugin) getLargeItemIndex(nonOptimizedInventoryItems []*ssm.InventoryItem, itemName string) (int, error) {
log := p.context.Log()
itemIndexToReturn := -1
nonOptimizedInventoryItemsCheck, err := json.Marshal(nonOptimizedInventoryItems)
if err != nil {
log.Debugf("internal error: JSON marshaling failed: %v", err)
return -1, err
}
//calculate size of the nonOptimizedInventoryItems
nonOptimizedInventoryItemsSize := float32(len(nonOptimizedInventoryItemsCheck))
log.Debugf("nonOptimizedInventoryItemsSize is %v", nonOptimizedInventoryItemsSize)
largeItemCheck := nonOptimizedInventoryItemsSize > largeSizeItem
for applicationIndex, application := range nonOptimizedInventoryItems {
// Return index for the given itemName in the optimizedInventoryItems list, given it meets
// the condition that size of items list > 1MB and itemName is present in optimizedInventoryItems.
if *application.TypeName == itemName && largeItemCheck && len(nonOptimizedInventoryItems) > 1 {
itemIndexToReturn = applicationIndex
}
}
// Return index as -1 if it doesn't meet the condition check, meaning we would not split the call
// and go with 1 putInventory call for all items.
log.Debugf("Returning index as %v", itemIndexToReturn)
return itemIndexToReturn, err
}
// ApplyInventoryFrequentCollector applies frequent collector regarding which gatherers to run
func (p Plugin) ApplyInventoryFrequentCollector(gatherers map[gatherers.T]model.Config, output iohandler.IOHandler) {
log := p.context.Log()
var dirtyItems []*ssm.InventoryItem
var items []model.Item
var err error
//execute all specified gatherers with their respective config
if items, err = p.RunGatherers(gatherers); err != nil {
log.Debugf("failed at RunGatherers, error : %#v", err)
log.Info(err.Error())
output.SetExitCode(1)
output.AppendError(err.Error())
return
}
//check if there is data to send to SSM
if len(items) == 0 {
//no data to send to ssm - no need to call PutInventory API
log.Info(msgWhenNoDataToReturnForInventoryPlugin)
output.SetExitCode(0)
output.AppendInfo(msgWhenNoDataToReturnForInventoryPlugin)
return
}
if dirtyItems, err = p.uploader.GetDirtySsmInventoryItems(items); err != nil {
log.Debugf("Encountered error in collecting dirty Inventory items - %#v. Skipping upload to SSM", err.Error())
output.SetExitCode(1)
output.AppendError(err.Error())
return
}
if len(dirtyItems) == 0 {
log.Debugf("No dirty inventory items found, skipping uploading")
log.Info(msgWhenNoDataToReturnForInventoryPlugin)
output.SetExitCode(0)
output.AppendInfo(msgWhenNoDataToReturnForInventoryPlugin)
return
}
if err = p.uploader.SendDataToSSM(dirtyItems); err != nil {
//some other error happened for which there is no need to retry - upload failed
log.Debugf(" Error happened while p.uploader.SendDataToSSM")
propagateSSMError(output, err, log)
return
}
log.Infof("%v uploaded inventory data from frequent collector to SSM", Name())
output.SetExitCode(0)
output.AppendInfo(successfulMsgForInventoryPlugin)
return
}
// shouldRetryWithNonOptimizedData will return true if the Exception occurred is one of ItemContentMismatchException
// or InvalidItemContentException and will retry sending data to SSM. It will return false, if any other error occurs.
func shouldRetryWithNonOptimizedData(err error, log log.T) bool {
awsErr, isAwsError := err.(awserr.Error)
if isAwsError {
//NOTE: awsErr.Code -> is not the error code but the exception name itself!!!!
if awsErr.Code() == "ItemContentMismatchException" || awsErr.Code() == "InvalidItemContentException" {
log.Debugf("%v encountered - inventory plugin will try sending data again", awsErr.Code())
return true
}
}
log.Debugf("Unexpected error encountered - %v. No point trying to send data again", err.Error())
return false
}
func propagateSSMError(output iohandler.IOHandler, err error, log log.T) {
message := fmt.Sprintf(errorMsgForInabilityToSendDataToSSM, err.Error())
log.Info(message)
output.SetExitCode(1)
output.AppendError(message)
}
func (p *Plugin) GetSupportedGatherer(gatherName string) (gatherers.T, bool) {
if gatherer, gathererPresent := p.supportedGatherers[gatherName]; gathererPresent {
return gatherer, true
}
return nil, false
}
// CanGathererRun returns true if the gatherer can run on given OS, else it returns false. It throws error if the
// gatherer is not recognized.
func (p *Plugin) CanGathererRun(context context.T, name string) (status bool, gatherer gatherers.T, err error) {
log := context.Log()
var isGathererSupported, isGathererInstalled bool
if gatherer, isGathererSupported = p.supportedGatherers[name]; !isGathererSupported {
if _, isGathererInstalled = p.installedGatherers[name]; isGathererInstalled {
log.Infof("%v inventory gatherer is installed but not supported to run on this platform", name)
status = false
} else {
err = fmt.Errorf("inventory gatherer - %v is not installed", name)
}
} else {
log.Infof("%v inventory gatherer is supported to run on this platform", name)
status = true
}
return
}
func (p *Plugin) validatePredefinedGatherer(context context.T, collectionPolicy, gathererName string) (status bool, gatherer gatherers.T, policy model.Config, err error) {
if collectionPolicy == model.Enabled {
if status, gatherer, err = p.CanGathererRun(context, gathererName); err != nil {
return
}
// check if gatherer can run - if not then no need to set policy
if status {
policy = model.Config{Collection: collectionPolicy}
}
}
return
}
func (p *Plugin) validateGathererWithFilters(context context.T, collectionPolicy, gathererName string, filters string) (status bool, gatherer gatherers.T, policy model.Config, err error) {
if filters != "" {
if status, gatherer, err = p.CanGathererRun(context, gathererName); err != nil {
return
}
if status {
policy = model.Config{Collection: collectionPolicy, Filters: filters}
}
}
return
}
func (p *Plugin) validateCustomGatherer(context context.T, collectionPolicy, location string) (status bool, gatherer gatherers.T, policy model.Config, err error) {
if collectionPolicy == model.Enabled {
if status, gatherer, err = p.CanGathererRun(context, custom.GathererName); err != nil {
return
}
// check if gatherer can run - if not then no need to set policy
if status {
policy = model.Config{Collection: collectionPolicy, Location: location}
}
}
return
}
// ValidateInventoryInput validates inventory input and returns a map of eligible gatherers & their corresponding config.
// It throws an error if gatherer is not recognized/installed.
func (p *Plugin) ValidateInventoryInput(context context.T, input PluginInput) (configuredGatherers map[gatherers.T]model.Config, err error) {
var dataB []byte
var canGathererRun bool
var gatherer gatherers.T
var cfg model.Config
configuredGatherers = make(map[gatherers.T]model.Config)
log := context.Log()
dataB, _ = json.Marshal(input)
log.Debugf("Validating gatherers from inventory input - \n%v", jsonutil.Indent(string(dataB)))
predefinedGatherers := map[string]string{
application.GathererName: input.Applications,
awscomponent.GathererName: input.AWSComponents,
role.GathererName: input.WindowsRoles,
service.GathererName: input.Services,
network.GathererName: input.NetworkConfig,
billinginfo.GathererName: input.BillingInfo,
windowsUpdate.GathererName: input.WindowsUpdates,
instancedetailedinformation.GathererName: input.InstanceDetailedInformation,
}
predefinedGatherersWithFilters := map[string]string{
file.GathererName: input.Files,
registry.GathererName: input.WindowsRegistry,
}
//NOTE:
// If the gatherer is installed but not supported by current platform, we will skip that gatherer. If the
// gatherer is not installed, we error out & don't send the data collected from other supported gatherers
// - this is because we don't send partial inventory data as part of 1 inventory policy.
for gathererName, collectionPolicy := range predefinedGatherers {
if canGathererRun, gatherer, cfg, err = p.validatePredefinedGatherer(context, collectionPolicy, gathererName); err != nil {
log.Errorf("Error while validating gatherer %v", err.Error())
return
} else if canGathererRun {
configuredGatherers[gatherer] = cfg
}
}
for gathererName, filters := range predefinedGatherersWithFilters {
if canGathererRun, gatherer, cfg, err = p.validateGathererWithFilters(context, "", gathererName, filters); err != nil {
log.Errorf("Error while validating gatherer %v", err.Error())
return
} else if canGathererRun {
configuredGatherers[gatherer] = cfg
}
}
//checking custom gatherer
if canGathererRun, gatherer, cfg, err = p.validateCustomGatherer(context, input.CustomInventory, input.CustomInventoryDirectory); err != nil {
log.Errorf("Error while validating gatherer %v", err.Error())
return
} else if canGathererRun {
configuredGatherers[gatherer] = cfg
}
return
}
// RunGatherers execute given array of gatherers and accordingly returns. It returns error if gatherer is not
// registered or if at any stage the data returned breaches size limit
func (p *Plugin) RunGatherers(gatherers map[gatherers.T]model.Config) (items []model.Item, err error) {
//NOTE: Currently all gatherers will be invoked in synchronous & sequential fashion.
//Parallel execution of gatherers hinges upon inventory plugin becoming a long running plugin - which will be
//mainly for custom inventory gatherer to send data independently of associate.
var gItems []model.Item
log := p.context.Log()
for gatherer, config := range gatherers {
name := gatherer.Name()
log.Infof("Invoking gatherer - %v", name)
start := time.Now()
if gItems, err = gatherer.Run(p.context, config); err != nil {
err = fmt.Errorf("Encountered error while executing %v. Error - %v", name, err.Error())
break
} else {
elapsed := time.Since(start)
log.Infof("execution time for gatherer - %v: %s", name, elapsed)
items = append(items, gItems...)
//TODO: Each gatherer shall check each item's size and stop collecting if size exceed immediately
//TODO: only check the total item size at this function, whenever total size exceed, stop
//TODO: immediately and raise association error
//return error if collected data breaches size limit
for _, v := range gItems {
if !p.VerifyInventoryDataSize(v, items) {
err = log.Errorf("the size of the collected data exceeded the maximum allowable size")
break
}
}
}
}
return
}
// VerifyInventoryDataSize returns true if size of collected inventory data is within size restrictions placed by SSM,
// else false.
func (p *Plugin) VerifyInventoryDataSize(item model.Item, items []model.Item) bool {
var itemSize, itemsSize float32
log := p.context.Log()
//calculating sizes
itemB, _ := json.Marshal(item)
itemSize = float32(len(itemB))
log.Infof("Size (Bytes) of %v - %v", item.Name, itemSize)
log.Debugf("Size (Bytes) of %v - %v", item.Name, itemSize)
itemsSizeB, _ := json.Marshal(items)
itemsSize = float32(len(itemsSizeB))
log.Debugf("Total size (Bytes) of inventory items after including %v - %v", item.Name, itemsSize)
//Refer to https://wiki.ubuntu.com/UnitsPolicy regarding KiB to bytes conversion.
//TODO: 200 KB limit might be too less for certain inventory types like Patch - we might have to revise that and
//use different limits for different category.
if (itemSize/1024) > model.SizeLimitKBPerInventoryType || (itemsSize/1024) > model.TotalSizeLimitKB {
return false
}
return true
}
// IsMulitpleAssociationPresent returns true if there are multiple associations for inventory plugin else it returns false.
func (p *Plugin) IsMulitpleAssociationPresent(currentAssociationID string, config contracts.Configuration) (status bool, othersfound string) {
var currentInventoryAssociations []string
err := jsonutil.Remarshal(config.CurrentAssociations, ¤tInventoryAssociations)
if err != nil {
p.context.Log().Errorf("failed to remarshal plugin settings: %v", err)
return false, ""
}
//test whether other associations are attached right now
for _, assocID := range currentInventoryAssociations {
if assocID != currentAssociationID {
return true, assocID
}
}
return false, ""
}
// IsInventoryBeingInvokedAsAssociation returns true if inventory plugin is invoked via ssm-associate or else it returns false.
// It throws error if the detection itself fails
func (p *Plugin) IsInventoryBeingInvokedAsAssociation(fileName string) (status bool, err error) {
var content string
var docState contracts.DocumentState
log := p.context.Log()
//since the document is still getting executed - it must be in Current folder
path := filepath.Join(appconfig.DefaultDataStorePath,
p.machineID,
appconfig.DefaultDocumentRootDirName,
appconfig.DefaultLocationOfState,
appconfig.DefaultLocationOfCurrent)
absPathOfDoc := filepath.Join(path, fileName)
//read file & then determine if document is of association type
if fileutilExists(absPathOfDoc) {
log.Debugf("Found the document that's executing inventory plugin - %v", absPathOfDoc)
//read file
if content, err = fileutilReadAllText(absPathOfDoc); err == nil {
if err = json.Unmarshal([]byte(content), &docState); err == nil {
status = docState.IsAssociation()
}
}
} else {
err = fmt.Errorf("Inventory plugin could not locate the execution document which invoked it. The document is expected to be in the location - %v", absPathOfDoc)
}
return
}
// ParseAssociationIdFromFileName parses associationID from the given input
// NOTE: Input will be of format - AssociationID.RunID -> as per the format of bookkeepingfilename for associate documents
func (p *Plugin) ParseAssociationIdFromFileName(input string) string {
return strings.Split(input, ".")[0]
}
// WorkerConfig plugin implementation
// Execute runs the inventory plugin
func (p *Plugin) Execute(config contracts.Configuration, cancelFlag task.CancelFlag, output iohandler.IOHandler) {
log := p.context.Log()
var errorMsg, associationID string
var dataB []byte
var err error
var isAssociation bool
var inventoryInput PluginInput
pluginName := Name()
dataB, _ = json.Marshal(config)
log.Debugf("Starting %v with configuration \n%v", pluginName, jsonutil.Indent(string(dataB)))
//TODO: take care of cancel flag (SSM-INV-233)
associationID = p.ParseAssociationIdFromFileName(config.BookKeepingFileName)
// Check if the inventory plugin is being invoked as association, if not or if detection fails for some reason,
// then fail association - because inventory plugin currently supports invocation via ssm associate only.
if isAssociation, err = p.IsInventoryBeingInvokedAsAssociation(config.BookKeepingFileName); err != nil || !isAssociation {
if err != nil {
errorMsg = fmt.Sprintf(errorMsgForUnableToDetectInvocationType, pluginName, err.Error())
} else {
errorMsg = fmt.Sprintf(errorMsgForExecutingInventoryViaAssociate, pluginName)
}
log.Error(errorMsg)
//setting up plugin output
output.SetExitCode(1)
output.SetStatus(contracts.ResultStatusFailed)
output.AppendError(errorMsg)
return
}
log.Debugf("%v plugin is being invoked via ssm-associate - proceeding ahead with execution", pluginName)
// Check if there exists multiple associations for software inventory plugin, if so - then fail association - because
// inventory plugin supports single association only.
// NOTE: as per contract with associate functionality - bookkeepingfilename will always contain associationId.
// bookkeepingfilename will be of format - associationID.RunID for associations, for command it will simply be commandID
if status, extraAssociationId := p.IsMulitpleAssociationPresent(associationID, config); status {
errorMsg = fmt.Sprintf(errorMsgForMultipleAssociations,
pluginName,
associationID,
extraAssociationId)
log.Error(errorMsg)
//setting up plugin output
output.SetExitCode(1)
output.SetStatus(contracts.ResultStatusFailed)
output.AppendError(errorMsg)
return
}
//loading Properties as map since aws:softwareInventory gets configuration in form of map
log.Infof("config.Properties %v", config.Properties)
if dataB, err = json.Marshal(config.Properties); err != nil {
errorMsg = fmt.Sprintf("Unable to marshal plugin input to %v due to %v", pluginName, err.Error())
log.Error(errorMsg)
//setting up plugin output
output.SetExitCode(1)
output.SetStatus(contracts.ResultStatusFailed)
output.AppendError(errorMsg)
return
}
if err = json.Unmarshal(dataB, &inventoryInput); err != nil {
errorMsg = fmt.Sprintf(errorMsgForInvalidInventoryInput, pluginName)
log.Error(errorMsg)
//setting up plugin output
output.SetExitCode(1)
output.SetStatus(contracts.ResultStatusFailed)
output.AppendError(errorMsg)
return
}
dataB, _ = json.Marshal(inventoryInput)
log.Infof("Inventory configuration after parsing - %v", string(dataB))
p.ApplyInventoryPolicy(inventoryInput, output)
//check inventory plugin output
if output.GetExitCode() != 0 {
log.Debugf("Execution of %v failed with configuration - %v because of - %v", pluginName, config, output.GetStderr())
output.SetStatus(contracts.ResultStatusFailed)
} else {
log.Debugf("Execution of %v was successful with configuration - %v with output - %v", pluginName, config, output.GetStdout())
output.SetStatus(contracts.ResultStatusSuccess)
}
return
}