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 }