// Copyright 2017 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/ // // Unless required by applicable law or agreed to in writing, software // distributed under the License 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 birdwatcherservice import ( "errors" "fmt" "reflect" "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/fileutil/artifact" "github.com/aws/amazon-ssm-agent/agent/plugins/configurepackage/birdwatcher" "github.com/aws/amazon-ssm-agent/agent/plugins/configurepackage/birdwatcher/archive" "github.com/aws/amazon-ssm-agent/agent/plugins/configurepackage/birdwatcher/birdwatcherarchive" "github.com/aws/amazon-ssm-agent/agent/plugins/configurepackage/birdwatcher/documentarchive" "github.com/aws/amazon-ssm-agent/agent/plugins/configurepackage/birdwatcher/facade" "github.com/aws/amazon-ssm-agent/agent/plugins/configurepackage/envdetect" "github.com/aws/amazon-ssm-agent/agent/plugins/configurepackage/packageservice" "github.com/aws/amazon-ssm-agent/agent/plugins/configurepackage/trace" "github.com/aws/aws-sdk-go/service/ssm" ) // NanoTime is helper interface for mocking time type NanoTime interface { NowUnixNano() int64 } type TimeImpl struct { } func (t *TimeImpl) NowUnixNano() int64 { return time.Now().UnixNano() } // PackageService is the concrete type for Birdwatcher PackageService type PackageService struct { Context context.T pkgSvcName string facadeClient facade.BirdwatcherFacade manifestCache packageservice.ManifestCache collector envdetect.Collector timeProvider NanoTime packageArchive archive.IPackageArchive } func NewBirdwatcherArchive(ctx context.T, facadeClient facade.BirdwatcherFacade, manifestCache packageservice.ManifestCache, context map[string]string) packageservice.PackageService { pkgArchive := birdwatcherarchive.New(facadeClient, context) pkgArchive.SetManifestCache(manifestCache) return New(ctx, pkgArchive, facadeClient, manifestCache, packageservice.PackageServiceName_birdwatcher) } func NewDocumentArchive(context context.T, facadeClient facade.BirdwatcherFacade, manifestCache packageservice.ManifestCache) packageservice.PackageService { pkgArchive := documentarchive.New(facadeClient) pkgArchive.SetManifestCache(manifestCache) return New(context, pkgArchive, facadeClient, manifestCache, packageservice.PackageServiceName_document) } // New constructor for PackageService func New(context context.T, pkgArchive archive.IPackageArchive, facadeClient facade.BirdwatcherFacade, manifestCache packageservice.ManifestCache, name string) packageservice.PackageService { return &PackageService{ Context: context, pkgSvcName: name, facadeClient: facadeClient, manifestCache: manifestCache, collector: &envdetect.CollectorImp{}, timeProvider: &TimeImpl{}, packageArchive: pkgArchive, } } func (ds *PackageService) PackageServiceName() string { return ds.pkgSvcName } func (ds *PackageService) GetPackageArnAndVersion(packageName string, packageVersion string) (name string, version string) { return ds.packageArchive.GetResourceVersion(packageName, packageVersion) } // DownloadManifest downloads the manifest for a given version (or latest) and returns the agent version specified in manifest func (ds *PackageService) DownloadManifest(tracer trace.Tracer, packageName string, version string) (string, string, bool, error) { manifest, isSameAsCache, err := downloadManifest(tracer, ds, packageName, version) if err != nil { return "", "", isSameAsCache, err } return ds.packageArchive.GetResourceArn(packageName, version), manifest.Version, isSameAsCache, nil } // DownloadArtifact downloads the platform matching artifact specified in the manifest func (ds *PackageService) DownloadArtifact(tracer trace.Tracer, packageName string, version string) (string, error) { trace := tracer.BeginSection("download artifact") defer trace.End() file, getManifestErr := getFileFromManifest(ds, packageName, version, trace, tracer) if getManifestErr != nil { return "", getManifestErr } return downloadFile(ds, tracer, file, packageName, version, false) } // ReportResult sents back the result of the install/upgrade/uninstall run back to Birdwatcher func (ds *PackageService) ReportResult(tracer trace.Tracer, result packageservice.PackageResult) error { env, err := ds.collector.CollectData(ds.Context) if err != nil { return fmt.Errorf("failed to collect data to report results: %v", err) } var previousPackageVersion *string if result.PreviousPackageVersion != "" { previousPackageVersion = &result.PreviousPackageVersion } var steps []*ssm.ConfigurePackageResultStep for _, t := range result.Trace { timing := (t.Timing - result.Timing) / 1000000 // converting nano to miliseconds steps = append(steps, &ssm.ConfigurePackageResultStep{ Action: &t.Operation, Result: &t.Exitcode, Timing: &timing, }) } overallTiming := (ds.timeProvider.NowUnixNano() - result.Timing) / 1000000 input := &ssm.PutConfigurePackageResultInput{ PackageName: &result.PackageName, PackageVersion: &result.Version, PreviousPackageVersion: previousPackageVersion, Operation: &result.Operation, OverallTiming: &overallTiming, Result: &result.Exitcode, Attributes: map[string]*string{ "platformName": &env.OperatingSystem.Platform, "platformVersion": &env.OperatingSystem.PlatformVersion, "architecture": &env.OperatingSystem.Architecture, "instanceID": &env.Ec2Infrastructure.InstanceID, "instanceType": &env.Ec2Infrastructure.InstanceType, "region": &env.Ec2Infrastructure.Region, "availabilityZone": &env.Ec2Infrastructure.AvailabilityZone, }, Steps: steps, } _, err = ds.facadeClient.PutConfigurePackageResult(input) if err != nil { return fmt.Errorf("failed to report results: %v", err) } return nil } // utils func downloadManifest(tracer trace.Tracer, ds *PackageService, packageName string, version string) (*birdwatcher.Manifest, bool, error) { isSameAsCache := false if ds == nil { return nil, isSameAsCache, fmt.Errorf("PackageService doesn't exist") } manifest, err := ds.packageArchive.DownloadArchiveInfo(tracer, packageName, version) if err != nil { return nil, isSameAsCache, fmt.Errorf("failed to download manifest - %v", err) } byteManifest := []byte(manifest) parsedManifest, err := archive.ParseManifest(&byteManifest) if err != nil { return nil, isSameAsCache, err } ds.packageArchive.SetResource(packageName, version, parsedManifest) cachedManifest, err := ds.packageArchive.ReadManifestFromCache(parsedManifest.PackageArn, parsedManifest.Version) if reflect.DeepEqual(parsedManifest, cachedManifest) { isSameAsCache = true } err = ds.packageArchive.WriteManifestToCache(parsedManifest.PackageArn, parsedManifest.Version, byteManifest) if err != nil { return nil, isSameAsCache, fmt.Errorf("failed to write manifest to file: %v", err) } return parsedManifest, isSameAsCache, nil } func (ds *PackageService) findFileFromManifest(tracer trace.Tracer, manifest *birdwatcher.Manifest) (*archive.File, error) { var fileInfo *birdwatcher.FileInfo var file archive.File var filename string pkginfo, err := ds.extractPackageInfo(tracer, manifest) if err != nil { return nil, fmt.Errorf("failed to find platform: %v", err) } for name, f := range manifest.Files { if name == pkginfo.FileName { fileInfo = f filename = name break } } if fileInfo == nil { return nil, fmt.Errorf("failed to find file for %+v", pkginfo) } file.Info = *fileInfo file.Name = filename return &file, nil } func getFileFromManifest(ds *PackageService, packageName string, version string, trace *trace.Trace, tracer trace.Tracer) (*archive.File, error) { manifest, err := ds.packageArchive.ReadManifestFromCache(packageName, version) if err != nil { trace.AppendInfof("error when reading the manifest from cache %v", err) manifest, _, err = downloadManifest(tracer, ds, packageName, version) if err != nil { trace.WithError(err) return nil, fmt.Errorf("failed to download the manifest: %v", err) } } file, err := ds.findFileFromManifest(tracer, manifest) if err != nil { trace.WithError(err) return nil, err } return file, nil } func downloadFile(ds *PackageService, tracer trace.Tracer, file *archive.File, packageName string, version string, isRecursiveRetry bool) (string, error) { if ds == nil || ds.packageArchive == nil || file == nil { return "", fmt.Errorf("Either package service does not exist or does not have archive information or the file information does not exist") } sourceUrl, err := ds.packageArchive.GetFileDownloadLocation(file, packageName, version) if err != nil { return "", err } downloadInput := artifact.DownloadInput{ SourceURL: sourceUrl, DestinationDirectory: appconfig.DownloadRoot, // TODO don't hardcode sha256 - use multiple checksums SourceChecksums: file.Info.Checksums, } log := tracer.CurrentTrace().Logger downloadOutput, downloadErr := birdwatcher.Networkdep.Download(ds.Context, downloadInput) if downloadErr != nil || downloadOutput.LocalFilePath == "" { errMessage := fmt.Sprintf("failed to download installation package reliably, %v", downloadInput.SourceURL) if downloadErr != nil { errMessage = fmt.Sprintf("%v, %v", errMessage, downloadErr.Error()) } if isRecursiveRetry { // TODO: attempt to clean up failed download folder? // return download error return "", errors.New(errMessage) } // Delete cached manifest and retry log.Info("There was an error downloading the installation reliably. Deleting the cached manifest and retrying.") ds.packageArchive.DeleteCachedManifest(packageName, version) downloadManifest(tracer, ds, packageName, version) file, getFileError := getFileFromManifest(ds, packageName, version, tracer.CurrentTrace(), tracer) if getFileError != nil { return "", getFileError } return downloadFile(ds, tracer, file, packageName, version, true) } return downloadOutput.LocalFilePath, nil } // ExtractPackageInfo returns the correct PackageInfo for the current instances platform/version/arch func (ds *PackageService) extractPackageInfo(tracer trace.Tracer, manifest *birdwatcher.Manifest) (*birdwatcher.PackageInfo, error) { env, err := ds.collector.CollectData(ds.Context) if err != nil { return nil, fmt.Errorf("failed to collect data: %v", err) } if keyplatform, ok := matchPackageSelectorPlatform(env.OperatingSystem.Platform, manifest.Packages); ok { if keyversion, ok := matchPackageSelectorVersion(env.OperatingSystem.PlatformVersion, manifest.Packages[keyplatform]); ok { if keyarch, ok := matchPackageSelectorArch(env.OperatingSystem.Architecture, manifest.Packages[keyplatform][keyversion]); ok { return manifest.Packages[keyplatform][keyversion][keyarch], nil } } } return nil, fmt.Errorf("no manifest found for platform: %s, version %s, architecture %s", env.OperatingSystem.Platform, env.OperatingSystem.PlatformVersion, env.OperatingSystem.Architecture) } func matchPackageSelectorPlatform(key string, dict map[string]map[string]map[string]*birdwatcher.PackageInfo) (string, bool) { if _, ok := dict[key]; ok { return key, true } else if _, ok := dict["_any"]; ok { return "_any", true } return "", false } func matchPackageSelectorVersion(key string, dict map[string]map[string]*birdwatcher.PackageInfo) (string, bool) { if _, ok := dict[key]; ok { return key, true } else { // Enumerate each dictionary key to do a prefix match. matchedLength := 0 matchedKey := "" for k := range dict { temp := k // According to /agent/plugins/configurepackage/envdetect/osdetect/windows/osdetect.go, windows nano version will have // the 'nano' suffix. Therefore, taking 'nano' out as the first step will allow the version match to fall back to the // non-special case scenario. if strings.HasSuffix(key, "nano") && strings.HasSuffix(temp, "nano") { temp = strings.TrimSuffix(temp, "nano") } else if strings.HasSuffix(key, "nano") || strings.HasSuffix(temp, "nano") { continue } // If there's no wild card, there's no match. if !strings.HasSuffix(temp, ".*") { continue } // Then handle the wild card scenario temp = strings.TrimSuffix(temp, ".*") tempArray := strings.Split(temp, ".") keyArray := strings.Split(key, ".") i := 0 matched := true for ; i < len(tempArray) && i < len(keyArray); i++ { if tempArray[i] != keyArray[i] { matched = false break } } // The more specific match will be preferred. if matched && i > matchedLength && i == len(tempArray) { matchedLength = i matchedKey = k } } if matchedKey != "" { return matchedKey, true } } // Evaluate _any and * last as more specific version takes precedence. if _, ok := dict["_any"]; ok { return "_any", true } if _, ok := dict["*"]; ok { return "*", true } return "", false } func matchPackageSelectorArch(key string, dict map[string]*birdwatcher.PackageInfo) (string, bool) { if _, ok := dict[key]; ok { return key, true } else if _, ok := dict["_any"]; ok { return "_any", true } return "", false }