From df3621b1df43e1f75a4e32f119a2ab6fb49893f3 Mon Sep 17 00:00:00 2001
From: Massimo <massimo.canonico@uniupo.it>
Date: Fri, 4 Oct 2024 15:18:51 +0000
Subject: [PATCH] Update manager.py

---
 .../modules/chameleon_libcloud/manager.py     | 285 ++++++++++++++----
 1 file changed, 218 insertions(+), 67 deletions(-)

diff --git a/easycloud/modules/chameleon_libcloud/manager.py b/easycloud/modules/chameleon_libcloud/manager.py
index 33be42f..baa89a0 100644
--- a/easycloud/modules/chameleon_libcloud/manager.py
+++ b/easycloud/modules/chameleon_libcloud/manager.py
@@ -3,19 +3,105 @@ EasyCloud Chameleon Cloud Manager.
 """
 
 import datetime
-from easycloud.common.libcloud import LibcloudInstance
+
+from openstack.config import loader
+
 from easycloud.core.actionbinder import bind_action
+from easycloud.core.compute import Instance, InstanceStatus
 from easycloud.core.metamanager import MetaManager
-from easycloud.modules.chameleon_libcloud.actions import ChameleonCloudAgentActions
-from easycloud.modules.chameleon_libcloud.confmanager import ChameleonCloudConfManager
-from easycloud.modules.chameleon_libcloud.monitor import ChameleonCloudMonitor
+from easycloud.modules.chameleon_openstacksdk.actions import ChameleonCloudAgentActions
+from easycloud.modules.chameleon_openstacksdk.confmanager import ChameleonCloudConfManager
+from easycloud.modules.chameleon_openstacksdk.monitor import ChameleonCloudMonitor
 from easycloud.tui.simpletui import SimpleTUI
-from libcloud.compute.providers import get_driver
-from libcloud.compute.types import Provider
-#import logging
+import logging
+import openstack
 import time
 
 
+class OpenStackInstance(Instance):
+    """ An OpenStack instance.
+
+    See:
+    - https://docs.openstack.org/openstacksdk/latest/user/resources/compute/v2/server.html#openstack.compute.v2.server.Server
+    """
+
+    _NODE_STATUS_MAP = {
+        'BUILD': InstanceStatus.PENDING,
+        'REBUILD': InstanceStatus.PENDING,
+        'ACTIVE': InstanceStatus.RUNNING,
+        'SUSPENDED': InstanceStatus.SUSPENDED,
+        'SHUTOFF': InstanceStatus.STOPPED,
+        'DELETED': InstanceStatus.TERMINATED,
+        'QUEUE_RESIZE': InstanceStatus.PENDING,
+        'PREP_RESIZE': InstanceStatus.PENDING,
+        'VERIFY_RESIZE': InstanceStatus.RUNNING,
+        'PASSWORD': InstanceStatus.PENDING,
+        'RESCUE': InstanceStatus.PENDING,
+        'REBOOT': InstanceStatus.REBOOTING,
+        'HARD_REBOOT': InstanceStatus.REBOOTING,
+        'SHARE_IP': InstanceStatus.PENDING,
+        'SHARE_IP_NO_CONFIG': InstanceStatus.PENDING,
+        'DELETE_IP': InstanceStatus.PENDING,
+        'ERROR': InstanceStatus.ERROR,
+        'UNKNOWN': InstanceStatus.UNKNOWN
+    }
+
+    def __init__(self, os_conn, os_instance):
+        # Example:
+        #       openstack.compute.v2.server.Server(OS-EXT-STS:task_state=None, addresses={'CH-820879-net': [{'OS-EXT-IPS-MAC:mac_addr': 'fa:16:3e:c5:d0:e6', 'version': 4, 'addr': '10.185.189.137', 'OS-EXT-IPS:type': 'fixed'}]}, links=[{'href': 'https://kvm.tacc.chameleoncloud.org:8774/v2.1/servers/a0a9cdbb-a8f6-42fc-af4b-0e6c68d70ec3', 'rel': 'self'}, {'href': 'https://kvm.tacc.chameleoncloud.org:8774/servers/a0a9cdbb-a8f6-42fc-af4b-0e6c68d70ec3', 'rel': 'bookmark'}], image={'id': '206874ef-3d93-43c6-bc37-7335478a27a7', 'links': [{'href': 'https://kvm.tacc.chameleoncloud.org:8774/images/206874ef-3d93-43c6-bc37-7335478a27a7', 'rel': 'bookmark'}]}, OS-EXT-SRV-ATTR:user_data=None, OS-EXT-STS:vm_state=stopped, OS-EXT-SRV-ATTR:instance_name=instance-00001a28, OS-EXT-SRV-ATTR:root_device_name=/dev/vda, OS-SRV-USG:launched_at=2020-04-20T12:02:29.000000, flavor={'ephemeral': 0, 'ram': 512, 'original_name': 'm1.tiny', 'vcpus': 1, '_extraspecs': {}, 'swap': 0, 'disk': 1}, id=a0a9cdbb-a8f6-42fc-af4b-0e6c68d70ec3, security_groups=[{'name': 'default'}], description=easycloud-047, user_id=520b8f26b6214b3d9b0fab8878e67e44, OS-EXT-SRV-ATTR:hostname=easycloud-047, OS-DCF:diskConfig=MANUAL, accessIPv4=, accessIPv6=, OS-EXT-SRV-ATTR:reservation_id=r-7wfc3f6v, OS-EXT-STS:power_state=4, OS-EXT-AZ:availability_zone=nova, config_drive=, status=SHUTOFF, OS-EXT-SRV-ATTR:ramdisk_id=, updated=2020-09-10T09:30:49Z, hostId=aa36609e24cc62db7565ad56156451578e6856b8b3e7c8e4cf8fa58f, OS-EXT-SRV-ATTR:host=c07-34, OS-SRV-USG:terminated_at=None, tags=[], key_name=sguazt _at_ wildcat, OS-EXT-SRV-ATTR:kernel_id=, locked=False, OS-EXT-SRV-ATTR:hypervisor_hostname=c07-34, name=easycloud-047, OS-EXT-SRV-ATTR:launch_index=0, created=2020-04-20T12:02:17Z, tenant_id=2c18b5d8ebfa4a08b603c151d967a04d, os-extended-volumes:volumes_attached=[], trusted_image_certificates=None, metadata={}, location=Munch({'cloud': 'chameleon', 'region_name': 'KVM@TACC', 'zone': 'nova', 'project': Munch({'id': '2c18b5d8ebfa4a08b603c151d967a04d', 'name': 'CH-820879', 'domain_id': 'default', 'domain_name': None})}))
+
+        self._os_conn = os_conn
+        self._os_inst = os_instance
+        self._status = self._NODE_STATUS_MAP.get(os_instance.status, InstanceStatus.UNKNOWN)
+        self._private_ips = []
+        self._public_ips = []  # TODO
+        for net_name, nics in os_instance.addresses.items():
+            for nic in nics:
+                self._private_ips.append(nic['addr'])
+
+    @property
+    def extra(self):
+        return self._os_inst
+
+    @property
+    def handle(self):
+        return self._os_inst
+
+    @property
+    def id(self):
+        return self._os_inst.id
+
+    @property
+    def private_ips(self):
+        return self._private_ips
+
+    @property
+    def public_ips(self):
+        return self._public_ips
+
+    @property
+    def name(self):
+        return self._os_inst.name
+
+    @property
+    def status(self):
+        return self._status
+
+    def destroy(self):
+        self._os_conn.compute.delete_server(self._os_inst)
+
+    def reboot(self):
+        self._os_conn.compute.reboot_server(self._os_inst, reboot_type='HARD')
+
+    def start(self):
+        self._os_conn.compute.start_server(self._os_inst)
+        # TODO: call "self._os_conn.compute.wait_for_server(self._os_inst, status='ACTIVE', wait = timeout)" to perform a synchronous version of this method (note, if timeout is None you should invoke this method without the "wait" parameter, catching the "ResourceTimeout" exception and, in case such an exception is thrown, calling again the "wait_for_server()" method)
+
+    def stop(self):
+        self._os_conn.compute.stop_server(self._os_inst)
+        # TODO: call "self._os_conn.compute.wait_for_server(self._os_inst, status='ACTIVE', wait = timeout)" to perform a synchronous version of this method (note, if timeout is None you should invoke this method without the "wait" parameter, catching the "ResourceTimeout" exception and, in case such an exception is thrown, calling again the "wait_for_server()" method)
+
+
 class ChameleonCloud(MetaManager):
     """
     EasyCloud Chameleon Cloud Manager.
@@ -42,13 +128,52 @@ class ChameleonCloud(MetaManager):
         """
         Connection to the endpoint specified in the configuration file
         """
-        cls = get_driver(Provider.OPENSTACK)
-        self.os_client = cls(self.conf.os_username, self.conf.os_password,
-                             api_version='2.1',
-                             ex_tenant_name=self.conf.os_project_name,
-                             ex_force_auth_url=self.conf.os_auth_url,
-                             ex_force_service_region=self.conf.os_region,
-                             ex_force_auth_version='3.x_password')  # Updated 03/05/19
+        # Configura il logging
+        logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
+        logger = logging.getLogger(__name__)
+
+        try:
+            # Load configurations from clouds.yaml
+            cloud_config_loader = loader.OpenStackConfig(
+                config_files=['easycloud/modules/chameleon_openstacksdk/clouds.yaml'])
+
+            # fetch all cloud projects
+            available_clouds = cloud_config_loader.get_all_clouds()
+
+            if not available_clouds:
+                raise ValueError("No cloud configurations found in the YAML file.")
+
+            # cloud's menu selection
+            cloud_name = self._select_cloud(available_clouds)
+
+            # load config for selected cloud project
+            cloud_config = cloud_config_loader.get_one_cloud(cloud_name)
+
+            # Connection to Openstack with loaded parameters
+            self.os_client = openstack.connection.Connection(config=cloud_config)
+            logger.info(f"Connessione a OpenStack riuscita per il cloud '{cloud_name}'.")
+        except Exception as e:
+            logger.error("Errore nella connessione a OpenStack: %s", e)
+            raise
+
+    def _select_cloud(self, available_clouds):
+        """
+        Display a command-line menu for the user to select a cloud project.
+        """
+        print("Available cloud projects:")
+        for i, cloud in enumerate(available_clouds, start=1):
+            print(f"{i}. {cloud.name}")
+
+        while True:
+            try:
+                choice = int(input("Select the cloud project by number: "))
+                if 1 <= choice <= len(available_clouds):
+                    return available_clouds[choice - 1].name
+                else:
+                    print(f"Please select a number between 1 and {len(available_clouds)}.")
+            except ValueError:
+                print("Invalid input. Please enter a number.")
+    
 
     def list_instances(self):
         """ Returns a list of instances.
@@ -56,10 +181,10 @@ class ChameleonCloud(MetaManager):
         Returns:
             list:    A list of ``easycloud.core.compute.Instance`` objects.
         """
