github.com/cloud-foundations/dominator@v0.0.0-20221004181915-6e4fee580046/imagepublishers/amipublisher/publish.go (about) 1 package amipublisher 2 3 import ( 4 "errors" 5 "os" 6 "path" 7 "sync" 8 9 iclient "github.com/Cloud-Foundations/Dominator/imageserver/client" 10 uclient "github.com/Cloud-Foundations/Dominator/imageunpacker/client" 11 "github.com/Cloud-Foundations/Dominator/lib/awsutil" 12 "github.com/Cloud-Foundations/Dominator/lib/filesystem" 13 "github.com/Cloud-Foundations/Dominator/lib/log" 14 "github.com/Cloud-Foundations/Dominator/lib/srpc" 15 libtags "github.com/Cloud-Foundations/Dominator/lib/tags" 16 proto "github.com/Cloud-Foundations/Dominator/proto/imageunpacker" 17 "github.com/aws/aws-sdk-go/aws" 18 "github.com/aws/aws-sdk-go/service/ec2" 19 ) 20 21 type sharingStateType struct { 22 sharingAccountName string 23 sync.Cond 24 sync.Mutex // Covers everything below. 25 results map[string]*TargetResult // Key: Region. 26 sharers map[string]*ec2.EC2 // Key: Region. 27 } 28 29 func (pData *publishData) publish(targets awsutil.TargetList, 30 skipList awsutil.TargetList, logger log.Logger) ( 31 Results, error) { 32 if pData.sharingAccountName != "" && pData.s3BucketExpression == "" { 33 return nil, errors.New("sharing not supported for EBS AMIs") 34 } 35 fs, err := pData.getFileSystem(logger) 36 if err != nil { 37 return nil, err 38 } 39 fs.TotalDataBytes = fs.EstimateUsage(0) 40 pData.fileSystem = fs 41 resultsChannel := make(chan TargetResult, 1) 42 sharingState := makeSharingState(pData.sharingAccountName) 43 numTargets, err := awsutil.ForEachTarget(targets, skipList, 44 func(awsService *ec2.EC2, account, region string, logger log.Logger) { 45 pData.publishToTargetWrapper(awsService, account, region, 46 sharingState, resultsChannel, logger) 47 }, 48 logger) 49 // Collect results. 50 results := make(Results, 0, numTargets) 51 for i := 0; i < numTargets; i++ { 52 result := <-resultsChannel 53 if result.AccountName == "" || result.Region == "" { 54 continue 55 } 56 results = append(results, result) 57 } 58 return results, err 59 } 60 61 func (pData *publishData) getFileSystem(logger log.Logger) ( 62 *filesystem.FileSystem, error) { 63 imageName := path.Join(pData.streamName, pData.imageLeafName) 64 logger.Printf("Loading image: %s...\n", imageName) 65 srpcClient, err := srpc.DialHTTP("tcp", pData.imageServerAddress, 0) 66 if err != nil { 67 return nil, err 68 } 69 defer srpcClient.Close() 70 image, err := iclient.GetImage(srpcClient, imageName) 71 if err != nil { 72 return nil, err 73 } 74 if image == nil { 75 return nil, errors.New("image: " + imageName + " not found") 76 } 77 logger.Printf("Loaded image: %s\n", imageName) 78 return image.FileSystem, nil 79 } 80 81 func (pData *publishData) publishToTargetWrapper(awsService *ec2.EC2, 82 accountProfileName string, region string, sharingState *sharingStateType, 83 channel chan<- TargetResult, logger log.Logger) { 84 target := awsutil.Target{AccountName: accountProfileName, Region: region} 85 resultMsg := TargetResult{} 86 res, err := pData.publishToTarget(awsService, accountProfileName, region, 87 sharingState, logger) 88 if res != nil { 89 resultMsg = *res 90 } 91 resultMsg.Target = target 92 resultMsg.Error = err 93 if err != nil { 94 logger.Println(err) 95 } 96 sharingState.publish(awsService, resultMsg) 97 channel <- resultMsg 98 } 99 100 func (pData *publishData) publishToTarget(awsService *ec2.EC2, 101 accountProfileName string, region string, sharingState *sharingStateType, 102 logger log.Logger) (*TargetResult, error) { 103 imageName := path.Join(pData.streamName, path.Base(pData.imageLeafName)) 104 if sharingState != nil && 105 sharingState.sharingAccountName != accountProfileName { 106 return sharingState.harvest(awsService, region, imageName, pData.tags, 107 logger) 108 } 109 unpackerInstance, srpcClient, err := getWorkingUnpacker(awsService, 110 pData.unpackerName, logger) 111 if err != nil { 112 return nil, err 113 } 114 defer srpcClient.Close() 115 logger.Printf("Preparing to unpack: %s\n", pData.streamName) 116 uclient.PrepareForUnpack(srpcClient, pData.streamName, true, false) 117 usageEstimate := pData.fileSystem.EstimateUsage(0) 118 minBytes := usageEstimate + usageEstimate>>2 // 25% extra for updating. 119 status, err := selectVolume(srpcClient, awsService, pData.streamName, 120 minBytes, pData.tags, unpackerInstance, logger) 121 if err != nil { 122 return nil, err 123 } 124 volumeId := status.ImageStreams[pData.streamName].DeviceId 125 if status.ImageStreams[pData.streamName].Status != 126 proto.StatusStreamScanned { 127 logger.Printf("Preparing to unpack again: %s\n", pData.streamName) 128 err := uclient.PrepareForUnpack(srpcClient, pData.streamName, true, 129 false) 130 if err != nil { 131 return nil, err 132 } 133 } 134 logger.Printf("Unpacking: %s\n", pData.streamName) 135 err = uclient.UnpackImage(srpcClient, pData.streamName, pData.imageLeafName) 136 if err != nil { 137 return nil, err 138 } 139 logger.Printf("Preparing to capture: %s\n", pData.streamName) 140 err = uclient.PrepareForCapture(srpcClient, pData.streamName) 141 if err != nil { 142 return nil, err 143 } 144 var snapshotId string 145 logger.Printf("Capturing: %s\n", pData.streamName) 146 var s3ManifestFile string 147 var s3Manifest string 148 s3Bucket := expandBucketName(pData.s3BucketExpression, accountProfileName, 149 region) 150 if s3Bucket == "" { 151 snapshotId, err = createSnapshot(awsService, volumeId, imageName, 152 pData.tags, logger) 153 if err != nil { 154 return nil, err 155 } 156 } else { 157 s3Location := path.Join(s3Bucket, pData.s3Folder, imageName) 158 s3ManifestFile = path.Join(pData.s3Folder, imageName, 159 "image.manifest.xml") 160 s3Manifest = path.Join(s3Bucket, s3ManifestFile) 161 logger.Printf("Exporting to S3: %s\n", s3Location) 162 err := uclient.ExportImage(srpcClient, pData.streamName, "s3", 163 s3Location) 164 if err != nil { 165 return nil, err 166 } 167 } 168 // Kick off scan for next time. 169 err = uclient.PrepareForUnpack(srpcClient, pData.streamName, false, true) 170 if err != nil { 171 return nil, err 172 } 173 logger.Printf("Registering AMI from: %s...\n", snapshotId) 174 volumeSize := status.Devices[volumeId].Size >> 30 175 imageBytes := usageEstimate + pData.minFreeBytes 176 imageGiB := imageBytes >> 30 177 if imageGiB<<30 < imageBytes { 178 imageGiB++ 179 } 180 if volumeSize > imageGiB { 181 imageGiB = volumeSize 182 } 183 amiId, err := registerAmi(awsService, snapshotId, s3Manifest, pData.amiName, 184 imageName, pData.tags, imageGiB, pData.publishOptions, logger) 185 if err != nil { 186 logger.Printf("Error registering AMI: %s\n", err) 187 } 188 return &TargetResult{ 189 SnapshotId: snapshotId, 190 S3Bucket: s3Bucket, 191 S3ManifestFile: s3ManifestFile, 192 AmiId: amiId, 193 Size: uint(imageGiB), 194 }, err 195 } 196 197 func selectVolume(srpcClient *srpc.Client, awsService *ec2.EC2, 198 streamName string, minBytes uint64, tags map[string]string, 199 instance *ec2.Instance, logger log.Logger) ( 200 proto.GetStatusResponse, error) { 201 status, err := uclient.GetStatus(srpcClient) 202 if err != nil { 203 return proto.GetStatusResponse{}, err 204 } 205 // Check if associated device is large enough. 206 var oldVolumeId string 207 if streamInfo, ok := status.ImageStreams[streamName]; ok { 208 if deviceInfo, ok := status.Devices[streamInfo.DeviceId]; ok { 209 if minBytes <= deviceInfo.Size { 210 return status, nil 211 } 212 oldVolumeId = streamInfo.DeviceId 213 } 214 } 215 // Search for an unassociated device which is large enough. 216 for deviceId, deviceInfo := range status.Devices { 217 if deviceInfo.StreamName == "" && minBytes <= deviceInfo.Size { 218 err := uclient.AssociateStreamWithDevice(srpcClient, streamName, 219 deviceId) 220 if err != nil { 221 return proto.GetStatusResponse{}, err 222 } 223 return uclient.GetStatus(srpcClient) 224 } 225 } 226 // Need to attach another volume. 227 volumeId, err := addVolume(srpcClient, awsService, minBytes, tags, instance, 228 logger) 229 if err != nil { 230 return proto.GetStatusResponse{}, err 231 } 232 err = uclient.AssociateStreamWithDevice(srpcClient, streamName, volumeId) 233 if err != nil { 234 return proto.GetStatusResponse{}, err 235 } 236 if oldVolumeId != "" { // Remove old volume. 237 logger.Printf("detaching old volume: %s\n", oldVolumeId) 238 if err := uclient.RemoveDevice(srpcClient, oldVolumeId); err != nil { 239 return proto.GetStatusResponse{}, err 240 } 241 instId := aws.StringValue(instance.InstanceId) 242 if err := detachVolume(awsService, instId, oldVolumeId); err != nil { 243 return proto.GetStatusResponse{}, err 244 } 245 logger.Printf("deleting old volume: %s\n", oldVolumeId) 246 if err := deleteVolume(awsService, oldVolumeId); err != nil { 247 return proto.GetStatusResponse{}, err 248 } 249 } 250 return uclient.GetStatus(srpcClient) 251 } 252 253 func addVolume(srpcClient *srpc.Client, awsService *ec2.EC2, 254 minBytes uint64, tags map[string]string, 255 instance *ec2.Instance, logger log.Logger) (string, error) { 256 volumeId, err := createVolume(awsService, 257 instance.Placement.AvailabilityZone, minBytes, tags, logger) 258 if err != nil { 259 return "", err 260 } 261 err = uclient.AddDevice(srpcClient, volumeId, func() error { 262 return attachVolume(awsService, instance, volumeId, logger) 263 }) 264 if err != nil { 265 return "", err 266 } 267 return volumeId, nil 268 } 269 270 func expandBucketName(expr, accountProfileName, region string) string { 271 if expr == "" { 272 return "" 273 } 274 return os.Expand(expr, func(variable string) string { 275 if variable == "region" { 276 return region 277 } 278 if variable == "accountName" { 279 return accountProfileName 280 } 281 return variable 282 }) 283 } 284 285 func makeSharingState(sharingAccountName string) *sharingStateType { 286 if sharingAccountName == "" { 287 return nil 288 } 289 sharingState := sharingStateType{sharingAccountName: sharingAccountName} 290 sharingState.Cond.L = &sharingState.Mutex 291 sharingState.results = make(map[string]*TargetResult) 292 sharingState.sharers = make(map[string]*ec2.EC2) 293 return &sharingState 294 } 295 296 func (ss *sharingStateType) publish(awsService *ec2.EC2, result TargetResult) { 297 if ss == nil { 298 return 299 } 300 if ss.sharingAccountName != result.AccountName { 301 return 302 } 303 ss.Lock() 304 defer ss.Unlock() 305 ss.results[result.Region] = &result 306 ss.sharers[result.Region] = awsService 307 ss.Broadcast() 308 } 309 310 func (ss *sharingStateType) harvest(awsService *ec2.EC2, region string, 311 imageName string, tags libtags.Tags, logger log.Logger) ( 312 *TargetResult, error) { 313 ownerId, err := getAccountId(awsService) 314 if err != nil { 315 return nil, err 316 } 317 logger.Printf("Waiting to harvest AMI from: %s\n", ss.sharingAccountName) 318 ss.Lock() 319 defer ss.Unlock() 320 for ss.results[region] == nil { 321 ss.Wait() 322 } 323 result := ss.results[region] 324 sharerService := ss.sharers[region] 325 if result.Error != nil { 326 return nil, result.Error 327 } 328 logger.Printf("Remote AMI ID: %s\n", result.AmiId) 329 _, err = sharerService.ModifyImageAttribute(&ec2.ModifyImageAttributeInput{ 330 ImageId: aws.String(result.AmiId), 331 LaunchPermission: &ec2.LaunchPermissionModifications{ 332 Add: []*ec2.LaunchPermission{ 333 { 334 UserId: aws.String(ownerId), 335 }, 336 }, 337 }, 338 }) 339 if err != nil { 340 return nil, err 341 } 342 tags = tags.Copy() 343 tags["Name"] = path.Dir(imageName) 344 if err := createTags(awsService, result.AmiId, tags); err != nil { 345 return nil, err 346 } 347 newResult := *result 348 newResult.SharedFrom = ss.sharingAccountName 349 return &newResult, nil 350 }