Skip to content

Commit d3688be

Browse files
committed
mantle/platform/aws: expand API to support windows li images
Expand the API to support building Windows License Included images. The added methods include creating, attaching, detaching, and deleting volumes, stopping instances, and creating images from instances. Update RemoveImage to only delete the s3 object if the bucket and path are specified to allow the method to be used to delete the WinLI image, does not have an uploaded s3 object associated with it. A ReplaceRootVolume method is also added to swap the root volume of an ec2 instance with a target volume.
1 parent 1739f0b commit d3688be

File tree

3 files changed

+382
-14
lines changed

3 files changed

+382
-14
lines changed

‎mantle/platform/api/aws/ec2.go

Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,9 +241,280 @@ func (a *API) CreateInstances(name, keyname, userdata string, count uint64, minD
241241
return nil, fmt.Errorf("waiting for instances to run: %v", err)
242242
}
243243

244+
// add tags to all created volumes
245+
var volumes []string
246+
tagMap := map[string]string{
247+
"CreatedBy": "mantle",
248+
}
249+
for _, inst := range insts {
250+
if len(inst.BlockDeviceMappings) > 0 {
251+
for _, mapping := range inst.BlockDeviceMappings {
252+
if mapping.Ebs != nil && mapping.Ebs.VolumeId != nil {
253+
volumes = append(volumes, *mapping.Ebs.VolumeId)
254+
}
255+
}
256+
}
257+
}
258+
err = a.CreateTags(volumes, tagMap)
259+
if err != nil {
260+
return nil, fmt.Errorf("error adding tags to volumes: %v", err)
261+
}
262+
244263
return insts, nil
245264
}
246265