-        #return self.os_client.list_nodes()
         instances = []
-        for node in self.os_client.list_nodes():
-            instances.append(LibcloudInstance(node))
+        for server in self.os_client.compute.servers():
+            instance = OpenStackInstance(self.os_client, server)
+            instances.append(instance)
         return instances
 
     # =============================================================================================== #
@@ -74,15 +199,20 @@ class ChameleonCloud(MetaManager):
 
     def _platform_list_all_images(self):
         """
-        Print all available images
-        Format: "ID", "Name", Image ID", "State"
+        List all available images with dynamic column handling.
         """
         self.images = self.os_client.list_images()
-        i = 1
         table_body = []
-        for image in self.images:
-            table_body.append([i, image.name, image.id, image.extra["status"]])
-            i = i + 1
+
+        for i, image in enumerate(self.images, start=1):
+            row = [
+                i,  # Indice
+                image.name,
+                image.id,
+                getattr(image, "status", "Unknown")  
+            ]
+            table_body.append(row)
+
         return table_body
 
     def _platform_list_all_availability_zones(self):
@@ -98,7 +228,7 @@ class ChameleonCloud(MetaManager):
         Print all instance types
         Format: "ID", "Instance Type ID", "vCPUs", "Ram (GB)", "Disk (GB)"
         """
-        self.instance_types = self.os_client.list_sizes()
+        self.instance_types = list(self.os_client.list_flavors())  # Convert generator to list
         i = 1
         table_body = []
         for instance_type in self.instance_types:
@@ -112,12 +242,14 @@ class ChameleonCloud(MetaManager):
         Print all security groups
         Format: "ID", "SG name", "SG description"
         """
