Skip to content
Snippets Groups Projects
Commit 8e706d54 authored by Masami  Suzuki's avatar Masami Suzuki Committed by Yas Naoi
Browse files

Issue #3302260 by Masami, yas: Add the function to attach/detach OpenStack Snapshot in the SPA

parent 1c5bebe7
No related branches found
No related tags found
3 merge requests!1316Issue #3310263: Release 4.5.0,!1260Issue #3307397: Release 4.4.0,!1204Issue #3302260: Add the function to attach/detach OpenStack Snapshot in the SPA
Showing
with 358 additions and 24 deletions
......@@ -63,6 +63,57 @@ const OPENSTACK_VOLUME_TEMPLATE: EntityFormTemplate[] = [
}
]
},
{
cloudServiceProvider: 'openstack',
entityName: 'volume',
actionType: 'attach',
entityRecords: [
{
type: 'label',
text: 'Are you sure you want to attach {{entityName}}: {{name}}?'
},
{
type: 'panel',
panelName: 'Volume Information',
keyValueRecords: [
{ type: 'default', labelName: 'Volume id', name: 'volume_id', defaultValue: '', readOnly: true },
{ type: 'default', labelName: 'Volume name', name: 'name', defaultValue: '', readOnly: true },
{ type: 'default', labelName: 'Device Name', name: 'device_name', defaultValue: '' },
{ type: 'select', labelName: 'Instance ID', name: 'instance_id',
url: '/cloud_dashboard/openstack/{cloud_context}/openstack_volume/{entity_id}/instances',
defaultValue: '', required: true
},
]
},
],
submitButtonLabel: 'Attach'
},
{
cloudServiceProvider: 'openstack',
entityName: 'volume',
actionType: 'detach',
entityRecords: [
{
type: 'label',
text: 'Are you sure you want to detach {{entityName}}: {{name}}?'
},
{
type: 'panel',
panelName: 'Volume Information',
keyValueRecords: [
{ type: 'default', labelName: 'Volume ID', name: 'volume_id', defaultValue: '', readOnly: true },
{ type: 'default', labelName: 'Volume name', name: 'name', defaultValue: '', readOnly: true },
{
type: 'join', labelName: 'Attached to instance', name: 'attachment_information', info: {
entityTypeId: 'openstack_instance',
keyColumn: 'instance_id',
}, defaultValue: '', readOnly: true
},
]
},
],
submitButtonLabel: 'Detach'
},
]
export default OPENSTACK_VOLUME_TEMPLATE;
......@@ -109,6 +109,11 @@ const getOperations = (dataRecord: DataRecord): string[] => {
|| dataRecord.value['instance_id'] === null
? ['Associate']
: ['Disassociate'];
case 'openstack_volume':
return dataRecord.value['attachment_information'] === ''
|| dataRecord.value['attachment_information'] === null
? ['Attach']
: ['Detach'];
case 'vmware_vm':
const list: string[] = [];
if (dataRecord.value['power_state'] === 'POWERED_OFF') {
......
......@@ -46,7 +46,7 @@ class VolumeDetachForm extends AwsDeleteUpdateEntityForm {
$attachment_info = $entity->getAttachmentInformation();
$msg .= $this->t(
'<ul><li>Volume id: %id</li><li>Volume name: %name</li><li>Attached to instance: %instance with id: %instance_id</li></ul>',
'<ul><li>Volume ID: %id</li><li>Volume name: %name</li><li>Attached to instance: %instance with ID: %instance_id</li></ul>',
[
'%id' => $volume_id,
'%name' => $name,
......
......@@ -966,3 +966,32 @@ entity.openstack_volume.edit:
_custom_access: '\Drupal\cloud\Controller\CloudConfigController::access'
options:
perm: 'edit any openstack volume+edit own openstack volume'
entity.openstack_volume.attach:
path: '/cloud_dashboard/openstack/{cloud_context}/openstack_volume/{entity_id}/attach'
defaults:
_controller: '\Drupal\openstack\Controller\ApiController::operateEntity'
entity_type_id: openstack_volume
command: attach
methods: [POST]
requirements:
_custom_access: '\Drupal\openstack\Controller\ApiController::access'
entity.openstack_volume.detach:
path: '/cloud_dashboard/openstack/{cloud_context}/openstack_volume/{entity_id}/detach'
defaults:
_controller: '\Drupal\openstack\Controller\ApiController::operateEntity'
entity_type_id: openstack_volume
command: detach
methods: [POST]
requirements:
_custom_access: '\Drupal\openstack\Controller\ApiController::access'
entity.openstack_volume.instances:
path: '/cloud_dashboard/openstack/{cloud_context}/openstack_volume/{entity_id}/instances'
defaults:
_controller: '\Drupal\openstack\Controller\ApiController::getInstanceOptions'
entity_type_id: openstack_volume
command: attach
requirements:
_custom_access: '\Drupal\openstack\Controller\ApiController::access'
......@@ -740,6 +740,16 @@ class ApiController extends ControllerBase implements ApiControllerInterface {
$method_name = 'editOpenStackVolume';
break;
case 'attach_openstack_volume':
$form_state->setValue('device_name', $request->get('device_name', ''));
$form_state->setValue('instance_id', $request->get('instance_id', ''));
$method_name = 'attachOpenStackVolume';
break;
case 'detach_openstack_volume':
$method_name = 'detachOpenStackVolume';
break;
}
// Execute the process.
......@@ -1096,4 +1106,48 @@ class ApiController extends ControllerBase implements ApiControllerInterface {
return new JsonResponse($options);
}
/**
* {@inheritdoc}
*/
public function getInstanceOptions(string $cloud_context, string $entity_id): JsonResponse {
/** @var \Drupal\openstack\Entity\OpenStackVolume $entity */
$entity = $this->entityTypeManager
->getStorage('openstack_volume')
->load($entity_id);
if (empty($entity)) {
return new JsonResponse([
'result' => 'NG',
'reason' => 'The volume has already been deleted.',
], 404);
}
$account = $this->currentUser();
$properties = [
'availability_zone' => $entity->getAvailabilityZone(),
'cloud_context' => $cloud_context,
];
if (!$account->hasPermission("view any openstack instance")) {
$properties['uid'] = $account->id();
}
$results = $this->entityTypeManager
->getStorage('openstack_instance')
->loadByProperties($properties);
$options = [];
foreach ($results ?: [] as $result) {
$options[] = [
'value' => $result->getInstanceId(),
'label' => $this->t('@name - @instance_id', [
'@name' => $result->getName(),
'@instance_id' => $result->getInstanceId(),
]),
];
}
return new JsonResponse($options);
}
}
......@@ -266,4 +266,20 @@ interface ApiControllerInterface {
*/
public function getAvailabilityZones($cloud_context): JsonResponse;
/**
* Helper function that loads all instances.
*
* @param string $cloud_context
* Cloud context to use in the query.
* @param string $entity_id
* The entity id of the OpenStack volume.
*
* @return \Symfony\Component\HttpFoundation\JsonResponse
* The JSON response.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
public function getInstanceOptions(string $cloud_context, string $entity_id): JsonResponse;
}
......@@ -14,6 +14,7 @@ use Drupal\Core\Entity\EntityTypeManager;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Messenger\Messenger;
use Drupal\Core\Plugin\CachedDiscoveryClearerInterface;
use Drupal\openstack\Service\OpenStackOperationsServiceInterface;
use Drupal\openstack\Service\OpenStackServiceFactoryInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
......@@ -29,6 +30,13 @@ class OpenStackVolumeAttachForm extends VolumeAttachForm {
*/
protected $openStackServiceFactory;
/**
* The OpenStack operations Service.
*
* @var \Drupal\openstack\Service\OpenStackOperationsServiceInterface
*/
protected $openStackOperationsService;
/**
* OpenStackVolumeAttachForm constructor.
*
......@@ -54,6 +62,8 @@ class OpenStackVolumeAttachForm extends VolumeAttachForm {
* The entity link render service.
* @param \Drupal\cloud\Plugin\cloud\config\CloudConfigPluginManagerInterface $cloud_config_plugin_manager
* The cloud service provider plugin manager (CloudConfigPluginManager).
* @param \Drupal\openstack\Service\OpenStackOperationsServiceInterface $openstack_operations_service
* The OpenStack Operations service.
*/
public function __construct(OpenStackServiceFactoryInterface $openstack_service_factory,
Ec2ServiceInterface $ec2_service,
......@@ -65,7 +75,8 @@ class OpenStackVolumeAttachForm extends VolumeAttachForm {
CacheBackendInterface $cacheRender,
CachedDiscoveryClearerInterface $plugin_cache_clearer,
EntityLinkRendererInterface $entity_link_renderer,
CloudConfigPluginManagerInterface $cloud_config_plugin_manager) {
CloudConfigPluginManagerInterface $cloud_config_plugin_manager,
OpenStackOperationsServiceInterface $openstack_operations_service) {
parent::__construct(
$ec2_service,
......@@ -81,6 +92,7 @@ class OpenStackVolumeAttachForm extends VolumeAttachForm {
);
$this->openStackServiceFactory = $openstack_service_factory;
$this->openStackOperationsService = $openstack_operations_service;
}
/**
......@@ -104,7 +116,8 @@ class OpenStackVolumeAttachForm extends VolumeAttachForm {
$container->get('cache.render'),
$container->get('plugin.cache_clearer'),
$container->get('entity.link_renderer'),
$container->get('plugin.manager.cloud_config_plugin')
$container->get('plugin.manager.cloud_config_plugin'),
$container->get('openstack.operations')
);
}
......@@ -136,11 +149,7 @@ class OpenStackVolumeAttachForm extends VolumeAttachForm {
* The current form state.
*/
public function submitForm(array &$form, FormStateInterface $form_state): void {
$entity = $this->entity;
// Switch OpenStack EC2 or REST service based on $entity->getCloudContext().
$this->ec2Service = $this->openStackServiceFactory->get($entity->getCloudContext());
parent::submitForm($form, $form_state);
$this->openStackOperationsService->attachOpenStackVolume($this->entity, $form, $form_state);
}
}
......@@ -2,7 +2,6 @@
namespace Drupal\openstack\Form;
use Drupal\openstack\Service\Rest\OpenStackService as OpenStackRestService;
use Drupal\aws_cloud\Form\Ec2\VolumeDetachForm;
use Drupal\aws_cloud\Service\Ec2\Ec2ServiceInterface;
use Drupal\cloud\Plugin\cloud\config\CloudConfigPluginManagerInterface;
......@@ -15,6 +14,7 @@ use Drupal\Core\Entity\EntityTypeManager;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Messenger\Messenger;
use Drupal\Core\Plugin\CachedDiscoveryClearerInterface;
use Drupal\openstack\Service\OpenStackOperationsServiceInterface;
use Drupal\openstack\Service\OpenStackServiceFactoryInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
......@@ -30,6 +30,13 @@ class OpenStackVolumeDetachForm extends VolumeDetachForm {
*/
protected $openStackServiceFactory;
/**
* The OpenStack operations Service.
*
* @var \Drupal\openstack\Service\OpenStackOperationsServiceInterface
*/
protected $openStackOperationsService;
/**
* OpenStackVolumeDetachForm constructor.
*
......@@ -55,6 +62,8 @@ class OpenStackVolumeDetachForm extends VolumeDetachForm {
* The entity link render service.
* @param \Drupal\cloud\Plugin\cloud\config\CloudConfigPluginManagerInterface $cloud_config_plugin_manager
* The cloud service provider plugin manager (CloudConfigPluginManager).
* @param \Drupal\openstack\Service\OpenStackOperationsServiceInterface $openstack_operations_service
* The OpenStack Operations service.
*/
public function __construct(OpenStackServiceFactoryInterface $openstack_service_factory,
Ec2ServiceInterface $ec2_service,
......@@ -66,7 +75,8 @@ class OpenStackVolumeDetachForm extends VolumeDetachForm {
CacheBackendInterface $cacheRender,
CachedDiscoveryClearerInterface $plugin_cache_clearer,
EntityLinkRendererInterface $entity_link_renderer,
CloudConfigPluginManagerInterface $cloud_config_plugin_manager) {
CloudConfigPluginManagerInterface $cloud_config_plugin_manager,
OpenStackOperationsServiceInterface $openstack_operations_service) {
parent::__construct(
$ec2_service,
......@@ -82,6 +92,7 @@ class OpenStackVolumeDetachForm extends VolumeDetachForm {
);
$this->openStackServiceFactory = $openstack_service_factory;
$this->openStackOperationsService = $openstack_operations_service;
}
/**
......@@ -105,7 +116,8 @@ class OpenStackVolumeDetachForm extends VolumeDetachForm {
$container->get('cache.render'),
$container->get('plugin.cache_clearer'),
$container->get('entity.link_renderer'),
$container->get('plugin.manager.cloud_config_plugin')
$container->get('plugin.manager.cloud_config_plugin'),
$container->get('openstack.operations')
);
}
......@@ -137,19 +149,7 @@ class OpenStackVolumeDetachForm extends VolumeDetachForm {
* The current form state.
*/
public function submitForm(array &$form, FormStateInterface $form_state): void {
$entity = $this->entity;
// Switch OpenStack EC2 or REST service based on $entity->getCloudContext().
$this->ec2Service = $this->openStackServiceFactory->get($entity->getCloudContext());
parent::submitForm($form, $form_state);
// Detach Volume of OpenStack REST API.
if ($this->ec2Service instanceof OpenStackRestService) {
$this->ec2Service->detachVolume([
'VolumeId' => $entity->getVolumeId(),
'InstanceId' => $entity->getAttachmentInformation(),
]);
}
$this->openStackOperationsService->detachOpenStackVolume($this->entity, $form, $form_state);
}
}
......@@ -2099,6 +2099,146 @@ class OpenStackOperationsService implements OpenStackOperationsServiceInterface
return TRUE;
}
/**
* {@inheritdoc}
*/
public function attachOpenStackVolume(VolumeInterface $entity, array &$form, FormStateInterface $form_state): bool {
/** @var \Drupal\aws_cloud\Entity\Ec2\VolumeInterface $entity */
$this->entity = $entity;
$this->ec2Service = $this->openStackServiceFactory->get($entity->getCloudContext());
$this->awsCloudOperationsService->setEc2Service($this->ec2Service);
return $this->attachVolume($form, $form_state);
}
/**
* Attach a volume.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*
* @throws \Drupal\Core\Entity\EntityStorageException
* @throws \Drupal\Core\Entity\EntityMalformedException
*/
private function attachVolume(array &$form, FormStateInterface $form_state): bool {
/** @var \Drupal\aws_cloud\Entity\Ec2\VolumeInterface $entity */
$entity = $this->entity;
$instance_id = $form_state->getValue('instance_id');
$volume_id = $entity->getVolumeId();
$device_name = $form_state->getValue('device_name');
$this->ec2Service->setCloudContext($this->entity->getCloudContext());
$result = $this->ec2Service->attachVolume([
'InstanceId' => $instance_id,
'VolumeId' => $volume_id,
'Device' => $device_name,
]);
if ($result === NULL) {
$this->processOperationErrorStatus($entity, 'attachded');
return FALSE;
}
$form_state->setRedirect("view.{$entity->getEntityTypeId()}.list", ['cloud_context' => $entity->getCloudContext()]);
if (!empty($result['SendToWorker'])) {
$this->messenger->addStatus($this->t('The Volume %volume is attaching to %instance remotely.', [
'%volume' => $volume_id,
'%instance' => $instance_id,
]));
return TRUE;
}
// Set the instance_id in the volume entity and save.
$entity->setAttachmentInformation($instance_id);
$entity->setState($result['State'] ?? 'in-use');
$entity->save();
$this->clearCacheValues($entity->getCacheTags());
$this->dispatchSubmitEvent($entity);
$this->messenger->addStatus($this->t('The Volume %volume is attaching to %instance.', [
'%volume' => $volume_id,
'%instance' => $instance_id,
]));
return TRUE;
}
/**
* {@inheritdoc}
*/
public function detachOpenStackVolume(VolumeInterface $entity, array &$form, FormStateInterface $form_state): bool {
/** @var \Drupal\aws_cloud\Entity\Ec2\VolumeInterface $entity */
$this->entity = $entity;
$this->ec2Service = $this->openStackServiceFactory->get($entity->getCloudContext());
$this->awsCloudOperationsService->setEc2Service($this->ec2Service);
$this->detachVolume($form, $form_state);
// Detach Volume of OpenStack REST API.
if ($this->ec2Service instanceof OpenStackRestService) {
$this->ec2Service->detachVolume([
'VolumeId' => $entity->getVolumeId(),
'InstanceId' => $entity->getAttachmentInformation(),
]);
}
return TRUE;
}
/**
* Detach a volume.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*
* @throws \Drupal\Core\Entity\EntityStorageException
* @throws \Drupal\Core\Entity\EntityMalformedException
*/
private function detachVolume(array &$form, FormStateInterface $form_state): bool {
/** @var \Drupal\aws_cloud\Entity\Ec2\VolumeInterface $entity */
$entity = $this->entity;
$volume_id = $entity->getVolumeId();
$instance_id = $entity->getAttachmentInformation();
$this->ec2Service->setCloudContext($this->entity->getCloudContext());
$result = $this->ec2Service->detachVolume([
'VolumeId' => $volume_id,
]);
if ($result === NULL) {
$this->processOperationErrorStatus($entity, 'detachded');
return FALSE;
}
$form_state->setRedirect("view.{$entity->getEntityTypeId()}.list", ['cloud_context' => $entity->getCloudContext()]);
if (!empty($result['SendToWorker'])) {
$this->messenger->addStatus($this->t('The volume %volume is detaching from %instance remotely', [
'%volume' => $volume_id,
'%instance' => $instance_id,
]));
return TRUE;
}
// Set the instance_id in the volume entity and save.
$entity->setAttachmentInformation('');
$entity->setState($result['State'] ?? 'available');
$entity->save();
$this->messenger->addStatus($this->t('The volume %volume is detaching from %instance', [
'%volume' => $volume_id,
'%instance' => $instance_id,
]));
$this->clearCacheValues($entity->getCacheTags());
$this->dispatchSubmitEvent($entity);
$form_state->setRedirect("view.{$entity->getEntityTypeId()}.list", ['cloud_context' => $entity->getCloudContext()]);
return TRUE;
}
/**
* Helper method to load instance by ID.
*
......
......@@ -471,4 +471,34 @@ interface OpenStackOperationsServiceInterface {
*/
public function importOpenStackKeyPair(KeyPairInterface $entity, array $form, FormStateInterface $form_state): bool;
/**
* Attache an OpenStack volume.
*
* @param \Drupal\aws_cloud\Entity\Ec2\VolumeInterface $entity
* The OpenStack image entity.
* @param array $form
* Array of form object.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current form state.
*
* @return bool
* TRUE when the process succeeds.
*/
public function attachOpenStackVolume(VolumeInterface $entity, array &$form, FormStateInterface $form_state): bool;
/**
* Detache an OpenStack volume.
*
* @param \Drupal\aws_cloud\Entity\Ec2\VolumeInterface $entity
* The OpenStack image entity.
* @param array $form
* Array of form object.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current form state.
*
* @return bool
* TRUE when the process succeeds.
*/
public function detachOpenStackVolume(VolumeInterface $entity, array &$form, FormStateInterface $form_state): bool;
}
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