266+
// StopInstances will stop all instances provided in the ids slice and will
267+
// block until all instances are in the "stopped" state
268+
func (a *API) StopInstances(ids []string) error {
269+
if len(ids) == 0 {
270+
return nil
271+
}
272+
input := &ec2.StopInstancesInput{
273+
InstanceIds: aws.StringSlice(ids),
274+
}
275+
276+
if _, err := a.ec2.StopInstances(input); err != nil {
277+
return err
278+
}
279+
280+
// loop until all machines are stopped
281+
var insts []*ec2.Instance
282+
timeout := 10 * time.Minute
283+
delay := 10 * time.Second
284+
err := util.WaitUntilReady(timeout, delay, func() (bool, error) {
285+
desc, err := a.ec2.DescribeInstances(&ec2.DescribeInstancesInput{
286+
InstanceIds: aws.StringSlice(ids),
287+
})
288+
if err != nil {
289+
// Keep retrying if the InstanceID disappears momentarily
290+
if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "InvalidInstanceID.NotFound" {
291+
plog.Debugf("instance ID not found, retrying: %v", err)
292+
return false, nil
293+
}
294+
return false, err
295+
}
296+
insts = desc.Reservations[0].Instances
297+
298+
for _, i := range insts {
299+
if *i.State.Name != ec2.InstanceStateNameStopped {
300+
return false, nil
301+
}
302+
}
303+
return true, nil
304+
})
305+
306+
if err != nil {
307+
if errTerminate := a.TerminateInstances(ids); errTerminate != nil {
308+
return fmt.Errorf("terminating instances failed: %v after instances failed to stop: %v", errTerminate, err)
309+
}
310+
return fmt.Errorf("waiting for instances to stop: %v", err)
311+
}
312+
313+
return nil
314+
}
315+
316+
// AttachVolume will attach the provided volume and will block until
317+
// the volume is in the "In-Use" state
318+
func (a *API) AttachVolume(instanceID string, volumeID string, device string) error {
319+
_, err := a.ec2.AttachVolume(&ec2.AttachVolumeInput{
320+
VolumeId: aws.String(volumeID),
321+
InstanceId: aws.String(instanceID),
322+
Device: aws.String(device),
323+
})
324+
if err != nil {
325+
return fmt.Errorf("error attaching volume: %v", err)
326+
}
327+
328+
// loop until the volume is attached
329+
var vol *ec2.Volume
330+
timeout := 10 * time.Minute
331+
delay := 10 * time.Second
332+
err = util.WaitUntilReady(timeout, delay, func() (bool, error) {
333+
desc, err := a.ec2.DescribeVolumes(&ec2.DescribeVolumesInput{
334+
VolumeIds: aws.StringSlice([]string{volumeID}),
335+
})
336+
if err != nil {
337+
// Keep retrying if the VolumeID disappears momentarily
338+
if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "InvalidVolume.NotFound" {
339+
plog.Debugf("volume ID not found, retrying: %v", err)
340+
return false, nil
341+
}
342+
return false, err
343+
}
344+
345+
vol = desc.Volumes[0]
346+
if *vol.State != ec2.VolumeStateInUse {
347+
return false, nil
348+
}
349+
return true, nil
350+
})
351+
352+
if err != nil {
353+
return fmt.Errorf("waiting for volume to attach: %v", err)
354+
}
355+
356+
return nil
357+
}
358+
359+
// DetachVolume will detach the provided volume and will block until
360+
// the volume is in the "Avalailable" state
361+
func (a *API) DetachVolume(volumeID string) error {
362+
_, err := a.ec2.DetachVolume(&ec2.DetachVolumeInput{
363+
VolumeId: aws.String(volumeID),
364+
})
365+
if err != nil {
366+
return fmt.Errorf("error detaching volume: %v", err)
367+
}
368+
369+
// loop until the volume is detached
370+
var vol *ec2.Volume
371+
timeout := 10 * time.Minute
372+
delay := 10 * time.Second
373+
err = util.WaitUntilReady(timeout, delay, func() (bool, error) {
374+
desc, err := a.ec2.DescribeVolumes(&ec2.DescribeVolumesInput{
375+
VolumeIds: aws.StringSlice([]string{volumeID}),
376+
})
377+
if err != nil {
378+
// Keep retrying if the VolumeID disappears momentarily
379+
if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "InvalidVolume.NotFound" {
380+
plog.Debugf("volume ID not found, retrying: %v", err)
381+
return false, nil
382+
}
383+
return false, err
384+
}
385+
386+
vol = desc.Volumes[0]
387+
if *vol.State != ec2.VolumeStateAvailable {
388+
return false, nil
389+
}
390+
return true, nil
391+
})
392+
393+
if err != nil {
394+
return fmt.Errorf("waiting for volume to detach: %v", err)
395+
}
396+
397+
return nil
398+
}
399+
400+
// DeleteVolumes schedules ec2 volumes for deletion
401+
func (a *API) DeleteVolumes(volumeIDs []string) error {
402+
for _, volumeID := range volumeIDs {
403+
_, err := a.ec2.DeleteVolume(&ec2.DeleteVolumeInput{
404+
VolumeId: aws.String(volumeID),
405+
})
406+
if err != nil {
407+
return fmt.Errorf("error deleting volume: %v", err)
408+
}
409+
}
410+
411+
return nil
412+
}
413+
414+
// GetInstanceVolumeIdByDevice returns the VolumeId of the volume
415+
// attached to the instance at the specified device name (e.g., "/dev/xvda").
416+
func (a *API) GetInstanceVolumeIdByDevice(instanceID string, deviceName string) (string, error) {
417+
timeout := 1 * time.Minute
418+
delay := 1 * time.Second
419+
var volume string
420+
err := util.RetryUntilTimeout(timeout, delay, func() error {
421+
desc, err := a.ec2.DescribeInstances(&ec2.DescribeInstancesInput{
422+
InstanceIds: aws.StringSlice([]string{instanceID}),
423+
})
424+
if err != nil {
425+
return fmt.Errorf("error describing instances: %v", err)
426+
}
427+
428+
for _, vol := range desc.Reservations[0].Instances[0].BlockDeviceMappings {
429+
if *vol.DeviceName == deviceName {
430+
volume = *vol.Ebs.VolumeId
431+
return nil
432+
}
433+
}
434+
return fmt.Errorf("failed to find volume id by device: %v", deviceName)
435+
})
436+
437+
if err != nil {
438+
return "", err
439+
}
440+
441+
return volume, nil
442+
}
443+
444+
// returns the VolumeID after creating a volume from a provided snapshot
445+
func (a *API) CreateVolumeFromSnapshot(name string, snapshotID string, volumetype string, availabilityZone string) (string, error) {
446+
newVolume, err := a.ec2.CreateVolume(&ec2.CreateVolumeInput{
447+
AvailabilityZone: aws.String(availabilityZone),
448+
SnapshotId: aws.String(snapshotID),
449+
VolumeType: aws.String(volumetype),
450+
TagSpecifications: tagSpecCreatedByMantle(name, ec2.ResourceTypeVolume),
451+
})
452+
if err != nil {
453+
return "", fmt.Errorf("failed to create volume: %v", err)
454+
}
455+
456+
// loop until the volume is available
457+
timeout := 10 * time.Minute
458+
delay := 10 * time.Second
459+
err = util.WaitUntilReady(timeout, delay, func() (bool, error) {
460+
desc, err := a.ec2.DescribeVolumes(&ec2.DescribeVolumesInput{
461+
VolumeIds: aws.StringSlice([]string{*newVolume.VolumeId}),
462+
})
463+
if err != nil {
464+
// Keep retrying if the VolumeID disappears momentarily
465+
if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "InvalidVolume.NotFound" {
466+
plog.Debugf("volume ID not found, retrying: %v", err)
467+
return false, nil
468+
}
469+
return false, err
470+
}
471+
472+
vol := desc.Volumes[0]
473+
if *vol.State != ec2.VolumeStateAvailable {
474+
return false, nil
475+
}
476+
return true, nil
477+
})
478+
479+
if err != nil {
480+
return "", fmt.Errorf("waiting for volume to detach: %v", err)
481+
}
482+
483+
return *newVolume.VolumeId, nil
484+
}
485+
486+
// ReplaceRootVolume will swap the root volume of `instanceID` at `deviceName` with `newRootVolumeID`
487+
// the replaced root volume is detached and deleted in the process
488+
func (a *API) ReplaceRootVolume(instanceID string, deviceName string, newRootVolumeID string) error {
489+
replacedRootVolume, err := a.GetInstanceVolumeIdByDevice(instanceID, deviceName)
490+
if err != nil {
491+
return fmt.Errorf("failed to find root volume %q", err)
492+
}
493+
494+
if err = a.DetachVolume(replacedRootVolume); err != nil {
495+
return fmt.Errorf("error detaching root volume: %v", err)
496+
}
497+
498+
if err = a.DeleteVolumes([]string{replacedRootVolume}); err != nil {
499+
return fmt.Errorf("error deleting root volume: %v", err)
500+
}
501+
502+
if err = a.AttachVolume(instanceID, newRootVolumeID, deviceName); err != nil {
503+
return fmt.Errorf("error attaching new root volume: %v", err)
504+
}
505+
506+
// verify the root volume of the instance matches the target volume
507+
vol, err := a.GetInstanceVolumeIdByDevice(instanceID, deviceName)
508+
if err != nil {
509+
return fmt.Errorf("failed to find replaced root volume %q", err)
510+
}
511+
if vol != newRootVolumeID {
512+
return fmt.Errorf("failed to replace root volume")
513+
}
514+
515+
return nil
516+
}
517+
247518
// gcEC2 will terminate ec2 instances older than gracePeriod.
248519
// It will only operate on ec2 instances tagged with 'mantle' to avoid stomping
249520
// on other resources in the account.

0 commit comments

Comments
 (0)