-        self.security_groups = self.os_client.ex_list_security_groups()
+        # Fetch security group
+        self.security_groups = list(self.os_client.network.security_groups())
+
         i = 1
         table_body = []
         for security_group in self.security_groups:
             table_body.append([i, security_group.name, security_group.description])
-            i = i + 1
+            i += 1
         return table_body
 
     def _platform_list_all_networks(self):
@@ -138,12 +270,13 @@ class ChameleonCloud(MetaManager):
         Print all key pairs
         Format: "ID", "Key name", "Key fingerprint"
         """
-        self.key_pairs = self.os_client.list_key_pairs()
-        i = 1
+        # Fetch the key pairs
+        self.key_pairs = list(self.os_client.compute.keypairs())
+
         table_body = []
-        for key_pair in self.key_pairs:
+        for i, key_pair in enumerate(self.key_pairs, start=1):
             table_body.append([i, key_pair.name, key_pair.fingerprint])
-            i = i + 1
+
         return table_body
 
     def _platform_list_all_instances(self):
@@ -151,14 +284,16 @@ class ChameleonCloud(MetaManager):
         Print instance id, image id, IP address and state for each active instance
         Format: "ID", "Instance Name", "Instance ID", "IP address", "Status", "Key Name", "Avail. Zone"
         """
-        self.instances = self.os_client.list_nodes()
+        self.instances = self.list_instances()
         i = 1
         table_body = []
         for instance in self.instances:
             if len(instance.public_ips) > 0 and None not in instance.public_ips:
-                table_body.append([i, instance.name, instance.id, ", ".join(instance.public_ips), instance.state, instance.extra["key_name"], instance.extra["availability_zone"]])
+                table_body.append([i, instance.name, instance.id, ", ".join(instance.public_ips), instance.state,
+                                   instance.extra["key_name"], instance.extra["availability_zone"]])
             else:
-                table_body.append([i, instance.name, instance.id, "-", instance.state, instance.extra["key_name"], instance.extra["availability_zone"]])
+                table_body.append([i, instance.name, instance.id, "-", instance.state, instance.extra["key_name"],
+                                   instance.extra["availability_zone"]])
             i = i + 1
         return table_body
 
@@ -171,11 +306,13 @@ class ChameleonCloud(MetaManager):
         i = 1
         table_body = []
         for volume in self.volumes:
-            created_at = datetime.datetime.strptime(volume.extra["created_at"], "%Y-%m-%dT%H:%M:%S.%f").strftime("%b %d %Y, %H:%M:%S") + " UTC"
+            created_at = datetime.datetime.strptime(volume.extra["created_at"], "%Y-%m-%dT%H:%M:%S.%f").strftime(
+                "%b %d %Y, %H:%M:%S") + " UTC"
             if "attachments" in volume.extra and len(volume.extra["attachments"]) > 0:
                 node = self.os_client.ex_get_node_details(volume.extra["attachments"][0]["server_id"])
                 table_body.append([i, volume.name, volume.id, created_at, volume.size,
-                                   node.name + " (" + volume.extra["attachments"][0]["device"] + ")", volume.state, volume.extra["location"]])
+                                   node.name + " (" + volume.extra["attachments"][0]["device"] + ")", volume.state,
+                                   volume.extra["location"]])
             else:
                 table_body.append([i, volume.name, volume.id, created_at, volume.size,
                                    "- (-)", volume.state, volume.extra["location"]])
@@ -191,9 +328,9 @@ class ChameleonCloud(MetaManager):
         i = 1
         table_body = []
         for floating_ip in self.floating_ips:
-            if(floating_ip.node_id is not None):
+            if (floating_ip.node_id is not None):
                 node = self.os_client.ex_get_node_details(floating_ip.node_id)
-                if(node is not None):
+                if (node is not None):
                     table_body.append([i, floating_ip.ip_address, floating_ip.id, node.name, "n/a"])
                 else:
                     table_body.append([i, floating_ip.ip_address, floating_ip.id, "Load Balancer", "n/a"])
@@ -243,6 +380,7 @@ class ChameleonCloud(MetaManager):
         if key_pair_index is None:
             return
         key_pair = self.key_pairs[key_pair_index - 1]
+
         # 7. Reservation id required if using CHI@TACC or CHI@UC (Optional)
         # For details about the fields, please visit
         # https://developer.openstack.org/api-ref/compute/?expanded=create-server-detail
@@ -266,27 +404,37 @@ class ChameleonCloud(MetaManager):
 
         # ask for confirm
         print("")
-        if(SimpleTUI.user_yn("Are you sure?")):
-            #ex_scheduler_hints is an invalid argument for create_node()
-            '''
-            instance = self.os_client.create_node(name=instance_name,
-                                                  image=image,
-                                                  size=instance_type,
-                                                  ex_keyname=key_pair.name,
-                                                  ex_security_groups=[security_group],
-                                                  ex_scheduler_hints=scheduler_hints)
-                                                  '''
-            instance = self.os_client.create_node(name=instance_name,
-                                                  image=image,
-                                                  size=instance_type,
-                                                  ex_keyname=key_pair.name,
-                                                  ex_security_groups=[security_group])
+        if (SimpleTUI.user_yn("Are you sure?")):
+            instance = self.os_client.compute.create_server(
+                name=instance_name,
+                image_id=image.id,
+                flavor_id=instance_type.id,
+                key_name=key_pair.name,
+                networks=[{"uuid": self._get_network_id()}],
+                # Assumendo che ci sia una funzione per ottenere il network ID
+                security_groups=[{"name": security_group.name}],
+                **({"scheduler_hints": scheduler_hints} if scheduler_hints else {})
+            )
             if instance is None:
                 return False
+            # Instance Monitoring
             if monitor_cmd_queue is not None and self.is_monitor_running():
                 monitor_cmd_queue.put({"command": "add", "instance_id": instance.id})
             return True
 
+        return False
+
+    def _get_network_id(self):
+        """
+        Retrieves the network ID to be used for instance creation.
+        Assumes that there is only one network or returns the first one found.
+        """
+        networks = list(self.os_client.network.networks())
+        if not networks:
+            raise Exception("No networks available.")
+        # Assuming you want the first available network
+        return networks[0].id
+
     def _platform_instance_action(self, instance, action):
         """
         Handle an instance action with Openstack API
@@ -301,7 +449,7 @@ class ChameleonCloud(MetaManager):
         Return a list of instances info
         """
         info = []
-        for instance in self.os_client.list_nodes():
+        for instance in self.os_client.compute.servers():
             info.append({"id": instance.id, "name": instance.name})
         return info
 
@@ -315,9 +463,9 @@ class ChameleonCloud(MetaManager):
         Returns:
             bool: True if the volume is successfully attached, False otherwise
         """
-        if(len(volume.extra["attachments"]) == 0):
+        if (len(volume.extra["attachments"]) == 0):
             return False
-        elif(volume.extra["attachments"][0]["server_id"] is None):
+        elif (volume.extra["attachments"][0]["server_id"] is None):
             return False
         return True
 
@@ -332,7 +480,7 @@ class ChameleonCloud(MetaManager):
             bool: True if the volume is successfully detached, False otherwise
         """
         result = self.os_client.detach_volume(volume)
-        if(result):
+        if (result):
             while True:
                 updated_volume = self.os_client.ex_get_volume(volume.id)
                 if not self._platform_is_volume_attached(updated_volume):
@@ -398,7 +546,7 @@ class ChameleonCloud(MetaManager):
         Returns:
             bool: True if the floating IP is assigned, False otherwise
         """
-        if(floating_ip.node_id is not None):
+        if (floating_ip.node_id is not None):
             return True
         return False
 
@@ -414,7 +562,7 @@ class ChameleonCloud(MetaManager):
         """
         node = self.os_client.ex_get_node_details(floating_ip.node_id)
         result = self.os_client.ex_detach_floating_ip_from_node(node, floating_ip)
