Spaces:
Runtime error
Runtime error
from typing import Optional, Any, Dict | |
from typing_extensions import Literal | |
from phi.aws.api_client import AwsApiClient | |
from phi.aws.resource.base import AwsResource | |
from phi.cli.console import print_info | |
from phi.utils.log import logger | |
class EbsVolume(AwsResource): | |
""" | |
Reference: | |
- https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ec2.html#volume | |
- https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ec2.html#EC2.Client.create_volume | |
""" | |
resource_type: Optional[str] = "EbsVolume" | |
service_name: str = "ec2" | |
# The unique name to give to your volume. | |
name: str | |
# The size of the volume, in GiBs. You must specify either a snapshot ID or a volume size. | |
# If you specify a snapshot, the default is the snapshot size. You can specify a volume size that is | |
# equal to or larger than the snapshot size. | |
# | |
# The following are the supported volumes sizes for each volume type: | |
# gp2 and gp3 : 1-16,384 | |
# io1 and io2 : 4-16,384 | |
# st1 and sc1 : 125-16,384 | |
# standard : 1-1,024 | |
size: int | |
# The Availability Zone in which to create the volume. | |
availability_zone: str | |
# Indicates whether the volume should be encrypted. The effect of setting the encryption state to | |
# true depends on the volume origin (new or from a snapshot), starting encryption state, ownership, | |
# and whether encryption by default is enabled. | |
# Encrypted Amazon EBS volumes must be attached to instances that support Amazon EBS encryption. | |
encrypted: Optional[bool] = None | |
# The number of I/O operations per second (IOPS). For gp3 , io1 , and io2 volumes, this represents the | |
# number of IOPS that are provisioned for the volume. For gp2 volumes, this represents the baseline | |
# performance of the volume and the rate at which the volume accumulates I/O credits for bursting. | |
# | |
# The following are the supported values for each volume type: | |
# gp3 : 3,000-16,000 IOPS | |
# io1 : 100-64,000 IOPS | |
# io2 : 100-64,000 IOPS | |
# | |
# This parameter is required for io1 and io2 volumes. | |
# The default for gp3 volumes is 3,000 IOPS. | |
# This parameter is not supported for gp2 , st1 , sc1 , or standard volumes. | |
iops: Optional[int] = None | |
# The identifier of the Key Management Service (KMS) KMS key to use for Amazon EBS encryption. | |
# If this parameter is not specified, your KMS key for Amazon EBS is used. If KmsKeyId is specified, | |
# the encrypted state must be true . | |
kms_key_id: Optional[str] = None | |
# The Amazon Resource Name (ARN) of the Outpost. | |
outpost_arn: Optional[str] = None | |
# The snapshot from which to create the volume. You must specify either a snapshot ID or a volume size. | |
snapshot_id: Optional[str] = None | |
# The volume type. This parameter can be one of the following values: | |
# | |
# General Purpose SSD: gp2 | gp3 | |
# Provisioned IOPS SSD: io1 | io2 | |
# Throughput Optimized HDD: st1 | |
# Cold HDD: sc1 | |
# Magnetic: standard | |
# | |
# Default: gp2 | |
volume_type: Optional[Literal["standard", "io_1", "io_2", "gp_2", "sc_1", "st_1", "gp_3"]] = None | |
# Checks whether you have the required permissions for the action, without actually making the request, | |
# and provides an error response. If you have the required permissions, the error response is DryRunOperation. | |
# Otherwise, it is UnauthorizedOperation . | |
dry_run: Optional[bool] = None | |
# The tags to apply to the volume during creation. | |
tags: Optional[Dict[str, str]] = None | |
# The tag to use for volume name | |
name_tag: str = "Name" | |
# Indicates whether to enable Amazon EBS Multi-Attach. If you enable Multi-Attach, you can attach the volume to | |
# up to 16 Instances built on the Nitro System in the same Availability Zone. This parameter is supported with | |
# io1 and io2 volumes only. | |
multi_attach_enabled: Optional[bool] = None | |
# The throughput to provision for a volume, with a maximum of 1,000 MiB/s. | |
# This parameter is valid only for gp3 volumes. | |
# Valid Range: Minimum value of 125. Maximum value of 1000. | |
throughput: Optional[int] = None | |
# Unique, case-sensitive identifier that you provide to ensure the idempotency of the request. | |
# This field is autopopulated if not provided. | |
client_token: Optional[str] = None | |
wait_for_create: bool = False | |
volume_id: Optional[str] = None | |
def _create(self, aws_client: AwsApiClient) -> bool: | |
"""Creates the EbsVolume | |
Args: | |
aws_client: The AwsApiClient for the current volume | |
""" | |
print_info(f"Creating {self.get_resource_type()}: {self.get_resource_name()}") | |
# Step 1: Build Volume configuration | |
# Add name as a tag because volumes do not have names | |
tags = {self.name_tag: self.name} | |
if self.tags is not None and isinstance(self.tags, dict): | |
tags.update(self.tags) | |
# create a dict of args which are not null, otherwise aws type validation fails | |
not_null_args: Dict[str, Any] = {} | |
if self.encrypted: | |
not_null_args["Encrypted"] = self.encrypted | |
if self.iops: | |
not_null_args["Iops"] = self.iops | |
if self.kms_key_id: | |
not_null_args["KmsKeyId"] = self.kms_key_id | |
if self.outpost_arn: | |
not_null_args["OutpostArn"] = self.outpost_arn | |
if self.snapshot_id: | |
not_null_args["SnapshotId"] = self.snapshot_id | |
if self.volume_type: | |
not_null_args["VolumeType"] = self.volume_type | |
if self.dry_run: | |
not_null_args["DryRun"] = self.dry_run | |
if tags: | |
not_null_args["TagSpecifications"] = [ | |
{ | |
"ResourceType": "volume", | |
"Tags": [{"Key": k, "Value": v} for k, v in tags.items()], | |
}, | |
] | |
if self.multi_attach_enabled: | |
not_null_args["MultiAttachEnabled"] = self.multi_attach_enabled | |
if self.throughput: | |
not_null_args["Throughput"] = self.throughput | |
if self.client_token: | |
not_null_args["ClientToken"] = self.client_token | |
# Step 2: Create Volume | |
service_client = self.get_service_client(aws_client) | |
try: | |
create_response = service_client.create_volume( | |
AvailabilityZone=self.availability_zone, | |
Size=self.size, | |
**not_null_args, | |
) | |
logger.debug(f"create_response: {create_response}") | |
# Validate Volume creation | |
if create_response is not None: | |
create_time = create_response.get("CreateTime", None) | |
self.volume_id = create_response.get("VolumeId", None) | |
logger.debug(f"create_time: {create_time}") | |
logger.debug(f"volume_id: {self.volume_id}") | |
if create_time is not None: | |
self.active_resource = create_response | |
return True | |
except Exception as e: | |
logger.error(f"{self.get_resource_type()} could not be created.") | |
logger.error(e) | |
return False | |
def post_create(self, aws_client: AwsApiClient) -> bool: | |
# Wait for Volume to be created | |
if self.wait_for_create: | |
try: | |
if self.volume_id is not None: | |
print_info(f"Waiting for {self.get_resource_type()} to be created.") | |
waiter = self.get_service_client(aws_client).get_waiter("volume_available") | |
waiter.wait( | |
VolumeIds=[self.volume_id], | |
WaiterConfig={ | |
"Delay": self.waiter_delay, | |
"MaxAttempts": self.waiter_max_attempts, | |
}, | |
) | |
else: | |
logger.warning("Skipping waiter, no volume_id found") | |
except Exception as e: | |
logger.error("Waiter failed.") | |
logger.error(e) | |
return True | |
def _read(self, aws_client: AwsApiClient) -> Optional[Any]: | |
"""Returns the EbsVolume | |
Args: | |
aws_client: The AwsApiClient for the current volume | |
""" | |
logger.debug(f"Reading {self.get_resource_type()}: {self.get_resource_name()}") | |
from botocore.exceptions import ClientError | |
service_client = self.get_service_client(aws_client) | |
try: | |
volume = None | |
describe_volumes = service_client.describe_volumes( | |
Filters=[ | |
{ | |
"Name": "tag:" + self.name_tag, | |
"Values": [self.name], | |
}, | |
], | |
) | |
# logger.debug(f"describe_volumes: {describe_volumes}") | |
for _volume in describe_volumes.get("Volumes", []): | |
_volume_tags = _volume.get("Tags", None) | |
if _volume_tags is not None and isinstance(_volume_tags, list): | |
for _tag in _volume_tags: | |
if _tag["Key"] == self.name_tag and _tag["Value"] == self.name: | |
volume = _volume | |
break | |
# found volume, break loop | |
if volume is not None: | |
break | |
if volume is not None: | |
create_time = volume.get("CreateTime", None) | |
logger.debug(f"create_time: {create_time}") | |
self.volume_id = volume.get("VolumeId", None) | |
logger.debug(f"volume_id: {self.volume_id}") | |
self.active_resource = volume | |
except ClientError as ce: | |
logger.debug(f"ClientError: {ce}") | |
except Exception as e: | |
logger.error(f"Error reading {self.get_resource_type()}.") | |
logger.error(e) | |
return self.active_resource | |
def _delete(self, aws_client: AwsApiClient) -> bool: | |
"""Deletes the EbsVolume | |
Args: | |
aws_client: The AwsApiClient for the current volume | |
""" | |
print_info(f"Deleting {self.get_resource_type()}: {self.get_resource_name()}") | |
self.active_resource = None | |
service_client = self.get_service_client(aws_client) | |
try: | |
volume = self._read(aws_client) | |
logger.debug(f"EbsVolume: {volume}") | |
if volume is None or self.volume_id is None: | |
logger.warning(f"No {self.get_resource_type()} to delete") | |
return True | |
# detach the volume from all instances | |
for attachment in volume.get("Attachments", []): | |
device = attachment.get("Device", None) | |
instance_id = attachment.get("InstanceId", None) | |
print_info(f"Detaching volume from device: {device}, instance_id: {instance_id}") | |
service_client.detach_volume( | |
Device=device, | |
InstanceId=instance_id, | |
VolumeId=self.volume_id, | |
) | |
# delete volume | |
service_client.delete_volume(VolumeId=self.volume_id) | |
return True | |
except Exception as e: | |
logger.error(f"{self.get_resource_type()} could not be deleted.") | |
logger.error("Please try again or delete resources manually.") | |
logger.error(e) | |
return False | |
def _update(self, aws_client: AwsApiClient) -> bool: | |
"""Updates the EbsVolume | |
Args: | |
aws_client: The AwsApiClient for the current volume | |
""" | |
print_info(f"Updating {self.get_resource_type()}: {self.get_resource_name()}") | |
# Step 1: Build Volume configuration | |
# Add name as a tag because volumes do not have names | |
tags = {self.name_tag: self.name} | |
if self.tags is not None and isinstance(self.tags, dict): | |
tags.update(self.tags) | |
# create a dict of args which are not null, otherwise aws type validation fails | |
not_null_args: Dict[str, Any] = {} | |
if self.iops: | |
not_null_args["Iops"] = self.iops | |
if self.volume_type: | |
not_null_args["VolumeType"] = self.volume_type | |
if self.dry_run: | |
not_null_args["DryRun"] = self.dry_run | |
if tags: | |
not_null_args["TagSpecifications"] = [ | |
{ | |
"ResourceType": "volume", | |
"Tags": [{"Key": k, "Value": v} for k, v in tags.items()], | |
}, | |
] | |
if self.multi_attach_enabled: | |
not_null_args["MultiAttachEnabled"] = self.multi_attach_enabled | |
if self.throughput: | |
not_null_args["Throughput"] = self.throughput | |
service_client = self.get_service_client(aws_client) | |
try: | |
volume = self._read(aws_client) | |
logger.debug(f"EbsVolume: {volume}") | |
if volume is None or self.volume_id is None: | |
logger.warning(f"No {self.get_resource_type()} to update") | |
return True | |
# update volume | |
update_response = service_client.modify_volume( | |
VolumeId=self.volume_id, | |
**not_null_args, | |
) | |
logger.debug(f"update_response: {update_response}") | |
# Validate Volume update | |
volume_modification = update_response.get("VolumeModification", None) | |
if volume_modification is not None: | |
volume_id_after_modification = volume_modification.get("VolumeId", None) | |
logger.debug(f"volume_id: {volume_id_after_modification}") | |
if volume_id_after_modification is not None: | |
return True | |
except Exception as e: | |
logger.error(f"{self.get_resource_type()} could not be updated.") | |
logger.error("Please try again or update resources manually.") | |
logger.error(e) | |
return False | |
def get_volume_id(self, aws_client: Optional[AwsApiClient] = None) -> Optional[str]: | |
"""Returns the volume_id of the EbsVolume""" | |
client = aws_client or self.get_aws_client() | |
if client is not None: | |
self._read(client) | |
return self.volume_id | |