From 9c41e806d085302e0fbb1795fe694c2687eeada8 Mon Sep 17 00:00:00 2001 From: Costya Date: Tue, 10 Sep 2024 13:49:16 -0500 Subject: [PATCH] added 2 handlers: disk, image. Updated instance handler. Completed vm_details_actions --- .../cp/gcp/actions/vm_details_actions.py | 170 ++++++++---------- cloudshell/cp/gcp/flows/cleanup_infra_flow.py | 6 +- cloudshell/cp/gcp/flows/prepare_infra_flow.py | 11 +- cloudshell/cp/gcp/flows/vm_details_flow.py | 21 ++- cloudshell/cp/gcp/handlers/disk.py | 53 ++++++ cloudshell/cp/gcp/handlers/image.py | 57 ++++++ cloudshell/cp/gcp/handlers/instance.py | 2 +- cloudshell/cp/gcp/helpers/name_generator.py | 2 +- cloudshell/cp/gcp/resource_conf.py | 4 +- 9 files changed, 209 insertions(+), 117 deletions(-) create mode 100644 cloudshell/cp/gcp/handlers/disk.py create mode 100644 cloudshell/cp/gcp/handlers/image.py diff --git a/cloudshell/cp/gcp/actions/vm_details_actions.py b/cloudshell/cp/gcp/actions/vm_details_actions.py index 6449f11..bcf6cfe 100644 --- a/cloudshell/cp/gcp/actions/vm_details_actions.py +++ b/cloudshell/cp/gcp/actions/vm_details_actions.py @@ -10,11 +10,15 @@ VmDetailsNetworkInterface, VmDetailsProperty, ) + +from cloudshell.cp.gcp.handlers.disk import DiskHandler +from cloudshell.cp.gcp.handlers.image import ImageHandler +from cloudshell.cp.gcp.helpers.constants import PUBLIC_IMAGE_PROJECTS from cloudshell.cp.gcp.helpers.interface_helper import InterfaceHelper if typing.TYPE_CHECKING: from cloudshell.cp.gcp.resource_conf import GCPResourceConfig - from cloudshell.cp.gcp.handlers.instance import Instance + from cloudshell.cp.gcp.handlers.instance import Instance, InstanceHandler @define @@ -23,79 +27,68 @@ class VMDetailsActions: logger: Logger @staticmethod - def _parse_image_name(resource_id): - """Get image name from the Azure image reference ID. - - :param str resource_id: Azure image reference ID - :return: Azure image name - :rtype: str - """ - match_images = re.match( - r".*images/(?P[^/]*).*", resource_id, flags=re.IGNORECASE - ) - return match_images.group("image_name") if match_images else "" - - @staticmethod - def _prepare_common_vm_instance_data(instance, resource_group_name: str): + def _prepare_common_vm_instance_data(instance_handler: InstanceHandler): """Prepare common VM instance data.""" - disks = instance.disks - os_disk = instance.disks[0] - # os_disk_type = convert_azure_to_cs_disk_type( - # azure_disk_type=os_disk.managed_disk.storage_account_type - # ) - if isinstance(instance.storage_profile.os_disk.os_type, str): - os_name = instance.storage_profile.os_disk.os_type - else: - os_name = instance.storage_profile.os_disk.os_type.name - + instance = instance_handler.instance vm_properties = [ VmDetailsProperty( - key="VM Size", value=instance.machine_type - ), - VmDetailsProperty( - key="Operating System", - value=os_name, - ), - # VmDetailsProperty( - # key="OS Disk Size", - # value=f"{instance.storage_profile.os_disk.disk_size_gb}GB " - # f"({os_disk_type})", - # ), - VmDetailsProperty( - key="Resource Group", - value=resource_group_name, + key="Machine Type", value=instance.machine_type.rsplit('/', 1)[-1], ), ] + disks = list(instance.disks) + os_disk = next((disk for disk in disks if disk.boot), None) + disks.remove(os_disk) + if os_disk: + os_disk_data = DiskHandler.get( + disk_name=os_disk.source.rsplit('/', 1)[-1], + zone=instance.zone.rsplit('/', 1)[-1], + credentials=instance_handler.credentials, + ) + image_data = ImageHandler.parse_image_name( + image_url=os_disk_data.source_image, + ) + + image_name = image_data.get("image_name", "N/A") + image_project = image_data.get("image_project", "N/A") + image_project = next( + (k for k, v in PUBLIC_IMAGE_PROJECTS.items() if v == image_project), + image_project) + disk_type = os_disk_data.disk_type + disk_size = os_disk_data.disk_size + vm_properties.extend( + [ + VmDetailsProperty(key="Instance Arch", + value=f"{os_disk_data.architecture.lower()}", ), + VmDetailsProperty(key="OS Disk Size", + value=f"{disk_size}GB ({disk_type})", ), + VmDetailsProperty(key="Image Name", value=image_name), + VmDetailsProperty(key="Image Project", value=image_project), + ] + ) + for disk_number, data_disk in enumerate( - instance.disks, start=1 + disks, start=1 ): - if data_disk.boot: - vm_properties.append(VmDetailsProperty( - key="OS Disk Size", - value=f"{instance.storage_profile.os_disk.disk_size_gb}GB " - f"({instance.storage_profile.os_disk.disk_type})",)) - - # else: - # disk_type = convert_azure_to_cs_disk_type( - # azure_disk_type=data_disk.managed_disk.storage_account_type - # ) - # disk_name_prop = VmDetailsProperty( - # key=f"Data Disk {disk_number} Name", - # value=get_display_data_disk_name( - # vm_name=instance.name, full_disk_name=data_disk.name - # ), - # ) - # disk_size_prop = VmDetailsProperty( - # key=f"Data Disk {disk_number} Size", - # value=f"{data_disk.disk_size_gb}GB ({disk_type})", - # ) - # vm_properties.append(disk_name_prop) - # vm_properties.append(disk_size_prop) + disk_data = DiskHandler.get( + disk_name=data_disk.name, + zone=instance.zone.rsplit('/', 1)[-1], + credentials=instance_handler.credentials, + ) + disk_name_prop = VmDetailsProperty( + key=f"Data Disk {disk_number} Name", + value=disk_data.disk.name, + ) + disk_size_prop = VmDetailsProperty( + key=f"Data Disk {disk_number} Size", + value=f"{data_disk.disk_size}GB ({disk_data.disk_type})", + ) + vm_properties.append(disk_name_prop) + vm_properties.append(disk_size_prop) return vm_properties - def _prepare_vm_network_data(self, instance: Instance): + def _prepare_vm_network_data(self, instance_handler: InstanceHandler): """Prepare VM Network data. :param instance: @@ -105,19 +98,17 @@ def _prepare_vm_network_data(self, instance: Instance): vm_network_interfaces = [] - for network_interface in instance.network_interfaces: + for network_interface in instance_handler.instance.network_interfaces: is_primary = False - index = instance.network_interfaces.index(network_interface) + index = instance_handler.instance.network_interfaces.index( + network_interface) if index == 0: is_primary = True interface_name = network_interface.name private_ip = network_interface.network_i_p - # nic_type - public_ip = InterfaceHelper(instance).get_public_ip(index) - network_data = [ - # VmDetailsProperty(key="IP", value=private_ip), - ] + public_ip = InterfaceHelper(instance_handler.instance).get_public_ip(index) + network_data = [] subnet_name = network_interface.subnetwork @@ -147,47 +138,30 @@ def _prepare_vm_network_data(self, instance: Instance): def _prepare_vm_details( self, - instance, + instance_handler: InstanceHandler, prepare_vm_instance_data_function: typing.Callable, ): """Prepare VM details.""" try: return VmDetailsData( - appName=instance.name, + appName=instance_handler.instance.name, vmInstanceData=prepare_vm_instance_data_function( - instance=instance, + instance_handler=instance_handler, ), vmNetworkData=self._prepare_vm_network_data( - instance=instance, + instance_handler=instance_handler, ), ) except Exception as e: - self._logger.exception( - f"Error getting VM details for {instance.name}" + self.logger.exception( + f"Error getting VM details for {instance_handler.instance.name}" ) - return VmDetailsData(appName=instance.name, errorMessage=str(e)) + return VmDetailsData(appName=instance_handler.instance.name, + errorMessage=str(e)) - def _prepare_vm_instance_data( - self, instance, resource_group_name: str - ): - """Prepare custom VM instance data.""" - image_resource_id = instance.storage_profile.image_reference.id - image_name = self._parse_image_name(resource_id=image_resource_id) - image_resource_group = self._parse_resource_group_name( - resource_id=image_resource_id - ) - - return [ - VmDetailsProperty(key="Image", value=image_name), - VmDetailsProperty(key="Image Project", value=image_resource_group), - ] + self._prepare_common_vm_instance_data( - instance=instance, - resource_group_name=resource_group_name, - ) - - def prepare_custom_vm_details(self, instance): + def prepare_vm_details(self, instance_handler: InstanceHandler): """Prepare custom VM details.""" return self._prepare_vm_details( - instance=instance, - prepare_vm_instance_data_function=self._prepare_vm_instance_data, + instance_handler=instance_handler, + prepare_vm_instance_data_function=self._prepare_common_vm_instance_data, ) \ No newline at end of file diff --git a/cloudshell/cp/gcp/flows/cleanup_infra_flow.py b/cloudshell/cp/gcp/flows/cleanup_infra_flow.py index 58d8bed..883d564 100644 --- a/cloudshell/cp/gcp/flows/cleanup_infra_flow.py +++ b/cloudshell/cp/gcp/flows/cleanup_infra_flow.py @@ -13,10 +13,10 @@ from cloudshell.cp.gcp.handlers.ssh_keys import SSHKeysHandler from cloudshell.cp.gcp.handlers.subnet import SubnetHandler from cloudshell.cp.gcp.handlers.vpc import VPCHandler -from cloudshell.cp.gcp.helpers.name_generator import generate_vpc_name - from cloudshell.cp.core.request_actions import CleanupSandboxInfraRequestActions +from cloudshell.cp.gcp.helpers.name_generator import GCPNameGenerator + if TYPE_CHECKING: from logging import Logger from cloudshell.cp.gcp.resource_conf import GCPResourceConfig @@ -37,7 +37,7 @@ def cleanup_sandbox_infra(self, request_actions: CleanupSandboxInfraRequestActio def _cleanup_network(self, cleanup_network_action: CleanupNetwork): sandbox_id = self.config.reservation_info.reservation_id storage_handler = SSHKeysHandler(self.config.credentials) - network_name = generate_vpc_name(sandbox_id) + network_name = GCPNameGenerator().network(sandbox_id) self._logger.info(f"Cleaning up network: quali{network_name} in region:" f" {self.config.region}") diff --git a/cloudshell/cp/gcp/flows/prepare_infra_flow.py b/cloudshell/cp/gcp/flows/prepare_infra_flow.py index ccaeef8..d1e40c2 100644 --- a/cloudshell/cp/gcp/flows/prepare_infra_flow.py +++ b/cloudshell/cp/gcp/flows/prepare_infra_flow.py @@ -13,8 +13,7 @@ from cloudshell.cp.gcp.handlers.ssh_keys import SSHKeysHandler from cloudshell.cp.gcp.handlers.subnet import SubnetHandler from cloudshell.cp.gcp.handlers.vpc import VPCHandler -from cloudshell.cp.gcp.helpers.name_generator import generate_name, generate_vpc_name - +from cloudshell.cp.gcp.helpers.name_generator import GCPNameGenerator if TYPE_CHECKING: from logging import Logger @@ -29,6 +28,7 @@ class PrepareGCPInfraFlow(AbstractPrepareSandboxInfraFlow): logger: Logger config: GCPResourceConfig vpc: str = field(init=False, default=None) + name_generator: GCPNameGenerator = field(init=False, default=GCPNameGenerator()) def __attrs_post_init__(self): super().__init__(self.logger) @@ -37,7 +37,8 @@ def prepare_common_objects(self, request_actions: RequestActions) -> None: """""" vpc_handler = VPCHandler(self.config.credentials) self.vpc = vpc_handler.get_or_create_vpc( - generate_vpc_name(self.config.reservation_info.reservation_id) + self.name_generator.network( + self.config.reservation_info.reservation_id) ) self._create_firewall_rules(request_actions, self.vpc) @@ -55,7 +56,9 @@ def prepare_subnets(self, request_actions: RequestActions) -> dict[str, str]: subnet_request.actionId: subnet_handler.get_or_create_subnet( network_name=self.vpc, ip_cidr_range=subnet_request.get_cidr(), - subnet_name=generate_name(subnet_request.get_alias()) + subnet_name=self.name_generator.subnet( + subnet_request.get_alias() + ), ) } ) diff --git a/cloudshell/cp/gcp/flows/vm_details_flow.py b/cloudshell/cp/gcp/flows/vm_details_flow.py index 8dacd16..d13aeac 100644 --- a/cloudshell/cp/gcp/flows/vm_details_flow.py +++ b/cloudshell/cp/gcp/flows/vm_details_flow.py @@ -1,5 +1,6 @@ from __future__ import annotations +import json from typing import TYPE_CHECKING from attr import define @@ -19,22 +20,26 @@ class GCPGetVMDetailsFlow(AbstractVMDetailsFlow): logger: Logger config: GCPResourceConfig + def __attrs_post_init__(self): + super().__init__(self.logger) + def _get_vm_details(self, deployed_app: BaseGCPDeployApp): """Get VM Details. :param deployed_app: :return: """ - # sandbox_id = self.config.reservation_info.reservation_id + name, zone = json.loads(deployed_app.vmdetails.uid).values() - vm_actions = InstanceHandler() + instance_handler = InstanceHandler.get( + instance_name=name, + credentials=self.config.credentials, + zone=zone, + ) vm_details_actions = VMDetailsActions( - config=self.config, logger=self._logger + config=self.config, logger=self.logger ) - # with self._cancellation_manager: - vm = vm_actions.get_vm_by_name( - vm_name=deployed_app.name, + return vm_details_actions.prepare_vm_details( + instance_handler=instance_handler ) - - return vm_details_actions.prepare_custom_vm_details(instance=vm) diff --git a/cloudshell/cp/gcp/handlers/disk.py b/cloudshell/cp/gcp/handlers/disk.py new file mode 100644 index 0000000..cd64d9e --- /dev/null +++ b/cloudshell/cp/gcp/handlers/disk.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +import logging + +from attr import define +from google.cloud import compute_v1 +from google.cloud.compute_v1.types import compute +from typing_extensions import TYPE_CHECKING + + +from cloudshell.cp.gcp.handlers.base import BaseGCPHandler + +if TYPE_CHECKING: + from google.auth.credentials import Credentials + from typing_extensions import Self + +logger = logging.getLogger(__name__) + +@define +class DiskHandler(BaseGCPHandler): + disk: compute.Disk + + @classmethod + def get(cls, disk_name: str, zone: str, credentials: Credentials) -> Self: + """Get disk object from GCP and create DiskHandler object.""" + logger.info(f"Getting Disk {disk_name}.") + client = compute_v1.DisksClient(credentials=credentials) + disk = client.get( + project=credentials.project_id, + zone=zone, + disk=disk_name + ) + return cls(disk=disk, credentials=credentials) + + @property + def disk_size(self) -> str: + """Get image name from disk.""" + return self.disk.size_gb + + @property + def disk_type(self) -> str: + """Get image project from disk.""" + return self.disk.type_.rsplit("/", 1)[-1] + + @property + def architecture(self) -> str: + """Get image project from disk.""" + return self.disk.architecture + + @property + def source_image(self) -> str: + """Get image project from disk.""" + return self.disk.source_image diff --git a/cloudshell/cp/gcp/handlers/image.py b/cloudshell/cp/gcp/handlers/image.py new file mode 100644 index 0000000..29d9872 --- /dev/null +++ b/cloudshell/cp/gcp/handlers/image.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +import logging +import re + +from attr import define +from google.cloud import compute_v1 +from google.cloud.compute_v1.types import compute +from typing_extensions import TYPE_CHECKING + + +from cloudshell.cp.gcp.handlers.base import BaseGCPHandler + +if TYPE_CHECKING: + from google.auth.credentials import Credentials + from typing_extensions import Self + +logger = logging.getLogger(__name__) + +@define +class ImageHandler(BaseGCPHandler): + image: compute.Disk + + @classmethod + def parse_image_name(cls, image_url: str) -> dict[str, str]: + """Get image data from the disk url. + + :rtype: str + """ + result = {} + # image_url = projects/debian-cloud/global/images/debian-12-bookworm-v20240815 + match_image = re.match( + r"^.*projects/(?P\S+)/global/images/(?P\S+)$", + image_url, flags=re.IGNORECASE + ) + if match_image: + result = match_image.groupdict() + return result + + @classmethod + def get( + cls, + credentials: Credentials, + image_url: str, + image_project: str | None = None, + ) -> Self: + """Get disk object from GCP and create DiskHandler object.""" + image_data = cls._parse_image_name(image_url) + image_name = image_data.get("image_name", "N/A") + image_project = image_data.get("image_project", image_project) + logger.info(f"Getting Image {image_name} from {image_project}.") + client = compute_v1.ImagesClient(credentials=credentials) + image = client.get( + project=image_project, + image=image_name + ) + return cls(image=image, credentials=credentials) \ No newline at end of file diff --git a/cloudshell/cp/gcp/handlers/instance.py b/cloudshell/cp/gcp/handlers/instance.py index be7895a..881b1d6 100644 --- a/cloudshell/cp/gcp/handlers/instance.py +++ b/cloudshell/cp/gcp/handlers/instance.py @@ -6,6 +6,7 @@ from attrs import define from google.cloud import compute_v1 +from google.cloud.compute_v1.types import compute from cloudshell.cp.gcp.handlers.base import BaseGCPHandler from cloudshell.cp.gcp.helpers.name_generator import GCPNameGenerator @@ -19,7 +20,6 @@ if TYPE_CHECKING: from google.auth.credentials import Credentials from typing_extensions import Self - from google.cloud.compute_v1.types import compute from cloudshell.cp.gcp.resource_conf import GCPResourceConfig logger = logging.getLogger(__name__) diff --git a/cloudshell/cp/gcp/helpers/name_generator.py b/cloudshell/cp/gcp/helpers/name_generator.py index 71a6190..2b3559a 100644 --- a/cloudshell/cp/gcp/helpers/name_generator.py +++ b/cloudshell/cp/gcp/helpers/name_generator.py @@ -17,7 +17,7 @@ class GCPNameGenerator: max_length: int = GCP_NAME_MAX_LENGTH prefix_length: int = field(init=False) max_core_length: int = field(init=False) - GCP_NAME_PATTERN: str = field(init=False) + GCP_NAME_PATTERN: int = field(init=False, default=r"") def __attrs_post_init__(self): self.prefix_length = len(self.prefix) diff --git a/cloudshell/cp/gcp/resource_conf.py b/cloudshell/cp/gcp/resource_conf.py index bb5027a..525314f 100644 --- a/cloudshell/cp/gcp/resource_conf.py +++ b/cloudshell/cp/gcp/resource_conf.py @@ -82,9 +82,9 @@ class GCPResourceConfig(BaseConfig): credentials: Credentials = attr( ATTR_NAMES.json_keys, is_password=True, converter=get_credentials ) - custom_tags: list = attr( + custom_tags: dict = attr( ATTR_NAMES.custom_tags, - default=[], + default="", converter=get_custom_tags ) availability_zone: str = attr(ATTR_NAMES.zone)