-        if(result):
+        if (result):
             while True:
                 updated_floating_ip = self.os_client.ex_get_floating_ip(floating_ip.ip_address)
                 if not self._platform_is_ip_assigned(updated_floating_ip):
@@ -475,7 +623,7 @@ class ChameleonCloud(MetaManager):
         """
         Print the extra Functions Menu (specific for each platform)
         """
-        while(True):
+        while (True):
             menu_header = self.platform_name + " Extra Commands"
             menu_subheader = ["Region: \033[1;94m" +
                               self._platform_get_region() + "\033[0m"]
@@ -488,7 +636,8 @@ class ChameleonCloud(MetaManager):
                 if self._is_barebone():
                     SimpleTUI.msg_dialog("Error", "Unimplemented functionality", SimpleTUI.DIALOG_ERROR)
                 else:
-                    SimpleTUI.msg_dialog("Reservations Manager", "Reservation manager is not available on OpenStack@TACC.\n" +
+                    SimpleTUI.msg_dialog("Reservations Manager",
+                                         "Reservation manager is not available on OpenStack@TACC.\n" +
                                          "Please use one of the following regions:\n\n" +
                                          "- CHI@TACC (https://chi.tacc.chameleoncloud.org)\n" +
                                          "- CHI@UC (https://chi.uc.chameleoncloud.org)", SimpleTUI.DIALOG_INFO)
@@ -520,7 +669,7 @@ class ChameleonCloud(MetaManager):
         if menu == "main" and choice in self.override_main_menu:
             if choice == 6 and self._is_barebone():  # Volumes on barebone are not supported
                 return True
-            #elif choice in [8, 9] and not self._is_barebone():  # Monitor is not available on KVM
+            # elif choice in [8, 9] and not self._is_barebone():  # Monitor is not available on KVM
             #    return True
         return False
 
@@ -539,10 +688,12 @@ class ChameleonCloud(MetaManager):
         if menu == "main":
             if choice == 6:  # Volumes on barebone
                 SimpleTUI.msg_dialog("Volumes Handler", "CHI@TACC and CHI@UC don't support volumes.\n" +
-                                     "Please use KVM (https://openstack.tacc.chameleoncloud.org)", SimpleTUI.DIALOG_INFO)
+                                     "Please use KVM (https://openstack.tacc.chameleoncloud.org)",
+                                     SimpleTUI.DIALOG_INFO)
                 return 0
             elif choice in [8, 9]:  # Monitor on KVM
-                SimpleTUI.msg_dialog("Monitoring", "Monitoring and Rule Management features are not available on OpenStack@TACC.\n" +
+                SimpleTUI.msg_dialog("Monitoring",
+                                     "Monitoring and Rule Management features are not available on OpenStack@TACC.\n" +
                                      "Please use one of the following regions:\n\n" +
                                      "- CHI@TACC (https://chi.tacc.chameleoncloud.org)\n" +
                                      "- CHI@UC (https://chi.uc.chameleoncloud.org)", SimpleTUI.DIALOG_INFO)
@@ -557,7 +708,7 @@ class ChameleonCloud(MetaManager):
 
     def _platform_get_monitor(self, commands_queue, measurements_queue, metrics_file=None):
         """
-        Create the Chameleon Cloud Resources Monitor using Gnocchi APIs
+        Create the Chameleon Cloud Resources Monitor
 
         Args:
             commands_queue (Queue): message queue for communicating with the main
@@ -570,10 +721,10 @@ class ChameleonCloud(MetaManager):
         Returns:
             MetaMonitor: the platform-specific monitor
         """
-        self.instances = self.os_client.list_nodes()
+        self.instances = self.list_instances()
         for instance in self.instances:
-            #print("Adding instance to monitor init: " + str({"command": "add", "instance_id": instance.id}))
-            #logging.debug("Adding instance to monitor init: " + str({"command": "add", "instance_id": instance.id}))
+            # print("Adding instance to monitor init: " + str({"command": "add", "instance_id": instance.id}))
+            # logging.debug("Adding instance to monitor init: " + str({"command": "add", "instance_id": instance.id}))
             commands_queue.put({"command": "add", "instance_id": instance.id})
         return ChameleonCloudMonitor(conf=self.conf,
                                      commands_queue=commands_queue,
-- 
GitLab