Skip to content
Snippets Groups Projects
Commit 07dfa4d3 authored by baldwinlouie's avatar baldwinlouie Committed by Yas Naoi
Browse files

Issue #3020077 by baldwinlouie, yas: Create an AMI Image from an existing instance

parent 37dc9396
No related branches found
No related tags found
No related merge requests found
Showing
with 376 additions and 6 deletions
......@@ -33,6 +33,13 @@ entity.aws_cloud_instance.collection:
- entity.aws_cloud_instance.edit_form
- entity.aws_cloud_instance.delete_form
entity.aws_cloud_instance.create_image_form:
route_name: entity.aws_cloud_instance.create_image_form
title: 'Create an Instance Image'
appears_on:
- entity.aws_cloud_instance.edit_form
- entity.aws_cloud_instance.canonical
####################
# AWS Cloud Image
......
......@@ -169,6 +169,15 @@ function aws_cloud_cron() {
$aws_ec2_service->updateSnapshots();
$aws_ec2_service->updateVolumes();
// Update any pending images in the DB. Pending images are usually
// created from an active instance.
$images = aws_cloud_get_pending_images($entity->getCloudContext());
if (count($images)) {
$aws_ec2_service->updateImages([
'ImageIds' => $images
]);
}
// @todo: re-enabled instance bundling
}
// Notify owners if their instances have been running for to long.
......@@ -212,6 +221,12 @@ function aws_cloud_entity_operation(EntityInterface $entity) {
];
}
}
// Create Image
$operations['create_image'] = [
'title' => t('Create Image'),
'url' => $entity->toUrl('create-image-form'),
'weight' => 21,
];
}
}
else if ($entity->getEntityTypeId() == 'aws_cloud_volume') {
......@@ -249,6 +264,11 @@ function aws_cloud_entity_operation_alter(array &$operations, EntityInterface $e
unset($operations['delete']);
}
}
if ($entity->getEntityTypeId() == 'aws_cloud_image') {
if ($entity->getStatus() == 'pending') {
unset($operations['delete']);
}
}
}
/**
......@@ -316,6 +336,23 @@ function aws_cloud_get_expired_instances($cloud_context) {
return $expired_instances;
}
function aws_cloud_get_pending_images($cloud_context) {
$images = [];
/* @var \Drupal\Core\Entity\EntityStorageInterface $entity_storage $entity_storage */
$entity_storage = \Drupal::entityTypeManager()->getStorage('aws_cloud_image');
$entity_ids = $entity_storage
->getQuery()
->condition('status', 'pending')
->condition('cloud_context', $cloud_context, '=')
->execute();
$entities = $entity_storage->loadMultiple($entity_ids);
foreach ($entities as $entity) {
/* @var \Drupal\aws_cloud\Entity\Ec2\Image $entity */
$images[] = $entity->getImageId();
}
return $images;
}
/**
* Implements hook_mail().
*/
......
......@@ -146,6 +146,14 @@ entity.aws_cloud_instance.start_form:
requirements:
_entity_access: 'aws_cloud_instance.edit'
entity.aws_cloud_instance.create_image_form:
path: '/clouds/aws_cloud/{cloud_context}/instance/{aws_cloud_instance}/create_image'
defaults:
_entity_form: 'aws_cloud_instance.create_image'
_title: 'Create Image from Instance'
requirements:
_entity_access: 'aws_cloud_instance.edit'
# AWS Cloud Images Routes
entity.aws_cloud_image.canonical:
......
......@@ -144,6 +144,21 @@ interface ImageInterface extends ContentEntityInterface, EntityOwnerInterface {
*/
public function setRefreshed($time);
/**
* {@inheritdoc}
*/
public function setImageOwnerId($owner);
/**
* {@inheritdoc}
*/
public function setName($name);
/**
* {@inheritdoc}
*/
public function setStatus($status);
/**
* {@inheritdoc}
*/
......
......@@ -229,6 +229,20 @@ class Image extends CloudContentEntityBase implements ImageInterface {
return $this->set('name', $name);
}
/**
* {@inheritdoc}
*/
public function setImageOwnerId($owner) {
return $this->set('owner', $owner);
}
/**
* {@inheritdoc}
*/
public function setStatus($status) {
return $this->set('status', $status);
}
/**
* {@inheritdoc}
*/
......
......@@ -42,7 +42,8 @@ use Drupal\Core\Field\BaseFieldDefinition;
* "edit" = "Drupal\aws_cloud\Form\Ec2\InstanceEditForm" ,
* "delete" = "Drupal\aws_cloud\Form\Ec2\InstanceDeleteForm",
* "stop" = "Drupal\aws_cloud\Form\Ec2\InstanceStopForm",
* "start" = "Drupal\aws_cloud\Form\Ec2\InstanceStartForm"
* "start" = "Drupal\aws_cloud\Form\Ec2\InstanceStartForm",
* "create_image" = "Drupal\aws_cloud\Form\Ec2\InstanceCreateImageForm"
* },
* "access" = "Drupal\aws_cloud\Controller\Ec2\InstanceAccessControlHandler",
* },
......@@ -60,7 +61,8 @@ use Drupal\Core\Field\BaseFieldDefinition;
* "delete-form" = "/clouds/aws_cloud/{cloud_context}/instance/{aws_cloud_instance}/terminate",
* "collection" = "/clouds/aws_cloud/{cloud_context}/instance",
* "stop-form" = "/clouds/aws_cloud/{cloud_context}/instance/{aws_cloud_instance}/stop",
* "start-form" = "/clouds/aws_cloud/{cloud_context}/instance/{aws_cloud_instance}/start"
* "start-form" = "/clouds/aws_cloud/{cloud_context}/instance/{aws_cloud_instance}/start",
* "create-image-form" = "/clouds/aws_cloud/{cloud_context}/instance/{aws_cloud_instance}/create_image"
* },
* field_ui_base_route = "aws_cloud_instance.settings"
* )
......
......@@ -5,10 +5,10 @@ namespace Drupal\aws_cloud\Form\Ec2;
use Drupal\aws_cloud\Service\AwsEc2ServiceInterface;
use Drupal\cloud\Form\CloudContentDeleteForm;
use Drupal\Core\Entity\EntityManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Messenger\Messenger;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Messenger\Messenger;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Class AwsDeleteForm - Base Delete class. This class injects the
......@@ -31,7 +31,7 @@ class AwsDeleteForm extends CloudContentDeleteForm {
/**
* AwsDeleteForm constructor.
* @param \Drupal\Core\Entity\EntityManagerInterface $manager
* @param \Drupal\aws_cloud\Form\Ec2\AwsEc2ServiceInterface $aws_ec2_service
* @param \Drupal\aws_cloud\Service\AwsEc2ServiceInterface $aws_ec2_service
* @param \Drupal\Core\Messenger\Messenger $messenger
* @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
* The entity repository service.
......
......@@ -44,7 +44,7 @@ class ImageDeleteForm extends AwsDeleteForm {
/**
* ImageDeleteForm constructor.
* @param \Drupal\Core\Entity\EntityManagerInterface $manager
* @param \Drupal\aws_cloud\Form\Ec2\AwsEc2ServiceInterface $aws_ec2_service
* @param \Drupal\aws_cloud\Service\AwsEc2ServiceInterface $aws_ec2_service
* @param \Drupal\Core\Messenger\Messenger $messenger
* @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
* The entity repository service.
......@@ -76,6 +76,27 @@ class ImageDeleteForm extends AwsDeleteForm {
);
}
/**
* {@inheritdoc}
*/
public function getDescription() {
if ($this->entity->getStatus() == 'pending') {
return $this->t('Cannot delete an instance in pending state');
}
return parent::getDescription();
}
/**
* {@inheritdoc}
*/
protected function actions(array $form, FormStateInterface $form_state) {
$actions = parent::actions($form, $form_state);
if ($this->entity->getStatus() == 'pending') {
unset($actions['submit']);
}
return $actions;
}
/**
* {@inheritdoc}
*/
......
<?php
namespace Drupal\aws_cloud\Form\Ec2;
use Drupal\Core\Form\FormStateInterface;
/**
* Create image from an instance
*/
class InstanceCreateImageForm extends AwsDeleteForm {
/**
* {@inheritdoc}
*/
public function getQuestion() {
$entity = $this->entity;
return $this->t('Create an image for instance %instance_id: %name?', [
'%instance_id' => $entity->getInstanceId(),
'%name' => $entity->label(),
]);
}
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return t('Create Image');
}
public function getDescription() {
return '';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form = parent::buildForm($form, $form_state);
$form['image_name'] = [
'#type' => 'textfield',
'#title' => $this->t('Image Name'),
'#description' => $this->t('A name for the new image'),
'#required'=> TRUE,
];
$form['no_reboot'] = [
'#type' => 'checkbox',
'#title' => $this->t('No Reboot'),
'#description' => $this->t('By default, Amazon EC2 attempts to shut down and reboot the instance before creating the image. If the \'No Reboot\' option is set, Amazon EC2 doesn\'t shut down the instance before creating the image. When this option is used, file system integrity on the created image can\'t be guaranteed.'),
'#default_value' => FALSE,
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
/* @var \Drupal\aws_cloud\Entity\Ec2\Instance $entity */
$entity = $this->entity;
$this->awsEc2Service->setCloudContext($entity->getCloudContext());
$result = $this->awsEc2Service->createImage([
'InstanceId' => $entity->getInstanceId(),
'Name' => $form_state->getValue('image_name'),
'NoReboot' => $form_state->getValue('no_reboot') == 0 ? FALSE : TRUE,
]);
if (isset($result['ImageId'])) {
// Call image update on this particular image.
$this->awsEc2Service->updateImages([
'ImageIds' => [$result['ImageId']]
]);
$message = $this->t('The %type %label (%image_id) has been created.', [
'%type' => $entity->getEntityType()->getLabel(),
'%label' => $entity->label(),
'%image_id' => $result['ImageId'],
]);
$this->messenger->addMessage($message);
$form_state->setRedirect('view.aws_images.page_1', ['cloud_context' => $entity->getCloudContext()]);
}
else {
$message = $this->t('The image for "%label" could not be create.', [
'%label' => $entity->getName(),
]);
$this->messenger->addError($message);
}
}
}
\ No newline at end of file
......@@ -755,6 +755,7 @@ class AwsEc2Service implements AwsEc2ServiceInterface {
if (!empty($entity_id)) {
$entity = Image::load($entity_id);
$entity->setRefreshed($timestamp);
$entity->setStatus($image['State']);
$entity->save();
continue;
}
......@@ -781,6 +782,7 @@ class AwsEc2Service implements AwsEc2ServiceInterface {
'description' => isset($image['Description']) ? $image['Description'] : '',
'visibility' => $image['Public'],
'block_devices' => implode(', ', $block_devices),
'status' => $image['State'],
'created' => strtotime($image['CreationDate']),
'changed' => $timestamp,
'refreshed' => $timestamp,
......
......@@ -18,4 +18,5 @@ DescribeImages:
Type: marketplace
ImageLocation: {{image_location}}
Public: true
State: {{state}}
CreationDate: '{{creation_date}}'
......@@ -41,6 +41,12 @@ class InstanceTest extends AwsCloudTestCase {
'list cloud server template',
'launch server template',
'add aws cloud image',
'list aws cloud image',
'view aws cloud image',
'edit aws cloud image',
'delete aws cloud image',
'administer aws_cloud',
];
}
......@@ -360,6 +366,150 @@ class InstanceTest extends AwsCloudTestCase {
}
}
/**
* Test creating an image from an instance
*/
public function testImageCreationFromInstance() {
$this->_testImageCreationFromInstance(AWS_CLOUD_INSTANCE_REPEAT_COUNT);
}
private function _testImageCreationFromInstance($max_test_repeat_count = 1) {
$cloud_context = $this->cloud_context;
for ($i = 0; $i < $max_test_repeat_count; $i ++) {
// Setup server template and instance.
$num = $i + 1;
$this->createServerTemplate();
$add = $this->createInstanceTestData();
$this->addInstanceMockData($add[$i]['name'], $add[$i]['key_pair_name']);
$this->drupalPostForm("/clouds/design/server_template/$cloud_context/$num/launch",
[],
t('Launch'));
$this->assertResponse(200, t('HTTP 200: Launch | A New Cloud Instance @num', [
'@num' => $i
]));
$this->assertNoText(t('Notice'), t('Launch | Make sure w/o Notice'));
$this->assertNoText(t('warning'), t('Launch | Make sure w/o Warnings'));
// Make sure instances are available.
$this->drupalGet("/clouds/aws_cloud/$cloud_context/instance/$num");
$this->assertResponse(200, t('HTTP 200: Instance created.'));
// Test image creation.
// $image_id = $add[$i]['image_id'];
$image_id = 'ami-' . $this->random->name(8, TRUE);
$image_name = $this->random->name(8, TRUE);
$image_params = [
'image_name' => $image_name,
'no_reboot' => 0,
];
// Update the mock data then create the image.
$this->updateImageCreationInMockData($image_id, $image_name, 'pending');
$this->drupalPostForm("/clouds/aws_cloud/$cloud_context/instance/$num/create_image",
$image_params,
t('Create Image'));
$this->assertText(t("The AWS Cloud Instance @label (@image_id) has been created.", [
'@image_id' => $image_id,
'@label' => $add[$i]['name'],
]));
// Make sure the image was created. Status should be pending.
// Click on the Image link from the image listing page.
$this->clickLink($image_name);
$this->assertResponse(200, t('HTTP 200: Image Entity.'));
$this->assertText($image_id, t('Image Id is present'));
$this->assertText('pending', t('Image created in pending state'));
// Go back to listing page. Make sure there is no delete link.
$this->drupalGet("/clouds/aws_cloud/$cloud_context/image");
$this->assertNoText('Delete', t('Cannot delete image in pending state'));
// Update the image to 'available'. Then delete the image
$this->updateImageCreationInMockData($image_id, $image_name, 'available');
// Run cron job to update images state.
$key = \Drupal::state()->get('system.cron_key');
$this->drupalGet('/cron/' . $key);
$this->assertResponse(204);
// Go back into the main image.
$this->drupalGet("/clouds/aws_cloud/$cloud_context/image");
// Click into the image. Make sure the status is now available.
$this->clickLink($image_name);
$this->assertText('available', t('Image status is available.'));
// Go back to main listing page
$this->drupalGet("/clouds/aws_cloud/$cloud_context/image");
// Delete the image.
$this->clickLink('Delete');
$this->drupalPostForm($this->getUrl(),
[],
t('Delete'));
$this->assertResponse(200, t('HTTP 200: Delete', [
'@num' => $i
]));
$this->assertNoText(t('Notice'), t('Delete | Make sure w/o Notice'));
$this->assertNoText(t('warning'), t('Delete | Make sure w/o Wanings'));
// Make sure image is deleted
$this->drupalGet("/clouds/aws_cloud/$cloud_context/image");
$this->assertNoText($image_id, t('Image deleted'));
// Test "Failed" Image. Failed Images should be allowed to be deleted.
// Reset the image_id and image_name variables.
$image_id = 'ami-' . $this->random->name(8, TRUE);
$image_name = $this->random->name(8, TRUE);
$image_params = [
'image_name' => $image_name,
'no_reboot' => 0,
];
// Update the image so it is in failed state.
$this->updateImageCreationInMockData($image_id, $image_name, 'failed');
$this->drupalPostForm("/clouds/aws_cloud/$cloud_context/instance/$num/create_image",
$image_params,
t('Create Image'));
$this->assertText(t("The AWS Cloud Instance @label (@image_id) has been created.", [
'@image_id' => $image_id,
'@label' => $add[$i]['name'],
]));
// Go to the main image page.
$this->drupalGet("/clouds/aws_cloud/$cloud_context/image");
// Make sure the status is now failed.
$this->clickLink($image_name);
$this->assertText('failed', t('Image status is failed'));
// Go to the main image page.
$this->drupalGet("/clouds/aws_cloud/$cloud_context/image");
// Delete the Failed image.
$this->clickLink('Delete');
$this->drupalPostForm($this->getUrl(),
[],
t('Delete'));
$this->assertResponse(200, t('HTTP 200: Delete', [
'@num' => $i
]));
$this->assertNoText(t('Notice'), t('Delete | Make sure w/o Notice'));
$this->assertNoText(t('warning'), t('Delete | Make sure w/o Wanings'));
// Make sure image is deleted
$this->drupalGet("/clouds/aws_cloud/$cloud_context/image");
$this->assertNoText($image_id, t('Image deleted'));
}
}
private function createInstanceTestData() {
$data = [];
......@@ -422,6 +572,9 @@ class InstanceTest extends AwsCloudTestCase {
$instance_mock_data_content = $this->getMockDataFileContent(get_class($this), $vars, '_instance');
$instance_mock_data = Yaml::decode($instance_mock_data_content);
// OwnerId and ReservationId need to be set.
$mock_data['DescribeInstances']['Reservations'][0]['OwnerId'] = $this->random->name(8, TRUE);
$mock_data['DescribeInstances']['Reservations'][0]['ReservationId'] = $this->random->name(8, TRUE);
$mock_data['DescribeInstances']['Reservations'][0]['Instances'][] = $instance_mock_data;
$this->updateMockDataToConfig($mock_data);
}
......@@ -460,6 +613,23 @@ class InstanceTest extends AwsCloudTestCase {
$this->updateMockDataToConfig($mock_data);
}
private function updateImageCreationInMockData($image_id, $image_name, $image_state) {
$mock_data = $this->getMockDataFromConfig();
$vars = [
'image_id' => $image_id,
'name' => $image_name,
'state' => $image_state,
];
// Unset DescribeImages so that the state can be updated.
unset($mock_data['DescribeImages']);
unset($mock_data['CreateImage']);
$image_mock_data_content = $this->getMockDataFileContent('Drupal\Tests\aws_cloud\Functional\Ec2\ImageTest', $vars);
$image_mock_data = Yaml::decode($image_mock_data_content);
$image_mock_data['CreateImage']['ImageId'] = $image_id;
$image_mock_data['DescribeImages']['Images'][0]['ImageId'] = $image_id;
$this->updateMockDataToConfig(array_merge($image_mock_data, $mock_data));
}
private function createRandomSecurityGroups() {
$random = $this->random;
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment