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  }