def _platform_attach_volume(self, volume, instance): """ Attach a volume using the Google Cloud Platform API Some specific steps are performed here: - Device name (the name assigned to this device visible through the VM OS) - Mount mode (read only or read write) - Disk interface (SCSI or NVME) Args: volume (<volume>): The volume to attach instance (<instance>): The instance where the volume is to be attached Returns: bool: True if the volume is attached successfully, False otherwise """ # Ask for mount ro/rw ro_rw = SimpleTUI.input_dialog( "Mount mode", question="Specify a mount mode (READ_WRITE, READ_ONLY)", return_type=str, regex="^(READ_WRITE|READ_ONLY)$") if ro_rw is None: return # Ask for disk interface interface = SimpleTUI.input_dialog( "Disk interface", question="Specify disk interface (SCSI, NVME)", return_type=str, regex="^(SCSI|NVME)$") return self.gcp_client.attach_volume(node=instance, volume=volume, device=volume.name, ex_boot=False, ex_auto_delete=False, ex_interface=interface)
def _platform_detach_volume(self, volume): """ Detach a volume using the Google Cloud Platform API Some specific steps are performed here: - Check if the selected volume is a boot disk Args: volume (<volume>): The volume to detach Returns: bool: True if the volume is successfully detached, False otherwise """ # Search for the instance the volume is attached to instance = self._get_instance_from_volume(volume) if instance is None: return False if not self._is_volume_removable(volume): SimpleTUI.msg_dialog( "Detaching status", "This volume is mounted as boot disk and cannot be detached until the VM is stopped!", SimpleTUI.DIALOG_ERROR) return result = self.gcp_client.detach_volume(volume, instance) if result: while True: # No direct way to refresh a Volume status, so we look if # it is still attached to its previous instance updated_volume = self.gcp_client.ex_get_volume(volume.name) if not self._platform_is_volume_attached(updated_volume): break time.sleep(3) return result
def close(self, code): """ Close the application Args: code (int): an exit code (0 for normal termination) """ SimpleTUI.clear_console() exit(code)
def print_global_floating_ips(self): """ Print ip and other useful information of all floating ip available in each region """ table_header = [ "ID", "Public Ip", "Floating IP ID", "Associated Instance", "Region" ] table_body = self._list_global_floating_ips() SimpleTUI.print_table(table_header, table_body) if len(self.global_floating_ips) == 0: SimpleTUI.info("There are no floating IPs available") return len(self.global_floating_ips)
def print_global_instances(self): """ Print instance id, image id, IP address and state for each active instance in each region """ table_header = [ "ID", "Instance Name", "Instance ID", "IP address", "Status", "Key Name", "Avail. Zone" ] table_body = self._list_global_instances() SimpleTUI.print_table(table_header, table_body) if len(self.global_instances) == 0: SimpleTUI.info("There are no running or pending instances") return len(self.global_instances)
def print_global_volumes(self): """ Print all the available volumes for each region and some informations """ table_header = [ "ID", "Volume Name", "Volume ID", "Creation", "Size (GB)", "Attached To", "Status", "Avail. Zone" ] table_body = self._list_global_volumes() SimpleTUI.print_table(table_header, table_body) if len(self.global_volumes) == 0: SimpleTUI.info("There are no volumes available") return len(self.global_volumes)
def promote_ephimeral_ip(self): """ Promote an Ephimeral IP to a Static one For more infos about Ephimeral/Static IPs on GCP, please visit https://cloud.google.com/compute/docs/ip-addresses/ """ # Select an instance floating_ip = None while (True): instance_index = SimpleTUI.list_dialog( "Instances available", self.print_all_instances, question= "Select the instance which floating IP has to be promoted to \"static\"" ) if instance_index is None: return instance = self.instances[instance_index - 1] # Check if the instance has an IP assigned (e.g. no IP is assigned while stopped) if len(instance.public_ips) == 0 or None in instance.public_ips: SimpleTUI.msg_dialog( "Promotion status", "This instance has no available floating IPs to promote!", SimpleTUI.DIALOG_ERROR) # Check if the instance has already a static IP assigned elif self._is_instance_floating_ip_static(instance): SimpleTUI.msg_dialog( "Promotion status", "This instance floating IP is already promoted to \"static\"!", SimpleTUI.DIALOG_ERROR) # Continue the ephimeral to static conversion else: floating_ip = instance.public_ips[0] break # Specify address name address_name = SimpleTUI.input_dialog( "Static Floating IP Name", question="Specify a name for the new Static Floating IP", return_type=str, regex="^[a-zA-Z0-9-]+$") if address_name is None: return if self._promote_ephimeral_ip_to_static(floating_ip, address_name): SimpleTUI.msg_dialog("Static Floating IP Promotion", "Floating IP promoted!", SimpleTUI.DIALOG_SUCCESS) else: SimpleTUI.msg_dialog( "Static Floating IP Promotion", "There was an error while promoting this Floating IP!", SimpleTUI.DIALOG_ERROR)
def print_all_nics(self): """ Print instance id, image id, IP address and state for each active instance ! NOTE: before using this method, please set an instance using self.instance = <instance> ! Returns: int: the number of items printed """ table_header = ["ID", "NIC Name"] table_body = self._list_all_nics() SimpleTUI.print_table(table_header, table_body) if len(self.instances) == 0: SimpleTUI.info("There are no NICs available for this instance") return len(self.instances)
def _platform_override_menu(self, menu, choice): """ Override a menu voice Args: menu (str): a menu identifier choice (int): a menu entry index Returns: int: 0 for normal, 1 for going back to the main menu of EasyCloud, 2 for closing the whole application """ if menu == "floating_ips": if choice == 5: # Detach floating ip SimpleTUI.msg_dialog( "Detach Floating IP", "Detaching an IP address from an instance is not supported on GCP: \n" + "an instance must always have an IP associated while running.\n" + "However, you can assign another address to the desired instance to\n" + "replace the current one.", SimpleTUI.DIALOG_INFO) return 0 SimpleTUI.error("Unavailable choice!") SimpleTUI.pause() SimpleTUI.clear_console() pass
def load_all_modules(self, no_libs_check): """ Loads all the Modules from the modules folder """ _all_modules = list(iter_modules(modules.__path__)) for _module in _all_modules: module_object = Module(_module[1]) try: if no_libs_check or self.check_dependencies(module_object): module_object.load_manager_class() self.loaded_modules.append(module_object) except ImportError as e: SimpleTUI.msg_dialog("Missing packages", "There was an error while importing a package for the\n" + module_object.platform_name + " module: \n\n" + str(e) + "\n\n" + "This module won't be loaded.", SimpleTUI.DIALOG_ERROR)
def _platform_extra_menu(self): """ Print the extra Functions Menu (specific for each platform) """ while (True): menu_header = self.platform_name + " Extra Commands" menu_subheader = [ "Region: \033[1;94m" + self._platform_get_region() + "\033[0m" ] menu_items = [ "Promote ephimeral IP to static", "Demote a static IP", "List instances for all the regions", "List volumes for all the regions", "List floating ips for all the regions", "Back to the Main Menu" ] choice = SimpleTUI.print_menu(menu_header, menu_items, menu_subheader) if choice == 1: # Promote IP self.promote_ephimeral_ip() elif choice == 2: # Demote IP answer = SimpleTUI.yn_dialog( "Demotion status", "You can demote a static IP easily deleting it through \"Manage floating IPs\" > \"Release a reserved Floating IP\".\n" + "NOTE: the static IP won't be removed from the associated instance until the latter is stopped/rebooted/deleted.\n" + "For more infos about Ephimeral/Static IPs on GCP, please visit https://cloud.google.com/compute/docs/ip-addresses/.\n" + "Would you like to start the \"Release a reserved Floating IP\" wizard now?", SimpleTUI.DIALOG_INFO) if answer: self.release_floating_ip() elif choice == 3: # List all the instances (Global) SimpleTUI.list_dialog("Instances available (Global view)", list_printer=self.print_global_instances) elif choice == 4: # List all the volumes (Global) SimpleTUI.list_dialog("Volumes available (Global view)", list_printer=self.print_global_volumes) elif choice == 5: # List all the floating ips (Global) SimpleTUI.list_dialog( "Floating IPs available (Global view)", list_printer=self.print_global_floating_ips) elif choice == 6: break else: SimpleTUI.msg_dialog("Error", "Unimplemented functionality", SimpleTUI.DIALOG_ERROR)
def _platform_create_new_instance(self, instance_name, image, instance_type, monitor_cmd_queue=None): """ Create a new instance using the Google Cloud Platform API Args: instance_name (str): The name of the instance image (<image_type>): The image to be used as a base system instance_type (<instance_type>): The VM flavor monitor_cmd_queue: the monitor commands Quue """ # Creation summary print("\n--- Creating a new instance with the following properties:") print("- %-20s %-30s" % ("Name", instance_name)) print("- %-20s %-30s" % ("Image", image.name)) print("- %-20s %-30s" % ("Instance Type", instance_type.name)) # ask for confirm print("") if (SimpleTUI.user_yn("Are you sure?")): instance = self.gcp_client.create_node(name=instance_name, size=instance_type, image=image) if instance is None: return False if monitor_cmd_queue is not None and self.is_monitor_running(): monitor_cmd_queue.put({ "command": "add", "instance_id": instance.id }) return True
def ask_for_data(self, section_name, param_name, return_type=None, regex=None): """ Ask for user input if a parameter is not defined Args: param_name (str): the name of the missing parameter return_type (<return_type>, optional): the return type assigned to user input regex (str): the regular expression the input must follow Returns: <return_type>: user input of type <return_type> """ return SimpleTUI.input_dialog( "Missing value", question= "A parameter required by this module has not been defined in settings.cfg!\n" "Please, define a value to assign to \"" + param_name + "\" (\"" + section_name + "\" section)", return_type=return_type, regex=regex, pause_on_exit=False, cannot_quit=True)
def _platform_create_volume(self, volume_name, volume_size): """ Create a new volume using the Google Cloud Platform API Some specific steps are performed here: - Volume type (pd-standard or pd-ssd) Args: volume_name (str): Volume name volume_size (int): Volume size in GB Returns: bool: True if the volume is successfully created, False otherwise """ # Volume type volume_type = SimpleTUI.input_dialog( "Volume type", question="Specify the volume type (pd-standard, pd-ssd)", return_type=str, regex="^(pd-standard|pd-ssd)$") if volume_type is None: return # Volume creation return self.gcp_client.create_volume(size=volume_size, name=volume_name, ex_disk_type=volume_type)
def _platform_create_volume(self, volume_name, volume_size): """ Create a new volume using the Amazon Web Services API Some specific steps are performed here: - Volume type (standard or io1) - IOPS (only if io1 is selected) - Zone selection (required) Args: volume_name (str): Volume name volume_size (int): Volume size in GB Returns: bool: True if the volume is successfully created, False otherwise """ # Volume type volume_type = SimpleTUI.input_dialog( "Volume type", question="Specify the volume type (standard, io1)", return_type=str, regex="^(standard|io1)$") if volume_type is None: return # IOPS iops = None if volume_type == "io1": iops = SimpleTUI.input_dialog( "IOPS limit", question= "Specify the number of IOPS (I/O operations per second) the volume has to support", return_type=int) if iops is None: return # Zone selection zone_index = SimpleTUI.list_dialog( "Zones available", self.print_all_availability_zones, question="Select a zone where the volume will be created") if zone_index is None: return zone = self.avail_zones[zone_index - 1] # Volume creation return self.ec2_client.create_volume(name=volume_name, size=volume_size, location=zone, ex_volume_type=volume_type, ex_iops=iops)
def _platform_extra_menu(self): """ Print the extra Functions Menu (specific for each platform) """ while (True): menu_header = self.platform_name + " Extra Commands" menu_subheader = [ "Region: \033[1;94m" + self._platform_get_region() + "\033[0m" ] menu_items = ["Back to the Main Menu"] choice = SimpleTUI.print_menu(menu_header, menu_items, menu_subheader) # if choice == 1 and self._is_barebone(): # Blazar menu if choice == 1: break else: SimpleTUI.msg_dialog("Error", "Unimplemented functionality", SimpleTUI.DIALOG_ERROR)
def _platform_delete_volume(self, volume): """ Delete a volume using the Google Cloud Platform API Some specific steps are performed here: - Check if the selected volume is a boot disk Args: volume (<volume>): The volume to delete Returns: bool: True if the volume is successfully deleted, False otherwise """ if not self._is_volume_removable(volume): SimpleTUI.msg_dialog( "Volume deletion", "This volume is mounted as boot disk and cannot be detached until the VM is stopped!", SimpleTUI.DIALOG_ERROR) return return self.gcp_client.destroy_volume(volume)
def get_instance(self, manager_class): """ Returns an existing instance of the manager_class provided in input if previously instantiated or creates a new instance. Args: manager_class (<manager_class>): class of the module manager Returns: <manager_class_instance>: an instance of <manager_class> """ for _instance in self.loaded_instances: if isinstance(_instance, manager_class): return _instance try: _new_instance = manager_class() self.loaded_instances.append(_new_instance) return _new_instance except Exception as e: SimpleTUI.exception_dialog(e) return None
def menu(self): """ Prints the modules menu """ global kill while True: # Header creation menu_header = "******************** EasyCloud ********************" # Subheader creation disclaimer = "\033[34;1mThis is a proof-of-concept build, not suitable\nfor production use.\033[0m\n" debug_status = None hffr_status = None if "LIBCLOUD_DEBUG" in environ: debug_status = "\033[93mLibcloud Debug Mode ON\033[0m" else: debug_status = "\033[90mLibcloud Debug Mode OFF\033[0m" if "LIBCLOUD_DEBUG_PRETTY_PRINT_RESPONSE" in environ and environ["LIBCLOUD_DEBUG_PRETTY_PRINT_RESPONSE"] == "1": hffr_status = "\033[93mLibcloud Human friendly formatted response ON\033[0m" else: hffr_status = "\033[90mLibcloud Human friendly formatted response OFF\033[0m" menu_subheader = [disclaimer, debug_status, hffr_status] # Menu items creation menu_items = [] for module in self.loaded_modules: menu_items.append(module.platform_name) menu_items.append("Close application") # Menu print choice = SimpleTUI.print_menu(menu_header, menu_items, subheader_items=menu_subheader, custom_question="Select a platform") try: if(choice >= 1 and choice <= len(self.loaded_modules)): return self.loaded_modules[choice - 1] # Load a module elif(choice == len(self.loaded_modules) + 1): # Close application self.close(0) else: SimpleTUI.msg_dialog("Error", "Unimplemented functionality", SimpleTUI.DIALOG_ERROR) except Exception as e: SimpleTUI.exception_dialog(e)
def start(self, no_libs_check=False): """ Main application loop """ # Resize the console window SimpleTUI.resize_console(30, 120) # Load all the modules self.load_all_modules(no_libs_check=no_libs_check) # Loop exit = False while(not exit): SimpleTUI.set_console_title("EasyCloud") platform = self.menu() manager = self.get_instance(platform.manager_class) try: close = False while(not close): action = manager.menu() # if action == 0: # Ignore this value # pass if action == 1: # Close current manager menu close = True elif action == 2: # Close this application close = True exit = True except Exception as e: SimpleTUI.exception_dialog(e) self.close(0)
def _platform_reserve_floating_ip(self): """ Reserve a floating IP using the Google Cloud Platform API """ address_name = SimpleTUI.input_dialog( "Floating IP Name", question="Specify a name for the new Floating IP", return_type=str, regex="^[a-zA-Z0-9-]+$") if address_name is None: return if self.gcp_client.ex_create_address(name=address_name): return True return False
def check_dependencies(self, module): """ Check if all the dependencies (pip packages) are satisfied for a certain module, and can install them if the user approves Args: module (Module): a module object (no manager class must be loaded through load_manager_class method of this object) Returns: bool: the result of the check """ required_packages = getattr(module, "dependencies") installed_packages = subprocess.check_output(['pip3', 'list', '--format=json'], stderr=subprocess.STDOUT).decode() missing_packages_names = [] # package names displayed to the user missing_packages_commands = [] # packages names/urls passed to pip3 for required_package in required_packages: # Format: pip-package-name:package-url|package-git (the latter is optional) # # Symbolic: pip package name (the one you see with "pip3 list") # Package URL (optional): package url from where pip3 will download the library # Package Git (optional): package git url in the form git+git://github.com/my_user/my_project.git(@branch) # # e.g. libcloud:apache-libcloud required_package_data = required_package.split(":", 1) if required_package_data[0] not in installed_packages: missing_packages_names.append(required_package_data[0]) if len(required_package_data) == 2: missing_packages_commands.append(required_package_data[1]) else: missing_packages_commands.append(required_package_data[0]) if len(missing_packages_names) > 0: packages_list = "" for missing_package_name in missing_packages_names: packages_list += "- " + missing_package_name + "\n" choice = SimpleTUI.yn_dialog("Missing packages", "The following packages are required by the " + module.platform_name + " module:\n" + "\n" + packages_list + "\nIf these packages are not installed, this module won't be loaded.\n" + "Do you want to install them through pip?", warning=True) if choice: SimpleTUI.msg_dialog("Library installer", "Installing the required libraries, this can take a bit...", SimpleTUI.DIALOG_INFO, pause_on_exit=False, clear_on_exit=False) if self.install_libraries(missing_packages_commands): SimpleTUI.msg_dialog("Library installer", "All the packages for " + module.platform_name + "\n" + "were successfully installed!\n\n" + packages_list, SimpleTUI.DIALOG_SUCCESS) return True else: SimpleTUI.msg_dialog("Library installer", "There was an error while installing the missing packages\n" + "for " + module.platform_name + ".\n" "Please check logs" + sep + "installer.log in the main directory for details.", SimpleTUI.DIALOG_ERROR) return False return False return True
def _platform_attach_volume(self, volume, instance): """ Attach a volume using the Amazon Web Services API Some specific steps are performed here: - Exposure point (e.g. /dev/sdb) Args: volume (<volume>): The volume to attach instance (<instance>): The instance where the volume is to be attached Returns: bool: True if the volume is attached successfully, False otherwise """ # Ask for exposure point (the GNU/Linux device where this volume will # be available) exposure_point = SimpleTUI.input_dialog( "Exposure point", question="Specify where the device is exposed, e.g. ‘/dev/sdb’", return_type=str, regex="^(/[^/ ]*)+/?$") if exposure_point is None: return return self.ec2_client.attach_volume(instance, volume, exposure_point)
def _platform_create_new_instance(self, instance_name, image, instance_type, monitor_cmd_queue=None): """ Create a new instance using the Amazon Web Services API Some specific steps are performed here: - Ask for security group - Ask for key pair - Instance creation summary Args: instance_name (str): The name of the instance image (<image_type>): The image to be used as a base system instance_type (<instance_type>): The VM flavor monitor_cmd_queue: the monitor commands Quue """ # 5. Security Group security_group_index = SimpleTUI.list_dialog( "Security Groups available", self.print_all_security_groups, question="Select security group") if security_group_index is None: return security_group = self.security_groups[security_group_index - 1] # 6. Key Pair key_pair_index = SimpleTUI.list_dialog("Key Pairs available", self.print_all_key_pairs, question="Select key pair") if key_pair_index is None: return key_pair = self.key_pairs[key_pair_index - 1] # Creation summary print("\n--- Creating a new instance with the following properties:") print("- %-20s %-30s" % ("Name", instance_name)) print("- %-20s %-30s" % ("Image", image.name)) print("- %-20s %-30s" % ("Instance Type", instance_type.name)) print("- %-20s %-30s" % ("Key Pair", key_pair.name)) print("- %-20s %-30s" % ("Security Group", security_group)) # ask for confirm print("") if (SimpleTUI.user_yn("Are you sure?")): instance = self.ec2_client.create_node( name=instance_name, image=image, size=instance_type, ex_keyname=key_pair.name, ex_security_groups=[security_group], ex_monitoring=True, ex_mincount=1, ex_maxcount=1) if instance is None: return False if monitor_cmd_queue is not None and self.is_monitor_running(): monitor_cmd_queue.put({ "command": "add", "instance_id": instance.id }) return True
def _platform_associate_floating_ip(self, floating_ip, instance): """ Associate a floating IP to an instance using the Google Cloud Platform API Some specific steps are performed here: - NIC (Network interface controller) selection - Access config name Args: floating_ip (GCEAddress): The floating IP to attach instance (Node): The instance where the floating IP is to be assigned Returns: bool: True if the floating IP is successfully associated, False otherwise """ # Set an instance, as required by print_all_nics() self.current_instance = instance nic_index = SimpleTUI.list_dialog( "NICs available", self.print_all_nics, question="Select the VM NIC to assign this IP") if nic_index is None: return nic = instance.extra["networkInterfaces"][ nic_index - 1] # serve nome per rimuovere # Check if there's already an active Access Configuration and ask the user for confirm remove_old_access_config = False if self._nic_has_access_config(nic): choice = SimpleTUI.yn_dialog( "Access Configuration Overwrite", "Warning: there's already an access configuration associated to this NIC.\n" + "Do you really want to continue (the current access configuration will be overwritten)?", warning=True) if not choice: return remove_old_access_config = True # Access Configuration name access_config_name = SimpleTUI.input_dialog( "Access configuration", question="Specify an access configuration name", return_type=str, regex="^[a-zA-Z0-9-]+$") if access_config_name is None: return # Remove the old access configuration if it's already existing if remove_old_access_config: SimpleTUI.msg_dialog("Access Configuration Overwrite", "Removing old access configuration...", SimpleTUI.DIALOG_INFO, pause_on_exit=False, clear_on_exit=False) if not self._delete_access_config(instance, nic): SimpleTUI.msg_dialog( "Access Configuration Overwrite", "There was an error while removing the current access configuration!", SimpleTUI.DIALOG_ERROR) return # Associate the Access Configuration to the NIC if self.gcp_client.ex_add_access_config(node=instance, name=access_config_name, nic=nic, nat_ip=floating_ip.address, config_type="ONE_TO_ONE_NAT"): return True return False