github.com/cloud-foundations/dominator@v0.0.0-20221004181915-6e4fee580046/imagepublishers/amipublisher/copyBootstrapImage.go (about)

     1  package amipublisher
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"os/exec"
     7  	"strings"
     8  	"sync"
     9  	"time"
    10  
    11  	uclient "github.com/Cloud-Foundations/Dominator/imageunpacker/client"
    12  	"github.com/Cloud-Foundations/Dominator/lib/awsutil"
    13  	"github.com/Cloud-Foundations/Dominator/lib/format"
    14  	"github.com/Cloud-Foundations/Dominator/lib/log"
    15  	"github.com/Cloud-Foundations/Dominator/lib/srpc"
    16  	libtags "github.com/Cloud-Foundations/Dominator/lib/tags"
    17  	proto "github.com/Cloud-Foundations/Dominator/proto/imageunpacker"
    18  	"github.com/aws/aws-sdk-go/aws"
    19  	"github.com/aws/aws-sdk-go/service/ec2"
    20  )
    21  
    22  type targetResult struct {
    23  	awsService *ec2.EC2
    24  	region     string
    25  	logger     log.Logger
    26  	image      *ec2.Image
    27  	instance   *ec2.Instance
    28  	client     *srpc.Client
    29  	status     proto.GetStatusResponse
    30  	mutex      sync.Mutex // Lock everything below.
    31  	prepared   bool
    32  }
    33  
    34  func copyBootstrapImage(streamName string, targets awsutil.TargetList,
    35  	skipList awsutil.TargetList, marketplaceImage, marketplaceLoginName string,
    36  	newImageTags libtags.Tags, unpackerName string,
    37  	vpcSearchTags, subnetSearchTags, securityGroupSearchTags libtags.Tags,
    38  	instanceType string, sshKeyName string, logger log.Logger) error {
    39  	imageSearchTags := libtags.Tags{"Name": streamName}
    40  	type resultType struct {
    41  		targetResult *targetResult
    42  		error        error
    43  	}
    44  	resultsChannel := make(chan *resultType, 1)
    45  	numTargets, err := awsutil.ForEachTarget(targets, skipList,
    46  		func(awsService *ec2.EC2, account, region string, logger log.Logger) {
    47  			result, err := probeTarget(awsService, streamName, imageSearchTags,
    48  				unpackerName, logger)
    49  			result.awsService = awsService
    50  			result.region = region
    51  			result.logger = logger
    52  			resultsChannel <- &resultType{targetResult: result, error: err}
    53  		},
    54  		logger)
    55  	// Collect results.
    56  	targetResults := make([]*targetResult, 0, numTargets)
    57  	haveSource := false
    58  	for i := 0; i < numTargets; i++ {
    59  		result := <-resultsChannel
    60  		if result.error != nil {
    61  			if err == nil {
    62  				err = result.error
    63  			}
    64  		} else {
    65  			target := result.targetResult
    66  			targetResults = append(targetResults, target)
    67  			if target.client != nil {
    68  				if stream, ok := target.status.ImageStreams[streamName]; ok {
    69  					if stream.DeviceId != "" {
    70  						haveSource = true
    71  					}
    72  				}
    73  			}
    74  		}
    75  	}
    76  	if err != nil {
    77  		return err
    78  	}
    79  	if !haveSource {
    80  		for _, target := range targetResults {
    81  			if target.client != nil {
    82  				target.client.Close()
    83  			}
    84  		}
    85  		return errors.New("no source found for: " + streamName)
    86  	}
    87  	errorChannel := make(chan error, 1)
    88  	for _, target := range targetResults {
    89  		go func(target *targetResult) {
    90  			logger := target.logger
    91  			e := target.bootstrap(streamName, targetResults, marketplaceImage,
    92  				marketplaceLoginName, newImageTags, vpcSearchTags,
    93  				subnetSearchTags, securityGroupSearchTags, instanceType,
    94  				sshKeyName, logger)
    95  			if e != nil {
    96  				logger.Println(e)
    97  			}
    98  			errorChannel <- e
    99  		}(target)
   100  	}
   101  	for range targetResults {
   102  		e := <-errorChannel
   103  		if e != nil && err == nil {
   104  			err = e
   105  		}
   106  	}
   107  	for _, target := range targetResults {
   108  		if target.client != nil {
   109  			target.client.Close()
   110  		}
   111  	}
   112  	return err
   113  }
   114  
   115  func probeTarget(awsService *ec2.EC2, streamName string,
   116  	imageSearchTags libtags.Tags, unpackerName string, logger log.Logger) (
   117  	*targetResult, error) {
   118  	var result targetResult
   119  	instance, client, err := getWorkingUnpacker(awsService, unpackerName,
   120  		logger)
   121  	if err == nil {
   122  		result.status, err = uclient.GetStatus(client)
   123  		if err == nil {
   124  			result.instance = instance
   125  			result.client = client
   126  		} else {
   127  			client.Close()
   128  		}
   129  	}
   130  	image, err := findImage(awsService, imageSearchTags)
   131  	if err != nil {
   132  		logger.Println(err)
   133  		return nil, err
   134  	}
   135  	result.image = image
   136  	return &result, nil
   137  }
   138  
   139  func (target *targetResult) bootstrap(streamName string,
   140  	targets []*targetResult, marketplaceImage, marketplaceLoginName string,
   141  	newImageTags libtags.Tags, vpcSearchTags, subnetSearchTags,
   142  	securityGroupSearchTags libtags.Tags, instanceType string,
   143  	sshKeyName string, logger log.Logger) error {
   144  	if target.image != nil {
   145  		return nil // Already have an image: nothing to copy in here.
   146  	}
   147  	sourceTarget, err := target.getSourceTarget(streamName, targets)
   148  	if err != nil {
   149  		logger.Println(err)
   150  		return err
   151  	}
   152  	awsService := target.awsService
   153  	image, err := findMarketplaceImage(awsService, marketplaceImage)
   154  	if err != nil {
   155  		return err
   156  	}
   157  	if image == nil {
   158  		return errors.New("no marketplace image found")
   159  	}
   160  	instanceTags := newImageTags.Copy()
   161  	instanceTags["ImageBeingCopied"] = newImageTags["Name"]
   162  	instanceTags["Name"] = "ImageCopier"
   163  	instance, err := launchInstance(awsService, image, 0, instanceTags,
   164  		vpcSearchTags, subnetSearchTags, securityGroupSearchTags, instanceType,
   165  		sshKeyName)
   166  	if err != nil {
   167  		return err
   168  	}
   169  	instanceId := aws.StringValue(instance.InstanceId)
   170  	instanceIP := aws.StringValue(instance.PrivateIpAddress)
   171  	defer libTerminateInstances(awsService, instanceId)
   172  	logger.Printf("launched: %s (%s)\n", instanceId, instanceIP)
   173  	err = awsService.WaitUntilInstanceRunning(&ec2.DescribeInstancesInput{
   174  		InstanceIds: aws.StringSlice([]string{instanceId}),
   175  	})
   176  	if err != nil {
   177  		return err
   178  	}
   179  	logger.Printf("running: %s\n", instanceId)
   180  	sourceDeviceId := sourceTarget.status.ImageStreams[streamName].DeviceId
   181  	sourceDevice := sourceTarget.status.Devices[sourceDeviceId]
   182  	volumeId, err := createVolume(awsService,
   183  		instance.Placement.AvailabilityZone, sourceDevice.Size, nil, logger)
   184  	if err != nil {
   185  		return err
   186  	}
   187  	if err := attachVolume(awsService, instance, volumeId, logger); err != nil {
   188  		deleteVolume(awsService, volumeId)
   189  		return err
   190  	}
   191  	devices, err := getDevices(instance, marketplaceLoginName, logger)
   192  	if err != nil {
   193  		return err
   194  	}
   195  	if len(devices) < 2 {
   196  		return fmt.Errorf("bad device count: %d", len(devices))
   197  	}
   198  	deviceName := devices[1]
   199  	logger.Printf("device: %s\n", deviceName)
   200  	remoteCommand := fmt.Sprintf("sudo chown %s /dev/%s",
   201  		marketplaceLoginName, deviceName)
   202  	cmd := makeSshCmd(instance, marketplaceLoginName, remoteCommand)
   203  	if out, err := cmd.CombinedOutput(); err != nil {
   204  		logger.Println(string(out))
   205  		return errors.New("error changing ownership of device: " + err.Error())
   206  	}
   207  	sshArgs := strings.Join([]string{
   208  		"-o CheckHostIP=no",
   209  		"-o ServerAliveInterval=17",
   210  		"-o StrictHostKeyChecking=no",
   211  		"-o UserKnownHostsFile=/dev/null",
   212  	}, " ")
   213  	destCommand := fmt.Sprintf("gunzip | sudo dd bs=64k of=/dev/%s; sync",
   214  		deviceName)
   215  	sourceCommand := fmt.Sprintf(
   216  		"sudo dd bs=64k if=/dev/%s | gzip | ssh %s %s@%s \"%s\"",
   217  		sourceDevice.DeviceName, sshArgs, marketplaceLoginName,
   218  		instanceIP, destCommand)
   219  	logger.Printf("copying image contents from %s in %s\n",
   220  		aws.StringValue(instance.PrivateIpAddress), sourceTarget.region)
   221  	startTime := time.Now()
   222  	cmd = makeSshCmd(sourceTarget.instance, sshKeyName, sourceCommand)
   223  	if out, err := cmd.CombinedOutput(); err != nil {
   224  		logger.Println(string(out))
   225  		return errors.New("error copying image contents: " + err.Error())
   226  	}
   227  	logger.Printf("copied in %s\n", format.Duration(time.Since(startTime)))
   228  	snapshotId, err := createSnapshot(awsService, volumeId, "bootstrap",
   229  		newImageTags, logger)
   230  	if err != nil {
   231  		return err
   232  	}
   233  	logger.Println("registering AMI...")
   234  	amiId, err := registerAmi(awsService, snapshotId, "", "",
   235  		streamName+"/bootstrap", newImageTags, 0, nil, logger)
   236  	if err != nil {
   237  		deleteSnapshot(awsService, snapshotId)
   238  		return err
   239  	}
   240  	logger.Printf("registered: %s\n", amiId)
   241  	return nil
   242  }
   243  
   244  func (target *targetResult) getSourceTarget(streamName string,
   245  	targets []*targetResult) (*targetResult, error) {
   246  	// Find nearest target with an image.
   247  	var sourceTarget *targetResult
   248  	nearness := -1
   249  	for _, remoteTarget := range targets {
   250  		if remoteTarget.client == nil {
   251  			continue
   252  		}
   253  		if streamInfo, ok := remoteTarget.status.ImageStreams[streamName]; !ok {
   254  			continue
   255  		} else if streamInfo.DeviceId == "" {
   256  			continue
   257  		}
   258  		numMatching := getNumMatching(target.region, remoteTarget.region)
   259  		if numMatching > nearness {
   260  			nearness = numMatching
   261  			sourceTarget = remoteTarget
   262  		}
   263  	}
   264  	sourceTarget.mutex.Lock()
   265  	defer sourceTarget.mutex.Unlock()
   266  	if sourceTarget.prepared {
   267  		return sourceTarget, nil
   268  	}
   269  	sourceTarget.logger.Printf("%s preparing for copy\n",
   270  		aws.StringValue(sourceTarget.instance.InstanceId))
   271  	err := uclient.PrepareForCopy(sourceTarget.client, streamName)
   272  	if err != nil {
   273  		return nil, err
   274  	}
   275  	sourceTarget.logger.Println("prepared for copy")
   276  	sourceTarget.prepared = true
   277  	return sourceTarget, nil
   278  }
   279  
   280  func getNumMatching(left, right string) int {
   281  	num := 0
   282  	for index := 0; index < len(left) && index < len(right); index++ {
   283  		if left[index] == right[index] {
   284  			num++
   285  		}
   286  	}
   287  	return num
   288  }
   289  
   290  func getDevices(instance *ec2.Instance, loginName string, logger log.Logger) (
   291  	[]string, error) {
   292  	stopTime := time.Now().Add(time.Minute * 10)
   293  	showedError := false
   294  	for time.Now().Before(stopTime) {
   295  		cmd := makeSshCmd(instance, loginName, "ls /sys/block")
   296  		output, err := cmd.Output()
   297  		if err != nil {
   298  			if !showedError {
   299  				logger.Println(err)
   300  				showedError = true
   301  			}
   302  			time.Sleep(time.Second * 17)
   303  			continue
   304  		}
   305  		return strings.Split(string(output), "\n"), nil
   306  	}
   307  	return nil, errors.New("timed out SSHing to instance")
   308  }
   309  
   310  func makeSshCmd(instance *ec2.Instance, loginName string,
   311  	remoteCommand string) *exec.Cmd {
   312  	return exec.Command("ssh",
   313  		"-A",
   314  		"-o", "CheckHostIP=no",
   315  		"-o", "StrictHostKeyChecking=no",
   316  		"-o", "User="+loginName,
   317  		"-o", "UserKnownHostsFile=/dev/null",
   318  		aws.StringValue(instance.PrivateIpAddress),
   319  		remoteCommand)
   320  }