github.com/openshift/installer@v1.4.17/pkg/gather/aws/aws.go (about) 1 package aws 2 3 import ( 4 "context" 5 "encoding/base64" 6 "fmt" 7 "os" 8 "path/filepath" 9 "strings" 10 "time" 11 12 "github.com/aws/aws-sdk-go/aws" 13 "github.com/aws/aws-sdk-go/aws/awserr" 14 "github.com/aws/aws-sdk-go/aws/request" 15 "github.com/aws/aws-sdk-go/aws/session" 16 "github.com/aws/aws-sdk-go/service/ec2" 17 "github.com/pkg/errors" 18 "github.com/sirupsen/logrus" 19 utilerrors "k8s.io/apimachinery/pkg/util/errors" 20 21 awssession "github.com/openshift/installer/pkg/asset/installconfig/aws" 22 "github.com/openshift/installer/pkg/gather" 23 "github.com/openshift/installer/pkg/gather/providers" 24 "github.com/openshift/installer/pkg/types" 25 "github.com/openshift/installer/pkg/version" 26 ) 27 28 // Filter holds the key/value pairs for the tags we will be matching against. 29 // 30 // A resource matches the filter if all of the key/value pairs are in its tags. 31 type Filter map[string]string 32 33 // Gather holds options for resources we want to gather. 34 type Gather struct { 35 logger logrus.FieldLogger 36 filters []Filter 37 region string 38 bootstrap string 39 masters []string 40 directory string 41 serialLogBundle string 42 43 // Session is the AWS session to be used for gathering. If nil, a new 44 // session will be created based on the usual credential configuration 45 // (AWS_PROFILE, AWS_ACCESS_KEY_ID, etc.). 46 session *session.Session 47 } 48 49 // New returns an AWS Gather from ClusterMetadata. 50 func New(logger logrus.FieldLogger, serialLogBundle string, bootstrap string, masters []string, metadata *types.ClusterMetadata) (providers.Gather, error) { 51 filters := make([]Filter, 0, len(metadata.ClusterPlatformMetadata.AWS.Identifier)) 52 for _, filter := range metadata.ClusterPlatformMetadata.AWS.Identifier { 53 filters = append(filters, filter) 54 } 55 region := metadata.ClusterPlatformMetadata.AWS.Region 56 session, err := awssession.GetSessionWithOptions( 57 awssession.WithRegion(region), 58 awssession.WithServiceEndpoints(region, metadata.ClusterPlatformMetadata.AWS.ServiceEndpoints), 59 ) 60 if err != nil { 61 return nil, err 62 } 63 64 return &Gather{ 65 logger: logger, 66 region: region, 67 filters: filters, 68 session: session, 69 serialLogBundle: serialLogBundle, 70 bootstrap: bootstrap, 71 masters: masters, 72 directory: filepath.Dir(serialLogBundle), 73 }, nil 74 } 75 76 // Run is the entrypoint to start the gather process. 77 func (g *Gather) Run() error { 78 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) 79 defer cancel() 80 81 awsSession := g.session 82 if awsSession == nil { 83 var err error 84 // Relying on appropriate AWS ENV vars (eg AWS_PROFILE, AWS_ACCESS_KEY_ID, etc) 85 awsSession, err = session.NewSession(aws.NewConfig().WithRegion(g.region)) 86 if err != nil { 87 return err 88 } 89 } 90 awsSession.Handlers.Build.PushBackNamed(request.NamedHandler{ 91 Name: "openshiftInstaller.OpenshiftInstallerUserAgentHandler", 92 Fn: request.MakeAddToUserAgentHandler("OpenShift/4.x Gather", version.Raw), 93 }) 94 95 ec2Client := ec2.New(awsSession) 96 97 instances, err := g.findEC2Instances(ctx, ec2Client) 98 if err != nil { 99 return err 100 } 101 102 if len(instances) == 0 { 103 g.logger.Infoln("Skipping console log gathering: no instances found") 104 return nil 105 } 106 107 serialLogBundleDir := strings.TrimSuffix(filepath.Base(g.serialLogBundle), ".tar.gz") 108 filePathDir := filepath.Join(g.directory, serialLogBundleDir) 109 err = os.MkdirAll(filePathDir, 0755) 110 if err != nil && !errors.Is(err, os.ErrExist) { 111 return err 112 } 113 114 var errs []error 115 var files []string 116 for _, instance := range instances { 117 filePath, err := g.downloadConsoleOutput(ctx, ec2Client, instance, filePathDir) 118 if err != nil { 119 errs = append(errs, err) 120 } else { 121 files = append(files, filePath) 122 } 123 } 124 125 if len(files) > 0 { 126 err := gather.CreateArchive(files, g.serialLogBundle) 127 if err != nil { 128 errs = append(errs, errors.Wrap(err, "failed to create archive")) 129 } 130 } 131 132 if err := gather.DeleteArchiveDirectory(filePathDir); err != nil { 133 // Note: cleanup is best effort, it shouldn't fail the gather 134 g.logger.Debugf("Failed to remove archive directory: %v", err) 135 } 136 137 return utilerrors.NewAggregate(errs) 138 } 139 140 // findEC2Instances returns the EC2 instances with tags that satisfy the filters. 141 func (g *Gather) findEC2Instances(ctx context.Context, ec2Client *ec2.EC2) ([]*ec2.Instance, error) { 142 if ec2Client.Config.Region == nil { 143 return nil, errors.New("EC2 client does not have region configured") 144 } 145 146 var instances []*ec2.Instance 147 for _, filter := range g.filters { 148 g.logger.Debugf("Search for matching instances by tag in %s matching %#+v", *ec2Client.Config.Region, filter) 149 instanceFilters := make([]*ec2.Filter, 0, len(g.filters)) 150 for key, value := range filter { 151 instanceFilters = append(instanceFilters, &ec2.Filter{ 152 Name: aws.String("tag:" + key), 153 Values: []*string{aws.String(value)}, 154 }) 155 } 156 157 err := ec2Client.DescribeInstancesPagesWithContext( 158 ctx, 159 &ec2.DescribeInstancesInput{Filters: instanceFilters}, 160 func(results *ec2.DescribeInstancesOutput, lastPage bool) bool { 161 for _, reservation := range results.Reservations { 162 if reservation.OwnerId == nil { 163 continue 164 } 165 166 for _, instance := range reservation.Instances { 167 if instance.InstanceId != nil { 168 instances = append(instances, instance) 169 } 170 } 171 } 172 return !lastPage 173 }, 174 ) 175 if err != nil { 176 err = errors.Wrap(err, "get ec2 instances") 177 return instances, err 178 } 179 } 180 181 return instances, nil 182 } 183 184 func (g *Gather) downloadConsoleOutput(ctx context.Context, ec2Client *ec2.EC2, instance *ec2.Instance, filePathDir string) (string, error) { 185 logger := g.logger.WithField("Instance", aws.StringValue(instance.InstanceId)) 186 187 input := &ec2.GetConsoleOutputInput{ 188 InstanceId: instance.InstanceId, 189 } 190 result, err := ec2Client.GetConsoleOutputWithContext(ctx, input) 191 if err != nil { 192 // Cast err to awserr.Error to get the Message from an error. 193 if aerr, ok := err.(awserr.Error); ok { 194 logger.Errorln(aerr.Error()) 195 } 196 return "", err 197 } 198 199 instanceName := aws.StringValue(result.InstanceId) 200 for _, tags := range instance.Tags { 201 if strings.EqualFold(aws.StringValue(tags.Key), "Name") { 202 instanceName = aws.StringValue(tags.Value) 203 } 204 } 205 206 logger.Debugf("Attemping to download console logs for %s", instanceName) 207 filePath, err := g.saveToFile(instanceName, aws.StringValue(result.Output), filePathDir) 208 if err != nil { 209 return "", err 210 } 211 logger.Debug("Download complete") 212 213 return filePath, nil 214 } 215 216 func (g *Gather) saveToFile(instanceName, content, filePathDir string) (string, error) { 217 data, err := base64.StdEncoding.DecodeString(content) 218 if err != nil { 219 return "", errors.Wrap(err, "failed to decode console output") 220 } 221 222 filename := filepath.Join(filePathDir, fmt.Sprintf("%s-serial.log", instanceName)) 223 224 file, err := os.Create(filename) 225 if err != nil { 226 return "", errors.Wrap(err, "failed to create file") 227 } 228 defer file.Close() 229 230 _, err = file.Write(data) 231 if err != nil { 232 return "", errors.Wrap(err, "failed to write to file") 233 } 234 235 return filename, nil 236 }