@@ -241,9 +241,280 @@ func (a *API) CreateInstances(name, keyname, userdata string, count uint64, minD
241
241
return nil , fmt .Errorf ("waiting for instances to run: %v" , err )
242
242
}
243
243
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
+
244
263
return insts , nil
245
264
}
246
265
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
+
247
518
// gcEC2 will terminate ec2 instances older than gracePeriod.
248
519
// It will only operate on ec2 instances tagged with 'mantle' to avoid stomping
249
520
// on other resources in the account.
0 commit comments